Upon loading my three.js scene, it appears distorted until I interact with the mouse on the website. The distortion can be seen in the image below:
https://i.sstatic.net/iAh8k.png
However, as soon as I move the mouse, the scene snaps back to normal. Surprisingly, the position of the cursor relative to the canvas where the scene is rendered doesn't seem to affect this issue at all. Here's how the scene looks after moving the mouse:
https://i.sstatic.net/z0q8U.png
The dependencies related to three.js that are being used include:
- "three": "^0.108.0"
- "three-orbitcontrols": "^2.102.2"
- "three.meshline": "^1.2.0"
I attempted updating "three" to the latest version (0.116.1), but unfortunately, that did not resolve the problem. This issue has been reproducible on Firefox and Edge, however, Chrome seems unaffected.
Some additional context: we utilize OffscreenCanvas for improved performance, sending mouse positions from the main thread to a web worker upon mousemove events. This information is then used to subtly adjust the camera and background positions (with offsets). Even with the mousemove handler logic temporarily removed from the web worker code, the issue persists, indicating an unrelated cause. Camera animations are made smooth using tween.js.
Highlighted snippets of code:
Scene setup:
const {scene, camera} = makeScene(elem, cameraPosX, 0, 60, 45);
const supportsWebp = (browser !== 'Safari');
imageLoader.load(backgroundImage, mapImage => {
const texture = new THREE.CanvasTexture(mapImage);
texture.anisotropy = renderer.capabilities.getMaxAnisotropy();
texture.minFilter = THREE.LinearFilter;
// Repeat background so we don't run out of it during offset changes on mousemove
texture.wrapS = THREE.MirroredRepeatWrapping;
texture.wrapT = THREE.MirroredRepeatWrapping;
scene.background = texture;
});
// Creating objects in the scene
let orbitingPlanet = getPlanet(0xffffff, true, 1 * mobilePlanetSizeAdjustment);
scene.add(orbitingPlanet);
// Ellipse class, which extends the virtual base class Curve
let curveMain = new THREE.EllipseCurve(
0, 0, // ax, aY
80, 30, // xRadius, yRadius
0, 2 * Math.PI, // aStartAngle, aEndAngle
false, // aClockwise
0.2 // aRotation
);
let ellipseMainGeometry = new THREE.Path(curveMain.getPoints(100)).createPointsGeometry(100);
let ellipseMainMaterial = new MeshLine.MeshLineMaterial({
color: new THREE.Color(0xffffff),
opacity: 0.2,
transparent: true,
});
let ellipseMain = new MeshLine.MeshLine();
ellipseMain.setGeometry(ellipseMainGeometry, function(p) {
return 0.2; // Line width
});
const ellipseMainMesh = new THREE.Mesh(ellipseMain.geometry, ellipseMainMaterial );
scene.add(ellipseMainMesh);
// Create a halfish curve on which one of the orbiting planets will move
let curveMainCut = new THREE.EllipseCurve(
0, 0, // ax, aY
80, 30, // xRadius, yRadius
0.5 * Math.PI, 1.15 * Math.PI, // aStartAngle, aEndAngle
false, // aClockwise
0.2 // aRotation
);
let lastTweenRendered = Date.now();
let startRotation = new THREE.Vector3(
camera.rotation.x,
camera.rotation.y,
camera.rotation.z);
let tweenie;
return (time, rect) => {
camera.aspect = state.width / state.height;
camera.updateProjectionMatrix();
let pt1 = curveMainCut.getPointAt(t_top_faster);
orbitingPlanet.position.set(pt1.x, pt1.y, 1);
t_top_faster = (t_top_faster >= 1) ? 0 : t_top_faster += 0.001;
// Slightly rotate the background on mouse move
if (scene && scene.background) {
// The rotation mush be between 0 and 0.01
scene.background.rotation =
Math.max(-0.001,Math.min(0.01, scene.background.rotation + 0.00005 * target.x));
let offsetX = scene.background.offset.x + 0.00015 * target.x;
let offsetY = scene.background.offset.y + 0.00015 * target.y;
scene.background.offset = new THREE.Vector2(
(offsetX > -0.05 && offsetX < 0.05) ? offsetX : scene.background.offset.x,
(offsetY > -0.05 && offsetY < 0.05) ? offsetY : scene.background.offset.y);
}
lastTweenRendered = tweenAnimateCamera(lastTweenRendered, tweenie, camera, startRotation, 200);
renderer.render(scene, camera);
};
makeScene function:
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(fieldOfView, state.width / state.height, 0.1, 100000000);
camera.position.set(camPosX, camPosY, camPosZ);
camera.lookAt(0, 0, 0);
scene.add(camera);
return {scene, camera};
Camera animation based on mouse positions:
function tweenAnimateCamera(lastTweenRendered, tween, camera, startRotation, period) {
target.x = (1 - mouse.x) * 0.002;
target.y = (1 - mouse.y) * 0.002;
let now = Date.now();
if ((
// Don't let the camera go too far
startRotation.x > -0.01 && startRotation.x < 0.01) &&
now - lastTweenRendered > (period / 2)) {
if (tween) {
tween.stop();
}
lastTweenRendered = now;
let endRotation = new THREE.Vector3(
camera.rotation.x + 0.005 * (target.y - camera.rotation.x),
camera.rotation.y + 0.005 * (target.x - camera.rotation.y),
camera.rotation.z);
tween = new TWEEN.Tween(startRotation)
.to(endRotation, period * 2)
.easing(TWEEN.Easing.Quadratic.InOut)
.onUpdate(function (v) {
camera.rotation.set(v.x, v.y, v.z);
})
.onComplete(function(v) {
startRotation = v.clone();
});
tween.start();
}
TWEEN.update();
return lastTweenRendered
}
Mouse position receiver logic:
if (e.data.type === 'mousePosUpdate') {
if (e.data.x !== -100000 && e.data.y !== -100000) {
mouse.x = ( e.data.x - state.width / 2 );
mouse.y = ( e.data.y - state.height / 2 );
target.x = ( 1 - mouse.x ) * 0.002;
target.y = ( 1 - mouse.y ) * 0.002;
}
}
Render loop:
function render(time) {
time *= 0.001;
for (const {elem, fn, ctx} of sceneElements) {
// get the viewport relative position of this element
canvasesUpdatedPos.forEach( canvasUpdate => {
if (canvasUpdate.id === elem.id) {
elem.rect = canvasUpdate.rect;
}
});
const rect = elem.rect;
const bottom = rect.bottom;
const height = rect.height;
const left = rect.left;
const right = rect.right;
const top = rect.top;
const width = rect.width;
const rendererCanvas = renderer.domElement;
const isOffscreen =
bottom < 0 ||
top > state.height ||
right < 0 ||
left > state.width;
if (!isOffscreen && width !== 0 && height !== 0) {
// make sure the renderer's canvas is big enough
let isResize = resizeRendererToDisplaySize(renderer, height, width);
// make sure the canvas for this area is the same size as the area
if (ctx.canvas.width !== width || ctx.canvas.height !== height) {
ctx.canvas.width = width;
ctx.canvas.height = height;
state.width = width;
state.height = height;
}
renderer.setScissor(0, 0, width, height);
renderer.setViewport(0, 0, width, height);
fn(time, rect);
// copy the rendered scene to this element's canvas
ctx.globalCompositeOperation = 'copy';
ctx.drawImage(
rendererCanvas,
0, rendererCanvas.height - height, width, height, // src rect
0, 0, width, height); // dst rect
}
}
// Limiting to 35 FPS.
setTimeout(function() {
if (!stopAnimating) {
requestAnimationFrame(render);
}
}, 1000 / 35);
}