Skip to content

Instantly share code, notes, and snippets.

@steffenvan
Created March 25, 2019 09:13
Show Gist options
  • Save steffenvan/7e3987d6db5bd751738b0b251ab8f0a2 to your computer and use it in GitHub Desktop.
Save steffenvan/7e3987d6db5bd751738b0b251ab8f0a2 to your computer and use it in GitHub Desktop.
Three.js Checker Shadow Illusion
<script src="https://unpkg.com/[email protected]/build/three.min.js"></script>
<script src="https://unpkg.com/[email protected]/examples/js/controls/OrbitControls.js"></script>
<script src="https://unpkg.com/[email protected]/examples/js/controls/TransformControls.js"></script>
<script src="https://unpkg.com/[email protected]/build/dat.gui.js"></script>
<canvas id="tex" width="512" height="512"></canvas>
<span id="info">
When "B" is under shadow, it is the same color as "A".
Drag the purple square over the tiles to compare colors.
</span>
<div id="container"></div>
var ctx = tex.getContext("2d");
const n = 512 / 5;
// Generate a checker pattern texture.
const checkerOpts = {
perimeter: 38,
flankingDiagonals: 30,
centerDiagonal: 88
};
function drawCheckers() {
const opts = checkerOpts;
ctx.clearRect(0, 0, n * 5, n * 5);
// background
ctx.fillStyle = "hsl(0, 0%, 100%)";
ctx.fillRect(0, 0, n * 5, n * 5);
// A
ctx.fillStyle = `hsl(0, 0%, ${opts.perimeter}%)`;
ctx.fillRect(0 * n, 1 * n, n, n);
ctx.fillRect(1 * n, 0 * n, n, n);
ctx.fillRect(3 * n, 4 * n, n, n);
ctx.fillRect(4 * n, 3 * n, n, n);
ctx.fillRect(4 * n, 1 * n, n, n);
ctx.fillRect(3 * n, 0 * n, n, n);
ctx.fillStyle = `hsl(0, 0%, ${opts.flankingDiagonals}%)`;
ctx.fillRect(2 * n, 1 * n, n, n);
ctx.fillRect(1 * n, 2 * n, n, n);
ctx.fillRect(0 * n, 3 * n, n, n);
ctx.fillRect(3 * n, 2 * n, n, n);
ctx.fillRect(2 * n, 3 * n, n, n);
ctx.fillRect(1 * n, 4 * n, n, n);
// B
ctx.fillStyle = `hsl(0, 0%, ${opts.centerDiagonal}%)`;
ctx.fillRect(2 * n, 2 * n, n, n);
ctx.fillRect(1 * n, 3 * n, n, n);
ctx.fillStyle = "black";
ctx.font = "30pt sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("A", 152, 50);
ctx.fillText("B", 255, 255);
}
drawCheckers();
// Setup 3D renderer
var renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
container.appendChild(renderer.domElement);
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight,
1,
1000
);
camera.position.set(2, 2, 4);
camera.lookAt(scene.position);
function setSize() {
renderer.setSize(window.innerWidth, window.innerHeight);
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
}
setSize();
window.addEventListener("resize", setSize);
// Lighting
const ambient = new THREE.AmbientLight(0xffffff, 0.8);
scene.add(ambient);
const light = new THREE.DirectionalLight(0xffffff, 2.9);
light.castShadow = true;
light.shadow.mapSize.height = light.shadow.mapSize.width = 256;
light.position.set(2, 1.1, -2);
scene.add(light);
// Create floor
var geometry = new THREE.BoxGeometry(3, 0.2, 3);
// Fix UV mapping on edges of floor
geometry.faceVertexUvs[0][8][1].y = 1;
geometry.faceVertexUvs[0][9][0].y = 2;
geometry.faceVertexUvs[0][9][1].y = 2;
geometry.faceVertexUvs[0][9][2].y = 2;
geometry.faceVertexUvs[0][0][1].y = 1;
geometry.faceVertexUvs[0][1][0].y = 2;
geometry.faceVertexUvs[0][1][1].y = 2;
geometry.faceVertexUvs[0][1][2].y = 2;
geometry.faceVertexUvs[0][10][1].y = 1;
geometry.faceVertexUvs[0][11][0].y = 2;
geometry.faceVertexUvs[0][11][1].y = 2;
geometry.faceVertexUvs[0][11][2].y = 2;
geometry.faceVertexUvs[0][2][1].y = 1;
geometry.faceVertexUvs[0][3][0].y = 2;
geometry.faceVertexUvs[0][3][1].y = 2;
geometry.faceVertexUvs[0][3][2].y = 2;
var map = new THREE.CanvasTexture(tex);
map.anisotropy = renderer.capabilities.getMaxAnisotropy();
var material = new THREE.MeshLambertMaterial({ color: 0xaaaaaa, map });
var floor = new THREE.Mesh(geometry, material);
floor.receiveShadow = true;
scene.add(floor);
function updateCheckers() {
drawCheckers();
map.needsUpdate = true;
}
// Green cylinder
const cylRadius = 0.6;
var geometry = new THREE.CylinderBufferGeometry(cylRadius, cylRadius, 1, 32);
var material = new THREE.MeshStandardMaterial({
color: 0x55aa55,
roughness: 1,
metalness: 0.7
});
var cyl = new THREE.Mesh(geometry, material);
cyl.castShadow = true;
cyl.position.set(1.5 - cylRadius, 0.45, -1.5 + cylRadius);
scene.add(cyl);
// Create a movable swatch for comparing colors
const swatchMat = new THREE.MeshBasicMaterial({ color: 0x777777 });
const swatchTrans = new THREE.Group();
const swatch = new THREE.Group();
swatch.position.set(-0.5, 0, 0.2);
swatchTrans.add(swatch);
swatch.add(
new THREE.Mesh(new THREE.BoxBufferGeometry(0.25, 0.1, 2), swatchMat)
);
swatch.children[0].position.x = -0.3;
swatch.add(
new THREE.Mesh(new THREE.BoxBufferGeometry(0.25, 0.1, 2), swatchMat)
);
swatch.children[1].position.x = 0.3;
swatch.add(
new THREE.Mesh(new THREE.BoxBufferGeometry(0.8, 0.1, 0.9), swatchMat)
);
swatch.add(
new THREE.Mesh(new THREE.BoxBufferGeometry(0.8, 0.1, 0.2), swatchMat)
);
swatch.children[3].position.z = 0.9;
swatch.add(
new THREE.Mesh(new THREE.BoxBufferGeometry(0.8, 0.1, 0.2), swatchMat)
);
swatch.children[4].position.z = -0.9;
swatchTrans.rotation.y = Math.sin(1 / 2);
swatchTrans.position.set(-1.8, 0.1, -0.5);
scene.add(swatchTrans);
// Camera controls
const orbit = new THREE.OrbitControls(camera, container);
function addXZTransformControls(obj) {
const transformControls = new THREE.TransformControls(
camera,
renderer.domElement
);
// Hide unwanted transform gizmos.
['handles', 'pickers', 'planes'].forEach(gizmo => {
for (const child of transformControls.children[0][gizmo].children) {
if (child.name === "XZ") {
continue;
}
child.visible = false;
}
})
transformControls.setSize(1);
scene.add(transformControls);
transformControls.attach(obj);
orbit.addEventListener("change", () => transformControls.update());
}
addXZTransformControls(swatchTrans);
addXZTransformControls(cyl);
// Render loop
function animate(delta) {
renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);
// Control UI
const gui = new dat.GUI();
gui.width = 300;
gui.closed = window.innerWidth < 1024;
gui
.add(checkerOpts, "perimeter", 0, 100, 0.1)
.name("Perimeter tiles")
.onChange(updateCheckers);
gui
.add(checkerOpts, "flankingDiagonals", 0, 100, 0.1)
.name("Flanking tiles")
.onChange(updateCheckers);
gui
.add(checkerOpts, "centerDiagonal", 0, 100, 0.1)
.name("Center tiles")
.onChange(updateCheckers);
gui
.add(ambient, "intensity", 0, 1)
.name("Ambient intensity")
.onChange(updateCheckers);
gui.add(light, "visible").name("Enable light");
gui.add(cyl, "visible").name("Show cylinder");
gui
.add({ texture: false }, "texture")
.name("Show texture")
.onChange(show => (tex.style.display = show ? "block" : "none"));
gui.add(
{
Reset: () => {
gui.revert(gui);
}
},
"Reset"
);
html,
body {
margin: 0;
padding: 0;
font-size: 0;
display: flex;
justify-content: center;
}
#tex {
position: absolute;
width: 128px;
left: 0;
display: none;
}
#info {
position: absolute;
color: white;
font-size: 12pt;
font-family: sans-serif;
background: black;
padding: 0.5em;
bottom: 0;
}
.dg.main {
font-size: 13px;
}
.dg .c input[type="checkbox"] {
width: 18px;
height: 18px;
margin-top: 4px;
}
.illusion .dg.main .close-button {
background: #333;
height: 30px;
display: flex;
justify-content: center;
align-items: center;
}

Three.js Checker Shadow Illusion

An interactive version of the infamous Checker Shadow Illusion built in three.js

A Pen by Steffen Van on CodePen.

License.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment