I am preparing some didactic materials about optical illusions. For this I decide to play around three.js. I’ve done illustrations for structure-from motion effect. The idea of this effect is that our visual system is always working in situation of insufficient information and should add lacking details. Thus, when we have light, shadows, perspective and so on we clearly perceive unambiguous shape (demo in snippet below). On this example we may clearly see that sphere rotates to right.
/* global THREE, getDefaultParams */
'use strict';
let camera;
let renderer;
const currentParameters = getDefaultParams();
// once everything is loaded, we run our Three.js stuff.
function init() {
// create a scene, that will hold all our elements such as objects, cameras and lights.
const scene = new THREE.Scene();
// create a camera, which defines where we're looking at.
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
// create a render and set the size
renderer = new THREE.WebGLRenderer();
renderer.setClearColor(new THREE.Color(currentParameters.renderer.color));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = currentParameters.renderer.shadows;
// create the landscape
const landscapeGeometry = new THREE.PlaneGeometry(
currentParameters.landscape.width,
currentParameters.landscape.height,
);
const landscapeMaterial = new THREE.MeshLambertMaterial({ color: currentParameters.landscape.color });
const landscape = new THREE.Mesh(landscapeGeometry, landscapeMaterial);
landscape.rotation.x = currentParameters.landscape.rotation.x;
landscape.rotation.y = currentParameters.landscape.rotation.y;
landscape.rotation.z = currentParameters.landscape.rotation.z;
landscape.position.x = currentParameters.landscape.position.x;
landscape.position.y = currentParameters.landscape.position.y;
landscape.position.z = currentParameters.landscape.position.z;
landscape.receiveShadow = currentParameters.landscape.shadows;
scene.add(landscape);
// create a SFM shapes
let parentGeometry = new THREE.SphereGeometry(
currentParameters.parentShape.size,
currentParameters.parentShape.numberOfVertices,
currentParameters.parentShape.numberOfVertices,
);
let parentMaterial = new THREE.MeshLambertMaterial({
color: 0x7777ff,
opacity: 0.3,
transparent: true,
});
let parentShape = new THREE.Mesh(parentGeometry, parentMaterial); // new THREE.Mesh();
const childShapeGeometry = new THREE.SphereGeometry(
currentParameters.childShape.size,
currentParameters.childShape.numberOfVertices,
currentParameters.childShape.numberOfVertices,
);
const childShapeMaterial = new THREE.MeshLambertMaterial({
color: currentParameters.childShape.color,
});
const childShapeTemplate = new THREE.Mesh(childShapeGeometry, childShapeMaterial);
childShapeTemplate.castShadow = currentParameters.childShape.shadows;
for (let i = 0, l = currentParameters.parentShape.numberOfChilds; i < l; i++) {
const childShape = childShapeTemplate.clone();
// add random dot to parent shape
[childShape.position.x, childShape.position.y, childShape.position.z] =
generateRandomDotOnSphere(0, 0, 0, currentParameters.parentShape.size);
parentShape.add(childShape);
}
scene.add(parentShape);
// add fixation point
let fixationGeometry = new THREE.TorusKnotGeometry(3, 1);
let fixationMaterial = new THREE.MeshLambertMaterial({ color: 0xffff00 });
let fixationShape = new THREE.Mesh(fixationGeometry, fixationMaterial);
fixationShape.castShadow = true;
fixationShape.receiveShadow = true;
scene.add(fixationShape);
// position and point the camera to the center of the scene
camera.position.copy({ x: 0, y: 0, z: 100 });
camera.lookAt(parentShape.position);
// add spotlight for the shadows
const spotLight = new THREE.SpotLight(0xffffff);
spotLight.position.set(-100, 100, 50);
spotLight.castShadow = true;
scene.add(spotLight);
// Output
document.getElementById('WebGL-output').appendChild(renderer.domElement);
renderScene();
function renderScene() {
parentShape.rotation.y += 0.02;
requestAnimationFrame(renderScene);
renderer.render(scene, camera);
}
}
function onResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
window.onload = init;
// listen to the resize events
window.addEventListener('resize', onResize, false);
// *********************************************
// *********HELPERS*****************************
function generateRandomDotOnSphere(x0, y0, z0, radius) {
/*
source: https://stackoverflow.com/questions/5531827/random-point-on-a-given-sphere
Returns a random point of a sphere, evenly distributed over the sphere.
The sphere is centered at (x0,y0,z0) with the passed in radius.
The returned point is returned as a three element array [x,y,z].
*/
let u = Math.random();
let v = Math.random();
let theta = 2 * Math.PI * u;
let phi = Math.acos(2 * v - 1);
let x = x0 + (radius * Math.sin(phi) * Math.cos(theta));
let y = y0 + (radius * Math.sin(phi) * Math.sin(theta));
let z = z0 + (radius * Math.cos(phi));
return [x, y, z];
}
function getDefaultParams() {
// Define default parameters
const defaultParameters = { // define all backgroun
renderer: {
color: 0xFFFFFF,
shadows: true,
}, // renderer
landscape: {
color: 0xFFFFFF,
shadows: true,
width: 80,
height: 80,
rotation: {
x: -0.5 * Math.PI,
y: 0,
z: 0,
}, // rotation
position: {
x: 10,
y: -30,
z: -20,
}, // position
}, // landscape
parentShape: {
numberOfVertices: 10,
size: 20,
numberOfChilds: 50,
}, // parent shape
childShape: {
numberOfVertices: 5,
size: 1,
color: 0x7777ff,
shadows: true,
}, // child shape
};
return defaultParameters;
}
<!DOCTYPE html>
<html>
<head>
<title>Structure_from_motion</title>
<script src="https://threejs.org/build/three.min.js"></script>
</head>
<body>
<div id="description">
</div>
<!-- Div which will hold the Output -->
<div id="WebGL-output",style="heigh:100px;"></div>
<!-- Javascript code that runs our Three.js examples -->
<script src="demoSFM_simple.js"></script>
</body>
</html>
If I will remove shadows, perspective (OrthographicCamera), make ambient light sphere will change the direction of it’s rotation from time to time.
/*global THREE, getDefaultParams */
'use strict';
let camera;
let renderer;
const currentParameters = getDefaultParams();
// once everything is loaded, we run our Three.js stuff.
function init() {
// create a scene, that will hold all our elements such as objects, cameras and lights.
const scene = new THREE.Scene();
// create a camera, which defines where we're looking at.
camera = new THREE.OrthographicCamera( window.innerWidth / - 7, window.innerWidth / 7, window.innerHeight / 7, window.innerHeight / - 7, 1, 1000 );
// create a render and set the size
renderer = new THREE.WebGLRenderer();
renderer.setClearColor(new THREE.Color(currentParameters.renderer.color));
renderer.setSize(window.innerWidth, window.innerHeight);
// create a SFM shapes
let parentShape = new THREE.Mesh();
const childShapeGeometry = new THREE.SphereGeometry(
currentParameters.childShape.size,
currentParameters.childShape.numberOfVertices,
currentParameters.childShape.numberOfVertices,
);
const childShapeMaterial = new THREE.MeshBasicMaterial({
color: currentParameters.childShape.color,
});
const childShapeTemplate = new THREE.Mesh(childShapeGeometry, childShapeMaterial);
for (let i = 0, l = currentParameters.parentShape.numberOfChilds; i < l; i++) {
const childShape = childShapeTemplate.clone();
// add random dot to parent shape
[childShape.position.x, childShape.position.y, childShape.position.z] =
generateRandomDotOnSphere(0, 0, 0, currentParameters.parentShape.size);
parentShape.add(childShape);
}
scene.add(parentShape);
// add fixation point
let fixationGeometry = new THREE.CircleGeometry(1, 10);
let fixationMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00 });
let fixationShape = new THREE.Mesh(fixationGeometry, fixationMaterial);
fixationShape.position.z = -100;
scene.add(fixationShape);
// position and point the camera to the center of the scene
camera.position.copy({ x: 0, y: 0, z: 100 });
camera.lookAt(parentShape.position);
// add ambientLight for the shadows
const ambientLight = new THREE.AmbientLight(0xffffff);
ambientLight.position.set(-100, 100, 50);
scene.add(ambientLight);
// Output
document.getElementById('WebGL-output').appendChild(renderer.domElement);
renderScene();
function renderScene() {
parentShape.rotation.y += 0.02;
requestAnimationFrame(renderScene);
renderer.render(scene, camera);
}
}
window.onload = init;
// *********************************************
// *********HELPERS*****************************
function generateRandomDotOnSphere(x0, y0, z0, radius) {
/*
source: https://stackoverflow.com/questions/5531827/random-point-on-a-given-sphere
Returns a random point of a sphere, evenly distributed over the sphere.
The sphere is centered at (x0,y0,z0) with the passed in radius.
The returned point is returned as a three element array [x,y,z].
*/
let u = Math.random();
let v = Math.random();
let theta = 2 * Math.PI * u;
let phi = Math.acos(2 * v - 1);
let x = x0 + (radius * Math.sin(phi) * Math.cos(theta));
let y = y0 + (radius * Math.sin(phi) * Math.sin(theta));
let z = z0 + (radius * Math.cos(phi));
return [x, y, z];
}
function getDefaultParams() {
// Define default parameters
const defaultParameters = { // define all backgroun
renderer: {
color: 0xFFFFFF,
}, // renderer
parentShape: {
numberOfVertices: 10,
size: 20,
numberOfChilds: 50,
}, // parent shape
childShape: {
numberOfVertices: 5,
size: 1,
color: 0x7777ff,
}, // child shape
};
return defaultParameters;
}
<!DOCTYPE html>
<html>
<head>
<title>Structure_from_motion</title>
<script src="https://threejs.org/build/three.min.js"></script>
</head>
<body>
<!-- Div which will hold the Output -->
<div id="WebGL-output",style="heigh:100px;"></div>
<!-- Javascript code that runs our Three.js examples -->
<script src="ambiguous.js"></script>
</body>
</html>
GitHub repo.
General Question/Problem:
I am satisfied by demos above, but only as concepts. First demo looks ugly. I am not an artist at all and I ask you to help to make this (first) demo looks pretty and as much natural as possible. Later I plan to add GUI controls to switch off each of disambiguating factors to obtain second demo.
Detailed questions:
Besides main problem to make unambiguous demo more natural and interesting I have several concrete questions:
animation looks not smooth and sometimes slow. Is it a limitation of three.js or it is possible to speedup and smooth animation?
y coordinate of camera is zero same as centre of transparent sphere (parent shape), but y coordinates of small spheres centres are change during rotation (i put ruler to my monitor). But It should be on the same level if I understand correctly.
How do I smooth the shadows? (thanks TheJim01).
What is the Physical Based Rendering and will it helps me? (thanks TheJim01)
I need to switch from
THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000)
toTHREE.OrthographicCamera()
. How I should adjust parameters of OrthographicCamera to keep the size of the parent shape?
TO BE CONTINUED...