Skip to content

Instantly share code, notes, and snippets.

@greggman
Last active January 19, 2026 11:04
Show Gist options
  • Select an option

  • Save greggman/7413449cf7b4b4db09a2373d0aad5bee to your computer and use it in GitHub Desktop.

Select an option

Save greggman/7413449cf7b4b4db09a2373d0aad5bee to your computer and use it in GitHub Desktop.
Three.js - Shadows - Spot Light w/CameraHelper
html, body {
margin: 0;
height: 100%;
}
#c {
width: 100%;
height: 100%;
display: block;
}
<canvas id="c"></canvas>
<script type="importmap">
{
"imports": {
"three": "https://raw.githack.com/mrdoob/three.js/dev/build/three.webgpu.js",
"three/webgpu": "https://raw.githack.com/mrdoob/three.js/dev/three.webgpu.js",
"three/tsl": "https://raw.githack.com/mrdoob/three.js/dev/build/three.tsl.js",
"three/addons/": "https://raw.githack.com/mrdoob/three.js/dev/examples/jsm/"
}
}
</script>
// Three.js - Shadows - Spot Light w/CameraHelper
// from https://threejs.org/manual/examples/shadows-point-light.html
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
GPUDevice.prototype.createShaderModule = (function(origFn) {
return function(desc) {
const {code} = desc;
// nodeVar3 = dot( normalView, nodeVar2 );
// nodeVar42 = clamp( dot( positionViewDirection, nodeVar41 ), 0.0, 1.0 );
// nodeVar57 = dot( normalView, positionViewDirection );
// nodeVar77 = dot( normalView, positionViewDirection );
// nodeVar113 = dot( normalView, positionViewDirection );
const newCode = code.replace(
'output.color = nodeVar128;',
'output.color = nodeVar128 + vec4f(0,0,0,0);'
)
.replace('nodeVar3 = dot( normalView, nodeVar2 );',
'nodeVar3 = 1.0;')
.replace('nodeVar42 = clamp( dot( positionViewDirection, nodeVar41 ), 0.0, 1.0 );',
'nodeVar42 = 1.0;')
.replace('nodeVar57 = dot( normalView, positionViewDirection );',
'nodeVar57 = 1.0;')
.replace('nodeVar77 = dot( normalView, positionViewDirection );',
'nodeVar77 = 1.0;')
.replace('nodeVar113 = dot( normalView, positionViewDirection );',
'nodeVar113 = 1.0;',
);
if (code !== newCode) {
console.log(code);
}
const result = origFn.call(this, {code: newCode});
return result;
};
})(GPUDevice.prototype.createShaderModule);
async function main() {
const canvas = document.querySelector( '#c' );
const renderer = new THREE.WebGPURenderer( { canvas } );
renderer.shadowMap.enabled = true;
const fov = 45;
const aspect = 2; // the canvas default
const near = 0.1;
const far = 100;
const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
camera.position.set( 0, 10, 20 );
const controls = new OrbitControls( camera, canvas );
controls.target.set( 0, 5, 0 );
controls.update();
const scene = new THREE.Scene();
scene.background = new THREE.Color( 'black' );
{
const planeSize = 40;
const loader = new THREE.TextureLoader();
const texture = loader.load( 'https://threejs.org/manual/examples/resources/images/checker.png' );
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.magFilter = THREE.NearestFilter;
texture.colorSpace = THREE.SRGBColorSpace;
const repeats = planeSize / 2;
texture.repeat.set( repeats, repeats );
const planeGeo = new THREE.PlaneGeometry( planeSize, planeSize );
const planeMat = new THREE.MeshStandardMaterial( {
map: texture,
side: THREE.DoubleSide,
} );
const mesh = new THREE.Mesh( planeGeo, planeMat );
mesh.receiveShadow = true;
mesh.rotation.x = Math.PI * - .5;
scene.add( mesh );
}
{
const cubeSize = 4;
const cubeGeo = new THREE.BoxGeometry( cubeSize, cubeSize, cubeSize );
const cubeMat = new THREE.MeshStandardMaterial( { color: '#8AC' } );
const mesh = new THREE.Mesh( cubeGeo, cubeMat );
mesh.castShadow = true;
mesh.receiveShadow = true;
mesh.position.set( cubeSize + 1, cubeSize / 2, 0 );
scene.add( mesh );
}
{
const cubeSize = 30;
const cubeGeo = new THREE.BoxGeometry( cubeSize, cubeSize, cubeSize );
const cubeMat = new THREE.MeshStandardMaterial( {
color: '#CCC',
side: THREE.BackSide,
} );
const mesh = new THREE.Mesh( cubeGeo, cubeMat );
mesh.receiveShadow = true;
mesh.position.set( 0, cubeSize / 2 - 0.1, 0 );
scene.add( mesh );
}
{
const sphereRadius = 3;
const sphereWidthDivisions = 32;
const sphereHeightDivisions = 16;
const sphereGeo = new THREE.SphereGeometry( sphereRadius, sphereWidthDivisions, sphereHeightDivisions );
const sphereMat = new THREE.MeshStandardMaterial( { color: '#CA8' } );
const mesh = new THREE.Mesh( sphereGeo, sphereMat );
mesh.castShadow = true;
mesh.receiveShadow = true;
mesh.position.set( - sphereRadius - 1, sphereRadius + 2, 0 );
scene.add( mesh );
}
class ColorGUIHelper {
constructor( object, prop ) {
this.object = object;
this.prop = prop;
}
get value() {
return `#${this.object[ this.prop ].getHexString()}`;
}
set value( hexString ) {
this.object[ this.prop ].set( hexString );
}
}
function makeXYZGUI( gui, vector3, name, onChangeFn ) {
const folder = gui.addFolder( name );
folder.add( vector3, 'x', - 10, 10 ).onChange( onChangeFn );
folder.add( vector3, 'y', 0, 10 ).onChange( onChangeFn );
folder.add( vector3, 'z', - 10, 10 ).onChange( onChangeFn );
// folder.open();
}
{
const color = 0xFFFFFF;
const intensity = 100;
const light = new THREE.PointLight( color, intensity );
light.castShadow = true;
light.position.set( 0, 10, 0 );
scene.add( light );
const helper = new THREE.PointLightHelper( light );
scene.add( helper );
function updateCamera() {
}
class MinMaxGUIHelper {
constructor( obj, minProp, maxProp, minDif ) {
this.obj = obj;
this.minProp = minProp;
this.maxProp = maxProp;
this.minDif = minDif;
}
get min() {
return this.obj[ this.minProp ];
}
set min( v ) {
this.obj[ this.minProp ] = v;
this.obj[ this.maxProp ] = Math.max( this.obj[ this.maxProp ], v + this.minDif );
}
get max() {
return this.obj[ this.maxProp ];
}
set max( v ) {
this.obj[ this.maxProp ] = v;
this.min = this.min; // this will call the min setter
}
}
const gui = new GUI();
gui.addColor( new ColorGUIHelper( light, 'color' ), 'value' ).name( 'color' );
gui.add( light, 'intensity', 0, 200 );
// gui.add( light, 'distance', 0, 40 ).onChange( updateCamera );
{
const folder = gui.addFolder( 'Shadow Camera' );
folder.open();
const minMaxGUIHelper = new MinMaxGUIHelper( light.shadow.camera, 'near', 'far', 0.1 );
folder.add( minMaxGUIHelper, 'min', 0.1, 50, 0.1 ).name( 'near' ).onChange( updateCamera );
folder.add( minMaxGUIHelper, 'max', 0.1, 50, 0.1 ).name( 'far' ).onChange( updateCamera );
}
makeXYZGUI( gui, light.position, 'position', updateCamera );
}
function resizeRendererToDisplaySize( renderer ) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if ( needResize ) {
renderer.setSize( width, height, false );
}
return needResize;
}
function render() {
resizeRendererToDisplaySize( renderer );
{
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
renderer.render( scene, camera );
requestAnimationFrame( render );
}
await renderer.init();
requestAnimationFrame( render );
}
main();
{"name":"Three.js - Shadows - Spot Light w/CameraHelper","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