Call CubeCamera.update in onBeforeRender cause stackoverflow with three.js? - three.js

I'm making a mirror shader in three.js.
here is my code:
mirror.onBeforeRender = function (renderer, scene, camera) {
//get mirror camera rendertexture and send to object.
}
function animate() {
requestAnimationFrame(animate);
cubeCamera.update(renderer,scene);
}
animate();
mirror 3DObject which will catch scene through camera in onBeforeRenderer function, but at the same time, I have another object dependence cubeCamera catch a enviroment texture.
I found that I can't catch mirror's reflection result in cubeCamera. It will display as whole black. So I tried to put cubeCamera.update(renderer,scene) into onBeforeRenderer, then it cause RangeError: Maximum call stack size exceeded.
Uncaught RangeError: Maximum call stack size exceeded
at PerspectiveCamera.updateMatrixWorld (three.module.js:11689:1)
at CubeCamera.updateMatrixWorld (three.module.js:8068:1)
at CubeCamera.update (three.module.js:11995:1)
at groundMirror.onBeforeRender (App.js:581:1)
at renderObject (three.module.js:28253:1)
at renderObjects (three.module.js:28243:1)
at renderScene (three.module.js:28165:1)
at WebGLRenderer.render (three.module.js:27983:1)
at CubeCamera.update (three.module.js:12014:1)
at groundMirror.onBeforeRender (App.js:581:1)
Is there any function or parameter can update cubeCamera after mirror.onBeforeRender ?
put into mirror.onBeforeRenderer but it return RangeError: Maximum call stack size exceeded.
set cubeCamera renderOrder as 2 but doesn't work.
2023/2/20 update
Thanks for comment of by Phil, I forgot consider that I could misunderstood of onBeforeRenderer usage!
Here is my code in onBeforeRenderer, basically is from THREE.water. I append a line to update cube camera at last, and it cause stack overflow. If I remove cubeCamera.update(), It will work expectly.
Hope it will make my question more clearly!
groundMirror.onBeforeRender = function (renderer, scene, camera) {
mirrorWorldPosition.setFromMatrixPosition(groundMirror.matrixWorld);
cameraWorldPosition.setFromMatrixPosition(camera.matrixWorld);
rotationMatrix.extractRotation(groundMirror.matrixWorld);
normal.set(0, 0, 1);
normal.applyMatrix4(rotationMatrix);
view.subVectors(mirrorWorldPosition, cameraWorldPosition);
// Avoid rendering when mirror is facing away
if (view.dot(normal) > 0) return;
view.reflect(normal).negate();
view.add(mirrorWorldPosition);
rotationMatrix.extractRotation(camera.matrixWorld);
lookAtPosition.set(0, 0, - 1);
lookAtPosition.applyMatrix4(rotationMatrix);
lookAtPosition.add(cameraWorldPosition);
target.subVectors(mirrorWorldPosition, lookAtPosition);
target.reflect(normal).negate();
target.add(mirrorWorldPosition);
mirrorCamera.position.copy(view);
mirrorCamera.up.set(0, 1, 0);
mirrorCamera.up.applyMatrix4(rotationMatrix);
mirrorCamera.up.reflect(normal);
mirrorCamera.lookAt(target);
mirrorCamera.far = camera.far; // Used in WebGLBackground
mirrorCamera.updateMatrixWorld();
mirrorCamera.projectionMatrix.copy(camera.projectionMatrix);
// Update the texture matrix
textureMatrix.set(
0.5, 0.0, 0.0, 0.5,
0.0, 0.5, 0.0, 0.5,
0.0, 0.0, 0.5, 0.5,
0.0, 0.0, 0.0, 1.0
);
textureMatrix.multiply(mirrorCamera.projectionMatrix)
textureMatrix.multiply(mirrorCamera.matrixWorldInverse);
mirrorPlane.setFromNormalAndCoplanarPoint(normal, mirrorWorldPosition);
mirrorPlane.applyMatrix4(mirrorCamera.matrixWorldInverse);
clipPlane.set(mirrorPlane.normal.x, mirrorPlane.normal.y, mirrorPlane.normal.z, mirrorPlane.constant);
var projectionMatrix = mirrorCamera.projectionMatrix;
q.x = (Math.sign(clipPlane.x) + projectionMatrix.elements[8]) / projectionMatrix.elements[0];
q.y = (Math.sign(clipPlane.y) + projectionMatrix.elements[9]) / projectionMatrix.elements[5];
q.z = - 1.0;
q.w = (1.0 + projectionMatrix.elements[10]) / projectionMatrix.elements[14];
// Calculate the scaled plane vector
clipPlane.multiplyScalar(2.0 / clipPlane.dot(q));
// Replacing the third row of the projection matrix
projectionMatrix.elements[2] = clipPlane.x;
projectionMatrix.elements[6] = clipPlane.y;
projectionMatrix.elements[10] = clipPlane.z + 1.0 - clipBias;
projectionMatrix.elements[14] = clipPlane.w;
eye.setFromMatrixPosition(camera.matrixWorld);
// Render
if (renderer.outputEncoding !== THREE.LinearEncoding) {
console.warn('THREE.Water: WebGLRenderer must use LinearEncoding as outputEncoding.');
groundMirror.onBeforeRender = function () { };
return;
}
if (renderer.toneMapping !== THREE.NoToneMapping) {
console.warn('THREE.Water: WebGLRenderer must use NoToneMapping as toneMapping.');
groundMirror.onBeforeRender = function () { };
return;
}
var currentRenderTarget = renderer.getRenderTarget();
var currentXrEnabled = renderer.xr.enabled;
var currentShadowAutoUpdate = renderer.shadowMap.autoUpdate;
groundMirror.visible = false;
renderer.xr.enabled = false; // Avoid camera modification and recursion
renderer.shadowMap.autoUpdate = false; // Avoid re-computing shadows
renderer.setRenderTarget(renderTarget);
renderer.state.buffers.depth.setMask(true); // make sure the depth buffer is writable so it can be properly cleared, see #18897
if (renderer.autoClear === false) renderer.clear();
renderer.render(scene, mirrorCamera);
// new BlurredEnvMapGenerator( renderer ).generate( img , 0 )
groundMirror.visible = true;
renderer.xr.enabled = currentXrEnabled;
renderer.shadowMap.autoUpdate = currentShadowAutoUpdate;
renderer.setRenderTarget(currentRenderTarget);
// Restore viewport
var viewport = camera.viewport;
if (viewport !== undefined) {
renderer.state.viewport(viewport);
}
**//update cubeCamera.update() here and cause stackOverflow.**
};

