Last active
December 19, 2025 03:20
-
-
Save greggman/1bf6513db065a669730110a771c5fb86 to your computer and use it in GitHub Desktop.
Three.js RGB Cube
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| :root { | |
| color-scheme: light dark; | |
| } | |
| body { | |
| margin: 0px; | |
| background-color: #333; | |
| font-family: monospace; | |
| height: 100%; | |
| } | |
| #labels { | |
| left: 0px; | |
| top: 0px; | |
| position: absolute; | |
| pointer-events: none; | |
| width: 100%; | |
| height: 100%; | |
| div { | |
| color: white; | |
| text-shadow: | |
| -1px -1px 0 #000, | |
| 0 -1px 0 #000, | |
| 1px -1px 0 #000, | |
| 1px 0 0 #000, | |
| 1px 1px 0 #000, | |
| 0 1px 0 #000, | |
| -1px 1px 0 #000, | |
| -1px 0 0 #000; | |
| } | |
| } | |
| #ui { | |
| position: absolute; | |
| z-index: 1; | |
| left: 0; | |
| top: 0; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!-- https://unpkg.com/three@0.182.0/build/three.tsl.js --> | |
| <script type="importmap"> | |
| { | |
| "imports": { | |
| "three": "https://unpkg.com/three@0.182.0/build/three.webgpu.js", | |
| "three/webgpu": "https://unpkg.com/three@0.182.0/build/three.webgpu.js", | |
| "three/tsl": "https://unpkg.com/three@0.182.0/build/three.tsl.js", | |
| "three/addons/": "https://unpkg.com/three@0.182.0/examples/jsm/" | |
| } | |
| } | |
| </script> | |
| <div id="ui"><input type="color"></div> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import * as THREE from 'three'; | |
| import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; | |
| import { | |
| Fn as tslFn, | |
| positionWorld, | |
| vec3, | |
| } from 'three/tsl'; | |
| import { CSS2DRenderer, CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js'; | |
| const rgbCube = tslFn(() => { | |
| const p = positionWorld; | |
| return p.mul(0.5).add(0.5); | |
| }); | |
| async function main() { | |
| const adapter = await navigator.gpu?.requestAdapter(); | |
| const device = await adapter?.requestDevice(); | |
| const canvas = document.createElement('canvas'); | |
| Object.assign(canvas.style, { | |
| width: '100%', | |
| height: '100%', | |
| aspectRatio: '1 / 1', | |
| }); | |
| const renderer = new THREE.WebGPURenderer({ device, canvas }); | |
| document.body.append(canvas); | |
| const labelRenderer = new CSS2DRenderer(); | |
| labelRenderer.domElement.id = 'labels'; | |
| document.body.appendChild(labelRenderer.domElement); | |
| const clipPlane1 = new THREE.Plane(new THREE.Vector3(-1, 0, 0), 0); | |
| const clipPlane2 = new THREE.Plane(new THREE.Vector3(0, -1, 0), 0); | |
| const clipPlane3 = new THREE.Plane(new THREE.Vector3(0, 0, -1), 0); | |
| const scene = new THREE.Scene(); | |
| const clippingGroup = new THREE.ClippingGroup(); | |
| clippingGroup.clippingPlanes = [ clipPlane1, clipPlane2, clipPlane3 ]; | |
| clippingGroup.enabled = true; | |
| clippingGroup.clipIntersection = true; | |
| scene.add(clippingGroup); | |
| const camera = new THREE.PerspectiveCamera(40, 1, 1, 10); | |
| camera.position.set(3.5, 3.5, 3.5); | |
| const controls = new OrbitControls(camera, renderer.domElement); | |
| controls.enablePan = false; | |
| controls.enableZoom = false; | |
| const lineMaterial = new THREE.MeshBasicNodeMaterial(); | |
| lineMaterial.colorNode = vec3(0.5, 0.5, 0.5); | |
| const cubeGeo = new THREE.BoxGeometry(2, 2, 2); | |
| const plane = new THREE.PlaneGeometry(2, 2); | |
| const rgbMaterial = new THREE.MeshBasicNodeMaterial({ | |
| side: THREE.DoubleSide, | |
| }); | |
| rgbMaterial.colorNode = rgbCube(); | |
| const cubeMesh = new THREE.Mesh(cubeGeo, rgbMaterial); | |
| const p1Mesh = new THREE.Mesh(plane, rgbMaterial); | |
| const p2Mesh = new THREE.Mesh(plane, rgbMaterial); | |
| p2Mesh.rotation.y = Math.PI * 0.5; | |
| const p3Mesh = new THREE.Mesh(plane, rgbMaterial); | |
| p3Mesh.rotation.x = Math.PI * 0.5; | |
| const p1 = new THREE.Object3D(); | |
| const p2 = new THREE.Object3D(); | |
| const p3 = new THREE.Object3D(); | |
| const cube = new THREE.Object3D(); | |
| p1.add(p1Mesh); | |
| p2.add(p2Mesh); | |
| p3.add(p3Mesh); | |
| cube.add(cubeMesh); | |
| scene.add(p1); | |
| scene.add(p2); | |
| scene.add(p3); | |
| const cylMat = new THREE.MeshBasicMaterial({color: 0xFFFFFF}); | |
| const cylGeo = new THREE.CylinderGeometry(0.01, 0.01, 2.1, 32); | |
| function makeAxis() { | |
| const elem = document.createElement('div'); | |
| elem.className = 'label'; | |
| const obj = new CSS2DObject(elem); | |
| obj.center.set(0, 1); | |
| const mesh = new THREE.Mesh(cylGeo, cylMat); | |
| mesh.position.x = 1.1; | |
| mesh.rotation.z = Math.PI * -0.5; | |
| const labelParent = new THREE.Object3D(); | |
| const root = new THREE.Object3D(); | |
| const dir = new THREE.Vector3( 1, 0, 0 ); | |
| const origin = new THREE.Vector3( 0, 0, 0 ); | |
| const length = 2.2; | |
| const arrow = new THREE.ArrowHelper(dir, origin, length, 0xFFFFFF, 0.05, 0.05); | |
| root.add(mesh); | |
| root.add(arrow); | |
| root.add(labelParent); | |
| labelParent.add(obj); | |
| labelParent.position.set(2.2, 0, 0); | |
| //mark.add(labelParent); | |
| return { elem, obj, root }; | |
| } | |
| const rAxis = makeAxis(); | |
| const gAxis = makeAxis(); | |
| const bAxis = makeAxis(); | |
| rAxis.root.position.set(-1, -1, 1.2); | |
| rAxis.root.rotation.set(Math.PI * 0, Math.PI * 0, Math.PI * 0); | |
| gAxis.root.position.set(-1, -1, 1.2); | |
| gAxis.root.rotation.z = Math.PI * 0.5; | |
| bAxis.root.position.set(1.2, -1, -1); | |
| bAxis.root.rotation.set(Math.PI * 1, Math.PI * 0.5, 0); | |
| scene.add(rAxis.root); | |
| scene.add(gAxis.root); | |
| scene.add(bAxis.root); | |
| const stuff = new THREE.Object3D(); | |
| stuff.add(cube); | |
| clippingGroup.add(stuff); | |
| const midDisc = new THREE.Object3D(); | |
| scene.add(midDisc); | |
| const markerMaterial = new THREE.MeshBasicMaterial({ | |
| color: 0x0FF0000, | |
| }); | |
| const marker = (() => { | |
| const sphere = new THREE.SphereGeometry(0.05); | |
| return new THREE.Mesh(sphere, markerMaterial); | |
| })(); | |
| scene.add(marker); | |
| { | |
| const gridSegments = 8; | |
| const points = []; | |
| for (let i = 0; i <= gridSegments; ++i) { | |
| const x = i / gridSegments * 2 - 1; | |
| points.push(new THREE.Vector3(x, -1, 0)); | |
| points.push(new THREE.Vector3(x, 1, 0)); | |
| } | |
| const gridGeo = new THREE.BufferGeometry().setFromPoints(points); | |
| const grids = []; | |
| for (let i = 0; i < 9; ++i) { | |
| const grid = new THREE.Object3D(); | |
| const l1 = new THREE.LineSegments(gridGeo, lineMaterial); | |
| const l2 = new THREE.LineSegments(gridGeo, lineMaterial); | |
| l2.rotation.z = Math.PI * 0.5; | |
| grid.add(l1); | |
| grid.add(l2); | |
| grids.push(grid); | |
| } | |
| stuff.add(grids[0]); | |
| stuff.add(grids[1]); | |
| stuff.add(grids[2]); | |
| stuff.add(grids[3]); | |
| stuff.add(grids[4]); | |
| stuff.add(grids[5]); | |
| grids[0].position.z = -1.001; | |
| grids[1].position.z = 1.001; | |
| grids[2].position.x = -1.001; | |
| grids[2].rotation.y = Math.PI * 0.5; | |
| grids[3].position.x = 1.001; | |
| grids[3].rotation.y = Math.PI * -0.5; | |
| grids[4].position.y = -1.001; | |
| grids[4].rotation.x = Math.PI * 0.5; | |
| grids[5].position.y = 1.001; | |
| grids[5].rotation.x = Math.PI * -0.5; | |
| p1.add(grids[6]); | |
| grids[6].position.z = 0.01; | |
| p2.add(grids[7]); | |
| p3.add(grids[8]); | |
| grids[7].position.x = 0.01; | |
| grids[7].rotation.y = Math.PI * 0.5; | |
| grids[8].position.y = 0.01; | |
| grids[8].rotation.x = Math.PI * 0.5; | |
| } | |
| const euclideanModulo = (n, m) => ((n % m) + m) % m; | |
| function rgbToHsl(rgb) { | |
| const min = Math.min(rgb[0], rgb[1], rgb[2]); | |
| const max = Math.max(rgb[0], rgb[1], rgb[2]); | |
| const delta = max - min; | |
| const l = (max + min) / 2; | |
| if (delta === 0) { | |
| return [0, 0, l]; | |
| } | |
| const s = l < 0.5 | |
| ? delta / (max + min) | |
| : delta / (2 - max - min); | |
| const deltaR = (((max - rgb[0]) / 6) + (delta / 2)) / delta; | |
| const deltaG = (((max - rgb[1]) / 6) + (delta / 2)) / delta; | |
| const deltaB = (((max - rgb[2]) / 6) + (delta / 2)) / delta; | |
| const h = rgb[0] === max | |
| ? deltaB - deltaG | |
| : rgb[1] === max | |
| ? (1 / 3) + deltaR - deltaB | |
| : (2 / 3) + deltaG - deltaR; | |
| return [euclideanModulo(h, 1), s, l]; | |
| } | |
| await renderer.init(); | |
| function render() { | |
| renderer.render(scene, camera); | |
| labelRenderer.render(scene, camera); | |
| } | |
| controls.addEventListener('change', render); | |
| const observer = new ResizeObserver(entries => { | |
| const entry = entries[0]; | |
| const width = entry.devicePixelContentBoxSize?.[0].inlineSize || | |
| entry.contentBoxSize[0].inlineSize * devicePixelRatio; | |
| const height = entry.devicePixelContentBoxSize?.[0].blockSize || | |
| entry.contentBoxSize[0].blockSize * devicePixelRatio; | |
| const w = Math.max(1, Math.min(width, device.limits.maxTextureDimension2D)); | |
| const h = Math.max(1, Math.min(height, device.limits.maxTextureDimension2D)); | |
| renderer.setSize(w, h, false); | |
| labelRenderer.setSize(labelRenderer.domElement.clientWidth, labelRenderer.domElement.clientHeight, false); | |
| // three.js messes up here. They refuse to learn how HTML and CSS work and keep fighting it (T_T) | |
| // Setting the width and height of the element means the browser can no longer make it larger or | |
| // smaller when the user sizes the window. So, we have to remove the damage from three.js so the | |
| // browser can do the correct thing. | |
| labelRenderer.domElement.style.width = ''; | |
| labelRenderer.domElement.style.height = ''; | |
| render(); | |
| }); | |
| observer.observe(renderer.domElement); | |
| function nf(label, v) { | |
| return `${label}:${v.toFixed(2)}`; | |
| } | |
| function setRGB(rgb) { | |
| const [x, y, z] = rgb.map(v => v * 2 - 1); | |
| marker.position.x = x; | |
| marker.position.z = z; | |
| marker.position.y = y; | |
| clipPlane1.constant = x; | |
| clipPlane2.constant = y; | |
| clipPlane3.constant = z; | |
| p2.position.x = x; | |
| p1.position.z = z; | |
| p3.position.y = y; | |
| rAxis.elem.textContent = nf('R', rgb[0]); | |
| gAxis.elem.textContent = nf('G', rgb[1]); | |
| bAxis.elem.textContent = nf('B', rgb[2]); | |
| rAxis.root.position.y = y; | |
| rAxis.root.position.z = z; | |
| //rAxis.mark.position.x = rgb[0] * 2; | |
| //rAxis.root.position.y = y; | |
| gAxis.root.position.x = x; | |
| gAxis.root.position.z = z; | |
| //gAxis.mark.position.x = rgb[1] * 2; | |
| //gAxis.root.position.x = x; | |
| bAxis.root.position.x = x; | |
| bAxis.root.position.y = y; | |
| //bAxis.mark.position.x = rgb[2] * 2; | |
| //bAxis.root.position.y = y; | |
| const [,,l] = rgbToHsl(rgb); | |
| markerMaterial.color.setHSL(0, 0, (l + 0.5) % 1); | |
| render(); | |
| } | |
| let rgb = [0.8, 0.7, 0.6]; | |
| const colorElem = document.querySelector('input[type="color"]'); | |
| const colorRE = /#(..)(..)(..)/ | |
| colorElem.addEventListener('input', function() { | |
| rgb = colorRE.exec(this.value).slice(1, 4).map(v => parseInt(v, 16) / 255); | |
| setRGB(rgb); | |
| }); | |
| colorElem.value = `#${rgb.map(v => (v * 255 | 0).toString(16).padStart(2, '0')).join('')}`; | |
| setRGB(rgb); | |
| } | |
| main(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| {"name":"Three.js RGB Cube","settings":{},"filenames":["index.html","index.css","index.js"]} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment