An interactive version of the infamous Checker Shadow Illusion built in three.js
A Pen by Steffen Van on CodePen.
| <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; | |
| } |
An interactive version of the infamous Checker Shadow Illusion built in three.js
A Pen by Steffen Van on CodePen.