Related

Effect Composer in three.js throwing error and not rendering

I made two effect composer one for the main screen and one for mini-map view of the whole scene in minified form in top of screen.
I did this:
function postProcessing(renderer) {
composerScreen = new EffectComposer(renderer)
// RenderPass is normally placed at the beginning of the chain in order to provide the rendered scene as an input for the next post-processing step.
const renderPass = new RenderPass(scene, camera)
// When using post-processing with WebGL, you have to use FXAA for antialiasing
// Passing { antialias: true } to true when creating WebGLRenderer activates MSAA but only if you render to the default framebuffer
// (directly to screen).
const pixelRatio = 1
let fxaaPass = new ShaderPass(FXAAShader)
fxaaPass.material.uniforms['resolution'].value.x = 1 / (window.innerWidth * pixelRatio)
fxaaPass.material.uniforms['resolution'].value.y = 1 / (window.innerHeight * pixelRatio)
// const copyPass = new ShaderPass(CopyShader)
// meaning of renderToScreen. I have to set to true only for the 'last' effect
fxaaPass.renderToScreen = true
console.log(composerScreen)
composerScreen.addPass(renderPass)
composerScreen.addPass(fxaaPass)
// when screen is resized update fxaa resolution uniform as well
// The width and height of the THREE.EffectComposer.renderTarget must match that of the WebGLRenderer.
// #TODO: WHAT? Why? https://stackoverflow.com/questions/16167897/three-js-how-to-add-copyshader-after-fxaashader-r58
composerMap = new EffectComposer(renderer, renderTarget)
composerMap.setSize(512, 512)
let renderPassMap = new RenderPass(scene, mapCamera)
composerMap.addPass(renderPassMap)
var effectFXAA_Map = new ShaderPass(FXAAShader)
effectFXAA_Map.uniforms['resolution'].value.set(1 / 512, 1 / 512)
composerMap.addPass(effectFXAA_Map)
const copyPass = new ShaderPass(CopyShader)
copyPass.renderToScreen = true
composerMap.addPass(copyPass)
}
and I call
postProcessing(renderer)
and renderer variable passed in that function is:
const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.autoClear = false
and mapCamera is
function initMapCamera() {
mapCamera = new THREE.OrthographicCamera(left, right, top, bottom, 0.1, 1000)
// for camera to see down up should be on z axis
mapCamera.up = new THREE.Vector3(0, 0, -1)
mapCamera.lookAt(0, 0, 0)
mapCamera.position.y = 512
scene.add(mapCamera)
}
my render code is:
function animate(now: number) {
requestAnimationFrame(animate)
const delta = now - lastTime
lastTime = now
TWEEN.update()
KeyBoardHandler.keyUpdate(handlers, keys, delta)
render()
}
function render() {
renderer.setViewport(0, 0, window.innerWidth, window.innerHeight)
console.log('composer screen', composerScreen)
composerScreen.render()
renderer.clear(false, true, false)
renderer.setViewport(20, window.innerHeight - 512, 256, 256)
composerMap.render()
// controls.update(clock.getDelta())
// labelRenderer.render(scene, camera)
// renderer.render(scene, camera)
}
I tried to add all relevant code here and this is the code repo:
https://github.com/pravinpoudel/building-annotation/blob/2bc49e956cd9a4af590aae3b66d4d64d2b9093b4/src/client/client.ts#L150
I have two branches, one is main and another is FPC which have the code with post-processing
It is not able to render anything and it is showing my error:
first one is
console.log(composerScreen)
and when i click on the error in console log
and it goes to composerScreen.render() inside render function

Three.js: Rotate object with lookAt() while located at the current lookAt() position

I'm trying to implement a simple turn-around-and-move feature with Three.js. On mouse click, the object is supposed to first turn around and then move to the clicked location.
Codepen
The rotation is achieved with raycasting and lookAt(). It works by itself and it always works on the first click. If you remove the translation, it works continuously. The issue occurs when rotation and translation are implemented together. If you click a second time, after the object has moved to the previous clicked location, it doesn't rotate as expected. Depending on the mouse location it can flip to the other side without rotating at all.
Clarification: When you click the first time, notice how the object slowly and steadily turns around to face that direction? But the second time, after the object has moved, the rotation is quicker and/or flimsier or it simply flips over and there is no rotation at all. It depends on where you click in relation to the object.
I believe the issue stems from trying to implement lookAt while being located at the current lookAt location? If I stop the translation half way, the next rotation will work better. But of course I need it to go all the way.
I'm somewhat lost on how to proceed with this issue. Any help would be appreciated.
/*** Setup scene ***/
let width = 800
let height = 600
let scene
let renderer
let worldAxis
let box
let angle
let boxAxes
scene = new THREE.Scene()
worldAxis = new THREE.AxesHelper(200);
scene.add(worldAxis);
// Setup renderer
renderer = new THREE.WebGLRenderer({alpha: true, antialias: true})
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(width, height)
document.body.appendChild(renderer.domElement)
// Setup camera
const camera = new THREE.OrthographicCamera(
width / - 2, // left
width / 2, // right
height / 2, // top
height / - 2, // bottom
0, // near
1000 ); // far
camera.position.set(0, 0, 500)
camera.updateProjectionMatrix()
// Setup box
let geometry = new THREE.BoxGeometry( 15, 15, 15 );
let material = new THREE.MeshBasicMaterial( { color: "grey" } );
box = new THREE.Mesh( geometry, material );
box.position.set(100, 150, 0)
box.lookAt(getPointOfIntersection(new THREE.Vector2(0, 0)))
addAngle()
boxAxes = new THREE.AxesHelper(50);
box.add(boxAxes)
scene.add(box)
renderer.render(scene, camera);
/*** Setup animation ***/
let animate = false
let currentlyObservedPoint = new THREE.Vector2();
let rotationIncrement = {}
let translationIncrement = {}
let frameCount = 0
document.addEventListener('click', (event) => {
let mousePosForRotate = getMousePos(event.clientX, event.clientY)
rotationIncrement.x = (mousePosForRotate.x - currentlyObservedPoint.x)/100
rotationIncrement.y = (mousePosForRotate.y - currentlyObservedPoint.y)/100
let mousePosForTranslate = getMousePosForTranslate(event)
translationIncrement.x = (mousePosForTranslate.x - box.position.x)/100
translationIncrement.y = (mousePosForTranslate.y - box.position.y)/100
animate = true
})
function animationLoop() {
if (animate === true) {
if (frameCount < 100) {
rotate()
} else if (frameCount < 200) {
translate()
} else {
animate = false
frameCount = 0
}
frameCount++
renderer.render(scene, camera)
}
requestAnimationFrame(animationLoop)
}
function rotate() {
currentlyObservedPoint.x += rotationIncrement.x
currentlyObservedPoint.y += rotationIncrement.y
let pointOfIntersection = getPointOfIntersection(currentlyObservedPoint)
box.lookAt(pointOfIntersection)
addAngle()
}
function translate() {
box.position.x += translationIncrement.x
box.position.y += translationIncrement.y
}
function getMousePos(x, y) {
let mousePos = new THREE.Vector3(
(x / width) * 2 - 1,
- (y / height) * 2 + 1,
0)
return mousePos
}
function getMousePosForTranslate(event) {
let rect = event.target.getBoundingClientRect();
let mousePos = { x: event.clientX - rect.top, y: event.clientY - rect.left }
let vec = getMousePos(mousePos.x, mousePos.y)
vec.unproject(camera);
vec.sub(camera.position).normalize();
let distance = - camera.position.z / vec.z;
let pos = new THREE.Vector3(0, 0, 0);
pos.copy(camera.position).add(vec.multiplyScalar(distance));
return pos
}
function getPointOfIntersection(mousePos) {
let plane = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0);
let pointOfIntersection = new THREE.Vector3()
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mousePos, camera)
raycaster.ray.intersectPlane(plane, pointOfIntersection)
return pointOfIntersection
}
function addAngle() {
let angle = box.rotation.x - 32
box.rotation.x = angle
}
animationLoop()
<script src='https://cdnjs.cloudflare.com/ajax/libs/three.js/105/three.min.js'></script>

partial texture update in three.js

I upload a large texture to my fragment shader as a map in three.js.
This texture is a 2D canvas on which I draw random shapes.
While drawing, I just want to update the part of the texture that has changed. Updating the whole texture (up to 3000x3000 pixel) is just too slow.
However, I can not get to perform a partial update. texSubImage2D doesn't have any effect in my scene.
No errors in the console - I expect that I am missing a step but can not figure it out yet.
//
// create drawing canvas 2D and related textures
//
this._canvas = document.createElement('canvas');
this._canvas.width = maxSliceDimensionsX;
this._canvas.height = maxSliceDimensionsY;
this._canvasContext = this._canvas.getContext('2d')!;
this._canvasContext.fillStyle = 'rgba(0, 0, 0, 0)';
this._canvasContext.fillRect(0, 0, window.innerWidth, window.innerHeight);
this._texture = new Texture(this._canvas);
this._texture.magFilter = NearestFilter;
this._texture.minFilter = NearestFilter;
// const data = new Uint8Array(maxSliceDimensionsX * maxSliceDimensionsY * 4);
// this._texture2 = new THREE.DataTexture(data, maxSliceDimensionsX, maxSliceDimensionsY, THREE.RGBAFormat);
this._brushMaterial = new MeshBasicMaterial({
map: this._texture,
side: DoubleSide,
transparent: true,
});
...
// in the render loop
renderLoop() {
...
// grab random values as a test
const imageData = this._canvasContext.getImageData(100, 100, 200, 200);
const uint8Array = new Uint8Array(imageData.data.buffer);
// activate texture?
renderer.setTexture2D(this._texture, 0 );
// update subtexture
const context = renderer.getContext();
context.texSubImage2D( context.TEXTURE_2D, 0, 0, 0, 200, 200, context.RGBA, context.UNSIGNED_BYTE, uint8Array);
// updating the whole texture works as expected but is slow
// this._texture.needsUpdate = true;
// Render new scene
renderer.render(scene, this._camera);
}
Thanks,
Nicolas
Right after creating the texture, we must notify three that the texture needs to be uploaded to the GPU.
We just have to set it 1 time at creation time and not in the render loop.
this._texture = new Texture(this._canvas);
this._texture.magFilter = NearestFilter;
this._texture.minFilter = NearestFilter;
// MUST BE ADDED
this._texture.needsUpdate = true;

ThreeJS - How do I scale down intersected object on "mouseout"

I have n+1 hexshapes in a honeycomb grid. The objects are stacked close together. With this code:
// Get intersected objects, a.k.a objects "hit" by mouse, a.k.a objects that are mouse-overed
const intersects = raycaster.intersectObjects(hexObjects);
// If there is one (or more) intersections
let scaleTween = null;
if (intersects.length > 0) {
// If mouse is not currently over an object
// Set cursor to pointer so that the user can see that the object is clickable
document.body.style.cursor = 'pointer';
// Get the last intersected object, it's most likely that object we are currently hovering
const is = intersects.length > 0 ? intersects.length - 1 : 0;
// Is the object hovered over for the first time?
if (INTERSECTED === null) {
// Save current hovered object
INTERSECTED = intersects[is].object;
// HIGHLIGHT
// Save current color
INTERSECTED.currentHex = INTERSECTED.material.color.getHex();
// Set highlight color
INTERSECTED.material.color.setHex(COLOR_HIGHLIGHT);
// SCALE UP
// Try to stop the current tween, if any, in progress, so we can proceed with the next, if any, tween
try {
scaleTween.stop();
} catch (e) {}
// Create tween, save it so we can try to stop it, if needed
scaleTween = scale_tween(
INTERSECTED,
INTERSECTED.scale.clone(),
{
x: 1.5,
y: 1.5
},
100
);
scaleTween.start();
// SET Z-INDEX
INTERSECTED.position.z = 10;
} else {
// If the mouse is over an object
// Do we have a previous hovered item?
if (INTERSECTED !== null) {
// Revert color
INTERSECTED.material.color.setHex(INTERSECTED.currentHex);
// SCALE DOWN
// Try to stop the current tween, if any, in progress, so we can proceed with the next, if any, tween
try {
scaleTween.stop();
} catch (e) {}
// Create tween, save it so we can try to stop it, if needed
scaleTween = scale_tween(
INTERSECTED,
INTERSECTED.scale.clone(),
{
x: 1,
y: 1
},
100
);
scaleTween.start();
// REVERT Z-INDEX
INTERSECTED.position.z = 1;
}
// Save current intersected object
INTERSECTED = intersects[is].object;
// HIGHLIGHT
// Save current color
INTERSECTED.currentHex = INTERSECTED.material.color.getHex();
// Set highlight color
INTERSECTED.material.color.setHex(COLOR_HIGHLIGHT);
// SCALE UP
// Try to stop the current tween, if any, in progress, so we can proceed with the next, if any, tween
try {
scaleTween.stop();
} catch (e) {}
// Create tween, save it so we can try to stop it, if needed
scaleTween = scale_tween(
INTERSECTED,
INTERSECTED.scale.clone(),
{
x: 1.5,
y: 1.5
},
100
);
scaleTween.start();
// SET Z-INDEX
INTERSECTED.position.z = 10;
}
} else {
// If there are no intersections
// Reset cursor
document.body.style.cursor = 'default';
// Restore previous intersection object (if it exists) to its original color
if (INTERSECTED !== null) {
// REVERT COLOR
INTERSECTED.material.color.setHex(INTERSECTED.currentHex);
// SCALE DOWN
// Try to stop the current tween, if any, in progress, so we can proceed with the next, if any, tween
try {
scaleTween.stop();
} catch (e) {}
// Create tween, save it so we can try to stop it, if needed
scaleTween = scale_tween(
INTERSECTED,
INTERSECTED.scale.clone(),
{
x: 1,
y: 1
},
100
);
scaleTween.start();
// REVERT "Z-INDEX"
INTERSECTED.position.z = 1;
}
// Remove previous intersection object reference by setting current intersection object to "nothing"
INTERSECTED = null;
}
I've managed to highlight the object and scale it up with a tween quite nicely, but when I move the mouse out of the object onto the next object (the scaled object is scaled over the next object a bit), the highlight is gone, but the scale persists. How do I manage to scale the object down? And preferably with a tween?
A pen for this code can be found here: https://codepen.io/phun-ky/pen/erBZZy, the relevant part is at about line 1284 or search for INTERSECTED.
I wrote my own one. It's hell imperfect, but, at least, it scales up and down the hexagons:
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 1000);
camera.position.set(0, 0, 10);
var renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0x101010);
document.body.appendChild(renderer.domElement);
var hexes = [];
var colCount = 5;
var rowCount = 4;
var hexDiameter = 3;
var xStart = -(colCount) * hexDiameter * 0.5;
var rowSpace = Math.sqrt(3) * hexDiameter * 0.5;
var yStart = (rowCount - 1) * rowSpace * 0.5;
var hexGeom = new THREE.CylinderGeometry(hexDiameter * 0.5, hexDiameter * 0.5, 0.0625, 6, 1);
hexGeom.rotateX(Math.PI * 0.5);
for (let j = 0; j < rowCount; j++) {
for (let i = 0; i < colCount + (j % 2 === 0 ? 0 : 1); i++) {
let hex = new THREE.Mesh(hexGeom, new THREE.MeshBasicMaterial({
color: Math.random() * 0x7e7e7e + 0x7e7e7e,
wireframe: false
}));
hex.position.set(xStart + i * hexDiameter + (j % 2 === 0 ? 0.5 * hexDiameter : 0), yStart - j * rowSpace, 0);
hex.userData.scaleUp = function(h) {
if (h.userData.scaleDownTween) h.userData.scaleDownTween.stop();
let initScale = h.scale.clone();
let finalScale = new THREE.Vector3().setScalar(2);
h.userData.scaleUpTween = new TWEEN.Tween(initScale).to(finalScale, 500).onUpdate(function(obj) {
h.scale.copy(obj)
}).start();
}
hex.userData.scaleDown = function(h) {
if (h.userData.scaleUpTween) h.userData.scaleUpTween.stop();
let initScale = h.scale.clone();
let finalScale = new THREE.Vector3().setScalar(1);
h.userData.scaleUpTween = new TWEEN.Tween(initScale).to(finalScale, 500).onUpdate(function(obj) {
h.scale.copy(obj)
}).start();
}
scene.add(hex);
hexes.push(hex);
}
}
window.addEventListener("mousemove", onMouseMove, false);
var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();
var intersects = [];
var intersected;
function onMouseMove(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
intersects = raycaster.intersectObjects(hexes);
if (intersects.length > 0) {
if (intersected != intersects[0].object) {
if (intersected) intersected.userData.scaleDown(intersected);
intersected = intersects[0].object;
intersected.userData.scaleUp(intersected);
}
} else {
if (intersected) intersected.userData.scaleDown(intersected);
intersected = null;
}
}
render();
function render() {
requestAnimationFrame(render);
TWEEN.update();
renderer.render(scene, camera);
}
body {
overflow: hidden;
margin: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/92/three.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tween.js/17.2.0/Tween.min.js"></script>

Three.js - uvsNeedUpdate = True not triggering update

I'm working on a Three.js scene that loads 5 meshes, each with a single material that contains a single image texture. Once those images load, I'm attempting to accomplish the following tasks:
load 20 higher resolution images
update the material property of each mesh to load an array of 5 materials (each with 1 image texture) into each mesh
update the faceVertexUvs of each mesh's geometry to point to the appropriate offsets within the new textures.
The documentation on Geometry has little to say about Geometry.faceVertexUvs, but this SO post suggests one can use the following approach to work with multiple materials in a geometry when using faceVertexUvs:
geometry.faceVertexUvs[ materialIndex ][ faceIndex ][ vertexIndex ]
The trouble is that after running through the steps above, I call meshes[meshIdx].geometry.uvsNeedUpdate = true; but my new materials are not appearing and my meshes stay unchanged. Does anyone have any idea why that might be?
I would be tremendously grateful for any pointers or insight others can offer on this question!
Here is my full code sample:
/**
* Globals
**/
// Create a store for all images contained in the visualization
var imageList = null;
// Create a store for the image atlas files. Each key will represent
// the index position of the atlas file, and the value will point
// to the material at that index position
var materials = {32: {}, 64: {}};
// Create global configs for image and atlas sizing
var image, atlas;
// Create a store of meshes
var meshes = [];
/**
* Create Scene
**/
// Create the scene and a camera to view it
var scene = new THREE.Scene();
/**
* Camera
**/
// Specify the portion of the scene visiable at any time (in degrees)
var fieldOfView = 75;
// Specify the camera's aspect ratio
var aspectRatio = window.innerWidth / window.innerHeight;
/*
Specify the near and far clipping planes. Only objects
between those planes will be rendered in the scene
(these values help control the number of items rendered
at any given time)
*/
var nearPlane = 100;
var farPlane = 50000;
// Use the values specified above to create a camera
var camera = new THREE.PerspectiveCamera(
fieldOfView, aspectRatio, nearPlane, farPlane
);
// Finally, set the camera's position
camera.position.z = 12000;
camera.position.y = -2000;
/**
* Renderer
**/
// Create the canvas with a renderer
var renderer = new THREE.WebGLRenderer({ antialias: true });
// Add support for retina displays
renderer.setPixelRatio( window.devicePixelRatio );
// Specify the size of the canvas
renderer.setSize( window.innerWidth, window.innerHeight );
// Add the canvas to the DOM
document.body.appendChild( renderer.domElement );
/**
* Load External Data
**/
// Identify data endpoint
var dataUrl = 'https://s3.amazonaws.com/duhaime/blog/tsne-webgl/data/';
// Create a store for image position information
var imagePositions = null;
// Load the image position JSON file
var fileLoader = new THREE.FileLoader();
fileLoader.load(dataUrl + 'image_tsne_projections.json', function(data) {
imagePositions = JSON.parse(data);
conditionallyBuildGeometries(32)
})
/**
* Load Atlas Textures
**/
// List of all textures to be loaded, the size of subimages
// in each, and the total number of atlas files for each size
var textureSets = {
32: { size: 32, count: 5 },
64: { size: 64, count: 20 }
}
// Create a texture loader so we can load our image files
var textureLoader = new THREE.TextureLoader();
function loadTextures(size) {
for (var i=0; i<textureSets[size].count; i++) {
var url = dataUrl + 'atlas_files/' + size + 'px/atlas-' + i + '.jpg';
textureLoader.load(url, handleTexture.bind(null, size, i));
}
}
// Callback function that adds the texture to the list of textures
// and calls the geometry builder if all textures have loaded
function handleTexture(size, idx, texture) {
var material = new THREE.MeshBasicMaterial({ map: texture });
materials[size][idx] = material;
conditionallyBuildGeometries(size, idx)
}
// If the textures and the mapping from image idx to positional information
// are all loaded, create the geometries
function conditionallyBuildGeometries(size, idx) {
if (size === 32) {
var nLoaded = Object.keys(materials[size]).length;
var nRequired = textureSets[size].count;
if (nLoaded === nRequired && imagePositions) {
document.querySelector('#loading').style.display = 'none';
buildGeometry(size);
loadTextures(64)
}
} else {
updateGeometry(size, idx)
}
}
loadTextures(32)
/**
* Build Image Geometry
**/
// Iterate over the textures in the current texture set
// and for each, add a new mesh to the scene
function buildGeometry(size) {
setImageAndAtlasSize(size);
for (var i=0; i<textureSets[size].count; i++) {
// Create one new geometry per atlas
var geometry = new THREE.Geometry();
for (var j=0; j<atlas.cols*atlas.rows; j++) {
geometry = updateVertices(geometry, i, j);
geometry = updateFaces(geometry);
geometry = updateFaceVertexUvs(geometry, j, 0);
}
buildMesh(geometry, materials[size][i]);
}
}
function setImageAndAtlasSize(size) {
// Identify the subimage size in px (width/height) and the
// size of the image as it will be displayed in the map
image = { width: size, height: size, shownWidth: 64, shownHeight: 64 };
// Identify the total number of cols & rows in the image atlas
atlas = { width: 2048, height: 2048, cols: 2048/size, rows: 2048/size };
}
// Get the x, y, z coords for the subimage at index position j
// of atlas in index position i
function getCoords(i, j) {
var idx = (i * atlas.rows * atlas.cols) + j;
var coords = imagePositions[idx];
coords.x *= 2200;
coords.y *= 1200;
coords.z = (-200 + j/100);
return coords;
}
// Add one vertex for each corner of the image, using the
// following order: lower left, lower right, upper right, upper left
function updateVertices(geometry, i, j) {
// Retrieve the x, y, z coords for this subimage
var coords = getCoords(i, j);
geometry.vertices.push(
new THREE.Vector3(
coords.x,
coords.y,
coords.z
),
new THREE.Vector3(
coords.x + image.shownWidth,
coords.y,
coords.z
),
new THREE.Vector3(
coords.x + image.shownWidth,
coords.y + image.shownHeight,
coords.z
),
new THREE.Vector3(
coords.x,
coords.y + image.shownHeight,
coords.z
)
);
return geometry;
}
// Create two new faces for a given subimage, then add those
// faces to the geometry
function updateFaces(geometry) {
// Add the first face (the lower-right triangle)
var faceOne = new THREE.Face3(
geometry.vertices.length-4,
geometry.vertices.length-3,
geometry.vertices.length-2
)
// Add the second face (the upper-left triangle)
var faceTwo = new THREE.Face3(
geometry.vertices.length-4,
geometry.vertices.length-2,
geometry.vertices.length-1
)
// Add those faces to the geometry
geometry.faces.push(faceOne, faceTwo);
return geometry;
}
function updateFaceVertexUvs(geometry, j, materialIdx) {
// Identify the relative width and height of the subimages
// within the image atlas
var relativeW = image.width / atlas.width;
var relativeH = image.height / atlas.height;
// Identify this subimage's offset in the x dimension
// An xOffset of 0 means the subimage starts flush with
// the left-hand edge of the atlas
var xOffset = (j % atlas.cols) * relativeW;
// Identify this subimage's offset in the y dimension
// A yOffset of 0 means the subimage starts flush with
// the bottom edge of the atlas
var yOffset = 1 - (Math.floor(j/atlas.cols) * relativeH) - relativeH;
// Create an empty list of faceVertexUvs for the given material Idx
// if it doesn't exist yet
if (!geometry.faceVertexUvs[materialIdx]) {
geometry.faceVertexUvs[materialIdx] = [];
}
// Use the xOffset and yOffset (and the knowledge that
// each row and column contains only 32 images) to specify
// the regions of the current image
geometry.faceVertexUvs[materialIdx][j*2] = [
new THREE.Vector2(xOffset, yOffset),
new THREE.Vector2(xOffset + relativeW, yOffset),
new THREE.Vector2(xOffset + relativeW, yOffset + relativeH)
];
// Map the region of the image described by the lower-left,
// upper-right, and upper-left vertices to `faceTwo`
geometry.faceVertexUvs[materialIdx][(j*2) + 1] = [
new THREE.Vector2(xOffset, yOffset),
new THREE.Vector2(xOffset + relativeW, yOffset + relativeH),
new THREE.Vector2(xOffset, yOffset + relativeH)
];
return geometry;
}
function buildMesh(geometry, material) {
// Convert the geometry to a BuferGeometry for additional performance
//var geometry = new THREE.BufferGeometry().fromGeometry(geometry);
// Combine the image geometry and material into a mesh
var mesh = new THREE.Mesh(geometry, [material]);
// Set the position of the image mesh in the x,y,z dimensions
mesh.position.set(0,0,0)
// Add the image to the scene
scene.add(mesh);
// Save this mesh
meshes.push(mesh);
}
/**
* Update Geometries with new VertexUvs and materials
**/
function updateGeometry(size, idx) {
// Update the image and atlas sizes
setImageAndAtlasSize(size)
// Determine how many of the higher resolution atlas files
// it takes to account for all subimages in a lower resolution
// atlas file
var lowResPerAtlas = (2048/32)**2;
var highResPerAtlas = (2048/64)**2;
var atlasRatio = lowResPerAtlas / highResPerAtlas;
// Determine which of the original meshes the newly-loaded high-res
// atlas corresponds to
var meshIdx = Math.floor(idx/atlasRatio);
// Determine the material index position to use in this mesh.
// The mesh's materials array will look like this:
// mesh.material = [32px, 64px_0, 64px_1, 64px_2, 64px_3, 64_px_4];
var materialIdx = (idx % atlasRatio) + 1;
// Add the newly loaded material into the appropriate mesh
meshes[meshIdx].material[materialIdx] = materials[size][idx];
//console.log(meshIdx, materialIdx, idx, meshes[materialIdx].material)
// Pluck out the geometry of this mesh update:
var geometry = meshes[meshIdx].geometry;
for (var j=0; j<highResPerAtlas; j++) {
geometry = updateFaceVertexUvs(geometry, j, materialIdx);
}
geometry.faceVertexUvs[0] = [];
meshes[meshIdx].geometry = geometry;
meshes[meshIdx].geometry.colorsNeedUpdate = true;
meshes[meshIdx].geometry.groupsNeedUpdate = true;
meshes[meshIdx].geometry.lineDistancesNeedUpdate = true;
meshes[meshIdx].geometry.normalsNeedUpdate = true;
meshes[meshIdx].geometry.uvsNeedUpdate = true;
meshes[meshIdx].geometry.verticesNeedUpdate = true;
// Indicate the material needs update
meshes[meshIdx].material.needsUpdate = true;
}
/**
* Lights
**/
// Add a point light with #fff color, .7 intensity, and 0 distance
var light = new THREE.PointLight( 0xffffff, 1, 0 );
// Specify the light's position
light.position.set(1, 1, 100);
// Add the light to the scene
scene.add(light)
/**
* Add Controls
**/
var controls = new THREE.TrackballControls(camera, renderer.domElement);
/**
* Handle window resizes
**/
window.addEventListener('resize', function() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
controls.handleResize();
});
/**
* Render!
**/
// The main animation function that re-renders the scene each animation frame
function animate() {
requestAnimationFrame( animate );
renderer.render( scene, camera );
controls.update();
}
animate();
* {
margin: 0;
padding: 0;
background: #000;
color: #fff;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/88/three.js"></script>
<script src="https://s3-us-west-2.amazonaws.com/yale-dh-staging/tsne/assets/vendor/js/trackball-controls.js"></script>
<div id='loading'>Loading</div>
This is a known bug: https://github.com/mrdoob/three.js/issues/7179
Instead of changing a faceVertexUv by index accessors, one must use a .set() method on the appropriate vector within the faceVertexUv, e.g.:
if (geometry.faceVertexUvs[0][faceIdx]) {
geometry.faceVertexUvs[0][faceIdx][0].set(xOffset, yOffset)
geometry.faceVertexUvs[0][faceIdx][1].set(xOffset + width, yOffset)
geometry.faceVertexUvs[0][faceIdx][2].set(xOffset + width, yOffset + height)
} else {
geometry.faceVertexUvs[0][faceIdx] = [
new THREE.Vector2(xOffset, yOffset),
new THREE.Vector2(xOffset + width, yOffset),
new THREE.Vector2(xOffset + width, yOffset + height)
]
}
Full example:
/**
* Globals
**/
// Create a store for all images contained in the visualization
var imageList = null;
// Create a store for the image atlas files. Each key will represent
// the index position of the atlas file, and the value will point
// to the material at that index position
var materials = {32: {}, 64: {}};
// Create global configs for image and atlas sizing
var image, atlas;
// Create a store of meshes
var meshes = [];
/**
* Create Scene
**/
// Create the scene and a camera to view it
var scene = new THREE.Scene();
/**
* Camera
**/
// Specify the portion of the scene visiable at any time (in degrees)
var fieldOfView = 75;
// Specify the camera's aspect ratio
var aspectRatio = window.innerWidth / window.innerHeight;
/*
Specify the near and far clipping planes. Only objects
between those planes will be rendered in the scene
(these values help control the number of items rendered
at any given time)
*/
var nearPlane = 100;
var farPlane = 50000;
// Use the values specified above to create a camera
var camera = new THREE.PerspectiveCamera(
fieldOfView, aspectRatio, nearPlane, farPlane
);
// Finally, set the camera's position
camera.position.z = 12000;
camera.position.y = -2000;
/**
* Lights
**/
// Add a point light with #fff color, .7 intensity, and 0 distance
var light = new THREE.PointLight( 0xffffff, 1, 0 );
// Specify the light's position
light.position.set(1, 1, 100);
// Add the light to the scene
scene.add(light)
/**
* Renderer
**/
// Create the canvas with a renderer
var renderer = new THREE.WebGLRenderer({ antialias: true });
// Add support for retina displays
renderer.setPixelRatio( window.devicePixelRatio );
// Specify the size of the canvas
renderer.setSize( window.innerWidth, window.innerHeight );
// Add the canvas to the DOM
document.body.appendChild( renderer.domElement );
/**
* Load External Data
**/
// Identify data endpoint
var dataUrl = 'https://s3.amazonaws.com/duhaime/blog/tsne-webgl/data/';
// Create a store for image position information
var imagePositions = null;
// Load the image position JSON file
var fileLoader = new THREE.FileLoader();
fileLoader.load(dataUrl + 'image_tsne_projections.json', function(data) {
imagePositions = JSON.parse(data);
conditionallyBuildGeometries(32)
})
/**
* Load Atlas Textures
**/
// List of all textures to be loaded, the size of subimages
// in each, and the total count of atlas files for each size
var textureSets = {
32: { size: 32, count: 5 },
64: { size: 64, count: 20 }
}
// Create a texture loader so we can load our image files
var textureLoader = new THREE.TextureLoader();
function loadTextures(size) {
for (var i=0; i<textureSets[size].count; i++) {
var url = dataUrl + 'atlas_files/' + size + 'px/atlas-' + i + '.jpg';
textureLoader.load(url, handleTexture.bind(null, size, i));
}
}
// Create a material from the new texture and call
// the geometry builder if all textures have loaded
function handleTexture(size, idx, texture) {
var material = new THREE.MeshBasicMaterial({ map: texture });
materials[size][idx] = material;
conditionallyBuildGeometries(size, idx)
}
// If the textures and the mapping from image idx to positional information
// are all loaded, create the geometries
function conditionallyBuildGeometries(size, idx) {
if (size === 32) {
var nLoaded = Object.keys(materials[size]).length;
var nRequired = textureSets[size].count;
if (nLoaded === nRequired && imagePositions) {
document.querySelector('#loading').style.display = 'none';
buildGeometry(size);
loadTextures(64)
}
} else {
updateGeometry(size, idx)
}
}
loadTextures(32)
/**
* Build Image Geometry
**/
// Iterate over the textures in the current texture set
// and for each, add a new mesh to the scene
function buildGeometry(size) {
setImageAndAtlasSize(size);
for (var i=0; i<textureSets[size].count; i++) {
// Create one new geometry per set of 1024 images
var geometry = new THREE.Geometry();
geometry.faceVertexUvs[0] = [];
for (var j=0; j<atlas.cols*atlas.rows; j++) {
geometry = updateVertices(geometry, i, j);
geometry = updateFaces(geometry);
geometry = updateFaceVertexUvs(geometry, j);
if ((j+1)%1024 === 0) {
buildMesh(geometry, materials[size][i]);
var geometry = new THREE.Geometry();
}
}
}
}
function setImageAndAtlasSize(size) {
// Identify the subimage size in px (width/height) and the
// size of the image as it will be displayed in the map
image = { width: size, height: size, shownWidth: 64, shownHeight: 64 };
// Identify the total number of cols & rows in the image atlas
atlas = { width: 2048, height: 2048, cols: 2048/size, rows: 2048/size };
}
// Get the x, y, z coords for the subimage at index position j
// of atlas in index position i
function getCoords(i, j) {
var idx = (i * atlas.rows * atlas.cols) + j;
var coords = imagePositions[idx];
coords.x *= 2200;
coords.y *= 1200;
coords.z = (-200 + j/100);
return coords;
}
// Add one vertex for each corner of the image, using the
// following order: lower left, lower right, upper right, upper left
function updateVertices(geometry, i, j) {
// Retrieve the x, y, z coords for this subimage
var coords = getCoords(i, j);
geometry.vertices.push(
new THREE.Vector3(
coords.x,
coords.y,
coords.z
),
new THREE.Vector3(
coords.x + image.shownWidth,
coords.y,
coords.z
),
new THREE.Vector3(
coords.x + image.shownWidth,
coords.y + image.shownHeight,
coords.z
),
new THREE.Vector3(
coords.x,
coords.y + image.shownHeight,
coords.z
)
);
return geometry;
}
// Create two new faces for a given subimage, then add those
// faces to the geometry
function updateFaces(geometry) {
// Add the first face (the lower-right triangle)
var faceOne = new THREE.Face3(
geometry.vertices.length-4,
geometry.vertices.length-3,
geometry.vertices.length-2
)
// Add the second face (the upper-left triangle)
var faceTwo = new THREE.Face3(
geometry.vertices.length-4,
geometry.vertices.length-2,
geometry.vertices.length-1
)
// Add those faces to the geometry
geometry.faces.push(faceOne, faceTwo);
return geometry;
}
function updateFaceVertexUvs(geometry, j) {
// Identify the relative width and height of the subimages
// within the image atlas
var relativeW = image.width / atlas.width;
var relativeH = image.height / atlas.height;
// Identify this subimage's offset in the x dimension
// An xOffset of 0 means the subimage starts flush with
// the left-hand edge of the atlas
var xOffset = (j % atlas.cols) * relativeW;
// Identify this subimage's offset in the y dimension
// A yOffset of 0 means the subimage starts flush with
// the bottom edge of the atlas
var yOffset = 1 - (Math.floor(j/atlas.cols) * relativeH) - relativeH;
// Determine the faceVertexUvs index position
var faceIdx = 2 * (j%1024);
// Use the xOffset and yOffset (and the knowledge that
// each row and column contains only 32 images) to specify
// the regions of the current image. Use .set() if the given
// faceVertex is already defined, due to a bug in updateVertexUvs:
// https://github.com/mrdoob/three.js/issues/7179
if (geometry.faceVertexUvs[0][faceIdx]) {
geometry.faceVertexUvs[0][faceIdx][0].set(xOffset, yOffset)
geometry.faceVertexUvs[0][faceIdx][1].set(xOffset + relativeW, yOffset)
geometry.faceVertexUvs[0][faceIdx][2].set(xOffset + relativeW, yOffset + relativeH)
} else {
geometry.faceVertexUvs[0][faceIdx] = [
new THREE.Vector2(xOffset, yOffset),
new THREE.Vector2(xOffset + relativeW, yOffset),
new THREE.Vector2(xOffset + relativeW, yOffset + relativeH)
]
}
// Map the region of the image described by the lower-left,
// upper-right, and upper-left vertices to `faceTwo`
if (geometry.faceVertexUvs[0][faceIdx+1]) {
geometry.faceVertexUvs[0][faceIdx+1][0].set(xOffset, yOffset)
geometry.faceVertexUvs[0][faceIdx+1][1].set(xOffset + relativeW, yOffset + relativeH)
geometry.faceVertexUvs[0][faceIdx+1][2].set(xOffset, yOffset + relativeH)
} else {
geometry.faceVertexUvs[0][faceIdx+1] = [
new THREE.Vector2(xOffset, yOffset),
new THREE.Vector2(xOffset + relativeW, yOffset + relativeH),
new THREE.Vector2(xOffset, yOffset + relativeH)
]
}
return geometry;
}
function buildMesh(geometry, material) {
// Convert the geometry to a BuferGeometry for additional performance
//var geometry = new THREE.BufferGeometry().fromGeometry(geometry);
// Combine the image geometry and material into a mesh
var mesh = new THREE.Mesh(geometry, material);
// Set the position of the image mesh in the x,y,z dimensions
mesh.position.set(0,0,0)
// Add the image to the scene
scene.add(mesh);
// Save this mesh
meshes.push(mesh);
return mesh;
}
/**
* Update Geometries with new VertexUvs and materials
**/
function updateGeometry(size, idx) {
// Update the image and atlas sizes
setImageAndAtlasSize(size)
// Update the appropriate material
meshes[idx].material = materials[size][idx];
meshes[idx].material.needsUpdate = true;
// Update the facevertexuvs
for (var j=0; j<atlas.cols*atlas.rows; j++) {
meshes[idx].geometry = updateFaceVertexUvs(meshes[idx].geometry, j);
}
meshes[idx].geometry.uvsNeedUpdate = true;
meshes[idx].geometry.verticesNeedUpdate = true;
}
/**
* Add Controls
**/
var controls = new THREE.TrackballControls(camera, renderer.domElement);
/**
* Handle window resizes
**/
window.addEventListener('resize', function() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
controls.handleResize();
});
/**
* Render!
**/
// The main animation function that re-renders the scene each animation frame
function animate() {
requestAnimationFrame( animate );
renderer.render( scene, camera );
controls.update();
}
animate();
* {
margin: 0;
padding: 0;
background: #000;
color: #fff;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/88/three.js"></script>
<script src="https://s3-us-west-2.amazonaws.com/yale-dh-staging/tsne/assets/vendor/js/trackball-controls.js"></script>
<div id='loading'>Loading</div>

Resources