From 5aa3ca5ac9477a6734969d6835501fc2147d59db Mon Sep 17 00:00:00 2001 From: como Date: Sat, 28 Mar 2026 13:57:54 +0800 Subject: [PATCH] =?UTF-8?q?=E7=A7=BB=E5=8A=A8skill=E4=BD=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/skills/threejs | 1 - .claude/skills/webgpu | 1 - .codex/skills/threejs-animation/SKILL.md | 552 +++++++++++++++ .codex/skills/threejs-fundamentals/SKILL.md | 488 +++++++++++++ .codex/skills/threejs-geometry/SKILL.md | 548 +++++++++++++++ .codex/skills/threejs-interaction/SKILL.md | 660 ++++++++++++++++++ .codex/skills/threejs-lighting/SKILL.md | 481 +++++++++++++ .codex/skills/threejs-loaders/SKILL.md | 623 +++++++++++++++++ .codex/skills/threejs-materials/SKILL.md | 520 ++++++++++++++ .codex/skills/threejs-postprocessing/SKILL.md | 602 ++++++++++++++++ .codex/skills/threejs-shaders/SKILL.md | 642 +++++++++++++++++ .codex/skills/threejs-textures/SKILL.md | 628 +++++++++++++++++ .codex/skills/webgpu-threejs-tsl/REFERENCE.md | 335 +++++++++ .codex/skills/webgpu-threejs-tsl/SKILL.md | 92 +++ .../docs/compute-shaders.md | 404 +++++++++++ .../webgpu-threejs-tsl/docs/core-concepts.md | 453 ++++++++++++ .../webgpu-threejs-tsl/docs/device-loss.md | 359 ++++++++++ .../webgpu-threejs-tsl/docs/materials.md | 353 ++++++++++ .../docs/post-processing.md | 475 +++++++++++++ .../docs/wgsl-integration.md | 324 +++++++++ .../examples/basic-setup.js | 87 +++ .../examples/custom-material.js | 170 +++++ .../examples/earth-shader.js | 292 ++++++++ .../examples/particle-system.js | 259 +++++++ .../examples/post-processing.js | 199 ++++++ .../templates/compute-shader.js | 305 ++++++++ .../templates/webgpu-project.js | 276 ++++++++ 27 files changed, 10127 insertions(+), 2 deletions(-) delete mode 160000 .claude/skills/threejs delete mode 160000 .claude/skills/webgpu create mode 100644 .codex/skills/threejs-animation/SKILL.md create mode 100644 .codex/skills/threejs-fundamentals/SKILL.md create mode 100644 .codex/skills/threejs-geometry/SKILL.md create mode 100644 .codex/skills/threejs-interaction/SKILL.md create mode 100644 .codex/skills/threejs-lighting/SKILL.md create mode 100644 .codex/skills/threejs-loaders/SKILL.md create mode 100644 .codex/skills/threejs-materials/SKILL.md create mode 100644 .codex/skills/threejs-postprocessing/SKILL.md create mode 100644 .codex/skills/threejs-shaders/SKILL.md create mode 100644 .codex/skills/threejs-textures/SKILL.md create mode 100644 .codex/skills/webgpu-threejs-tsl/REFERENCE.md create mode 100644 .codex/skills/webgpu-threejs-tsl/SKILL.md create mode 100644 .codex/skills/webgpu-threejs-tsl/docs/compute-shaders.md create mode 100644 .codex/skills/webgpu-threejs-tsl/docs/core-concepts.md create mode 100644 .codex/skills/webgpu-threejs-tsl/docs/device-loss.md create mode 100644 .codex/skills/webgpu-threejs-tsl/docs/materials.md create mode 100644 .codex/skills/webgpu-threejs-tsl/docs/post-processing.md create mode 100644 .codex/skills/webgpu-threejs-tsl/docs/wgsl-integration.md create mode 100644 .codex/skills/webgpu-threejs-tsl/examples/basic-setup.js create mode 100644 .codex/skills/webgpu-threejs-tsl/examples/custom-material.js create mode 100644 .codex/skills/webgpu-threejs-tsl/examples/earth-shader.js create mode 100644 .codex/skills/webgpu-threejs-tsl/examples/particle-system.js create mode 100644 .codex/skills/webgpu-threejs-tsl/examples/post-processing.js create mode 100644 .codex/skills/webgpu-threejs-tsl/templates/compute-shader.js create mode 100644 .codex/skills/webgpu-threejs-tsl/templates/webgpu-project.js diff --git a/.claude/skills/threejs b/.claude/skills/threejs deleted file mode 160000 index b1c6230..0000000 --- a/.claude/skills/threejs +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b1c623076c661fc9b03dac19292e825a5d106823 diff --git a/.claude/skills/webgpu b/.claude/skills/webgpu deleted file mode 160000 index 4adcfe8..0000000 --- a/.claude/skills/webgpu +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4adcfe8ef2317eae0801dbf4a396844954f8e996 diff --git a/.codex/skills/threejs-animation/SKILL.md b/.codex/skills/threejs-animation/SKILL.md new file mode 100644 index 0000000..91742ce --- /dev/null +++ b/.codex/skills/threejs-animation/SKILL.md @@ -0,0 +1,552 @@ +--- +name: threejs-animation +description: Three.js animation - keyframe animation, skeletal animation, morph targets, animation mixing. Use when animating objects, playing GLTF animations, creating procedural motion, or blending animations. +--- + +# Three.js Animation + +## Quick Start + +```javascript +import * as THREE from "three"; + +// Simple procedural animation +const clock = new THREE.Clock(); + +function animate() { + const delta = clock.getDelta(); + const elapsed = clock.getElapsedTime(); + + mesh.rotation.y += delta; + mesh.position.y = Math.sin(elapsed) * 0.5; + + requestAnimationFrame(animate); + renderer.render(scene, camera); +} +animate(); +``` + +## Animation System Overview + +Three.js animation system has three main components: + +1. **AnimationClip** - Container for keyframe data +2. **AnimationMixer** - Plays animations on a root object +3. **AnimationAction** - Controls playback of a clip + +## AnimationClip + +Stores keyframe animation data. + +```javascript +// Create animation clip +const times = [0, 1, 2]; // Keyframe times (seconds) +const values = [0, 1, 0]; // Values at each keyframe + +const track = new THREE.NumberKeyframeTrack( + ".position[y]", // Property path + times, + values, +); + +const clip = new THREE.AnimationClip("bounce", 2, [track]); +``` + +### KeyframeTrack Types + +```javascript +// Number track (single value) +new THREE.NumberKeyframeTrack(".opacity", times, [1, 0]); +new THREE.NumberKeyframeTrack(".material.opacity", times, [1, 0]); + +// Vector track (position, scale) +new THREE.VectorKeyframeTrack(".position", times, [ + 0, + 0, + 0, // t=0 + 1, + 2, + 0, // t=1 + 0, + 0, + 0, // t=2 +]); + +// Quaternion track (rotation) +const q1 = new THREE.Quaternion().setFromEuler(new THREE.Euler(0, 0, 0)); +const q2 = new THREE.Quaternion().setFromEuler(new THREE.Euler(0, Math.PI, 0)); +new THREE.QuaternionKeyframeTrack( + ".quaternion", + [0, 1], + [q1.x, q1.y, q1.z, q1.w, q2.x, q2.y, q2.z, q2.w], +); + +// Color track +new THREE.ColorKeyframeTrack(".material.color", times, [ + 1, + 0, + 0, // red + 0, + 1, + 0, // green + 0, + 0, + 1, // blue +]); + +// Boolean track +new THREE.BooleanKeyframeTrack(".visible", [0, 0.5, 1], [true, false, true]); + +// String track (for morph targets) +new THREE.StringKeyframeTrack( + ".morphTargetInfluences[smile]", + [0, 1], + ["0", "1"], +); +``` + +### Interpolation Modes + +```javascript +const track = new THREE.VectorKeyframeTrack(".position", times, values); + +// Interpolation +track.setInterpolation(THREE.InterpolateLinear); // Default +track.setInterpolation(THREE.InterpolateSmooth); // Cubic spline +track.setInterpolation(THREE.InterpolateDiscrete); // Step function +``` + +## AnimationMixer + +Plays animations on an object and its descendants. + +```javascript +const mixer = new THREE.AnimationMixer(model); + +// Create action from clip +const action = mixer.clipAction(clip); +action.play(); + +// Update in animation loop +function animate() { + const delta = clock.getDelta(); + mixer.update(delta); // Required! + + requestAnimationFrame(animate); + renderer.render(scene, camera); +} +``` + +### Mixer Events + +```javascript +mixer.addEventListener("finished", (e) => { + console.log("Animation finished:", e.action.getClip().name); +}); + +mixer.addEventListener("loop", (e) => { + console.log("Animation looped:", e.action.getClip().name); +}); +``` + +## AnimationAction + +Controls playback of an animation clip. + +```javascript +const action = mixer.clipAction(clip); + +// Playback control +action.play(); +action.stop(); +action.reset(); +action.halt(fadeOutDuration); + +// Playback state +action.isRunning(); +action.isScheduled(); + +// Time control +action.time = 0.5; // Current time +action.timeScale = 1; // Playback speed (negative = reverse) +action.paused = false; + +// Weight (for blending) +action.weight = 1; // 0-1, contribution to final pose +action.setEffectiveWeight(1); + +// Loop modes +action.loop = THREE.LoopRepeat; // Default: loop forever +action.loop = THREE.LoopOnce; // Play once and stop +action.loop = THREE.LoopPingPong; // Alternate forward/backward +action.repetitions = 3; // Number of loops (Infinity default) + +// Clamping +action.clampWhenFinished = true; // Hold last frame when done + +// Blending +action.blendMode = THREE.NormalAnimationBlendMode; +action.blendMode = THREE.AdditiveAnimationBlendMode; +``` + +### Fade In/Out + +```javascript +// Fade in +action.reset().fadeIn(0.5).play(); + +// Fade out +action.fadeOut(0.5); + +// Crossfade between animations +const action1 = mixer.clipAction(clip1); +const action2 = mixer.clipAction(clip2); + +action1.play(); + +// Later, crossfade to action2 +action1.crossFadeTo(action2, 0.5, true); +action2.play(); +``` + +## Loading GLTF Animations + +Most common source of skeletal animations. + +```javascript +import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js"; + +const loader = new GLTFLoader(); +loader.load("model.glb", (gltf) => { + const model = gltf.scene; + scene.add(model); + + // Create mixer + const mixer = new THREE.AnimationMixer(model); + + // Get all clips + const clips = gltf.animations; + console.log( + "Available animations:", + clips.map((c) => c.name), + ); + + // Play first animation + if (clips.length > 0) { + const action = mixer.clipAction(clips[0]); + action.play(); + } + + // Play specific animation by name + const walkClip = THREE.AnimationClip.findByName(clips, "Walk"); + if (walkClip) { + mixer.clipAction(walkClip).play(); + } + + // Store mixer for update loop + window.mixer = mixer; +}); + +// Animation loop +function animate() { + const delta = clock.getDelta(); + if (window.mixer) window.mixer.update(delta); + + requestAnimationFrame(animate); + renderer.render(scene, camera); +} +``` + +## Skeletal Animation + +### Skeleton and Bones + +```javascript +// Access skeleton from skinned mesh +const skinnedMesh = model.getObjectByProperty("type", "SkinnedMesh"); +const skeleton = skinnedMesh.skeleton; + +// Access bones +skeleton.bones.forEach((bone) => { + console.log(bone.name, bone.position, bone.rotation); +}); + +// Find specific bone by name +const headBone = skeleton.bones.find((b) => b.name === "Head"); +if (headBone) headBone.rotation.y = Math.PI / 4; // Turn head + +// Skeleton helper +const helper = new THREE.SkeletonHelper(model); +scene.add(helper); +``` + +### Programmatic Bone Animation + +```javascript +function animate() { + const time = clock.getElapsedTime(); + + // Animate bone + const headBone = skeleton.bones.find((b) => b.name === "Head"); + if (headBone) { + headBone.rotation.y = Math.sin(time) * 0.3; + } + + // Update mixer if also playing clips + mixer.update(clock.getDelta()); +} +``` + +### Bone Attachments + +```javascript +// Attach object to bone +const weapon = new THREE.Mesh(weaponGeometry, weaponMaterial); +const handBone = skeleton.bones.find((b) => b.name === "RightHand"); +if (handBone) handBone.add(weapon); + +// Offset attachment +weapon.position.set(0, 0, 0.5); +weapon.rotation.set(0, Math.PI / 2, 0); +``` + +## Morph Targets + +Blend between different mesh shapes. + +```javascript +// Morph targets are stored in geometry +const geometry = mesh.geometry; +console.log("Morph attributes:", Object.keys(geometry.morphAttributes)); + +// Access morph target influences +mesh.morphTargetInfluences; // Array of weights +mesh.morphTargetDictionary; // Name -> index mapping + +// Set morph target by index +mesh.morphTargetInfluences[0] = 0.5; + +// Set by name +const smileIndex = mesh.morphTargetDictionary["smile"]; +mesh.morphTargetInfluences[smileIndex] = 1; +``` + +### Animating Morph Targets + +```javascript +// Procedural +function animate() { + const t = clock.getElapsedTime(); + mesh.morphTargetInfluences[0] = (Math.sin(t) + 1) / 2; +} + +// With keyframe animation +const track = new THREE.NumberKeyframeTrack( + ".morphTargetInfluences[smile]", + [0, 0.5, 1], + [0, 1, 0], +); +const clip = new THREE.AnimationClip("smile", 1, [track]); +mixer.clipAction(clip).play(); +``` + +## Animation Blending + +Mix multiple animations together. + +```javascript +// Setup actions +const idleAction = mixer.clipAction(idleClip); +const walkAction = mixer.clipAction(walkClip); +const runAction = mixer.clipAction(runClip); + +// Play all with different weights +idleAction.play(); +walkAction.play(); +runAction.play(); + +// Set initial weights +idleAction.setEffectiveWeight(1); +walkAction.setEffectiveWeight(0); +runAction.setEffectiveWeight(0); + +// Blend based on speed +function updateAnimations(speed) { + if (speed < 0.1) { + idleAction.setEffectiveWeight(1); + walkAction.setEffectiveWeight(0); + runAction.setEffectiveWeight(0); + } else if (speed < 5) { + const t = speed / 5; + idleAction.setEffectiveWeight(1 - t); + walkAction.setEffectiveWeight(t); + runAction.setEffectiveWeight(0); + } else { + const t = Math.min((speed - 5) / 5, 1); + idleAction.setEffectiveWeight(0); + walkAction.setEffectiveWeight(1 - t); + runAction.setEffectiveWeight(t); + } +} +``` + +### Additive Blending + +```javascript +// Base pose +const baseAction = mixer.clipAction(baseClip); +baseAction.play(); + +// Additive layer (e.g., breathing) +const additiveAction = mixer.clipAction(additiveClip); +additiveAction.blendMode = THREE.AdditiveAnimationBlendMode; +additiveAction.play(); + +// Convert clip to additive +THREE.AnimationUtils.makeClipAdditive(additiveClip); +``` + +## Animation Utilities + +```javascript +import * as THREE from "three"; + +// Find clip by name +const clip = THREE.AnimationClip.findByName(clips, "Walk"); + +// Create subclip +const subclip = THREE.AnimationUtils.subclip(clip, "subclip", 0, 30, 30); + +// Convert to additive +THREE.AnimationUtils.makeClipAdditive(clip); +THREE.AnimationUtils.makeClipAdditive(clip, 0, referenceClip); + +// Clone clip +const clone = clip.clone(); + +// Get clip duration +clip.duration; + +// Optimize clip (remove redundant keyframes) +clip.optimize(); + +// Reset clip to first frame +clip.resetDuration(); +``` + +## Procedural Animation Patterns + +### Smooth Damping + +```javascript +// Smooth follow/lerp +const target = new THREE.Vector3(); +const current = new THREE.Vector3(); +const velocity = new THREE.Vector3(); + +function smoothDamp(current, target, velocity, smoothTime, deltaTime) { + const omega = 2 / smoothTime; + const x = omega * deltaTime; + const exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x); + const change = current.clone().sub(target); + const temp = velocity + .clone() + .add(change.clone().multiplyScalar(omega)) + .multiplyScalar(deltaTime); + velocity.sub(temp.clone().multiplyScalar(omega)).multiplyScalar(exp); + return target.clone().add(change.add(temp).multiplyScalar(exp)); +} + +function animate() { + current.copy(smoothDamp(current, target, velocity, 0.3, delta)); + mesh.position.copy(current); +} +``` + +### Spring Physics + +```javascript +class Spring { + constructor(stiffness = 100, damping = 10) { + this.stiffness = stiffness; + this.damping = damping; + this.position = 0; + this.velocity = 0; + this.target = 0; + } + + update(dt) { + const force = -this.stiffness * (this.position - this.target); + const dampingForce = -this.damping * this.velocity; + this.velocity += (force + dampingForce) * dt; + this.position += this.velocity * dt; + return this.position; + } +} + +const spring = new Spring(100, 10); +spring.target = 1; + +function animate() { + mesh.position.y = spring.update(delta); +} +``` + +### Oscillation + +```javascript +function animate() { + const t = clock.getElapsedTime(); + + // Sine wave + mesh.position.y = Math.sin(t * 2) * 0.5; + + // Bouncing + mesh.position.y = Math.abs(Math.sin(t * 3)) * 2; + + // Circular motion + mesh.position.x = Math.cos(t) * 2; + mesh.position.z = Math.sin(t) * 2; + + // Figure 8 + mesh.position.x = Math.sin(t) * 2; + mesh.position.z = Math.sin(t * 2) * 1; +} +``` + +## Performance Tips + +1. **Share clips**: Same AnimationClip can be used on multiple mixers +2. **Optimize clips**: Call `clip.optimize()` to remove redundant keyframes +3. **Disable when off-screen**: Stop mixer updates for invisible objects +4. **Use LOD for animations**: Simpler rigs for distant characters +5. **Limit active mixers**: Each mixer.update() has a cost + +```javascript +// Pause animation when not visible +mesh.onBeforeRender = () => { + action.paused = false; +}; + +mesh.onAfterRender = () => { + // Check if will be visible next frame + if (!isInFrustum(mesh)) { + action.paused = true; + } +}; + +// Cache clips +const clipCache = new Map(); +function getClip(name) { + if (!clipCache.has(name)) { + clipCache.set(name, loadClip(name)); + } + return clipCache.get(name); +} +``` + +## See Also + +- `threejs-loaders` - Loading animated GLTF models +- `threejs-fundamentals` - Clock and animation loop +- `threejs-shaders` - Vertex animation in shaders diff --git a/.codex/skills/threejs-fundamentals/SKILL.md b/.codex/skills/threejs-fundamentals/SKILL.md new file mode 100644 index 0000000..21b6341 --- /dev/null +++ b/.codex/skills/threejs-fundamentals/SKILL.md @@ -0,0 +1,488 @@ +--- +name: threejs-fundamentals +description: Three.js scene setup, cameras, renderer, Object3D hierarchy, coordinate systems. Use when setting up 3D scenes, creating cameras, configuring renderers, managing object hierarchies, or working with transforms. +--- + +# Three.js Fundamentals + +## Quick Start + +```javascript +import * as THREE from "three"; + +// Create scene, camera, renderer +const scene = new THREE.Scene(); +const camera = new THREE.PerspectiveCamera( + 75, + window.innerWidth / window.innerHeight, + 0.1, + 1000, +); +const renderer = new THREE.WebGLRenderer({ antialias: true }); + +renderer.setSize(window.innerWidth, window.innerHeight); +renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); +document.body.appendChild(renderer.domElement); + +// Add a mesh +const geometry = new THREE.BoxGeometry(1, 1, 1); +const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 }); +const cube = new THREE.Mesh(geometry, material); +scene.add(cube); + +// Add light +scene.add(new THREE.AmbientLight(0xffffff, 0.5)); +const dirLight = new THREE.DirectionalLight(0xffffff, 1); +dirLight.position.set(5, 5, 5); +scene.add(dirLight); + +camera.position.z = 5; + +// Animation loop +function animate() { + requestAnimationFrame(animate); + cube.rotation.x += 0.01; + cube.rotation.y += 0.01; + renderer.render(scene, camera); +} +animate(); + +// Handle resize +window.addEventListener("resize", () => { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + renderer.setSize(window.innerWidth, window.innerHeight); +}); +``` + +## Core Classes + +### Scene + +Container for all 3D objects, lights, and cameras. + +```javascript +const scene = new THREE.Scene(); +scene.background = new THREE.Color(0x000000); // Solid color +scene.background = texture; // Skybox texture +scene.background = cubeTexture; // Cubemap +scene.environment = envMap; // Environment map for PBR +scene.fog = new THREE.Fog(0xffffff, 1, 100); // Linear fog +scene.fog = new THREE.FogExp2(0xffffff, 0.02); // Exponential fog +``` + +### Cameras + +**PerspectiveCamera** - Most common, simulates human eye. + +```javascript +// PerspectiveCamera(fov, aspect, near, far) +const camera = new THREE.PerspectiveCamera( + 75, // Field of view (degrees) + window.innerWidth / window.innerHeight, // Aspect ratio + 0.1, // Near clipping plane + 1000, // Far clipping plane +); + +camera.position.set(0, 5, 10); +camera.lookAt(0, 0, 0); +camera.updateProjectionMatrix(); // Call after changing fov, aspect, near, far +``` + +**OrthographicCamera** - No perspective distortion, good for 2D/isometric. + +```javascript +// OrthographicCamera(left, right, top, bottom, near, far) +const aspect = window.innerWidth / window.innerHeight; +const frustumSize = 10; +const camera = new THREE.OrthographicCamera( + (frustumSize * aspect) / -2, + (frustumSize * aspect) / 2, + frustumSize / 2, + frustumSize / -2, + 0.1, + 1000, +); +``` + +**ArrayCamera** - Multiple viewports with sub-cameras. + +```javascript +const cameras = []; +for (let i = 0; i < 4; i++) { + const subcamera = new THREE.PerspectiveCamera(40, 1, 0.1, 100); + subcamera.viewport = new THREE.Vector4( + Math.floor(i % 2) * 0.5, + Math.floor(i / 2) * 0.5, + 0.5, + 0.5, + ); + cameras.push(subcamera); +} +const arrayCamera = new THREE.ArrayCamera(cameras); +``` + +**CubeCamera** - Renders environment maps for reflections. + +```javascript +const cubeRenderTarget = new THREE.WebGLCubeRenderTarget(256); +const cubeCamera = new THREE.CubeCamera(0.1, 1000, cubeRenderTarget); +scene.add(cubeCamera); + +// Use for reflections +material.envMap = cubeRenderTarget.texture; + +// Update each frame (expensive!) +cubeCamera.position.copy(reflectiveMesh.position); +cubeCamera.update(renderer, scene); +``` + +### WebGLRenderer + +```javascript +const renderer = new THREE.WebGLRenderer({ + canvas: document.querySelector("#canvas"), // Optional existing canvas + antialias: true, // Smooth edges + alpha: true, // Transparent background + powerPreference: "high-performance", // GPU hint + preserveDrawingBuffer: true, // For screenshots +}); + +renderer.setSize(width, height); +renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); + +// Tone mapping +renderer.toneMapping = THREE.ACESFilmicToneMapping; +renderer.toneMappingExposure = 1.0; + +// Color space (Three.js r152+) +renderer.outputColorSpace = THREE.SRGBColorSpace; + +// Shadows +renderer.shadowMap.enabled = true; +renderer.shadowMap.type = THREE.PCFSoftShadowMap; + +// Clear color +renderer.setClearColor(0x000000, 1); + +// Render +renderer.render(scene, camera); +``` + +### Object3D + +Base class for all 3D objects. Mesh, Group, Light, Camera all extend Object3D. + +```javascript +const obj = new THREE.Object3D(); + +// Transform +obj.position.set(x, y, z); +obj.rotation.set(x, y, z); // Euler angles (radians) +obj.quaternion.set(x, y, z, w); // Quaternion rotation +obj.scale.set(x, y, z); + +// Local vs World transforms +obj.getWorldPosition(targetVector); +obj.getWorldQuaternion(targetQuaternion); +obj.getWorldDirection(targetVector); + +// Hierarchy +obj.add(child); +obj.remove(child); +obj.parent; +obj.children; + +// Visibility +obj.visible = false; + +// Layers (for selective rendering/raycasting) +obj.layers.set(1); +obj.layers.enable(2); +obj.layers.disable(0); + +// Traverse hierarchy +obj.traverse((child) => { + if (child.isMesh) child.material.color.set(0xff0000); +}); + +// Matrix updates +obj.matrixAutoUpdate = true; // Default: auto-update matrices +obj.updateMatrix(); // Manual matrix update +obj.updateMatrixWorld(true); // Update world matrix recursively +``` + +### Group + +Empty container for organizing objects. + +```javascript +const group = new THREE.Group(); +group.add(mesh1); +group.add(mesh2); +scene.add(group); + +// Transform entire group +group.position.x = 5; +group.rotation.y = Math.PI / 4; +``` + +### Mesh + +Combines geometry and material. + +```javascript +const mesh = new THREE.Mesh(geometry, material); + +// Multiple materials (one per geometry group) +const mesh = new THREE.Mesh(geometry, [material1, material2]); + +// Useful properties +mesh.geometry; +mesh.material; +mesh.castShadow = true; +mesh.receiveShadow = true; + +// Frustum culling +mesh.frustumCulled = true; // Default: skip if outside camera view + +// Render order +mesh.renderOrder = 10; // Higher = rendered later +``` + +## Coordinate System + +Three.js uses a **right-handed coordinate system**: + +- **+X** points right +- **+Y** points up +- **+Z** points toward viewer (out of screen) + +```javascript +// Axes helper +const axesHelper = new THREE.AxesHelper(5); +scene.add(axesHelper); // Red=X, Green=Y, Blue=Z +``` + +## Math Utilities + +### Vector3 + +```javascript +const v = new THREE.Vector3(x, y, z); +v.set(x, y, z); +v.copy(otherVector); +v.clone(); + +// Operations (modify in place) +v.add(v2); +v.sub(v2); +v.multiply(v2); +v.multiplyScalar(2); +v.divideScalar(2); +v.normalize(); +v.negate(); +v.clamp(min, max); +v.lerp(target, alpha); + +// Calculations (return new value) +v.length(); +v.lengthSq(); // Faster than length() +v.distanceTo(v2); +v.dot(v2); +v.cross(v2); // Modifies v +v.angleTo(v2); + +// Transform +v.applyMatrix4(matrix); +v.applyQuaternion(q); +v.project(camera); // World to NDC +v.unproject(camera); // NDC to world +``` + +### Matrix4 + +```javascript +const m = new THREE.Matrix4(); +m.identity(); +m.copy(other); +m.clone(); + +// Build transforms +m.makeTranslation(x, y, z); +m.makeRotationX(theta); +m.makeRotationY(theta); +m.makeRotationZ(theta); +m.makeRotationFromQuaternion(q); +m.makeScale(x, y, z); + +// Compose/decompose +m.compose(position, quaternion, scale); +m.decompose(position, quaternion, scale); + +// Operations +m.multiply(m2); // m = m * m2 +m.premultiply(m2); // m = m2 * m +m.invert(); +m.transpose(); + +// Camera matrices +m.makePerspective(left, right, top, bottom, near, far); +m.makeOrthographic(left, right, top, bottom, near, far); +m.lookAt(eye, target, up); +``` + +### Quaternion + +```javascript +const q = new THREE.Quaternion(); +q.setFromEuler(euler); +q.setFromAxisAngle(axis, angle); +q.setFromRotationMatrix(matrix); + +q.multiply(q2); +q.slerp(target, t); // Spherical interpolation +q.normalize(); +q.invert(); +``` + +### Euler + +```javascript +const euler = new THREE.Euler(x, y, z, "XYZ"); // Order matters! +euler.setFromQuaternion(q); +euler.setFromRotationMatrix(m); + +// Rotation orders: 'XYZ', 'YXZ', 'ZXY', 'XZY', 'YZX', 'ZYX' +``` + +### Color + +```javascript +const color = new THREE.Color(0xff0000); +const color = new THREE.Color("red"); +const color = new THREE.Color("rgb(255, 0, 0)"); +const color = new THREE.Color("#ff0000"); + +color.setHex(0x00ff00); +color.setRGB(r, g, b); // 0-1 range +color.setHSL(h, s, l); // 0-1 range + +color.lerp(otherColor, alpha); +color.multiply(otherColor); +color.multiplyScalar(2); +``` + +### MathUtils + +```javascript +THREE.MathUtils.clamp(value, min, max); +THREE.MathUtils.lerp(start, end, alpha); +THREE.MathUtils.mapLinear(value, inMin, inMax, outMin, outMax); +THREE.MathUtils.degToRad(degrees); +THREE.MathUtils.radToDeg(radians); +THREE.MathUtils.randFloat(min, max); +THREE.MathUtils.randInt(min, max); +THREE.MathUtils.smoothstep(x, min, max); +THREE.MathUtils.smootherstep(x, min, max); +``` + +## Common Patterns + +### Proper Cleanup + +```javascript +function dispose() { + // Dispose geometries + mesh.geometry.dispose(); + + // Dispose materials + if (Array.isArray(mesh.material)) { + mesh.material.forEach((m) => m.dispose()); + } else { + mesh.material.dispose(); + } + + // Dispose textures + texture.dispose(); + + // Remove from scene + scene.remove(mesh); + + // Dispose renderer + renderer.dispose(); +} +``` + +### Clock for Animation + +```javascript +const clock = new THREE.Clock(); + +function animate() { + const delta = clock.getDelta(); // Time since last frame (seconds) + const elapsed = clock.getElapsedTime(); // Total time (seconds) + + mesh.rotation.y += delta * 0.5; // Consistent speed regardless of framerate + + requestAnimationFrame(animate); + renderer.render(scene, camera); +} +``` + +### Responsive Canvas + +```javascript +function onWindowResize() { + const width = window.innerWidth; + const height = window.innerHeight; + + camera.aspect = width / height; + camera.updateProjectionMatrix(); + + renderer.setSize(width, height); + renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); +} +window.addEventListener("resize", onWindowResize); +``` + +### Loading Manager + +```javascript +const manager = new THREE.LoadingManager(); + +manager.onStart = (url, loaded, total) => console.log("Started loading"); +manager.onLoad = () => console.log("All loaded"); +manager.onProgress = (url, loaded, total) => console.log(`${loaded}/${total}`); +manager.onError = (url) => console.error(`Error loading ${url}`); + +const textureLoader = new THREE.TextureLoader(manager); +const gltfLoader = new GLTFLoader(manager); +``` + +## Performance Tips + +1. **Limit draw calls**: Merge geometries, use instancing, atlas textures +2. **Frustum culling**: Enabled by default, ensure bounding boxes are correct +3. **LOD (Level of Detail)**: Use `THREE.LOD` for distance-based mesh switching +4. **Object pooling**: Reuse objects instead of creating/destroying +5. **Avoid `getWorldPosition` in loops**: Cache results + +```javascript +// Merge static geometries +import { mergeGeometries } from "three/examples/jsm/utils/BufferGeometryUtils.js"; +const merged = mergeGeometries([geo1, geo2, geo3]); + +// LOD +const lod = new THREE.LOD(); +lod.addLevel(highDetailMesh, 0); +lod.addLevel(medDetailMesh, 50); +lod.addLevel(lowDetailMesh, 100); +scene.add(lod); +``` + +## See Also + +- `threejs-geometry` - Geometry creation and manipulation +- `threejs-materials` - Material types and properties +- `threejs-lighting` - Light types and shadows diff --git a/.codex/skills/threejs-geometry/SKILL.md b/.codex/skills/threejs-geometry/SKILL.md new file mode 100644 index 0000000..e077e2d --- /dev/null +++ b/.codex/skills/threejs-geometry/SKILL.md @@ -0,0 +1,548 @@ +--- +name: threejs-geometry +description: Three.js geometry creation - built-in shapes, BufferGeometry, custom geometry, instancing. Use when creating 3D shapes, working with vertices, building custom meshes, or optimizing with instanced rendering. +--- + +# Three.js Geometry + +## Quick Start + +```javascript +import * as THREE from "three"; + +// Built-in geometry +const box = new THREE.BoxGeometry(1, 1, 1); +const sphere = new THREE.SphereGeometry(0.5, 32, 32); +const plane = new THREE.PlaneGeometry(10, 10); + +// Create mesh +const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 }); +const mesh = new THREE.Mesh(box, material); +scene.add(mesh); +``` + +## Built-in Geometries + +### Basic Shapes + +```javascript +// Box - width, height, depth, widthSegments, heightSegments, depthSegments +new THREE.BoxGeometry(1, 1, 1, 1, 1, 1); + +// Sphere - radius, widthSegments, heightSegments, phiStart, phiLength, thetaStart, thetaLength +new THREE.SphereGeometry(1, 32, 32); +new THREE.SphereGeometry(1, 32, 32, 0, Math.PI * 2, 0, Math.PI); // Full sphere +new THREE.SphereGeometry(1, 32, 32, 0, Math.PI); // Hemisphere + +// Plane - width, height, widthSegments, heightSegments +new THREE.PlaneGeometry(10, 10, 1, 1); + +// Circle - radius, segments, thetaStart, thetaLength +new THREE.CircleGeometry(1, 32); +new THREE.CircleGeometry(1, 32, 0, Math.PI); // Semicircle + +// Cylinder - radiusTop, radiusBottom, height, radialSegments, heightSegments, openEnded +new THREE.CylinderGeometry(1, 1, 2, 32, 1, false); +new THREE.CylinderGeometry(0, 1, 2, 32); // Cone +new THREE.CylinderGeometry(1, 1, 2, 6); // Hexagonal prism + +// Cone - radius, height, radialSegments, heightSegments, openEnded +new THREE.ConeGeometry(1, 2, 32, 1, false); + +// Torus - radius, tube, radialSegments, tubularSegments, arc +new THREE.TorusGeometry(1, 0.4, 16, 100); + +// TorusKnot - radius, tube, tubularSegments, radialSegments, p, q +new THREE.TorusKnotGeometry(1, 0.4, 100, 16, 2, 3); + +// Ring - innerRadius, outerRadius, thetaSegments, phiSegments +new THREE.RingGeometry(0.5, 1, 32, 1); +``` + +### Advanced Shapes + +```javascript +// Capsule - radius, length, capSegments, radialSegments +new THREE.CapsuleGeometry(0.5, 1, 4, 8); + +// Dodecahedron - radius, detail +new THREE.DodecahedronGeometry(1, 0); + +// Icosahedron - radius, detail (0 = 20 faces, higher = smoother) +new THREE.IcosahedronGeometry(1, 0); + +// Octahedron - radius, detail +new THREE.OctahedronGeometry(1, 0); + +// Tetrahedron - radius, detail +new THREE.TetrahedronGeometry(1, 0); + +// Polyhedron - vertices, indices, radius, detail +const vertices = [1, 1, 1, -1, -1, 1, -1, 1, -1, 1, -1, -1]; +const indices = [2, 1, 0, 0, 3, 2, 1, 3, 0, 2, 3, 1]; +new THREE.PolyhedronGeometry(vertices, indices, 1, 0); +``` + +### Path-Based Shapes + +```javascript +// Lathe - points[], segments, phiStart, phiLength +const points = [ + new THREE.Vector2(0, 0), + new THREE.Vector2(0.5, 0), + new THREE.Vector2(0.5, 1), + new THREE.Vector2(0, 1), +]; +new THREE.LatheGeometry(points, 32); + +// Extrude - shape, options +const shape = new THREE.Shape(); +shape.moveTo(0, 0); +shape.lineTo(1, 0); +shape.lineTo(1, 1); +shape.lineTo(0, 1); +shape.lineTo(0, 0); + +const extrudeSettings = { + steps: 2, + depth: 1, + bevelEnabled: true, + bevelThickness: 0.1, + bevelSize: 0.1, + bevelSegments: 3, +}; +new THREE.ExtrudeGeometry(shape, extrudeSettings); + +// Tube - path, tubularSegments, radius, radialSegments, closed +const curve = new THREE.CatmullRomCurve3([ + new THREE.Vector3(-1, 0, 0), + new THREE.Vector3(0, 1, 0), + new THREE.Vector3(1, 0, 0), +]); +new THREE.TubeGeometry(curve, 64, 0.2, 8, false); +``` + +### Text Geometry + +```javascript +import { FontLoader } from "three/examples/jsm/loaders/FontLoader.js"; +import { TextGeometry } from "three/examples/jsm/geometries/TextGeometry.js"; + +const loader = new FontLoader(); +loader.load("fonts/helvetiker_regular.typeface.json", (font) => { + const geometry = new TextGeometry("Hello", { + font: font, + size: 1, + depth: 0.2, // Was 'height' in older versions + curveSegments: 12, + bevelEnabled: true, + bevelThickness: 0.03, + bevelSize: 0.02, + bevelSegments: 5, + }); + + // Center text + geometry.computeBoundingBox(); + geometry.center(); + + const mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); +}); +``` + +## BufferGeometry + +The base class for all geometries. Stores data as typed arrays for GPU efficiency. + +### Custom BufferGeometry + +```javascript +const geometry = new THREE.BufferGeometry(); + +// Vertices (3 floats per vertex: x, y, z) +const vertices = new Float32Array([ + -1, + -1, + 0, // vertex 0 + 1, + -1, + 0, // vertex 1 + 1, + 1, + 0, // vertex 2 + -1, + 1, + 0, // vertex 3 +]); +geometry.setAttribute("position", new THREE.BufferAttribute(vertices, 3)); + +// Indices (for indexed geometry - reuse vertices) +const indices = new Uint16Array([ + 0, + 1, + 2, // triangle 1 + 0, + 2, + 3, // triangle 2 +]); +geometry.setIndex(new THREE.BufferAttribute(indices, 1)); + +// Normals (required for lighting) +const normals = new Float32Array([0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1]); +geometry.setAttribute("normal", new THREE.BufferAttribute(normals, 3)); + +// UVs (for texturing) +const uvs = new Float32Array([0, 0, 1, 0, 1, 1, 0, 1]); +geometry.setAttribute("uv", new THREE.BufferAttribute(uvs, 2)); + +// Colors (per-vertex colors) +const colors = new Float32Array([ + 1, + 0, + 0, // red + 0, + 1, + 0, // green + 0, + 0, + 1, // blue + 1, + 1, + 0, // yellow +]); +geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3)); +// Use with: material.vertexColors = true +``` + +### BufferAttribute Types + +```javascript +// Common attribute types +new THREE.BufferAttribute(array, itemSize); + +// Typed array options +new Float32Array(count * itemSize); // Positions, normals, UVs +new Uint16Array(count); // Indices (up to 65535 vertices) +new Uint32Array(count); // Indices (larger meshes) +new Uint8Array(count * itemSize); // Colors (0-255 range) + +// Item sizes +// Position: 3 (x, y, z) +// Normal: 3 (x, y, z) +// UV: 2 (u, v) +// Color: 3 (r, g, b) or 4 (r, g, b, a) +// Index: 1 +``` + +### Modifying BufferGeometry + +```javascript +const positions = geometry.attributes.position; + +// Modify vertex +positions.setXYZ(index, x, y, z); + +// Access vertex +const x = positions.getX(index); +const y = positions.getY(index); +const z = positions.getZ(index); + +// Flag for GPU update +positions.needsUpdate = true; + +// Recompute normals after position changes +geometry.computeVertexNormals(); + +// Recompute bounding box/sphere after changes +geometry.computeBoundingBox(); +geometry.computeBoundingSphere(); +``` + +### Interleaved Buffers (Advanced) + +```javascript +// More efficient memory layout for large meshes +const interleavedBuffer = new THREE.InterleavedBuffer( + new Float32Array([ + // pos.x, pos.y, pos.z, uv.u, uv.v (repeated per vertex) + -1, -1, 0, 0, 0, 1, -1, 0, 1, 0, 1, 1, 0, 1, 1, -1, 1, 0, 0, 1, + ]), + 5, // stride (floats per vertex) +); + +geometry.setAttribute( + "position", + new THREE.InterleavedBufferAttribute(interleavedBuffer, 3, 0), +); // size 3, offset 0 +geometry.setAttribute( + "uv", + new THREE.InterleavedBufferAttribute(interleavedBuffer, 2, 3), +); // size 2, offset 3 +``` + +## EdgesGeometry & WireframeGeometry + +```javascript +// Edge lines (only hard edges) +const edges = new THREE.EdgesGeometry(boxGeometry, 15); // 15 = threshold angle +const edgeMesh = new THREE.LineSegments( + edges, + new THREE.LineBasicMaterial({ color: 0xffffff }), +); + +// Wireframe (all triangles) +const wireframe = new THREE.WireframeGeometry(boxGeometry); +const wireMesh = new THREE.LineSegments( + wireframe, + new THREE.LineBasicMaterial({ color: 0xffffff }), +); +``` + +## Points + +```javascript +// Create point cloud +const geometry = new THREE.BufferGeometry(); +const positions = new Float32Array(1000 * 3); + +for (let i = 0; i < 1000; i++) { + positions[i * 3] = (Math.random() - 0.5) * 10; + positions[i * 3 + 1] = (Math.random() - 0.5) * 10; + positions[i * 3 + 2] = (Math.random() - 0.5) * 10; +} + +geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3)); + +const material = new THREE.PointsMaterial({ + size: 0.1, + sizeAttenuation: true, // Size decreases with distance + color: 0xffffff, +}); + +const points = new THREE.Points(geometry, material); +scene.add(points); +``` + +## Lines + +```javascript +// Line (connected points) +const points = [ + new THREE.Vector3(-1, 0, 0), + new THREE.Vector3(0, 1, 0), + new THREE.Vector3(1, 0, 0), +]; +const geometry = new THREE.BufferGeometry().setFromPoints(points); +const line = new THREE.Line( + geometry, + new THREE.LineBasicMaterial({ color: 0xff0000 }), +); + +// LineLoop (closed loop) +const loop = new THREE.LineLoop(geometry, material); + +// LineSegments (pairs of points) +const segmentsGeometry = new THREE.BufferGeometry(); +segmentsGeometry.setAttribute( + "position", + new THREE.BufferAttribute( + new Float32Array([ + -1, + 0, + 0, + 0, + 1, + 0, // segment 1 + 0, + 1, + 0, + 1, + 0, + 0, // segment 2 + ]), + 3, + ), +); +const segments = new THREE.LineSegments(segmentsGeometry, material); +``` + +## InstancedMesh + +Efficiently render many copies of the same geometry. + +```javascript +const geometry = new THREE.BoxGeometry(1, 1, 1); +const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 }); +const count = 1000; + +const instancedMesh = new THREE.InstancedMesh(geometry, material, count); + +// Set transforms for each instance +const dummy = new THREE.Object3D(); +const matrix = new THREE.Matrix4(); + +for (let i = 0; i < count; i++) { + dummy.position.set( + (Math.random() - 0.5) * 20, + (Math.random() - 0.5) * 20, + (Math.random() - 0.5) * 20, + ); + dummy.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, 0); + dummy.scale.setScalar(0.5 + Math.random()); + dummy.updateMatrix(); + + instancedMesh.setMatrixAt(i, dummy.matrix); +} + +// Flag for GPU update +instancedMesh.instanceMatrix.needsUpdate = true; + +// Optional: per-instance colors +instancedMesh.instanceColor = new THREE.InstancedBufferAttribute( + new Float32Array(count * 3), + 3, +); +for (let i = 0; i < count; i++) { + instancedMesh.setColorAt( + i, + new THREE.Color(Math.random(), Math.random(), Math.random()), + ); +} +instancedMesh.instanceColor.needsUpdate = true; + +scene.add(instancedMesh); +``` + +### Update Instance at Runtime + +```javascript +// Update single instance +const matrix = new THREE.Matrix4(); +instancedMesh.getMatrixAt(index, matrix); +// Modify matrix... +instancedMesh.setMatrixAt(index, matrix); +instancedMesh.instanceMatrix.needsUpdate = true; + +// Raycasting with instanced mesh +const intersects = raycaster.intersectObject(instancedMesh); +if (intersects.length > 0) { + const instanceId = intersects[0].instanceId; +} +``` + +## InstancedBufferGeometry (Advanced) + +For custom per-instance attributes beyond transform/color. + +```javascript +const geometry = new THREE.InstancedBufferGeometry(); +geometry.copy(new THREE.BoxGeometry(1, 1, 1)); + +// Add per-instance attribute +const offsets = new Float32Array(count * 3); +for (let i = 0; i < count; i++) { + offsets[i * 3] = Math.random() * 10; + offsets[i * 3 + 1] = Math.random() * 10; + offsets[i * 3 + 2] = Math.random() * 10; +} +geometry.setAttribute("offset", new THREE.InstancedBufferAttribute(offsets, 3)); + +// Use in shader +// attribute vec3 offset; +// vec3 transformed = position + offset; +``` + +## Geometry Utilities + +```javascript +import * as BufferGeometryUtils from "three/examples/jsm/utils/BufferGeometryUtils.js"; + +// Merge geometries (must have same attributes) +const merged = BufferGeometryUtils.mergeGeometries([geo1, geo2, geo3]); + +// Merge with groups (for multi-material) +const merged = BufferGeometryUtils.mergeGeometries([geo1, geo2], true); + +// Compute tangents (required for normal maps) +BufferGeometryUtils.computeTangents(geometry); + +// Interleave attributes for better performance +const interleaved = BufferGeometryUtils.interleaveAttributes([ + geometry.attributes.position, + geometry.attributes.normal, + geometry.attributes.uv, +]); +``` + +## Common Patterns + +### Center Geometry + +```javascript +geometry.computeBoundingBox(); +geometry.center(); // Move vertices so center is at origin +``` + +### Scale to Fit + +```javascript +geometry.computeBoundingBox(); +const size = new THREE.Vector3(); +geometry.boundingBox.getSize(size); +const maxDim = Math.max(size.x, size.y, size.z); +geometry.scale(1 / maxDim, 1 / maxDim, 1 / maxDim); +``` + +### Clone and Transform + +```javascript +const clone = geometry.clone(); +clone.rotateX(Math.PI / 2); +clone.translate(0, 1, 0); +clone.scale(2, 2, 2); +``` + +### Morph Targets + +```javascript +// Base geometry +const geometry = new THREE.BoxGeometry(1, 1, 1, 4, 4, 4); + +// Create morph target +const morphPositions = geometry.attributes.position.array.slice(); +for (let i = 0; i < morphPositions.length; i += 3) { + morphPositions[i] *= 2; // Scale X + morphPositions[i + 1] *= 0.5; // Squash Y +} + +geometry.morphAttributes.position = [ + new THREE.BufferAttribute(new Float32Array(morphPositions), 3), +]; + +const mesh = new THREE.Mesh(geometry, material); +mesh.morphTargetInfluences[0] = 0.5; // 50% blend +``` + +## Performance Tips + +1. **Use indexed geometry**: Reuse vertices with indices +2. **Merge static meshes**: Reduce draw calls with `mergeGeometries` +3. **Use InstancedMesh**: For many identical objects +4. **Choose appropriate segment counts**: More segments = smoother but slower +5. **Dispose unused geometry**: `geometry.dispose()` + +```javascript +// Good segment counts for common uses +new THREE.SphereGeometry(1, 32, 32); // Good quality +new THREE.SphereGeometry(1, 64, 64); // High quality +new THREE.SphereGeometry(1, 16, 16); // Performance mode + +// Dispose when done +geometry.dispose(); +``` + +## See Also + +- `threejs-fundamentals` - Scene setup and Object3D +- `threejs-materials` - Material types for meshes +- `threejs-shaders` - Custom vertex manipulation diff --git a/.codex/skills/threejs-interaction/SKILL.md b/.codex/skills/threejs-interaction/SKILL.md new file mode 100644 index 0000000..9c93424 --- /dev/null +++ b/.codex/skills/threejs-interaction/SKILL.md @@ -0,0 +1,660 @@ +--- +name: threejs-interaction +description: Three.js interaction - raycasting, controls, mouse/touch input, object selection. Use when handling user input, implementing click detection, adding camera controls, or creating interactive 3D experiences. +--- + +# Three.js Interaction + +## Quick Start + +```javascript +import * as THREE from "three"; +import { OrbitControls } from "three/addons/controls/OrbitControls.js"; + +// Camera controls +const controls = new OrbitControls(camera, renderer.domElement); +controls.enableDamping = true; + +// Raycasting for click detection +const raycaster = new THREE.Raycaster(); +const mouse = new THREE.Vector2(); + +function onClick(event) { + mouse.x = (event.clientX / window.innerWidth) * 2 - 1; + mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; + + raycaster.setFromCamera(mouse, camera); + const intersects = raycaster.intersectObjects(scene.children); + + if (intersects.length > 0) { + console.log("Clicked:", intersects[0].object); + } +} + +window.addEventListener("click", onClick); +``` + +## Raycaster + +### Basic Raycasting + +```javascript +const raycaster = new THREE.Raycaster(); + +// From camera (mouse picking) +raycaster.setFromCamera(mousePosition, camera); + +// From any origin and direction +raycaster.set(origin, direction); // origin: Vector3, direction: normalized Vector3 + +// Get intersections +const intersects = raycaster.intersectObjects(objects, recursive); + +// intersects array contains: +// { +// distance: number, // Distance from ray origin +// point: Vector3, // Intersection point in world coords +// face: Face3, // Intersected face +// faceIndex: number, // Face index +// object: Object3D, // Intersected object +// uv: Vector2, // UV coordinates at intersection +// uv1: Vector2, // Second UV channel +// normal: Vector3, // Interpolated face normal +// instanceId: number // For InstancedMesh +// } +``` + +### Mouse Position Conversion + +```javascript +const mouse = new THREE.Vector2(); + +function updateMouse(event) { + // For full window + mouse.x = (event.clientX / window.innerWidth) * 2 - 1; + mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; +} + +// For specific canvas element +function updateMouseCanvas(event, canvas) { + const rect = canvas.getBoundingClientRect(); + mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; + mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; +} +``` + +### Touch Support + +```javascript +function onTouchStart(event) { + event.preventDefault(); + + if (event.touches.length === 1) { + const touch = event.touches[0]; + mouse.x = (touch.clientX / window.innerWidth) * 2 - 1; + mouse.y = -(touch.clientY / window.innerHeight) * 2 + 1; + + raycaster.setFromCamera(mouse, camera); + const intersects = raycaster.intersectObjects(clickableObjects); + + if (intersects.length > 0) { + handleSelection(intersects[0]); + } + } +} + +renderer.domElement.addEventListener("touchstart", onTouchStart); +``` + +### Raycaster Options + +```javascript +const raycaster = new THREE.Raycaster(); + +// Near/far clipping (default: 0, Infinity) +raycaster.near = 0; +raycaster.far = 100; + +// Line/Points precision +raycaster.params.Line.threshold = 0.1; +raycaster.params.Points.threshold = 0.1; + +// Layers (only intersect objects on specific layers) +raycaster.layers.set(1); +``` + +### Efficient Raycasting + +```javascript +// Only check specific objects +const clickables = [mesh1, mesh2, mesh3]; +const intersects = raycaster.intersectObjects(clickables, false); + +// Use layers for filtering +mesh1.layers.set(1); // Clickable layer +raycaster.layers.set(1); + +// Throttle raycast for hover effects +let lastRaycast = 0; +function onMouseMove(event) { + const now = Date.now(); + if (now - lastRaycast < 50) return; // 20fps max + lastRaycast = now; + + // Raycast here +} +``` + +## Camera Controls + +### OrbitControls + +```javascript +import { OrbitControls } from "three/addons/controls/OrbitControls.js"; + +const controls = new OrbitControls(camera, renderer.domElement); + +// Damping (smooth movement) +controls.enableDamping = true; +controls.dampingFactor = 0.05; + +// Rotation limits +controls.minPolarAngle = 0; // Top +controls.maxPolarAngle = Math.PI / 2; // Horizon +controls.minAzimuthAngle = -Math.PI / 4; // Left +controls.maxAzimuthAngle = Math.PI / 4; // Right + +// Zoom limits +controls.minDistance = 2; +controls.maxDistance = 50; + +// Enable/disable features +controls.enableRotate = true; +controls.enableZoom = true; +controls.enablePan = true; + +// Auto-rotate +controls.autoRotate = true; +controls.autoRotateSpeed = 2.0; + +// Target (orbit point) +controls.target.set(0, 1, 0); + +// Update in animation loop +function animate() { + controls.update(); // Required for damping and auto-rotate + renderer.render(scene, camera); +} +``` + +### FlyControls + +```javascript +import { FlyControls } from "three/addons/controls/FlyControls.js"; + +const controls = new FlyControls(camera, renderer.domElement); +controls.movementSpeed = 10; +controls.rollSpeed = Math.PI / 24; +controls.dragToLook = true; + +// Update with delta +function animate() { + controls.update(clock.getDelta()); + renderer.render(scene, camera); +} +``` + +### FirstPersonControls + +```javascript +import { FirstPersonControls } from "three/addons/controls/FirstPersonControls.js"; + +const controls = new FirstPersonControls(camera, renderer.domElement); +controls.movementSpeed = 10; +controls.lookSpeed = 0.1; +controls.lookVertical = true; +controls.constrainVertical = true; +controls.verticalMin = Math.PI / 4; +controls.verticalMax = (Math.PI * 3) / 4; + +function animate() { + controls.update(clock.getDelta()); +} +``` + +### PointerLockControls + +```javascript +import { PointerLockControls } from "three/addons/controls/PointerLockControls.js"; + +const controls = new PointerLockControls(camera, document.body); + +// Lock pointer on click +document.addEventListener("click", () => { + controls.lock(); +}); + +controls.addEventListener("lock", () => { + console.log("Pointer locked"); +}); + +controls.addEventListener("unlock", () => { + console.log("Pointer unlocked"); +}); + +// Movement +const velocity = new THREE.Vector3(); +const direction = new THREE.Vector3(); +const moveForward = false; +const moveBackward = false; + +document.addEventListener("keydown", (event) => { + switch (event.code) { + case "KeyW": + moveForward = true; + break; + case "KeyS": + moveBackward = true; + break; + } +}); + +function animate() { + if (controls.isLocked) { + direction.z = Number(moveForward) - Number(moveBackward); + direction.normalize(); + + velocity.z -= direction.z * 0.1; + velocity.z *= 0.9; // Friction + + controls.moveForward(-velocity.z); + } +} +``` + +### TrackballControls + +```javascript +import { TrackballControls } from "three/addons/controls/TrackballControls.js"; + +const controls = new TrackballControls(camera, renderer.domElement); +controls.rotateSpeed = 2.0; +controls.zoomSpeed = 1.2; +controls.panSpeed = 0.8; +controls.staticMoving = true; + +function animate() { + controls.update(); +} +``` + +### MapControls + +```javascript +import { MapControls } from "three/addons/controls/MapControls.js"; + +const controls = new MapControls(camera, renderer.domElement); +controls.enableDamping = true; +controls.dampingFactor = 0.05; +controls.screenSpacePanning = false; +controls.maxPolarAngle = Math.PI / 2; +``` + +## TransformControls + +Gizmo for moving/rotating/scaling objects. + +```javascript +import { TransformControls } from "three/addons/controls/TransformControls.js"; + +const transformControls = new TransformControls(camera, renderer.domElement); +scene.add(transformControls); + +// Attach to object +transformControls.attach(selectedMesh); + +// Switch modes +transformControls.setMode("translate"); // 'translate', 'rotate', 'scale' + +// Change space +transformControls.setSpace("local"); // 'local', 'world' + +// Size +transformControls.setSize(1); + +// Events +transformControls.addEventListener("dragging-changed", (event) => { + // Disable orbit controls while dragging + orbitControls.enabled = !event.value; +}); + +transformControls.addEventListener("change", () => { + renderer.render(scene, camera); +}); + +// Keyboard shortcuts +window.addEventListener("keydown", (event) => { + switch (event.key) { + case "g": + transformControls.setMode("translate"); + break; + case "r": + transformControls.setMode("rotate"); + break; + case "s": + transformControls.setMode("scale"); + break; + case "Escape": + transformControls.detach(); + break; + } +}); +``` + +## DragControls + +Drag objects directly. + +```javascript +import { DragControls } from "three/addons/controls/DragControls.js"; + +const draggableObjects = [mesh1, mesh2, mesh3]; +const dragControls = new DragControls( + draggableObjects, + camera, + renderer.domElement, +); + +dragControls.addEventListener("dragstart", (event) => { + orbitControls.enabled = false; + event.object.material.emissive.set(0xaaaaaa); +}); + +dragControls.addEventListener("drag", (event) => { + // Constrain to ground plane + event.object.position.y = 0; +}); + +dragControls.addEventListener("dragend", (event) => { + orbitControls.enabled = true; + event.object.material.emissive.set(0x000000); +}); +``` + +## Selection System + +### Click to Select + +```javascript +const raycaster = new THREE.Raycaster(); +const mouse = new THREE.Vector2(); +let selectedObject = null; + +function onMouseDown(event) { + mouse.x = (event.clientX / window.innerWidth) * 2 - 1; + mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; + + raycaster.setFromCamera(mouse, camera); + const intersects = raycaster.intersectObjects(selectableObjects); + + // Deselect previous + if (selectedObject) { + selectedObject.material.emissive.set(0x000000); + } + + // Select new + if (intersects.length > 0) { + selectedObject = intersects[0].object; + selectedObject.material.emissive.set(0x444444); + } else { + selectedObject = null; + } +} +``` + +### Box Selection + +```javascript +import { SelectionBox } from "three/addons/interactive/SelectionBox.js"; +import { SelectionHelper } from "three/addons/interactive/SelectionHelper.js"; + +const selectionBox = new SelectionBox(camera, scene); +const selectionHelper = new SelectionHelper(renderer, "selectBox"); // CSS class + +document.addEventListener("pointerdown", (event) => { + selectionBox.startPoint.set( + (event.clientX / window.innerWidth) * 2 - 1, + -(event.clientY / window.innerHeight) * 2 + 1, + 0.5, + ); +}); + +document.addEventListener("pointermove", (event) => { + if (selectionHelper.isDown) { + selectionBox.endPoint.set( + (event.clientX / window.innerWidth) * 2 - 1, + -(event.clientY / window.innerHeight) * 2 + 1, + 0.5, + ); + } +}); + +document.addEventListener("pointerup", (event) => { + selectionBox.endPoint.set( + (event.clientX / window.innerWidth) * 2 - 1, + -(event.clientY / window.innerHeight) * 2 + 1, + 0.5, + ); + + const selected = selectionBox.select(); + console.log("Selected objects:", selected); +}); +``` + +### Hover Effects + +```javascript +const raycaster = new THREE.Raycaster(); +const mouse = new THREE.Vector2(); +let hoveredObject = null; + +function onMouseMove(event) { + mouse.x = (event.clientX / window.innerWidth) * 2 - 1; + mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; + + raycaster.setFromCamera(mouse, camera); + const intersects = raycaster.intersectObjects(hoverableObjects); + + // Reset previous hover + if (hoveredObject) { + hoveredObject.material.color.set(hoveredObject.userData.originalColor); + document.body.style.cursor = "default"; + } + + // Apply new hover + if (intersects.length > 0) { + hoveredObject = intersects[0].object; + if (!hoveredObject.userData.originalColor) { + hoveredObject.userData.originalColor = + hoveredObject.material.color.getHex(); + } + hoveredObject.material.color.set(0xff6600); + document.body.style.cursor = "pointer"; + } else { + hoveredObject = null; + } +} + +window.addEventListener("mousemove", onMouseMove); +``` + +## Keyboard Input + +```javascript +const keys = {}; + +document.addEventListener("keydown", (event) => { + keys[event.code] = true; +}); + +document.addEventListener("keyup", (event) => { + keys[event.code] = false; +}); + +function update() { + const speed = 0.1; + + if (keys["KeyW"]) player.position.z -= speed; + if (keys["KeyS"]) player.position.z += speed; + if (keys["KeyA"]) player.position.x -= speed; + if (keys["KeyD"]) player.position.x += speed; + if (keys["Space"]) player.position.y += speed; + if (keys["ShiftLeft"]) player.position.y -= speed; +} +``` + +## World-Screen Coordinate Conversion + +### World to Screen + +```javascript +function worldToScreen(position, camera) { + const vector = position.clone(); + vector.project(camera); + + return { + x: ((vector.x + 1) / 2) * window.innerWidth, + y: (-(vector.y - 1) / 2) * window.innerHeight, + }; +} + +// Position HTML element over 3D object +const screenPos = worldToScreen(mesh.position, camera); +element.style.left = screenPos.x + "px"; +element.style.top = screenPos.y + "px"; +``` + +### Screen to World + +```javascript +function screenToWorld(screenX, screenY, camera, targetZ = 0) { + const vector = new THREE.Vector3( + (screenX / window.innerWidth) * 2 - 1, + -(screenY / window.innerHeight) * 2 + 1, + 0.5, + ); + + vector.unproject(camera); + + const dir = vector.sub(camera.position).normalize(); + const distance = (targetZ - camera.position.z) / dir.z; + + return camera.position.clone().add(dir.multiplyScalar(distance)); +} +``` + +### Ray-Plane Intersection + +```javascript +function getRayPlaneIntersection(mouse, camera, plane) { + const raycaster = new THREE.Raycaster(); + raycaster.setFromCamera(mouse, camera); + + const intersection = new THREE.Vector3(); + raycaster.ray.intersectPlane(plane, intersection); + + return intersection; +} + +// Ground plane +const groundPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0); +const worldPos = getRayPlaneIntersection(mouse, camera, groundPlane); +``` + +## Event Handling Best Practices + +```javascript +class InteractionManager { + constructor(camera, renderer, scene) { + this.camera = camera; + this.renderer = renderer; + this.scene = scene; + this.raycaster = new THREE.Raycaster(); + this.mouse = new THREE.Vector2(); + this.clickables = []; + + this.bindEvents(); + } + + bindEvents() { + const canvas = this.renderer.domElement; + + canvas.addEventListener("click", (e) => this.onClick(e)); + canvas.addEventListener("mousemove", (e) => this.onMouseMove(e)); + canvas.addEventListener("touchstart", (e) => this.onTouchStart(e)); + } + + updateMouse(event) { + const rect = this.renderer.domElement.getBoundingClientRect(); + this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; + this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; + } + + getIntersects() { + this.raycaster.setFromCamera(this.mouse, this.camera); + return this.raycaster.intersectObjects(this.clickables, true); + } + + onClick(event) { + this.updateMouse(event); + const intersects = this.getIntersects(); + + if (intersects.length > 0) { + const object = intersects[0].object; + if (object.userData.onClick) { + object.userData.onClick(intersects[0]); + } + } + } + + addClickable(object, callback) { + this.clickables.push(object); + object.userData.onClick = callback; + } + + dispose() { + // Remove event listeners + } +} + +// Usage +const interaction = new InteractionManager(camera, renderer, scene); +interaction.addClickable(mesh, (intersect) => { + console.log("Clicked at:", intersect.point); +}); +``` + +## Performance Tips + +1. **Limit raycasts**: Throttle mousemove handlers +2. **Use layers**: Filter raycast targets +3. **Simple collision meshes**: Use invisible simpler geometry for raycasting +4. **Disable controls when not needed**: `controls.enabled = false` +5. **Batch updates**: Group interaction checks + +```javascript +// Use simpler geometry for raycasting +const complexMesh = loadedModel; +const collisionMesh = new THREE.Mesh( + new THREE.BoxGeometry(1, 1, 1), + new THREE.MeshBasicMaterial({ visible: false }), +); +collisionMesh.userData.target = complexMesh; +clickables.push(collisionMesh); +``` + +## See Also + +- `threejs-fundamentals` - Camera and scene setup +- `threejs-animation` - Animating interactions +- `threejs-shaders` - Visual feedback effects diff --git a/.codex/skills/threejs-lighting/SKILL.md b/.codex/skills/threejs-lighting/SKILL.md new file mode 100644 index 0000000..575cb0c --- /dev/null +++ b/.codex/skills/threejs-lighting/SKILL.md @@ -0,0 +1,481 @@ +--- +name: threejs-lighting +description: Three.js lighting - light types, shadows, environment lighting. Use when adding lights, configuring shadows, setting up IBL, or optimizing lighting performance. +--- + +# Three.js Lighting + +## Quick Start + +```javascript +import * as THREE from "three"; + +// Basic lighting setup +const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); +scene.add(ambientLight); + +const directionalLight = new THREE.DirectionalLight(0xffffff, 1); +directionalLight.position.set(5, 5, 5); +scene.add(directionalLight); +``` + +## Light Types Overview + +| Light | Description | Shadow Support | Cost | +| ---------------- | ---------------------- | -------------- | -------- | +| AmbientLight | Uniform everywhere | No | Very Low | +| HemisphereLight | Sky/ground gradient | No | Very Low | +| DirectionalLight | Parallel rays (sun) | Yes | Low | +| PointLight | Omnidirectional (bulb) | Yes | Medium | +| SpotLight | Cone-shaped | Yes | Medium | +| RectAreaLight | Area light (window) | No\* | High | + +\*RectAreaLight shadows require custom solutions + +## AmbientLight + +Illuminates all objects equally. No direction, no shadows. + +```javascript +// AmbientLight(color, intensity) +const ambient = new THREE.AmbientLight(0xffffff, 0.5); +scene.add(ambient); + +// Modify at runtime +ambient.color.set(0xffffcc); +ambient.intensity = 0.3; +``` + +## HemisphereLight + +Gradient from sky to ground color. Good for outdoor scenes. + +```javascript +// HemisphereLight(skyColor, groundColor, intensity) +const hemi = new THREE.HemisphereLight(0x87ceeb, 0x8b4513, 0.6); +hemi.position.set(0, 50, 0); +scene.add(hemi); + +// Properties +hemi.color; // Sky color +hemi.groundColor; // Ground color +hemi.intensity; +``` + +## DirectionalLight + +Parallel light rays. Simulates distant light source (sun). + +```javascript +// DirectionalLight(color, intensity) +const dirLight = new THREE.DirectionalLight(0xffffff, 1); +dirLight.position.set(5, 10, 5); + +// Light points at target (default: 0, 0, 0) +dirLight.target.position.set(0, 0, 0); +scene.add(dirLight.target); + +scene.add(dirLight); +``` + +### DirectionalLight Shadows + +```javascript +dirLight.castShadow = true; + +// Shadow map size (higher = sharper, more expensive) +dirLight.shadow.mapSize.width = 2048; +dirLight.shadow.mapSize.height = 2048; + +// Shadow camera (orthographic) +dirLight.shadow.camera.near = 0.5; +dirLight.shadow.camera.far = 50; +dirLight.shadow.camera.left = -10; +dirLight.shadow.camera.right = 10; +dirLight.shadow.camera.top = 10; +dirLight.shadow.camera.bottom = -10; + +// Shadow softness +dirLight.shadow.radius = 4; // Blur radius (PCFSoftShadowMap only) + +// Shadow bias (fixes shadow acne) +dirLight.shadow.bias = -0.0001; +dirLight.shadow.normalBias = 0.02; + +// Helper to visualize shadow camera +const helper = new THREE.CameraHelper(dirLight.shadow.camera); +scene.add(helper); +``` + +## PointLight + +Emits light in all directions from a point. Like a light bulb. + +```javascript +// PointLight(color, intensity, distance, decay) +const pointLight = new THREE.PointLight(0xffffff, 1, 100, 2); +pointLight.position.set(0, 5, 0); +scene.add(pointLight); + +// Properties +pointLight.distance; // Maximum range (0 = infinite) +pointLight.decay; // Light falloff (physically correct = 2) +``` + +### PointLight Shadows + +```javascript +pointLight.castShadow = true; +pointLight.shadow.mapSize.width = 1024; +pointLight.shadow.mapSize.height = 1024; + +// Shadow camera (perspective - 6 directions for cube map) +pointLight.shadow.camera.near = 0.5; +pointLight.shadow.camera.far = 50; + +pointLight.shadow.bias = -0.005; +``` + +## SpotLight + +Cone-shaped light. Like a flashlight or stage light. + +```javascript +// SpotLight(color, intensity, distance, angle, penumbra, decay) +const spotLight = new THREE.SpotLight(0xffffff, 1, 100, Math.PI / 6, 0.5, 2); +spotLight.position.set(0, 10, 0); + +// Target (light points at this) +spotLight.target.position.set(0, 0, 0); +scene.add(spotLight.target); + +scene.add(spotLight); + +// Properties +spotLight.angle; // Cone angle (radians, max Math.PI/2) +spotLight.penumbra; // Soft edge (0-1) +spotLight.distance; // Range +spotLight.decay; // Falloff +``` + +### SpotLight Shadows + +```javascript +spotLight.castShadow = true; +spotLight.shadow.mapSize.width = 1024; +spotLight.shadow.mapSize.height = 1024; + +// Shadow camera (perspective) +spotLight.shadow.camera.near = 0.5; +spotLight.shadow.camera.far = 50; +spotLight.shadow.camera.fov = 30; + +spotLight.shadow.bias = -0.0001; + +// Focus (affects shadow projection) +spotLight.shadow.focus = 1; +``` + +## RectAreaLight + +Rectangular area light. Great for soft, realistic lighting. + +```javascript +import { RectAreaLightHelper } from "three/examples/jsm/helpers/RectAreaLightHelper.js"; +import { RectAreaLightUniformsLib } from "three/examples/jsm/lights/RectAreaLightUniformsLib.js"; + +// Must initialize uniforms first +RectAreaLightUniformsLib.init(); + +// RectAreaLight(color, intensity, width, height) +const rectLight = new THREE.RectAreaLight(0xffffff, 5, 4, 2); +rectLight.position.set(0, 5, 0); +rectLight.lookAt(0, 0, 0); +scene.add(rectLight); + +// Helper +const helper = new RectAreaLightHelper(rectLight); +rectLight.add(helper); + +// Note: Only works with MeshStandardMaterial and MeshPhysicalMaterial +// Does not cast shadows natively +``` + +## Shadow Setup + +### Enable Shadows + +```javascript +// 1. Enable on renderer +renderer.shadowMap.enabled = true; +renderer.shadowMap.type = THREE.PCFSoftShadowMap; + +// Shadow map types: +// THREE.BasicShadowMap - fastest, low quality +// THREE.PCFShadowMap - default, filtered +// THREE.PCFSoftShadowMap - softer edges +// THREE.VSMShadowMap - variance shadow map + +// 2. Enable on light +light.castShadow = true; + +// 3. Enable on objects +mesh.castShadow = true; +mesh.receiveShadow = true; + +// Ground plane +floor.receiveShadow = true; +floor.castShadow = false; // Usually false for floors +``` + +### Optimizing Shadows + +```javascript +// Tight shadow camera frustum +const d = 10; +dirLight.shadow.camera.left = -d; +dirLight.shadow.camera.right = d; +dirLight.shadow.camera.top = d; +dirLight.shadow.camera.bottom = -d; +dirLight.shadow.camera.near = 0.5; +dirLight.shadow.camera.far = 30; + +// Fix shadow acne +dirLight.shadow.bias = -0.0001; // Depth bias +dirLight.shadow.normalBias = 0.02; // Bias along normal + +// Shadow map size (balance quality vs performance) +// 512 - low quality +// 1024 - medium quality +// 2048 - high quality +// 4096 - very high quality (expensive) +``` + +### Contact Shadows (Fake, Fast) + +```javascript +import { ContactShadows } from "three/examples/jsm/objects/ContactShadows.js"; + +const contactShadows = new ContactShadows({ + resolution: 512, + blur: 2, + opacity: 0.5, + scale: 10, + position: [0, 0, 0], +}); +scene.add(contactShadows); +``` + +## Light Helpers + +```javascript +import { RectAreaLightHelper } from "three/examples/jsm/helpers/RectAreaLightHelper.js"; + +// DirectionalLight helper +const dirHelper = new THREE.DirectionalLightHelper(dirLight, 5); +scene.add(dirHelper); + +// PointLight helper +const pointHelper = new THREE.PointLightHelper(pointLight, 1); +scene.add(pointHelper); + +// SpotLight helper +const spotHelper = new THREE.SpotLightHelper(spotLight); +scene.add(spotHelper); + +// Hemisphere helper +const hemiHelper = new THREE.HemisphereLightHelper(hemiLight, 5); +scene.add(hemiHelper); + +// RectAreaLight helper +const rectHelper = new RectAreaLightHelper(rectLight); +rectLight.add(rectHelper); + +// Update helpers when light changes +dirHelper.update(); +spotHelper.update(); +``` + +## Environment Lighting (IBL) + +Image-Based Lighting using HDR environment maps. + +```javascript +import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader.js"; + +const rgbeLoader = new RGBELoader(); +rgbeLoader.load("environment.hdr", (texture) => { + texture.mapping = THREE.EquirectangularReflectionMapping; + + // Set as scene environment (affects all PBR materials) + scene.environment = texture; + + // Optional: also use as background + scene.background = texture; + scene.backgroundBlurriness = 0; // 0-1, blur the background + scene.backgroundIntensity = 1; +}); + +// PMREMGenerator for better reflections +const pmremGenerator = new THREE.PMREMGenerator(renderer); +pmremGenerator.compileEquirectangularShader(); + +rgbeLoader.load("environment.hdr", (texture) => { + const envMap = pmremGenerator.fromEquirectangular(texture).texture; + scene.environment = envMap; + texture.dispose(); + pmremGenerator.dispose(); +}); +``` + +### Cube Texture Environment + +```javascript +const cubeLoader = new THREE.CubeTextureLoader(); +const envMap = cubeLoader.load([ + "px.jpg", + "nx.jpg", + "py.jpg", + "ny.jpg", + "pz.jpg", + "nz.jpg", +]); + +scene.environment = envMap; +scene.background = envMap; +``` + +## Light Probes (Advanced) + +Capture lighting from a point in space for ambient lighting. + +```javascript +import { LightProbeGenerator } from "three/examples/jsm/lights/LightProbeGenerator.js"; + +// Generate from cube texture +const lightProbe = new THREE.LightProbe(); +scene.add(lightProbe); + +lightProbe.copy(LightProbeGenerator.fromCubeTexture(cubeTexture)); + +// Or from render target +const cubeCamera = new THREE.CubeCamera( + 0.1, + 100, + new THREE.WebGLCubeRenderTarget(256), +); +cubeCamera.update(renderer, scene); +lightProbe.copy( + LightProbeGenerator.fromCubeRenderTarget(renderer, cubeCamera.renderTarget), +); +``` + +## Common Lighting Setups + +### Three-Point Lighting + +```javascript +// Key light (main light) +const keyLight = new THREE.DirectionalLight(0xffffff, 1); +keyLight.position.set(5, 5, 5); +scene.add(keyLight); + +// Fill light (softer, opposite side) +const fillLight = new THREE.DirectionalLight(0xffffff, 0.5); +fillLight.position.set(-5, 3, 5); +scene.add(fillLight); + +// Back light (rim lighting) +const backLight = new THREE.DirectionalLight(0xffffff, 0.3); +backLight.position.set(0, 5, -5); +scene.add(backLight); + +// Ambient fill +const ambient = new THREE.AmbientLight(0x404040, 0.3); +scene.add(ambient); +``` + +### Outdoor Daylight + +```javascript +// Sun +const sun = new THREE.DirectionalLight(0xffffcc, 1.5); +sun.position.set(50, 100, 50); +sun.castShadow = true; +scene.add(sun); + +// Sky ambient +const hemi = new THREE.HemisphereLight(0x87ceeb, 0x8b4513, 0.6); +scene.add(hemi); +``` + +### Indoor Studio + +```javascript +// Multiple area lights +RectAreaLightUniformsLib.init(); + +const light1 = new THREE.RectAreaLight(0xffffff, 5, 2, 2); +light1.position.set(3, 3, 3); +light1.lookAt(0, 0, 0); +scene.add(light1); + +const light2 = new THREE.RectAreaLight(0xffffff, 3, 2, 2); +light2.position.set(-3, 3, 3); +light2.lookAt(0, 0, 0); +scene.add(light2); + +// Ambient fill +const ambient = new THREE.AmbientLight(0x404040, 0.2); +scene.add(ambient); +``` + +## Light Animation + +```javascript +const clock = new THREE.Clock(); + +function animate() { + const time = clock.getElapsedTime(); + + // Orbit light around scene + light.position.x = Math.cos(time) * 5; + light.position.z = Math.sin(time) * 5; + + // Pulsing intensity + light.intensity = 1 + Math.sin(time * 2) * 0.5; + + // Color cycling + light.color.setHSL((time * 0.1) % 1, 1, 0.5); + + // Update helpers if using + lightHelper.update(); +} +``` + +## Performance Tips + +1. **Limit light count**: Each light adds shader complexity +2. **Use baked lighting**: For static scenes, bake to textures +3. **Smaller shadow maps**: 512-1024 often sufficient +4. **Tight shadow frustums**: Only cover needed area +5. **Disable unused shadows**: Not all lights need shadows +6. **Use light layers**: Exclude objects from certain lights + +```javascript +// Light layers +light.layers.set(1); // Light only affects layer 1 +mesh.layers.enable(1); // Mesh is on layer 1 +otherMesh.layers.disable(1); // Other mesh not affected + +// Selective shadows +mesh.castShadow = true; +mesh.receiveShadow = true; +decorMesh.castShadow = false; // Small objects often don't need to cast +``` + +## See Also + +- `threejs-materials` - Material light response +- `threejs-textures` - Lightmaps and environment maps +- `threejs-postprocessing` - Bloom and other light effects diff --git a/.codex/skills/threejs-loaders/SKILL.md b/.codex/skills/threejs-loaders/SKILL.md new file mode 100644 index 0000000..4754a7a --- /dev/null +++ b/.codex/skills/threejs-loaders/SKILL.md @@ -0,0 +1,623 @@ +--- +name: threejs-loaders +description: Three.js asset loading - GLTF, textures, images, models, async patterns. Use when loading 3D models, textures, HDR environments, or managing loading progress. +--- + +# Three.js Loaders + +## Quick Start + +```javascript +import * as THREE from "three"; +import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js"; + +// Load GLTF model +const loader = new GLTFLoader(); +loader.load("model.glb", (gltf) => { + scene.add(gltf.scene); +}); + +// Load texture +const textureLoader = new THREE.TextureLoader(); +const texture = textureLoader.load("texture.jpg"); +``` + +## LoadingManager + +Coordinate multiple loaders and track progress. + +```javascript +const manager = new THREE.LoadingManager(); + +// Callbacks +manager.onStart = (url, loaded, total) => { + console.log(`Started loading: ${url}`); +}; + +manager.onLoad = () => { + console.log("All assets loaded!"); + startGame(); +}; + +manager.onProgress = (url, loaded, total) => { + const progress = (loaded / total) * 100; + console.log(`Loading: ${progress.toFixed(1)}%`); + updateProgressBar(progress); +}; + +manager.onError = (url) => { + console.error(`Error loading: ${url}`); +}; + +// Use manager with loaders +const textureLoader = new THREE.TextureLoader(manager); +const gltfLoader = new GLTFLoader(manager); + +// Load assets +textureLoader.load("texture1.jpg"); +textureLoader.load("texture2.jpg"); +gltfLoader.load("model.glb"); +// onLoad fires when ALL are complete +``` + +## Texture Loading + +### TextureLoader + +```javascript +const loader = new THREE.TextureLoader(); + +// Callback style +loader.load( + "texture.jpg", + (texture) => { + // onLoad + material.map = texture; + material.needsUpdate = true; + }, + undefined, // onProgress - not supported for image loading + (error) => { + // onError + console.error("Error loading texture", error); + }, +); + +// Synchronous (returns texture, loads async) +const texture = loader.load("texture.jpg"); +material.map = texture; +``` + +### Texture Configuration + +```javascript +const texture = loader.load("texture.jpg", (tex) => { + // Color space (important for color accuracy) + tex.colorSpace = THREE.SRGBColorSpace; // For color/albedo maps + // tex.colorSpace = THREE.LinearSRGBColorSpace; // For data maps (normal, roughness) + + // Wrapping + tex.wrapS = THREE.RepeatWrapping; + tex.wrapT = THREE.RepeatWrapping; + // ClampToEdgeWrapping, RepeatWrapping, MirroredRepeatWrapping + + // Repeat/offset + tex.repeat.set(2, 2); + tex.offset.set(0.5, 0.5); + tex.rotation = Math.PI / 4; + tex.center.set(0.5, 0.5); + + // Filtering + tex.minFilter = THREE.LinearMipmapLinearFilter; // Default + tex.magFilter = THREE.LinearFilter; // Default + // NearestFilter - pixelated + // LinearFilter - smooth + // LinearMipmapLinearFilter - smooth with mipmaps + + // Anisotropic filtering (sharper at angles) + tex.anisotropy = renderer.capabilities.getMaxAnisotropy(); + + // Flip Y (usually true for standard textures) + tex.flipY = true; + + tex.needsUpdate = true; +}); +``` + +### CubeTextureLoader + +For environment maps and skyboxes. + +```javascript +const loader = new THREE.CubeTextureLoader(); + +// Load 6 faces +const cubeTexture = loader.load([ + "px.jpg", + "nx.jpg", // positive/negative X + "py.jpg", + "ny.jpg", // positive/negative Y + "pz.jpg", + "nz.jpg", // positive/negative Z +]); + +// Use as background +scene.background = cubeTexture; + +// Use as environment map +scene.environment = cubeTexture; +material.envMap = cubeTexture; +``` + +### HDR/EXR Loading + +```javascript +import { RGBELoader } from "three/addons/loaders/RGBELoader.js"; +import { EXRLoader } from "three/addons/loaders/EXRLoader.js"; + +// HDR +const rgbeLoader = new RGBELoader(); +rgbeLoader.load("environment.hdr", (texture) => { + texture.mapping = THREE.EquirectangularReflectionMapping; + scene.environment = texture; + scene.background = texture; +}); + +// EXR +const exrLoader = new EXRLoader(); +exrLoader.load("environment.exr", (texture) => { + texture.mapping = THREE.EquirectangularReflectionMapping; + scene.environment = texture; +}); +``` + +### PMREMGenerator + +Generate prefiltered environment maps for PBR. + +```javascript +import { RGBELoader } from "three/addons/loaders/RGBELoader.js"; + +const pmremGenerator = new THREE.PMREMGenerator(renderer); +pmremGenerator.compileEquirectangularShader(); + +new RGBELoader().load("environment.hdr", (texture) => { + const envMap = pmremGenerator.fromEquirectangular(texture).texture; + + scene.environment = envMap; + scene.background = envMap; + + texture.dispose(); + pmremGenerator.dispose(); +}); +``` + +## GLTF/GLB Loading + +The most common 3D format for web. + +```javascript +import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js"; + +const loader = new GLTFLoader(); + +loader.load("model.glb", (gltf) => { + // The loaded scene + const model = gltf.scene; + scene.add(model); + + // Animations + const animations = gltf.animations; + if (animations.length > 0) { + const mixer = new THREE.AnimationMixer(model); + animations.forEach((clip) => { + mixer.clipAction(clip).play(); + }); + } + + // Cameras (if any) + const cameras = gltf.cameras; + + // Asset info + console.log(gltf.asset); // Version, generator, etc. + + // User data from Blender/etc + console.log(gltf.userData); +}); +``` + +### GLTF with Draco Compression + +```javascript +import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js"; +import { DRACOLoader } from "three/addons/loaders/DRACOLoader.js"; + +const dracoLoader = new DRACOLoader(); +dracoLoader.setDecoderPath( + "https://www.gstatic.com/draco/versioned/decoders/1.5.6/", +); +dracoLoader.preload(); + +const gltfLoader = new GLTFLoader(); +gltfLoader.setDRACOLoader(dracoLoader); + +gltfLoader.load("compressed-model.glb", (gltf) => { + scene.add(gltf.scene); +}); +``` + +### GLTF with KTX2 Textures + +```javascript +import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js"; +import { KTX2Loader } from "three/addons/loaders/KTX2Loader.js"; + +const ktx2Loader = new KTX2Loader(); +ktx2Loader.setTranscoderPath( + "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/libs/basis/", +); +ktx2Loader.detectSupport(renderer); + +const gltfLoader = new GLTFLoader(); +gltfLoader.setKTX2Loader(ktx2Loader); + +gltfLoader.load("model-with-ktx2.glb", (gltf) => { + scene.add(gltf.scene); +}); +``` + +### Process GLTF Content + +```javascript +loader.load("model.glb", (gltf) => { + const model = gltf.scene; + + // Enable shadows + model.traverse((child) => { + if (child.isMesh) { + child.castShadow = true; + child.receiveShadow = true; + } + }); + + // Find specific mesh + const head = model.getObjectByName("Head"); + + // Adjust materials + model.traverse((child) => { + if (child.isMesh && child.material) { + child.material.envMapIntensity = 0.5; + } + }); + + // Center and scale + const box = new THREE.Box3().setFromObject(model); + const center = box.getCenter(new THREE.Vector3()); + const size = box.getSize(new THREE.Vector3()); + + model.position.sub(center); + const maxDim = Math.max(size.x, size.y, size.z); + model.scale.setScalar(1 / maxDim); + + scene.add(model); +}); +``` + +## Other Model Formats + +### OBJ + MTL + +```javascript +import { OBJLoader } from "three/addons/loaders/OBJLoader.js"; +import { MTLLoader } from "three/addons/loaders/MTLLoader.js"; + +const mtlLoader = new MTLLoader(); +mtlLoader.load("model.mtl", (materials) => { + materials.preload(); + + const objLoader = new OBJLoader(); + objLoader.setMaterials(materials); + objLoader.load("model.obj", (object) => { + scene.add(object); + }); +}); +``` + +### FBX + +```javascript +import { FBXLoader } from "three/addons/loaders/FBXLoader.js"; + +const loader = new FBXLoader(); +loader.load("model.fbx", (object) => { + // FBX often has large scale + object.scale.setScalar(0.01); + + // Animations + const mixer = new THREE.AnimationMixer(object); + object.animations.forEach((clip) => { + mixer.clipAction(clip).play(); + }); + + scene.add(object); +}); +``` + +### STL + +```javascript +import { STLLoader } from "three/addons/loaders/STLLoader.js"; + +const loader = new STLLoader(); +loader.load("model.stl", (geometry) => { + const material = new THREE.MeshStandardMaterial({ color: 0x888888 }); + const mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); +}); +``` + +### PLY + +```javascript +import { PLYLoader } from "three/addons/loaders/PLYLoader.js"; + +const loader = new PLYLoader(); +loader.load("model.ply", (geometry) => { + geometry.computeVertexNormals(); + const material = new THREE.MeshStandardMaterial({ vertexColors: true }); + const mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); +}); +``` + +## Async/Promise Loading + +### Promisified Loader + +```javascript +function loadModel(url) { + return new Promise((resolve, reject) => { + loader.load(url, resolve, undefined, reject); + }); +} + +// Usage +async function init() { + try { + const gltf = await loadModel("model.glb"); + scene.add(gltf.scene); + } catch (error) { + console.error("Failed to load model:", error); + } +} +``` + +### Load Multiple Assets + +```javascript +async function loadAssets() { + const [modelGltf, envTexture, colorTexture] = await Promise.all([ + loadGLTF("model.glb"), + loadRGBE("environment.hdr"), + loadTexture("color.jpg"), + ]); + + scene.add(modelGltf.scene); + scene.environment = envTexture; + material.map = colorTexture; +} + +// Helper functions +function loadGLTF(url) { + return new Promise((resolve, reject) => { + new GLTFLoader().load(url, resolve, undefined, reject); + }); +} + +function loadRGBE(url) { + return new Promise((resolve, reject) => { + new RGBELoader().load( + url, + (texture) => { + texture.mapping = THREE.EquirectangularReflectionMapping; + resolve(texture); + }, + undefined, + reject, + ); + }); +} + +function loadTexture(url) { + return new Promise((resolve, reject) => { + new THREE.TextureLoader().load(url, resolve, undefined, reject); + }); +} +``` + +## Caching + +### Built-in Cache + +```javascript +// Enable cache +THREE.Cache.enabled = true; + +// Clear cache +THREE.Cache.clear(); + +// Manual cache management +THREE.Cache.add("key", data); +THREE.Cache.get("key"); +THREE.Cache.remove("key"); +``` + +### Custom Asset Manager + +```javascript +class AssetManager { + constructor() { + this.textures = new Map(); + this.models = new Map(); + this.gltfLoader = new GLTFLoader(); + this.textureLoader = new THREE.TextureLoader(); + } + + async loadTexture(key, url) { + if (this.textures.has(key)) { + return this.textures.get(key); + } + + const texture = await new Promise((resolve, reject) => { + this.textureLoader.load(url, resolve, undefined, reject); + }); + + this.textures.set(key, texture); + return texture; + } + + async loadModel(key, url) { + if (this.models.has(key)) { + return this.models.get(key).clone(); + } + + const gltf = await new Promise((resolve, reject) => { + this.gltfLoader.load(url, resolve, undefined, reject); + }); + + this.models.set(key, gltf.scene); + return gltf.scene.clone(); + } + + dispose() { + this.textures.forEach((t) => t.dispose()); + this.textures.clear(); + this.models.clear(); + } +} + +// Usage +const assets = new AssetManager(); +const texture = await assets.loadTexture("brick", "brick.jpg"); +const model = await assets.loadModel("tree", "tree.glb"); +``` + +## Loading from Different Sources + +### Data URL / Base64 + +```javascript +const loader = new THREE.TextureLoader(); +const texture = loader.load("data:image/png;base64,iVBORw0KGgo..."); +``` + +### Blob URL + +```javascript +async function loadFromBlob(blob) { + const url = URL.createObjectURL(blob); + const texture = await loadTexture(url); + URL.revokeObjectURL(url); + return texture; +} +``` + +### ArrayBuffer + +```javascript +// From fetch +const response = await fetch("model.glb"); +const buffer = await response.arrayBuffer(); + +// Parse with loader +const loader = new GLTFLoader(); +loader.parse(buffer, "", (gltf) => { + scene.add(gltf.scene); +}); +``` + +### Custom Path/URL + +```javascript +// Set base path +loader.setPath("assets/models/"); +loader.load("model.glb"); // Loads from assets/models/model.glb + +// Set resource path (for textures referenced in model) +loader.setResourcePath("assets/textures/"); + +// Custom URL modifier +manager.setURLModifier((url) => { + return `https://cdn.example.com/${url}`; +}); +``` + +## Error Handling + +```javascript +// Graceful fallback +async function loadWithFallback(primaryUrl, fallbackUrl) { + try { + return await loadModel(primaryUrl); + } catch (error) { + console.warn(`Primary failed, trying fallback: ${error}`); + return await loadModel(fallbackUrl); + } +} + +// Retry logic +async function loadWithRetry(url, maxRetries = 3) { + for (let i = 0; i < maxRetries; i++) { + try { + return await loadModel(url); + } catch (error) { + if (i === maxRetries - 1) throw error; + await new Promise((r) => setTimeout(r, 1000 * (i + 1))); + } + } +} + +// Timeout +async function loadWithTimeout(url, timeout = 30000) { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeout); + + try { + const response = await fetch(url, { signal: controller.signal }); + clearTimeout(timeoutId); + return response; + } catch (error) { + if (error.name === "AbortError") { + throw new Error("Loading timed out"); + } + throw error; + } +} +``` + +## Performance Tips + +1. **Use compressed formats**: DRACO for geometry, KTX2/Basis for textures +2. **Load progressively**: Show placeholders while loading +3. **Lazy load**: Only load what's needed +4. **Use CDN**: Faster asset delivery +5. **Enable cache**: `THREE.Cache.enabled = true` + +```javascript +// Progressive loading with placeholder +const placeholder = new THREE.Mesh( + new THREE.BoxGeometry(1, 1, 1), + new THREE.MeshBasicMaterial({ wireframe: true }), +); +scene.add(placeholder); + +loadModel("model.glb").then((gltf) => { + scene.remove(placeholder); + scene.add(gltf.scene); +}); +``` + +## See Also + +- `threejs-textures` - Texture configuration +- `threejs-animation` - Playing loaded animations +- `threejs-materials` - Material from loaded models diff --git a/.codex/skills/threejs-materials/SKILL.md b/.codex/skills/threejs-materials/SKILL.md new file mode 100644 index 0000000..4731f28 --- /dev/null +++ b/.codex/skills/threejs-materials/SKILL.md @@ -0,0 +1,520 @@ +--- +name: threejs-materials +description: Three.js materials - PBR, basic, phong, shader materials, material properties. Use when styling meshes, working with textures, creating custom shaders, or optimizing material performance. +--- + +# Three.js Materials + +## Quick Start + +```javascript +import * as THREE from "three"; + +// PBR material (recommended for realistic rendering) +const material = new THREE.MeshStandardMaterial({ + color: 0x00ff00, + roughness: 0.5, + metalness: 0.5, +}); + +const mesh = new THREE.Mesh(geometry, material); +``` + +## Material Types Overview + +| Material | Use Case | Lighting | +| -------------------- | ------------------------------------- | ------------------ | +| MeshBasicMaterial | Unlit, flat colors, wireframes | No | +| MeshLambertMaterial | Matte surfaces, performance | Yes (diffuse only) | +| MeshPhongMaterial | Shiny surfaces, specular highlights | Yes | +| MeshStandardMaterial | PBR, realistic materials | Yes (PBR) | +| MeshPhysicalMaterial | Advanced PBR, clearcoat, transmission | Yes (PBR+) | +| MeshToonMaterial | Cel-shaded, cartoon look | Yes (toon) | +| MeshNormalMaterial | Debug normals | No | +| MeshDepthMaterial | Depth visualization | No | +| ShaderMaterial | Custom GLSL shaders | Custom | +| RawShaderMaterial | Full shader control | Custom | + +## MeshBasicMaterial + +No lighting calculations. Fast, always visible. + +```javascript +const material = new THREE.MeshBasicMaterial({ + color: 0xff0000, + transparent: true, + opacity: 0.5, + side: THREE.DoubleSide, // FrontSide, BackSide, DoubleSide + wireframe: false, + map: texture, // Color/diffuse texture + alphaMap: alphaTexture, // Transparency texture + envMap: envTexture, // Reflection texture + reflectivity: 1, // Env map intensity + fog: true, // Affected by scene fog +}); +``` + +## MeshLambertMaterial + +Diffuse-only lighting. Fast, no specular highlights. + +```javascript +const material = new THREE.MeshLambertMaterial({ + color: 0x00ff00, + emissive: 0x111111, // Self-illumination color + emissiveIntensity: 1, + map: texture, + emissiveMap: emissiveTexture, + envMap: envTexture, + reflectivity: 0.5, +}); +``` + +## MeshPhongMaterial + +Specular highlights. Good for shiny, plastic-like surfaces. + +```javascript +const material = new THREE.MeshPhongMaterial({ + color: 0x0000ff, + specular: 0xffffff, // Highlight color + shininess: 100, // Highlight sharpness (0-1000) + emissive: 0x000000, + flatShading: false, // Flat vs smooth shading + map: texture, + specularMap: specTexture, // Per-pixel shininess + normalMap: normalTexture, + normalScale: new THREE.Vector2(1, 1), + bumpMap: bumpTexture, + bumpScale: 1, + displacementMap: dispTexture, + displacementScale: 1, +}); +``` + +## MeshStandardMaterial (PBR) + +Physically-based rendering. Recommended for realistic results. + +```javascript +const material = new THREE.MeshStandardMaterial({ + color: 0xffffff, + roughness: 0.5, // 0 = mirror, 1 = diffuse + metalness: 0.0, // 0 = dielectric, 1 = metal + + // Textures + map: colorTexture, // Albedo/base color + roughnessMap: roughTexture, // Per-pixel roughness + metalnessMap: metalTexture, // Per-pixel metalness + normalMap: normalTexture, // Surface detail + normalScale: new THREE.Vector2(1, 1), + aoMap: aoTexture, // Ambient occlusion (uses uv2!) + aoMapIntensity: 1, + displacementMap: dispTexture, // Vertex displacement + displacementScale: 0.1, + displacementBias: 0, + + // Emissive + emissive: 0x000000, + emissiveIntensity: 1, + emissiveMap: emissiveTexture, + + // Environment + envMap: envTexture, + envMapIntensity: 1, + + // Other + flatShading: false, + wireframe: false, + fog: true, +}); + +// Note: aoMap requires second UV channel +geometry.setAttribute("uv2", geometry.attributes.uv); +``` + +## MeshPhysicalMaterial (Advanced PBR) + +Extends MeshStandardMaterial with advanced features. + +```javascript +const material = new THREE.MeshPhysicalMaterial({ + // All MeshStandardMaterial properties plus: + + // Clearcoat (car paint, lacquer) + clearcoat: 1.0, // 0-1 clearcoat layer strength + clearcoatRoughness: 0.1, + clearcoatMap: ccTexture, + clearcoatRoughnessMap: ccrTexture, + clearcoatNormalMap: ccnTexture, + clearcoatNormalScale: new THREE.Vector2(1, 1), + + // Transmission (glass, water) + transmission: 1.0, // 0 = opaque, 1 = fully transparent + transmissionMap: transTexture, + thickness: 0.5, // Volume thickness for refraction + thicknessMap: thickTexture, + attenuationDistance: 1, // Absorption distance + attenuationColor: new THREE.Color(0xffffff), + + // Refraction + ior: 1.5, // Index of refraction (1-2.333) + + // Sheen (fabric, velvet) + sheen: 1.0, + sheenRoughness: 0.5, + sheenColor: new THREE.Color(0xffffff), + sheenColorMap: sheenTexture, + sheenRoughnessMap: sheenRoughTexture, + + // Iridescence (soap bubbles, oil slicks) + iridescence: 1.0, + iridescenceIOR: 1.3, + iridescenceThicknessRange: [100, 400], + iridescenceMap: iridTexture, + iridescenceThicknessMap: iridThickTexture, + + // Anisotropy (brushed metal) + anisotropy: 1.0, + anisotropyRotation: 0, + anisotropyMap: anisoTexture, + + // Specular + specularIntensity: 1, + specularColor: new THREE.Color(0xffffff), + specularIntensityMap: specIntTexture, + specularColorMap: specColorTexture, +}); +``` + +### Glass Material Example + +```javascript +const glass = new THREE.MeshPhysicalMaterial({ + color: 0xffffff, + metalness: 0, + roughness: 0, + transmission: 1, + thickness: 0.5, + ior: 1.5, + envMapIntensity: 1, +}); +``` + +### Car Paint Example + +```javascript +const carPaint = new THREE.MeshPhysicalMaterial({ + color: 0xff0000, + metalness: 0.9, + roughness: 0.5, + clearcoat: 1, + clearcoatRoughness: 0.1, +}); +``` + +## MeshToonMaterial + +Cel-shaded cartoon look. + +```javascript +const material = new THREE.MeshToonMaterial({ + color: 0x00ff00, + gradientMap: gradientTexture, // Optional: custom shading gradient +}); + +// Create step gradient texture +const colors = new Uint8Array([0, 128, 255]); +const gradientMap = new THREE.DataTexture(colors, 3, 1, THREE.RedFormat); +gradientMap.minFilter = THREE.NearestFilter; +gradientMap.magFilter = THREE.NearestFilter; +gradientMap.needsUpdate = true; +``` + +## MeshNormalMaterial + +Visualize surface normals. Useful for debugging. + +```javascript +const material = new THREE.MeshNormalMaterial({ + flatShading: false, + wireframe: false, +}); +``` + +## MeshDepthMaterial + +Render depth values. Used for shadow maps, DOF effects. + +```javascript +const material = new THREE.MeshDepthMaterial({ + depthPacking: THREE.RGBADepthPacking, +}); +``` + +## PointsMaterial + +For point clouds. + +```javascript +const material = new THREE.PointsMaterial({ + color: 0xffffff, + size: 0.1, + sizeAttenuation: true, // Scale with distance + map: pointTexture, + alphaMap: alphaTexture, + transparent: true, + alphaTest: 0.5, // Discard pixels below threshold + vertexColors: true, // Use per-vertex colors +}); + +const points = new THREE.Points(geometry, material); +``` + +## LineBasicMaterial & LineDashedMaterial + +```javascript +// Solid lines +const lineMaterial = new THREE.LineBasicMaterial({ + color: 0xffffff, + linewidth: 1, // Note: >1 only works on some systems + linecap: "round", + linejoin: "round", +}); + +// Dashed lines +const dashedMaterial = new THREE.LineDashedMaterial({ + color: 0xffffff, + dashSize: 0.5, + gapSize: 0.25, + scale: 1, +}); + +// Required for dashed lines +const line = new THREE.Line(geometry, dashedMaterial); +line.computeLineDistances(); +``` + +## ShaderMaterial + +Custom GLSL shaders with Three.js uniforms. + +```javascript +const material = new THREE.ShaderMaterial({ + uniforms: { + time: { value: 0 }, + color: { value: new THREE.Color(0xff0000) }, + texture1: { value: texture }, + }, + vertexShader: ` + varying vec2 vUv; + uniform float time; + + void main() { + vUv = uv; + vec3 pos = position; + pos.z += sin(pos.x * 10.0 + time) * 0.1; + gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0); + } + `, + fragmentShader: ` + varying vec2 vUv; + uniform vec3 color; + uniform sampler2D texture1; + + void main() { + // Use texture2D() for GLSL 1.0, texture() for GLSL 3.0 (glslVersion: THREE.GLSL3) + vec4 texColor = texture2D(texture1, vUv); + gl_FragColor = vec4(color * texColor.rgb, 1.0); + } + `, + transparent: true, + side: THREE.DoubleSide, +}); + +// Update uniform in animation loop +material.uniforms.time.value = clock.getElapsedTime(); +``` + +### Built-in Uniforms (auto-provided) + +```glsl +// Vertex shader +uniform mat4 modelMatrix; // Object to world +uniform mat4 modelViewMatrix; // Object to camera +uniform mat4 projectionMatrix; // Camera projection +uniform mat4 viewMatrix; // World to camera +uniform mat3 normalMatrix; // For transforming normals +uniform vec3 cameraPosition; // Camera world position + +// Attributes +attribute vec3 position; +attribute vec3 normal; +attribute vec2 uv; +``` + +## RawShaderMaterial + +Full control - no built-in uniforms/attributes. + +```javascript +const material = new THREE.RawShaderMaterial({ + uniforms: { + projectionMatrix: { value: camera.projectionMatrix }, + modelViewMatrix: { value: new THREE.Matrix4() }, + }, + vertexShader: ` + precision highp float; + attribute vec3 position; + uniform mat4 projectionMatrix; + uniform mat4 modelViewMatrix; + + void main() { + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); + } + `, + fragmentShader: ` + precision highp float; + + void main() { + gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); + } + `, +}); +``` + +## Common Material Properties + +All materials share these base properties: + +```javascript +// Visibility +material.visible = true; +material.transparent = false; +material.opacity = 1.0; +material.alphaTest = 0; // Discard pixels with alpha < value + +// Rendering +material.side = THREE.FrontSide; // FrontSide, BackSide, DoubleSide +material.depthTest = true; +material.depthWrite = true; +material.colorWrite = true; + +// Blending +material.blending = THREE.NormalBlending; +// NormalBlending, AdditiveBlending, SubtractiveBlending, MultiplyBlending, CustomBlending + +// Stencil +material.stencilWrite = false; +material.stencilFunc = THREE.AlwaysStencilFunc; +material.stencilRef = 0; +material.stencilMask = 0xff; + +// Polygon offset (z-fighting fix) +material.polygonOffset = false; +material.polygonOffsetFactor = 0; +material.polygonOffsetUnits = 0; + +// Misc +material.dithering = false; +material.toneMapped = true; +``` + +## Multiple Materials + +```javascript +// Assign different materials to geometry groups +const geometry = new THREE.BoxGeometry(1, 1, 1); +const materials = [ + new THREE.MeshBasicMaterial({ color: 0xff0000 }), // right + new THREE.MeshBasicMaterial({ color: 0x00ff00 }), // left + new THREE.MeshBasicMaterial({ color: 0x0000ff }), // top + new THREE.MeshBasicMaterial({ color: 0xffff00 }), // bottom + new THREE.MeshBasicMaterial({ color: 0xff00ff }), // front + new THREE.MeshBasicMaterial({ color: 0x00ffff }), // back +]; +const mesh = new THREE.Mesh(geometry, materials); + +// Custom groups +geometry.clearGroups(); +geometry.addGroup(0, 6, 0); // start, count, materialIndex +geometry.addGroup(6, 6, 1); +``` + +## Environment Maps + +```javascript +// Load cube texture +const cubeLoader = new THREE.CubeTextureLoader(); +const envMap = cubeLoader.load([ + "px.jpg", + "nx.jpg", // positive/negative X + "py.jpg", + "ny.jpg", // positive/negative Y + "pz.jpg", + "nz.jpg", // positive/negative Z +]); + +// Apply to material +material.envMap = envMap; +material.envMapIntensity = 1; + +// Or set as scene environment (affects all PBR materials) +scene.environment = envMap; + +// HDR environment (recommended) +import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader.js"; +const rgbeLoader = new RGBELoader(); +rgbeLoader.load("environment.hdr", (texture) => { + texture.mapping = THREE.EquirectangularReflectionMapping; + scene.environment = texture; + scene.background = texture; +}); +``` + +## Material Cloning and Modification + +```javascript +// Clone material +const clone = material.clone(); +clone.color.set(0x00ff00); + +// Modify at runtime +material.color.set(0xff0000); +material.needsUpdate = true; // Only needed for some changes + +// When needsUpdate is required: +// - Changing flat shading +// - Changing texture +// - Changing transparent +// - Custom shader code changes +``` + +## Performance Tips + +1. **Reuse materials**: Same material = batched draw calls +2. **Avoid transparent when possible**: Transparent materials require sorting +3. **Use alphaTest instead of transparency**: When applicable, faster +4. **Choose simpler materials**: Basic > Lambert > Phong > Standard > Physical +5. **Limit active lights**: Each light adds shader complexity + +```javascript +// Material pooling +const materialCache = new Map(); +function getMaterial(color) { + const key = color.toString(16); + if (!materialCache.has(key)) { + materialCache.set(key, new THREE.MeshStandardMaterial({ color })); + } + return materialCache.get(key); +} + +// Dispose when done +material.dispose(); +``` + +## See Also + +- `threejs-textures` - Texture loading and configuration +- `threejs-shaders` - Custom shader development +- `threejs-lighting` - Light interaction with materials diff --git a/.codex/skills/threejs-postprocessing/SKILL.md b/.codex/skills/threejs-postprocessing/SKILL.md new file mode 100644 index 0000000..62675b9 --- /dev/null +++ b/.codex/skills/threejs-postprocessing/SKILL.md @@ -0,0 +1,602 @@ +--- +name: threejs-postprocessing +description: Three.js post-processing - EffectComposer, bloom, DOF, screen effects. Use when adding visual effects, color grading, blur, glow, or creating custom screen-space shaders. +--- + +# Three.js Post-Processing + +## Quick Start + +```javascript +import * as THREE from "three"; +import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js"; +import { RenderPass } from "three/addons/postprocessing/RenderPass.js"; +import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js"; + +// Setup composer +const composer = new EffectComposer(renderer); + +// Render scene +const renderPass = new RenderPass(scene, camera); +composer.addPass(renderPass); + +// Add bloom +const bloomPass = new UnrealBloomPass( + new THREE.Vector2(window.innerWidth, window.innerHeight), + 1.5, // strength + 0.4, // radius + 0.85, // threshold +); +composer.addPass(bloomPass); + +// Animation loop - use composer instead of renderer +function animate() { + requestAnimationFrame(animate); + composer.render(); // NOT renderer.render() +} +``` + +## EffectComposer Setup + +```javascript +import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js"; +import { RenderPass } from "three/addons/postprocessing/RenderPass.js"; + +const composer = new EffectComposer(renderer); + +// First pass: render scene +const renderPass = new RenderPass(scene, camera); +composer.addPass(renderPass); + +// Add more passes... +composer.addPass(effectPass); + +// Last pass should render to screen +effectPass.renderToScreen = true; // Default for last pass + +// Handle resize +function onResize() { + const width = window.innerWidth; + const height = window.innerHeight; + + camera.aspect = width / height; + camera.updateProjectionMatrix(); + + renderer.setSize(width, height); + composer.setSize(width, height); +} +``` + +## Common Effects + +### Bloom (Glow) + +```javascript +import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js"; + +const bloomPass = new UnrealBloomPass( + new THREE.Vector2(window.innerWidth, window.innerHeight), + 1.5, // strength - intensity of glow + 0.4, // radius - spread of glow + 0.85, // threshold - brightness threshold +); + +composer.addPass(bloomPass); + +// Adjust at runtime +bloomPass.strength = 2.0; +bloomPass.threshold = 0.5; +bloomPass.radius = 0.8; +``` + +### Selective Bloom + +Apply bloom only to specific objects. + +```javascript +import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js"; +import { ShaderPass } from "three/addons/postprocessing/ShaderPass.js"; + +// Layer setup +const BLOOM_LAYER = 1; +const bloomLayer = new THREE.Layers(); +bloomLayer.set(BLOOM_LAYER); + +// Mark objects to bloom +glowingMesh.layers.enable(BLOOM_LAYER); + +// Dark material for non-blooming objects +const darkMaterial = new THREE.MeshBasicMaterial({ color: 0x000000 }); +const materials = {}; + +function darkenNonBloomed(obj) { + if (obj.isMesh && !bloomLayer.test(obj.layers)) { + materials[obj.uuid] = obj.material; + obj.material = darkMaterial; + } +} + +function restoreMaterial(obj) { + if (materials[obj.uuid]) { + obj.material = materials[obj.uuid]; + delete materials[obj.uuid]; + } +} + +// Custom render loop +function render() { + // Render bloom pass + scene.traverse(darkenNonBloomed); + composer.render(); + scene.traverse(restoreMaterial); + + // Render final scene over bloom + renderer.render(scene, camera); +} +``` + +### FXAA (Anti-Aliasing) + +```javascript +import { ShaderPass } from "three/addons/postprocessing/ShaderPass.js"; +import { FXAAShader } from "three/addons/shaders/FXAAShader.js"; + +const fxaaPass = new ShaderPass(FXAAShader); +fxaaPass.material.uniforms["resolution"].value.set( + 1 / window.innerWidth, + 1 / window.innerHeight, +); + +composer.addPass(fxaaPass); + +// Update on resize +function onResize() { + fxaaPass.material.uniforms["resolution"].value.set( + 1 / window.innerWidth, + 1 / window.innerHeight, + ); +} +``` + +### SMAA (Better Anti-Aliasing) + +```javascript +import { SMAAPass } from "three/addons/postprocessing/SMAAPass.js"; + +const smaaPass = new SMAAPass( + window.innerWidth * renderer.getPixelRatio(), + window.innerHeight * renderer.getPixelRatio(), +); + +composer.addPass(smaaPass); +``` + +### SSAO (Ambient Occlusion) + +```javascript +import { SSAOPass } from "three/addons/postprocessing/SSAOPass.js"; + +const ssaoPass = new SSAOPass( + scene, + camera, + window.innerWidth, + window.innerHeight, +); +ssaoPass.kernelRadius = 16; +ssaoPass.minDistance = 0.005; +ssaoPass.maxDistance = 0.1; + +composer.addPass(ssaoPass); + +// Output modes +ssaoPass.output = SSAOPass.OUTPUT.Default; +// SSAOPass.OUTPUT.Default - Final composited output +// SSAOPass.OUTPUT.SSAO - Just the AO +// SSAOPass.OUTPUT.Blur - Blurred AO +// SSAOPass.OUTPUT.Depth - Depth buffer +// SSAOPass.OUTPUT.Normal - Normal buffer +``` + +### Depth of Field (DOF) + +```javascript +import { BokehPass } from "three/addons/postprocessing/BokehPass.js"; + +const bokehPass = new BokehPass(scene, camera, { + focus: 10.0, // Focus distance + aperture: 0.025, // Aperture (smaller = more DOF) + maxblur: 0.01, // Max blur amount +}); + +composer.addPass(bokehPass); + +// Update focus dynamically +bokehPass.uniforms["focus"].value = distanceToTarget; +``` + +### Film Grain + +```javascript +import { FilmPass } from "three/addons/postprocessing/FilmPass.js"; + +const filmPass = new FilmPass( + 0.35, // noise intensity + 0.5, // scanline intensity + 648, // scanline count + false, // grayscale +); + +composer.addPass(filmPass); +``` + +### Vignette + +```javascript +import { ShaderPass } from "three/addons/postprocessing/ShaderPass.js"; +import { VignetteShader } from "three/addons/shaders/VignetteShader.js"; + +const vignettePass = new ShaderPass(VignetteShader); +vignettePass.uniforms["offset"].value = 1.0; // Vignette size +vignettePass.uniforms["darkness"].value = 1.0; // Vignette intensity + +composer.addPass(vignettePass); +``` + +### Color Correction + +```javascript +import { ShaderPass } from "three/addons/postprocessing/ShaderPass.js"; +import { ColorCorrectionShader } from "three/addons/shaders/ColorCorrectionShader.js"; + +const colorPass = new ShaderPass(ColorCorrectionShader); +colorPass.uniforms["powRGB"].value = new THREE.Vector3(1.2, 1.2, 1.2); // Power +colorPass.uniforms["mulRGB"].value = new THREE.Vector3(1.0, 1.0, 1.0); // Multiply + +composer.addPass(colorPass); +``` + +### Gamma Correction + +```javascript +import { GammaCorrectionShader } from "three/addons/shaders/GammaCorrectionShader.js"; + +const gammaPass = new ShaderPass(GammaCorrectionShader); +composer.addPass(gammaPass); +``` + +### Pixelation + +```javascript +import { RenderPixelatedPass } from "three/addons/postprocessing/RenderPixelatedPass.js"; + +const pixelPass = new RenderPixelatedPass(6, scene, camera); // 6 = pixel size + +composer.addPass(pixelPass); +``` + +### Glitch Effect + +```javascript +import { GlitchPass } from "three/addons/postprocessing/GlitchPass.js"; + +const glitchPass = new GlitchPass(); +glitchPass.goWild = false; // Continuous glitching + +composer.addPass(glitchPass); +``` + +### Halftone + +```javascript +import { HalftonePass } from "three/addons/postprocessing/HalftonePass.js"; + +const halftonePass = new HalftonePass(window.innerWidth, window.innerHeight, { + shape: 1, // 1 = dot, 2 = ellipse, 3 = line, 4 = square + radius: 4, // Dot size + rotateR: Math.PI / 12, + rotateB: (Math.PI / 12) * 2, + rotateG: (Math.PI / 12) * 3, + scatter: 0, + blending: 1, + blendingMode: 1, + greyscale: false, +}); + +composer.addPass(halftonePass); +``` + +### Outline + +```javascript +import { OutlinePass } from "three/addons/postprocessing/OutlinePass.js"; + +const outlinePass = new OutlinePass( + new THREE.Vector2(window.innerWidth, window.innerHeight), + scene, + camera, +); + +outlinePass.edgeStrength = 3; +outlinePass.edgeGlow = 0; +outlinePass.edgeThickness = 1; +outlinePass.pulsePeriod = 0; +outlinePass.visibleEdgeColor.set(0xffffff); +outlinePass.hiddenEdgeColor.set(0x190a05); + +// Select objects to outline +outlinePass.selectedObjects = [mesh1, mesh2]; + +composer.addPass(outlinePass); +``` + +## Custom ShaderPass + +Create your own post-processing effects. + +```javascript +import { ShaderPass } from "three/addons/postprocessing/ShaderPass.js"; + +const CustomShader = { + uniforms: { + tDiffuse: { value: null }, // Required: input texture + time: { value: 0 }, + intensity: { value: 1.0 }, + }, + vertexShader: ` + varying vec2 vUv; + + void main() { + vUv = uv; + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); + } + `, + fragmentShader: ` + uniform sampler2D tDiffuse; + uniform float time; + uniform float intensity; + varying vec2 vUv; + + void main() { + vec2 uv = vUv; + + // Wave distortion + uv.x += sin(uv.y * 10.0 + time) * 0.01 * intensity; + + vec4 color = texture2D(tDiffuse, uv); + gl_FragColor = color; + } + `, +}; + +const customPass = new ShaderPass(CustomShader); +composer.addPass(customPass); + +// Update in animation loop +customPass.uniforms.time.value = clock.getElapsedTime(); +``` + +### Invert Colors Shader + +```javascript +const InvertShader = { + uniforms: { + tDiffuse: { value: null }, + }, + vertexShader: ` + varying vec2 vUv; + void main() { + vUv = uv; + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); + } + `, + fragmentShader: ` + uniform sampler2D tDiffuse; + varying vec2 vUv; + + void main() { + vec4 color = texture2D(tDiffuse, vUv); + gl_FragColor = vec4(1.0 - color.rgb, color.a); + } + `, +}; +``` + +### Chromatic Aberration + +```javascript +const ChromaticAberrationShader = { + uniforms: { + tDiffuse: { value: null }, + amount: { value: 0.005 }, + }, + vertexShader: ` + varying vec2 vUv; + void main() { + vUv = uv; + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); + } + `, + fragmentShader: ` + uniform sampler2D tDiffuse; + uniform float amount; + varying vec2 vUv; + + void main() { + vec2 dir = vUv - 0.5; + float dist = length(dir); + + float r = texture2D(tDiffuse, vUv - dir * amount * dist).r; + float g = texture2D(tDiffuse, vUv).g; + float b = texture2D(tDiffuse, vUv + dir * amount * dist).b; + + gl_FragColor = vec4(r, g, b, 1.0); + } + `, +}; +``` + +## Combining Multiple Effects + +```javascript +import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js"; +import { RenderPass } from "three/addons/postprocessing/RenderPass.js"; +import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js"; +import { ShaderPass } from "three/addons/postprocessing/ShaderPass.js"; +import { FXAAShader } from "three/addons/shaders/FXAAShader.js"; +import { VignetteShader } from "three/addons/shaders/VignetteShader.js"; +import { GammaCorrectionShader } from "three/addons/shaders/GammaCorrectionShader.js"; + +const composer = new EffectComposer(renderer); + +// 1. Render scene +composer.addPass(new RenderPass(scene, camera)); + +// 2. Bloom +const bloomPass = new UnrealBloomPass( + new THREE.Vector2(window.innerWidth, window.innerHeight), + 0.5, + 0.4, + 0.85, +); +composer.addPass(bloomPass); + +// 3. Vignette +const vignettePass = new ShaderPass(VignetteShader); +vignettePass.uniforms["offset"].value = 0.95; +vignettePass.uniforms["darkness"].value = 1.0; +composer.addPass(vignettePass); + +// 4. Gamma correction +composer.addPass(new ShaderPass(GammaCorrectionShader)); + +// 5. Anti-aliasing (always last before output) +const fxaaPass = new ShaderPass(FXAAShader); +fxaaPass.uniforms["resolution"].value.set( + 1 / window.innerWidth, + 1 / window.innerHeight, +); +composer.addPass(fxaaPass); +``` + +## Render to Texture + +```javascript +// Create render target +const renderTarget = new THREE.WebGLRenderTarget(512, 512); + +// Render scene to target +renderer.setRenderTarget(renderTarget); +renderer.render(scene, camera); +renderer.setRenderTarget(null); + +// Use texture +const texture = renderTarget.texture; +otherMaterial.map = texture; +``` + +## Multi-Pass Rendering + +```javascript +// Multiple composers for different scenes/layers +const bgComposer = new EffectComposer(renderer); +bgComposer.addPass(new RenderPass(bgScene, camera)); + +const fgComposer = new EffectComposer(renderer); +fgComposer.addPass(new RenderPass(fgScene, camera)); +fgComposer.addPass(bloomPass); + +// Combine in render loop +function animate() { + // Render background without clearing + renderer.autoClear = false; + renderer.clear(); + + bgComposer.render(); + + // Render foreground over it + renderer.clearDepth(); + fgComposer.render(); +} +``` + +## WebGPU Post-Processing (Three.js r150+) + +```javascript +import { postProcessing } from "three/addons/nodes/Nodes.js"; +import { pass, bloom, dof } from "three/addons/nodes/Nodes.js"; + +// Using node-based system +const scenePass = pass(scene, camera); +const bloomNode = bloom(scenePass, 0.5, 0.4, 0.85); + +const postProcessing = new THREE.PostProcessing(renderer); +postProcessing.outputNode = bloomNode; + +// Render +function animate() { + postProcessing.render(); +} +``` + +## Performance Tips + +1. **Limit passes**: Each pass adds a full-screen render +2. **Lower resolution**: Use smaller render targets for blur passes +3. **Disable unused effects**: Toggle passes on/off +4. **Use FXAA over MSAA**: Less expensive anti-aliasing +5. **Profile with DevTools**: Check GPU usage + +```javascript +// Disable pass +bloomPass.enabled = false; + +// Reduce bloom resolution +const bloomPass = new UnrealBloomPass( + new THREE.Vector2(window.innerWidth / 2, window.innerHeight / 2), + strength, + radius, + threshold, +); + +// Only apply effects in high-performance scenarios +const isMobile = /iPhone|iPad|Android/i.test(navigator.userAgent); +if (!isMobile) { + composer.addPass(expensivePass); +} +``` + +## Handle Resize + +```javascript +function onWindowResize() { + const width = window.innerWidth; + const height = window.innerHeight; + const pixelRatio = renderer.getPixelRatio(); + + camera.aspect = width / height; + camera.updateProjectionMatrix(); + + renderer.setSize(width, height); + composer.setSize(width, height); + + // Update pass-specific resolutions + if (fxaaPass) { + fxaaPass.material.uniforms["resolution"].value.set( + 1 / (width * pixelRatio), + 1 / (height * pixelRatio), + ); + } + + if (bloomPass) { + bloomPass.resolution.set(width, height); + } +} + +window.addEventListener("resize", onWindowResize); +``` + +## See Also + +- `threejs-shaders` - Custom shader development +- `threejs-textures` - Render targets +- `threejs-fundamentals` - Renderer setup diff --git a/.codex/skills/threejs-shaders/SKILL.md b/.codex/skills/threejs-shaders/SKILL.md new file mode 100644 index 0000000..f0dff82 --- /dev/null +++ b/.codex/skills/threejs-shaders/SKILL.md @@ -0,0 +1,642 @@ +--- +name: threejs-shaders +description: Three.js shaders - GLSL, ShaderMaterial, uniforms, custom effects. Use when creating custom visual effects, modifying vertices, writing fragment shaders, or extending built-in materials. +--- + +# Three.js Shaders + +## Quick Start + +```javascript +import * as THREE from "three"; + +const material = new THREE.ShaderMaterial({ + uniforms: { + time: { value: 0 }, + color: { value: new THREE.Color(0xff0000) }, + }, + vertexShader: ` + void main() { + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); + } + `, + fragmentShader: ` + uniform vec3 color; + + void main() { + gl_FragColor = vec4(color, 1.0); + } + `, +}); + +// Update in animation loop +material.uniforms.time.value = clock.getElapsedTime(); +``` + +## ShaderMaterial vs RawShaderMaterial + +### ShaderMaterial + +Three.js provides built-in uniforms and attributes. + +```javascript +const material = new THREE.ShaderMaterial({ + vertexShader: ` + // Built-in uniforms available: + // uniform mat4 modelMatrix; + // uniform mat4 modelViewMatrix; + // uniform mat4 projectionMatrix; + // uniform mat4 viewMatrix; + // uniform mat3 normalMatrix; + // uniform vec3 cameraPosition; + + // Built-in attributes available: + // attribute vec3 position; + // attribute vec3 normal; + // attribute vec2 uv; + + void main() { + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); + } + `, + fragmentShader: ` + void main() { + gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); + } + `, +}); +``` + +### RawShaderMaterial + +Full control - you define everything. + +```javascript +const material = new THREE.RawShaderMaterial({ + uniforms: { + projectionMatrix: { value: camera.projectionMatrix }, + modelViewMatrix: { value: new THREE.Matrix4() }, + }, + vertexShader: ` + precision highp float; + + attribute vec3 position; + uniform mat4 projectionMatrix; + uniform mat4 modelViewMatrix; + + void main() { + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); + } + `, + fragmentShader: ` + precision highp float; + + void main() { + gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); + } + `, +}); +``` + +## Uniforms + +### Uniform Types + +```javascript +const material = new THREE.ShaderMaterial({ + uniforms: { + // Numbers + floatValue: { value: 1.5 }, + intValue: { value: 1 }, + + // Vectors + vec2Value: { value: new THREE.Vector2(1, 2) }, + vec3Value: { value: new THREE.Vector3(1, 2, 3) }, + vec4Value: { value: new THREE.Vector4(1, 2, 3, 4) }, + + // Colors (converted to vec3) + colorValue: { value: new THREE.Color(0xff0000) }, + + // Matrices + mat3Value: { value: new THREE.Matrix3() }, + mat4Value: { value: new THREE.Matrix4() }, + + // Textures + textureValue: { value: texture }, + cubeTextureValue: { value: cubeTexture }, + + // Arrays + floatArray: { value: [1.0, 2.0, 3.0] }, + vec3Array: { + value: [new THREE.Vector3(1, 0, 0), new THREE.Vector3(0, 1, 0)], + }, + }, +}); +``` + +### GLSL Declarations + +```glsl +// In shader +uniform float floatValue; +uniform int intValue; +uniform vec2 vec2Value; +uniform vec3 vec3Value; +uniform vec3 colorValue; // Color becomes vec3 +uniform vec4 vec4Value; +uniform mat3 mat3Value; +uniform mat4 mat4Value; +uniform sampler2D textureValue; +uniform samplerCube cubeTextureValue; +uniform float floatArray[3]; +uniform vec3 vec3Array[2]; +``` + +### Updating Uniforms + +```javascript +// Direct assignment +material.uniforms.time.value = clock.getElapsedTime(); + +// Vector/Color updates +material.uniforms.position.value.set(x, y, z); +material.uniforms.color.value.setHSL(hue, 1, 0.5); + +// Matrix updates +material.uniforms.matrix.value.copy(mesh.matrixWorld); +``` + +## Varyings + +Pass data from vertex to fragment shader. + +```javascript +const material = new THREE.ShaderMaterial({ + vertexShader: ` + varying vec2 vUv; + varying vec3 vNormal; + varying vec3 vPosition; + + void main() { + vUv = uv; + vNormal = normalize(normalMatrix * normal); + vPosition = (modelViewMatrix * vec4(position, 1.0)).xyz; + + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); + } + `, + fragmentShader: ` + varying vec2 vUv; + varying vec3 vNormal; + varying vec3 vPosition; + + void main() { + // Use interpolated values + gl_FragColor = vec4(vNormal * 0.5 + 0.5, 1.0); + } + `, +}); +``` + +## Common Shader Patterns + +### Texture Sampling + +```javascript +const material = new THREE.ShaderMaterial({ + uniforms: { + map: { value: texture }, + }, + vertexShader: ` + varying vec2 vUv; + + void main() { + vUv = uv; + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); + } + `, + fragmentShader: ` + uniform sampler2D map; + varying vec2 vUv; + + void main() { + vec4 texColor = texture2D(map, vUv); + gl_FragColor = texColor; + } + `, +}); +``` + +### Vertex Displacement + +```javascript +const material = new THREE.ShaderMaterial({ + uniforms: { + time: { value: 0 }, + amplitude: { value: 0.5 }, + }, + vertexShader: ` + uniform float time; + uniform float amplitude; + + void main() { + vec3 pos = position; + + // Wave displacement + pos.z += sin(pos.x * 5.0 + time) * amplitude; + pos.z += sin(pos.y * 5.0 + time) * amplitude; + + gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0); + } + `, + fragmentShader: ` + void main() { + gl_FragColor = vec4(0.5, 0.8, 1.0, 1.0); + } + `, +}); +``` + +### Fresnel Effect + +```javascript +const material = new THREE.ShaderMaterial({ + vertexShader: ` + varying vec3 vNormal; + varying vec3 vWorldPosition; + + void main() { + vNormal = normalize(normalMatrix * normal); + vWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz; + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); + } + `, + fragmentShader: ` + varying vec3 vNormal; + varying vec3 vWorldPosition; + + void main() { + // cameraPosition is auto-provided by ShaderMaterial + vec3 viewDirection = normalize(cameraPosition - vWorldPosition); + float fresnel = pow(1.0 - dot(viewDirection, vNormal), 3.0); + + vec3 baseColor = vec3(0.0, 0.0, 0.5); + vec3 fresnelColor = vec3(0.5, 0.8, 1.0); + + gl_FragColor = vec4(mix(baseColor, fresnelColor, fresnel), 1.0); + } + `, +}); +``` + +### Noise-Based Effects + +```glsl +// Simple noise function +float random(vec2 st) { + return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453); +} + +// Value noise +float noise(vec2 st) { + vec2 i = floor(st); + vec2 f = fract(st); + + float a = random(i); + float b = random(i + vec2(1.0, 0.0)); + float c = random(i + vec2(0.0, 1.0)); + float d = random(i + vec2(1.0, 1.0)); + + vec2 u = f * f * (3.0 - 2.0 * f); + + return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y; +} + +// Usage +float n = noise(vUv * 10.0 + time); +``` + +### Gradient + +```glsl +// Linear gradient +vec3 color = mix(colorA, colorB, vUv.y); + +// Radial gradient +float dist = distance(vUv, vec2(0.5)); +vec3 color = mix(centerColor, edgeColor, dist * 2.0); + +// Smooth gradient with custom curve +float t = smoothstep(0.0, 1.0, vUv.y); +vec3 color = mix(colorA, colorB, t); +``` + +### Rim Lighting + +```javascript +const material = new THREE.ShaderMaterial({ + vertexShader: ` + varying vec3 vNormal; + varying vec3 vViewPosition; + + void main() { + vNormal = normalize(normalMatrix * normal); + vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); + vViewPosition = mvPosition.xyz; + gl_Position = projectionMatrix * mvPosition; + } + `, + fragmentShader: ` + varying vec3 vNormal; + varying vec3 vViewPosition; + + void main() { + vec3 viewDir = normalize(-vViewPosition); + float rim = 1.0 - max(0.0, dot(viewDir, vNormal)); + rim = pow(rim, 4.0); + + vec3 baseColor = vec3(0.2, 0.2, 0.8); + vec3 rimColor = vec3(1.0, 0.5, 0.0); + + gl_FragColor = vec4(baseColor + rimColor * rim, 1.0); + } + `, +}); +``` + +### Dissolve Effect + +```glsl +uniform float progress; +uniform sampler2D noiseMap; + +void main() { + float noise = texture2D(noiseMap, vUv).r; + + if (noise < progress) { + discard; + } + + // Edge glow + float edge = smoothstep(progress, progress + 0.1, noise); + vec3 edgeColor = vec3(1.0, 0.5, 0.0); + vec3 baseColor = vec3(0.5); + + gl_FragColor = vec4(mix(edgeColor, baseColor, edge), 1.0); +} +``` + +## Extending Built-in Materials + +### onBeforeCompile + +Modify existing material shaders. + +```javascript +const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 }); + +material.onBeforeCompile = (shader) => { + // Add custom uniform + shader.uniforms.time = { value: 0 }; + + // Store reference for updates + material.userData.shader = shader; + + // Modify vertex shader + shader.vertexShader = shader.vertexShader.replace( + "#include ", + ` + #include + transformed.y += sin(position.x * 10.0 + time) * 0.1; + `, + ); + + // Add uniform declaration + shader.vertexShader = "uniform float time;\n" + shader.vertexShader; +}; + +// Update in animation loop +if (material.userData.shader) { + material.userData.shader.uniforms.time.value = clock.getElapsedTime(); +} +``` + +### Common Injection Points + +```javascript +// Vertex shader chunks +"#include "; // After position is calculated +"#include "; // After gl_Position +"#include "; // Normal calculation start + +// Fragment shader chunks +"#include "; // After diffuse color +"#include "; // Final output +"#include "; // After fog applied +``` + +## GLSL Built-in Functions + +### Math Functions + +```glsl +// Basic +abs(x), sign(x), floor(x), ceil(x), fract(x) +mod(x, y), min(x, y), max(x, y), clamp(x, min, max) +mix(a, b, t), step(edge, x), smoothstep(edge0, edge1, x) + +// Trigonometry +sin(x), cos(x), tan(x) +asin(x), acos(x), atan(y, x), atan(x) +radians(degrees), degrees(radians) + +// Exponential +pow(x, y), exp(x), log(x), exp2(x), log2(x) +sqrt(x), inversesqrt(x) +``` + +### Vector Functions + +```glsl +// Length and distance +length(v), distance(p0, p1), dot(x, y), cross(x, y) + +// Normalization +normalize(v) + +// Reflection and refraction +reflect(I, N), refract(I, N, eta) + +// Component-wise +lessThan(x, y), lessThanEqual(x, y) +greaterThan(x, y), greaterThanEqual(x, y) +equal(x, y), notEqual(x, y) +any(bvec), all(bvec) +``` + +### Texture Functions + +```glsl +// GLSL 1.0 (default) - use texture2D/textureCube +texture2D(sampler, coord) +texture2D(sampler, coord, bias) +textureCube(sampler, coord) + +// GLSL 3.0 (glslVersion: THREE.GLSL3) - use texture() +// texture(sampler, coord) replaces texture2D/textureCube +// Also use: out vec4 fragColor instead of gl_FragColor + +// Texture size (GLSL 1.30+) +textureSize(sampler, lod) +``` + +## Common Material Properties + +```javascript +const material = new THREE.ShaderMaterial({ + uniforms: { + /* ... */ + }, + vertexShader: "/* ... */", + fragmentShader: "/* ... */", + + // Rendering + transparent: true, + opacity: 1.0, + side: THREE.DoubleSide, + depthTest: true, + depthWrite: true, + + // Blending + blending: THREE.NormalBlending, + // AdditiveBlending, SubtractiveBlending, MultiplyBlending + + // Wireframe + wireframe: false, + wireframeLinewidth: 1, // Note: >1 has no effect on most platforms (WebGL limitation) + + // Extensions + extensions: { + derivatives: true, // For fwidth, dFdx, dFdy + fragDepth: true, // gl_FragDepth + drawBuffers: true, // Multiple render targets + shaderTextureLOD: true, // texture2DLod + }, + + // GLSL version + glslVersion: THREE.GLSL3, // For WebGL2 features +}); +``` + +## Shader Includes + +### Using Three.js Shader Chunks + +```javascript +import { ShaderChunk } from "three"; + +const fragmentShader = ` + ${ShaderChunk.common} + ${ShaderChunk.packing} + + uniform sampler2D depthTexture; + varying vec2 vUv; + + void main() { + float depth = texture2D(depthTexture, vUv).r; + float linearDepth = perspectiveDepthToViewZ(depth, 0.1, 1000.0); + gl_FragColor = vec4(vec3(-linearDepth / 100.0), 1.0); + } +`; +``` + +### External Shader Files + +```javascript +// With vite/webpack +import vertexShader from "./shaders/vertex.glsl"; +import fragmentShader from "./shaders/fragment.glsl"; + +const material = new THREE.ShaderMaterial({ + vertexShader, + fragmentShader, +}); +``` + +## Instanced Shaders + +```javascript +// Instanced attribute +const offsets = new Float32Array(instanceCount * 3); +// Fill offsets... +geometry.setAttribute("offset", new THREE.InstancedBufferAttribute(offsets, 3)); + +const material = new THREE.ShaderMaterial({ + vertexShader: ` + attribute vec3 offset; + + void main() { + vec3 pos = position + offset; + gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0); + } + `, + fragmentShader: ` + void main() { + gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); + } + `, +}); +``` + +## Debugging Shaders + +```javascript +// Check for compile errors +material.onBeforeCompile = (shader) => { + console.log("Vertex Shader:", shader.vertexShader); + console.log("Fragment Shader:", shader.fragmentShader); +}; + +// Visual debugging +fragmentShader: ` + void main() { + // Debug UV + gl_FragColor = vec4(vUv, 0.0, 1.0); + + // Debug normals + gl_FragColor = vec4(vNormal * 0.5 + 0.5, 1.0); + + // Debug position + gl_FragColor = vec4(vPosition * 0.1 + 0.5, 1.0); + } +`; + +// Check WebGL errors +renderer.debug.checkShaderErrors = true; +``` + +## Performance Tips + +1. **Minimize uniforms**: Group related values into vectors +2. **Avoid conditionals**: Use mix/step instead of if/else +3. **Precalculate**: Move calculations to JS when possible +4. **Use textures**: For complex functions, use lookup tables +5. **Limit overdraw**: Avoid transparent objects when possible + +```glsl +// Instead of: +if (value > 0.5) { + color = colorA; +} else { + color = colorB; +} + +// Use: +color = mix(colorB, colorA, step(0.5, value)); +``` + +## See Also + +- `threejs-materials` - Built-in material types +- `threejs-postprocessing` - Full-screen shader effects +- `threejs-textures` - Texture sampling in shaders diff --git a/.codex/skills/threejs-textures/SKILL.md b/.codex/skills/threejs-textures/SKILL.md new file mode 100644 index 0000000..5364205 --- /dev/null +++ b/.codex/skills/threejs-textures/SKILL.md @@ -0,0 +1,628 @@ +--- +name: threejs-textures +description: Three.js textures - texture types, UV mapping, environment maps, texture settings. Use when working with images, UV coordinates, cubemaps, HDR environments, or texture optimization. +--- + +# Three.js Textures + +## Quick Start + +```javascript +import * as THREE from "three"; + +// Load texture +const loader = new THREE.TextureLoader(); +const texture = loader.load("texture.jpg"); + +// Apply to material +const material = new THREE.MeshStandardMaterial({ + map: texture, +}); +``` + +## Texture Loading + +### Basic Loading + +```javascript +const loader = new THREE.TextureLoader(); + +// Async with callbacks +loader.load( + "texture.jpg", + (texture) => console.log("Loaded"), + (progress) => console.log("Progress"), + (error) => console.error("Error"), +); + +// Synchronous style (loads async internally) +const texture = loader.load("texture.jpg"); +material.map = texture; +``` + +### Promise Wrapper + +```javascript +function loadTexture(url) { + return new Promise((resolve, reject) => { + new THREE.TextureLoader().load(url, resolve, undefined, reject); + }); +} + +// Usage +const [colorMap, normalMap, roughnessMap] = await Promise.all([ + loadTexture("color.jpg"), + loadTexture("normal.jpg"), + loadTexture("roughness.jpg"), +]); +``` + +## Texture Configuration + +### Color Space + +Critical for accurate color reproduction. + +```javascript +// Color/albedo textures - use sRGB +colorTexture.colorSpace = THREE.SRGBColorSpace; + +// Data textures (normal, roughness, metalness, AO) - leave as default +// Do NOT set colorSpace for data textures (NoColorSpace is default) +``` + +### Wrapping Modes + +```javascript +texture.wrapS = THREE.RepeatWrapping; // Horizontal +texture.wrapT = THREE.RepeatWrapping; // Vertical + +// Options: +// THREE.ClampToEdgeWrapping - Stretches edge pixels (default) +// THREE.RepeatWrapping - Tiles the texture +// THREE.MirroredRepeatWrapping - Tiles with mirror flip +``` + +### Repeat, Offset, Rotation + +```javascript +// Tile texture 4x4 +texture.repeat.set(4, 4); +texture.wrapS = THREE.RepeatWrapping; +texture.wrapT = THREE.RepeatWrapping; + +// Offset (0-1 range) +texture.offset.set(0.5, 0.5); + +// Rotation (radians, around center) +texture.rotation = Math.PI / 4; +texture.center.set(0.5, 0.5); // Rotation pivot +``` + +### Filtering + +```javascript +// Minification (texture larger than screen pixels) +texture.minFilter = THREE.LinearMipmapLinearFilter; // Default, smooth +texture.minFilter = THREE.NearestFilter; // Pixelated +texture.minFilter = THREE.LinearFilter; // Smooth, no mipmaps + +// Magnification (texture smaller than screen pixels) +texture.magFilter = THREE.LinearFilter; // Smooth (default) +texture.magFilter = THREE.NearestFilter; // Pixelated (retro games) + +// Anisotropic filtering (sharper at angles) +texture.anisotropy = renderer.capabilities.getMaxAnisotropy(); +``` + +### Generate Mipmaps + +```javascript +// Usually true by default +texture.generateMipmaps = true; + +// Disable for non-power-of-2 textures or data textures +texture.generateMipmaps = false; +texture.minFilter = THREE.LinearFilter; +``` + +## Texture Types + +### Regular Texture + +```javascript +const texture = new THREE.Texture(image); +texture.needsUpdate = true; +``` + +### Data Texture + +Create texture from raw data. + +```javascript +// Create gradient texture +const size = 256; +const data = new Uint8Array(size * size * 4); + +for (let i = 0; i < size; i++) { + for (let j = 0; j < size; j++) { + const index = (i * size + j) * 4; + data[index] = i; // R + data[index + 1] = j; // G + data[index + 2] = 128; // B + data[index + 3] = 255; // A + } +} + +const texture = new THREE.DataTexture(data, size, size); +texture.needsUpdate = true; +``` + +### Canvas Texture + +```javascript +const canvas = document.createElement("canvas"); +canvas.width = 256; +canvas.height = 256; +const ctx = canvas.getContext("2d"); + +// Draw on canvas +ctx.fillStyle = "red"; +ctx.fillRect(0, 0, 256, 256); +ctx.fillStyle = "white"; +ctx.font = "48px Arial"; +ctx.fillText("Hello", 50, 150); + +const texture = new THREE.CanvasTexture(canvas); + +// Update when canvas changes +texture.needsUpdate = true; +``` + +### Video Texture + +```javascript +const video = document.createElement("video"); +video.src = "video.mp4"; +video.loop = true; +video.muted = true; +video.play(); + +const texture = new THREE.VideoTexture(video); +texture.colorSpace = THREE.SRGBColorSpace; + +// No need to set needsUpdate - auto-updates +``` + +### Compressed Textures + +```javascript +import { KTX2Loader } from "three/examples/jsm/loaders/KTX2Loader.js"; + +const ktx2Loader = new KTX2Loader(); +ktx2Loader.setTranscoderPath("path/to/basis/"); +ktx2Loader.detectSupport(renderer); + +ktx2Loader.load("texture.ktx2", (texture) => { + material.map = texture; +}); +``` + +## Cube Textures + +For environment maps and skyboxes. + +### CubeTextureLoader + +```javascript +const loader = new THREE.CubeTextureLoader(); +const cubeTexture = loader.load([ + "px.jpg", + "nx.jpg", // +X, -X + "py.jpg", + "ny.jpg", // +Y, -Y + "pz.jpg", + "nz.jpg", // +Z, -Z +]); + +// As background +scene.background = cubeTexture; + +// As environment map +scene.environment = cubeTexture; +material.envMap = cubeTexture; +``` + +### Equirectangular to Cubemap + +```javascript +import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader.js"; + +const pmremGenerator = new THREE.PMREMGenerator(renderer); +pmremGenerator.compileEquirectangularShader(); + +new RGBELoader().load("environment.hdr", (texture) => { + const envMap = pmremGenerator.fromEquirectangular(texture).texture; + scene.environment = envMap; + scene.background = envMap; + + texture.dispose(); + pmremGenerator.dispose(); +}); +``` + +## HDR Textures + +### RGBELoader + +```javascript +import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader.js"; + +const loader = new RGBELoader(); +loader.load("environment.hdr", (texture) => { + texture.mapping = THREE.EquirectangularReflectionMapping; + scene.environment = texture; + scene.background = texture; +}); +``` + +### EXRLoader + +```javascript +import { EXRLoader } from "three/examples/jsm/loaders/EXRLoader.js"; + +const loader = new EXRLoader(); +loader.load("environment.exr", (texture) => { + texture.mapping = THREE.EquirectangularReflectionMapping; + scene.environment = texture; +}); +``` + +### Background Options + +```javascript +scene.background = texture; +scene.backgroundBlurriness = 0.5; // 0-1, blur background +scene.backgroundIntensity = 1.0; // Brightness +scene.backgroundRotation.y = Math.PI; // Rotate background +``` + +## Render Targets + +Render to texture for effects. + +```javascript +// Create render target +const renderTarget = new THREE.WebGLRenderTarget(512, 512, { + minFilter: THREE.LinearFilter, + magFilter: THREE.LinearFilter, + format: THREE.RGBAFormat, +}); + +// Render scene to target +renderer.setRenderTarget(renderTarget); +renderer.render(scene, camera); +renderer.setRenderTarget(null); // Back to screen + +// Use as texture +material.map = renderTarget.texture; +``` + +### Depth Texture + +```javascript +const renderTarget = new THREE.WebGLRenderTarget(512, 512); +renderTarget.depthTexture = new THREE.DepthTexture( + 512, + 512, + THREE.UnsignedShortType, +); + +// Access depth +const depthTexture = renderTarget.depthTexture; +``` + +### Multi-Sample Render Target + +```javascript +const renderTarget = new THREE.WebGLRenderTarget(512, 512, { + samples: 4, // MSAA +}); +``` + +## CubeCamera + +Dynamic environment maps for reflections. + +```javascript +const cubeRenderTarget = new THREE.WebGLCubeRenderTarget(256, { + generateMipmaps: true, + minFilter: THREE.LinearMipmapLinearFilter, +}); + +const cubeCamera = new THREE.CubeCamera(0.1, 1000, cubeRenderTarget); +scene.add(cubeCamera); + +// Apply to reflective material +reflectiveMaterial.envMap = cubeRenderTarget.texture; + +// Update in animation loop (expensive!) +function animate() { + // Hide reflective object, update env map, show again + reflectiveObject.visible = false; + cubeCamera.position.copy(reflectiveObject.position); + cubeCamera.update(renderer, scene); + reflectiveObject.visible = true; +} +``` + +## UV Mapping + +### Accessing UVs + +```javascript +const uvs = geometry.attributes.uv; + +// Read UV +const u = uvs.getX(vertexIndex); +const v = uvs.getY(vertexIndex); + +// Modify UV +uvs.setXY(vertexIndex, newU, newV); +uvs.needsUpdate = true; +``` + +### Second UV Channel (for AO maps) + +```javascript +// Required for aoMap +geometry.setAttribute("uv2", geometry.attributes.uv); + +// Or create custom second UV +const uv2 = new Float32Array(vertexCount * 2); +// ... fill uv2 data +geometry.setAttribute("uv2", new THREE.BufferAttribute(uv2, 2)); +``` + +### UV Transform in Shader + +```javascript +const material = new THREE.ShaderMaterial({ + uniforms: { + map: { value: texture }, + uvOffset: { value: new THREE.Vector2(0, 0) }, + uvScale: { value: new THREE.Vector2(1, 1) }, + }, + vertexShader: ` + varying vec2 vUv; + uniform vec2 uvOffset; + uniform vec2 uvScale; + + void main() { + vUv = uv * uvScale + uvOffset; + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); + } + `, + fragmentShader: ` + varying vec2 vUv; + uniform sampler2D map; + + void main() { + gl_FragColor = texture2D(map, vUv); + } + `, +}); +``` + +## Texture Atlas + +Multiple images in one texture. + +```javascript +// Atlas with 4 sprites (2x2 grid) +const atlas = loader.load("atlas.png"); +atlas.wrapS = THREE.ClampToEdgeWrapping; +atlas.wrapT = THREE.ClampToEdgeWrapping; + +// Select sprite by UV offset/scale +function selectSprite(row, col, gridSize = 2) { + atlas.offset.set(col / gridSize, 1 - (row + 1) / gridSize); + atlas.repeat.set(1 / gridSize, 1 / gridSize); +} + +// Select top-left sprite +selectSprite(0, 0); +``` + +## Material Texture Maps + +### PBR Texture Set + +```javascript +const material = new THREE.MeshStandardMaterial({ + // Base color (sRGB) + map: colorTexture, + + // Surface detail (Linear) + normalMap: normalTexture, + normalScale: new THREE.Vector2(1, 1), + + // Roughness (Linear, grayscale) + roughnessMap: roughnessTexture, + roughness: 1, // Multiplier + + // Metalness (Linear, grayscale) + metalnessMap: metalnessTexture, + metalness: 1, // Multiplier + + // Ambient occlusion (Linear, uses uv2) + aoMap: aoTexture, + aoMapIntensity: 1, + + // Self-illumination (sRGB) + emissiveMap: emissiveTexture, + emissive: 0xffffff, + emissiveIntensity: 1, + + // Vertex displacement (Linear) + displacementMap: displacementTexture, + displacementScale: 0.1, + displacementBias: 0, + + // Alpha (Linear) + alphaMap: alphaTexture, + transparent: true, +}); + +// Don't forget UV2 for AO +geometry.setAttribute("uv2", geometry.attributes.uv); +``` + +### Normal Map Types + +```javascript +// OpenGL style normals (default) +material.normalMapType = THREE.TangentSpaceNormalMap; + +// Object space normals +material.normalMapType = THREE.ObjectSpaceNormalMap; +``` + +## Procedural Textures + +### Noise Texture + +```javascript +function generateNoiseTexture(size = 256) { + const data = new Uint8Array(size * size * 4); + + for (let i = 0; i < size * size; i++) { + const value = Math.random() * 255; + data[i * 4] = value; + data[i * 4 + 1] = value; + data[i * 4 + 2] = value; + data[i * 4 + 3] = 255; + } + + const texture = new THREE.DataTexture(data, size, size); + texture.needsUpdate = true; + return texture; +} +``` + +### Gradient Texture + +```javascript +function generateGradientTexture(color1, color2, size = 256) { + const canvas = document.createElement("canvas"); + canvas.width = size; + canvas.height = 1; + const ctx = canvas.getContext("2d"); + + const gradient = ctx.createLinearGradient(0, 0, size, 0); + gradient.addColorStop(0, color1); + gradient.addColorStop(1, color2); + + ctx.fillStyle = gradient; + ctx.fillRect(0, 0, size, 1); + + return new THREE.CanvasTexture(canvas); +} +``` + +## Texture Memory Management + +### Dispose Textures + +```javascript +// Single texture +texture.dispose(); + +// Material textures +function disposeMaterial(material) { + const maps = [ + "map", + "normalMap", + "roughnessMap", + "metalnessMap", + "aoMap", + "emissiveMap", + "displacementMap", + "alphaMap", + "envMap", + "lightMap", + "bumpMap", + "specularMap", + ]; + + maps.forEach((mapName) => { + if (material[mapName]) { + material[mapName].dispose(); + } + }); + + material.dispose(); +} +``` + +### Texture Pooling + +```javascript +class TexturePool { + constructor() { + this.textures = new Map(); + this.loader = new THREE.TextureLoader(); + } + + async get(url) { + if (this.textures.has(url)) { + return this.textures.get(url); + } + + const texture = await new Promise((resolve, reject) => { + this.loader.load(url, resolve, undefined, reject); + }); + + this.textures.set(url, texture); + return texture; + } + + dispose(url) { + const texture = this.textures.get(url); + if (texture) { + texture.dispose(); + this.textures.delete(url); + } + } + + disposeAll() { + this.textures.forEach((t) => t.dispose()); + this.textures.clear(); + } +} +``` + +## Performance Tips + +1. **Use power-of-2 dimensions**: 256, 512, 1024, 2048 +2. **Compress textures**: KTX2/Basis for web delivery +3. **Use texture atlases**: Reduce texture switches +4. **Enable mipmaps**: For distant objects +5. **Limit texture size**: 2048 usually sufficient for web +6. **Reuse textures**: Same texture = better batching + +```javascript +// Check texture memory +console.log(renderer.info.memory.textures); + +// Optimize for mobile +const maxSize = renderer.capabilities.maxTextureSize; +const isMobile = /iPhone|iPad|Android/i.test(navigator.userAgent); +const textureSize = isMobile ? 1024 : 2048; +``` + +## See Also + +- `threejs-materials` - Applying textures to materials +- `threejs-loaders` - Loading texture files +- `threejs-shaders` - Custom texture sampling diff --git a/.codex/skills/webgpu-threejs-tsl/REFERENCE.md b/.codex/skills/webgpu-threejs-tsl/REFERENCE.md new file mode 100644 index 0000000..2ee0a84 --- /dev/null +++ b/.codex/skills/webgpu-threejs-tsl/REFERENCE.md @@ -0,0 +1,335 @@ +# TSL Quick Reference + +## Imports + +```javascript +// WebGPU Three.js +import * as THREE from 'three/webgpu'; + +// Core TSL +import { + float, int, uint, bool, + vec2, vec3, vec4, color, + mat2, mat3, mat4, + uniform, texture, uv, + Fn, If, Loop, Break, Continue, + time, deltaTime +} from 'three/tsl'; +``` + +## Types + +| TSL | WGSL | Example | +|-----|------|---------| +| `float(1.0)` | `f32` | Scalar float | +| `int(1)` | `i32` | Signed integer | +| `uint(1)` | `u32` | Unsigned integer | +| `bool(true)` | `bool` | Boolean | +| `vec2(x, y)` | `vec2` | 2D vector | +| `vec3(x, y, z)` | `vec3` | 3D vector | +| `vec4(x, y, z, w)` | `vec4` | 4D vector | +| `color(0xff0000)` | `vec3` | RGB color | +| `uniform(value)` | uniform | Dynamic value | + +## Operators + +| Operation | TSL | GLSL Equivalent | +|-----------|-----|-----------------| +| Add | `a.add(b)` | `a + b` | +| Subtract | `a.sub(b)` | `a - b` | +| Multiply | `a.mul(b)` | `a * b` | +| Divide | `a.div(b)` | `a / b` | +| Modulo | `a.mod(b)` | `mod(a, b)` | +| Negate | `a.negate()` | `-a` | +| Less Than | `a.lessThan(b)` | `a < b` | +| Greater Than | `a.greaterThan(b)` | `a > b` | +| Equal | `a.equal(b)` | `a == b` | +| And | `a.and(b)` | `a && b` | +| Or | `a.or(b)` | `a \|\| b` | +| Assign | `a.assign(b)` | `a = b` | +| Add Assign | `a.addAssign(b)` | `a += b` | + +## Swizzling + +```javascript +const v = vec3(1, 2, 3); +v.x // 1 +v.xy // vec2(1, 2) +v.zyx // vec3(3, 2, 1) +v.rgb // same as xyz +``` + +## Math Functions + +| Function | Description | +|----------|-------------| +| `abs(x)` | Absolute value | +| `sign(x)` | Sign (-1, 0, 1) | +| `floor(x)` | Round down | +| `ceil(x)` | Round up | +| `fract(x)` | Fractional part | +| `min(a, b)` | Minimum | +| `max(a, b)` | Maximum | +| `clamp(x, lo, hi)` | Clamp to range | +| `mix(a, b, t)` | Linear interpolation | +| `step(edge, x)` | Step function | +| `smoothstep(a, b, x)` | Smooth step | +| `sin(x)`, `cos(x)` | Trigonometry | +| `pow(x, y)` | Power | +| `sqrt(x)` | Square root | +| `length(v)` | Vector length | +| `distance(a, b)` | Distance | +| `dot(a, b)` | Dot product | +| `cross(a, b)` | Cross product | +| `normalize(v)` | Unit vector | +| `reflect(i, n)` | Reflection | + +## Geometry Nodes + +| Node | Description | +|------|-------------| +| `positionLocal` | Model space position | +| `positionWorld` | World space position | +| `positionView` | Camera space position | +| `normalLocal` | Model space normal | +| `normalWorld` | World space normal | +| `normalView` | Camera space normal | +| `uv()` | UV coordinates | +| `uv(1)` | Secondary UVs | +| `tangentLocal` | Tangent vector | +| `vertexColor()` | Vertex colors | + +## Camera Nodes + +| Node | Description | +|------|-------------| +| `cameraPosition` | Camera world position | +| `cameraNear` | Near plane | +| `cameraFar` | Far plane | +| `cameraViewMatrix` | View matrix | +| `cameraProjectionMatrix` | Projection matrix | +| `screenUV` | Screen UV (0-1) | +| `screenSize` | Screen dimensions | + +## Time + +| Node | Description | +|------|-------------| +| `time` | Seconds since start | +| `deltaTime` | Frame delta | +| `oscSine(t)` | Sine wave (0-1) | +| `oscSquare(t)` | Square wave | +| `oscTriangle(t)` | Triangle wave | +| `oscSawtooth(t)` | Sawtooth wave | + +## Material Properties + +```javascript +const mat = new THREE.MeshStandardNodeMaterial(); + +// Basic +mat.colorNode = color(0xff0000); +mat.opacityNode = float(0.8); +mat.alphaTestNode = float(0.5); + +// PBR +mat.roughnessNode = float(0.5); +mat.metalnessNode = float(0.0); +mat.emissiveNode = color(0x000000); +mat.normalNode = normalMap(tex); + +// Physical (MeshPhysicalNodeMaterial) +mat.clearcoatNode = float(1.0); +mat.transmissionNode = float(0.9); +mat.iridescenceNode = float(1.0); +mat.sheenNode = float(1.0); + +// Vertex +mat.positionNode = displaced; +``` + +## Control Flow + +```javascript +// If-Else +If(condition, () => { + // true +}).ElseIf(other, () => { + // other true +}).Else(() => { + // false +}); + +// Select (ternary) +const result = select(condition, trueVal, falseVal); + +// Loop +Loop(10, ({ i }) => { + // i = 0 to 9 +}); + +// Loop control +Break(); +Continue(); +Discard(); // Fragment only +``` + +## Custom Functions + +```javascript +// Basic function +const myFn = Fn(([a, b]) => { + return a.add(b); +}); + +// With defaults +const myFn = Fn(([a = 1.0, b = 2.0]) => { + return a.add(b); +}); + +// Usage +myFn(x, y); +myFn(); // uses defaults +``` + +## Compute Shaders + +```javascript +// Storage buffers +const positions = instancedArray(count, 'vec3'); +const values = instancedArray(count, 'float'); + +// Compute shader +const compute = Fn(() => { + const pos = positions.element(instanceIndex); + pos.addAssign(vec3(0.01, 0, 0)); +})().compute(count); + +// Execute +await renderer.computeAsync(compute); // Once +renderer.compute(compute); // Each frame +``` + +## Post-Processing + +```javascript +import { pass } from 'three/tsl'; +import { bloom } from 'three/addons/tsl/display/BloomNode.js'; + +// Setup +const postProcessing = new THREE.PostProcessing(renderer); +const scenePass = pass(scene, camera); +const color = scenePass.getTextureNode('output'); + +// Apply effects +const bloomPass = bloom(color); +postProcessing.outputNode = color.add(bloomPass); + +// Render +postProcessing.render(); +``` + +## Common Patterns + +### Fresnel + +```javascript +const viewDir = cameraPosition.sub(positionWorld).normalize(); +const fresnel = float(1).sub(normalWorld.dot(viewDir).saturate()).pow(3); +``` + +### Animated UV + +```javascript +const animUV = uv().add(vec2(time.mul(0.1), 0)); +``` + +### Noise Hash + +```javascript +const noise = fract(position.dot(vec3(12.9898, 78.233, 45.543)).sin().mul(43758.5453)); +``` + +### Dissolve + +```javascript +const noise = hash(positionLocal.mul(50)); +If(noise.lessThan(threshold), () => Discard()); +``` + +### Color Gradient + +```javascript +const gradient = mix(colorA, colorB, positionLocal.y.mul(0.5).add(0.5)); +``` + +## Node Materials + +| Material | Use Case | +|----------|----------| +| `MeshBasicNodeMaterial` | Unlit | +| `MeshStandardNodeMaterial` | PBR | +| `MeshPhysicalNodeMaterial` | Advanced PBR | +| `MeshPhongNodeMaterial` | Phong shading | +| `MeshToonNodeMaterial` | Cel shading | +| `PointsNodeMaterial` | Point clouds | +| `LineBasicNodeMaterial` | Lines | +| `SpriteNodeMaterial` | Sprites | + +## Device Loss Handling + +```javascript +// Listen for device loss +renderer.backend.device.lost.then((info) => { + if (info.reason === 'unknown') { + // Unexpected loss - recover + renderer.dispose(); + initWebGPU(); // Reinitialize + } +}); + +// Simulate loss for testing +renderer.backend.device.destroy(); +``` + +| Loss Reason | Meaning | +|-------------|---------| +| `'destroyed'` | Intentional via `destroy()` | +| `'unknown'` | Unexpected (driver crash, timeout, etc.) | + +**Recovery tips:** +- Always get fresh adapter before new device +- Save/restore application state (not transient data) +- Use Chrome `about:gpucrash` to test real GPU crashes + +## Compute Shader Built-ins + +| Node | Description | +|------|-------------| +| `instanceIndex` | Current instance/invocation index | +| `vertexIndex` | Current vertex index | +| `drawIndex` | Current draw call index | +| `globalId` | Global invocation position (uvec3) | +| `localId` | Local workgroup position (uvec3) | +| `workgroupId` | Workgroup index (uvec3) | +| `numWorkgroups` | Number of workgroups dispatched (uvec3) | +| `subgroupSize` | Size of the subgroup | + +## Version Notes + +**r178+:** +- `PI2` is deprecated → use `TWO_PI` +- `transformedNormalView` → use `normalView` +- `transformedNormalWorld` → use `normalWorld` + +**r171+:** +- Recommended minimum version for stable TSL +- Requires separate `three/webgpu` import map entry + +## Resources + +- [TSL Wiki](https://github.com/mrdoob/three.js/wiki/Three.js-Shading-Language) +- [TSL Docs](https://threejs.org/docs/pages/TSL.html) +- [WebGPU Examples](https://github.com/mrdoob/three.js/tree/master/examples) +- [Three.js Docs](https://threejs.org/docs/) +- [WebGPU Best Practices - Device Loss](https://toji.dev/webgpu-best-practices/device-loss) diff --git a/.codex/skills/webgpu-threejs-tsl/SKILL.md b/.codex/skills/webgpu-threejs-tsl/SKILL.md new file mode 100644 index 0000000..a0a7732 --- /dev/null +++ b/.codex/skills/webgpu-threejs-tsl/SKILL.md @@ -0,0 +1,92 @@ +--- +name: webgpu-threejs-tsl +description: Comprehensive guide for developing WebGPU-enabled Three.js applications using TSL (Three.js Shading Language). Covers WebGPU renderer setup, TSL syntax and node materials, compute shaders, post-processing effects, and WGSL integration. Use this skill when working with Three.js WebGPU, TSL shaders, node materials, or GPU compute in Three.js. +--- + +# WebGPU Three.js with TSL + +TSL (Three.js Shading Language) is a node-based shader abstraction that lets you write GPU shaders in JavaScript instead of GLSL/WGSL strings. + +## Quick Start + +```javascript +import * as THREE from 'three/webgpu'; +import { color, time, oscSine } from 'three/tsl'; + +const renderer = new THREE.WebGPURenderer(); +await renderer.init(); + +const material = new THREE.MeshStandardNodeMaterial(); +material.colorNode = color(0xff0000).mul(oscSine(time)); +``` + +## Skill Contents + +### Documentation +- `docs/core-concepts.md` - Types, operators, uniforms, control flow +- `docs/materials.md` - Node materials and all properties +- `docs/compute-shaders.md` - GPU compute with instanced arrays +- `docs/post-processing.md` - Built-in and custom effects +- `docs/wgsl-integration.md` - Custom WGSL functions +- `docs/device-loss.md` - Handling GPU device loss and recovery + +### Examples +- `examples/basic-setup.js` - Minimal WebGPU project +- `examples/custom-material.js` - Custom shader material +- `examples/particle-system.js` - GPU compute particles +- `examples/post-processing.js` - Effect pipeline +- `examples/earth-shader.js` - Complete Earth with atmosphere + +### Templates +- `templates/webgpu-project.js` - Starter project template +- `templates/compute-shader.js` - Compute shader template + +### Reference +- `REFERENCE.md` - Quick reference cheatsheet + +## Key Concepts + +### Import Pattern +```javascript +// Always use the WebGPU entry point +import * as THREE from 'three/webgpu'; +import { /* TSL functions */ } from 'three/tsl'; +``` + +### Node Materials +Replace standard material properties with TSL nodes: +```javascript +material.colorNode = texture(map); // instead of material.map +material.roughnessNode = float(0.5); // instead of material.roughness +material.positionNode = displaced; // vertex displacement +``` + +### Method Chaining +TSL uses method chaining for operations: +```javascript +// Instead of: sin(time * 2.0 + offset) * 0.5 + 0.5 +time.mul(2.0).add(offset).sin().mul(0.5).add(0.5) +``` + +### Custom Functions +Use `Fn()` for reusable shader logic: +```javascript +const fresnel = Fn(([power = 2.0]) => { + const nDotV = normalWorld.dot(viewDir).saturate(); + return float(1.0).sub(nDotV).pow(power); +}); +``` + +## When to Use This Skill + +- Setting up Three.js with WebGPU renderer +- Creating custom shader materials with TSL +- Writing GPU compute shaders +- Building post-processing pipelines +- Migrating from GLSL to TSL +- Implementing visual effects (particles, water, terrain, etc.) + +## Resources + +- [Three.js TSL Wiki](https://github.com/mrdoob/three.js/wiki/Three.js-Shading-Language) +- [WebGPU Examples](https://github.com/mrdoob/three.js/tree/master/examples) (files prefixed with `webgpu_`) diff --git a/.codex/skills/webgpu-threejs-tsl/docs/compute-shaders.md b/.codex/skills/webgpu-threejs-tsl/docs/compute-shaders.md new file mode 100644 index 0000000..d531809 --- /dev/null +++ b/.codex/skills/webgpu-threejs-tsl/docs/compute-shaders.md @@ -0,0 +1,404 @@ +# TSL Compute Shaders + +Compute shaders run on the GPU for parallel processing of data. TSL makes them accessible through JavaScript. + +## Basic Setup + +```javascript +import * as THREE from 'three/webgpu'; +import { Fn, instancedArray, instanceIndex, vec3 } from 'three/tsl'; + +// Create storage buffer +const count = 100000; +const positions = instancedArray(count, 'vec3'); + +// Create compute shader +const computeShader = Fn(() => { + const position = positions.element(instanceIndex); + position.x.addAssign(0.01); +})().compute(count); + +// Execute +renderer.compute(computeShader); +``` + +## Storage Buffers + +### Instanced Arrays + +```javascript +import { instancedArray } from 'three/tsl'; + +// Create typed storage buffers +const positions = instancedArray(count, 'vec3'); +const velocities = instancedArray(count, 'vec3'); +const colors = instancedArray(count, 'vec4'); +const indices = instancedArray(count, 'uint'); +const values = instancedArray(count, 'float'); +``` + +### Accessing Elements + +```javascript +const computeShader = Fn(() => { + // Get element at current index + const position = positions.element(instanceIndex); + const velocity = velocities.element(instanceIndex); + + // Read values + const x = position.x; + const speed = velocity.length(); + + // Write values + position.assign(vec3(0, 0, 0)); + position.x.assign(1.0); + position.addAssign(velocity); +})().compute(count); +``` + +### Accessing Other Elements + +```javascript +const computeShader = Fn(() => { + const myIndex = instanceIndex; + const neighborIndex = myIndex.add(1).mod(count); + + const myPos = positions.element(myIndex); + const neighborPos = positions.element(neighborIndex); + + // Calculate distance to neighbor + const dist = myPos.distance(neighborPos); +})().compute(count); +``` + +## Compute Shader Patterns + +### Initialize Particles + +```javascript +const computeInit = Fn(() => { + const position = positions.element(instanceIndex); + const velocity = velocities.element(instanceIndex); + + // Random positions using hash + position.x.assign(hash(instanceIndex).mul(10).sub(5)); + position.y.assign(hash(instanceIndex.add(1)).mul(10).sub(5)); + position.z.assign(hash(instanceIndex.add(2)).mul(10).sub(5)); + + // Zero velocity + velocity.assign(vec3(0)); +})().compute(count); + +// Run once at startup +await renderer.computeAsync(computeInit); +``` + +### Physics Update + +```javascript +const gravity = uniform(-9.8); +const deltaTimeUniform = uniform(0); +const groundY = uniform(0); + +const computeUpdate = Fn(() => { + const position = positions.element(instanceIndex); + const velocity = velocities.element(instanceIndex); + const dt = deltaTimeUniform; + + // Apply gravity + velocity.y.addAssign(gravity.mul(dt)); + + // Update position + position.addAssign(velocity.mul(dt)); + + // Ground collision + If(position.y.lessThan(groundY), () => { + position.y.assign(groundY); + velocity.y.assign(velocity.y.negate().mul(0.8)); // Bounce + velocity.xz.mulAssign(0.95); // Friction + }); +})().compute(count); + +// In animation loop +function animate() { + deltaTimeUniform.value = clock.getDelta(); + renderer.compute(computeUpdate); + renderer.render(scene, camera); +} +``` + +### Attraction to Point + +```javascript +const attractorPos = uniform(new THREE.Vector3(0, 0, 0)); +const attractorStrength = uniform(1.0); + +const computeAttract = Fn(() => { + const position = positions.element(instanceIndex); + const velocity = velocities.element(instanceIndex); + + // Direction to attractor + const toAttractor = attractorPos.sub(position); + const distance = toAttractor.length(); + const direction = toAttractor.normalize(); + + // Apply force (inverse square falloff) + const force = direction.mul(attractorStrength).div(distance.mul(distance).add(0.1)); + velocity.addAssign(force.mul(deltaTimeUniform)); +})().compute(count); +``` + +### Neighbor Interaction (Boids-like) + +```javascript +const computeBoids = Fn(() => { + const myPos = positions.element(instanceIndex); + const myVel = velocities.element(instanceIndex); + + const separation = vec3(0).toVar(); + const alignment = vec3(0).toVar(); + const cohesion = vec3(0).toVar(); + const neighborCount = int(0).toVar(); + + // Check nearby particles + Loop(count, ({ i }) => { + If(i.notEqual(instanceIndex), () => { + const otherPos = positions.element(i); + const otherVel = velocities.element(i); + const dist = myPos.distance(otherPos); + + If(dist.lessThan(2.0), () => { + // Separation + const diff = myPos.sub(otherPos).normalize().div(dist); + separation.addAssign(diff); + + // Alignment + alignment.addAssign(otherVel); + + // Cohesion + cohesion.addAssign(otherPos); + + neighborCount.addAssign(1); + }); + }); + }); + + If(neighborCount.greaterThan(0), () => { + const n = neighborCount.toFloat(); + alignment.divAssign(n); + cohesion.divAssign(n); + cohesion.assign(cohesion.sub(myPos)); + + myVel.addAssign(separation.mul(0.05)); + myVel.addAssign(alignment.sub(myVel).mul(0.05)); + myVel.addAssign(cohesion.mul(0.05)); + }); + + // Limit speed + const speed = myVel.length(); + If(speed.greaterThan(2.0), () => { + myVel.assign(myVel.normalize().mul(2.0)); + }); + + myPos.addAssign(myVel.mul(deltaTimeUniform)); +})().compute(count); +``` + +## Workgroups and Synchronization + +### Workgroup Size + +```javascript +// Default workgroup size is typically 64 or 256 +const computeShader = Fn(() => { + // shader code +})().compute(count, { workgroupSize: 64 }); +``` + +### Barriers + +```javascript +import { workgroupBarrier, storageBarrier, textureBarrier } from 'three/tsl'; + +const computeShader = Fn(() => { + // Write data + sharedData.element(localIndex).assign(value); + + // Ensure all workgroup threads reach this point + workgroupBarrier(); + + // Now safe to read data written by other threads + const neighborValue = sharedData.element(localIndex.add(1)); +})().compute(count); +``` + +## Atomic Operations + +For thread-safe read-modify-write operations: + +```javascript +import { atomicAdd, atomicSub, atomicMax, atomicMin, atomicAnd, atomicOr, atomicXor } from 'three/tsl'; + +const counter = instancedArray(1, 'uint'); + +const computeShader = Fn(() => { + // Atomically increment counter + atomicAdd(counter.element(0), 1); + + // Atomic max + atomicMax(maxValue.element(0), localValue); +})().compute(count); +``` + +## Using Compute Results in Materials + +### Instanced Mesh with Computed Positions + +```javascript +// Create instanced mesh +const geometry = new THREE.SphereGeometry(0.1, 16, 16); +const material = new THREE.MeshStandardNodeMaterial(); + +// Use computed positions +material.positionNode = positions.element(instanceIndex); + +// Optionally use computed colors +material.colorNode = colors.element(instanceIndex); + +const mesh = new THREE.InstancedMesh(geometry, material, count); +scene.add(mesh); +``` + +### Points with Computed Positions + +```javascript +const geometry = new THREE.BufferGeometry(); +geometry.setAttribute('position', new THREE.Float32BufferAttribute(new Float32Array(count * 3), 3)); + +const material = new THREE.PointsNodeMaterial(); +material.positionNode = positions.element(instanceIndex); +material.colorNode = colors.element(instanceIndex); +material.sizeNode = float(5.0); + +const points = new THREE.Points(geometry, material); +scene.add(points); +``` + +## Execution Methods + +```javascript +// Synchronous compute (blocks until complete) +renderer.compute(computeShader); + +// Asynchronous compute (returns promise) +await renderer.computeAsync(computeShader); + +// Multiple computes +renderer.compute(computeInit); +renderer.compute(computePhysics); +renderer.compute(computeCollisions); +``` + +## Reading Back Data (GPU to CPU) + +```javascript +// Create buffer for readback +const readBuffer = new Float32Array(count * 3); + +// Read data back from GPU +await renderer.readRenderTargetPixelsAsync( + computeTexture, + 0, 0, width, height, + readBuffer +); +``` + +## Complete Example: Particle System + +```javascript +import * as THREE from 'three/webgpu'; +import { + Fn, If, instancedArray, instanceIndex, uniform, + vec3, float, hash, time +} from 'three/tsl'; + +// Setup +const count = 50000; +const positions = instancedArray(count, 'vec3'); +const velocities = instancedArray(count, 'vec3'); +const lifetimes = instancedArray(count, 'float'); + +// Uniforms +const emitterPos = uniform(new THREE.Vector3(0, 0, 0)); +const gravity = uniform(-2.0); +const dt = uniform(0); + +// Initialize +const computeInit = Fn(() => { + const pos = positions.element(instanceIndex); + const vel = velocities.element(instanceIndex); + const life = lifetimes.element(instanceIndex); + + pos.assign(emitterPos); + + // Random velocity in cone + const angle = hash(instanceIndex).mul(Math.PI * 2); + const speed = hash(instanceIndex.add(1)).mul(2).add(1); + vel.x.assign(angle.cos().mul(speed).mul(0.3)); + vel.y.assign(speed); + vel.z.assign(angle.sin().mul(speed).mul(0.3)); + + // Random lifetime + life.assign(hash(instanceIndex.add(2)).mul(2).add(1)); +})().compute(count); + +// Update +const computeUpdate = Fn(() => { + const pos = positions.element(instanceIndex); + const vel = velocities.element(instanceIndex); + const life = lifetimes.element(instanceIndex); + + // Apply gravity + vel.y.addAssign(gravity.mul(dt)); + + // Update position + pos.addAssign(vel.mul(dt)); + + // Decrease lifetime + life.subAssign(dt); + + // Respawn dead particles + If(life.lessThan(0), () => { + pos.assign(emitterPos); + const angle = hash(instanceIndex.add(time.mul(1000))).mul(Math.PI * 2); + const speed = hash(instanceIndex.add(time.mul(1000)).add(1)).mul(2).add(1); + vel.x.assign(angle.cos().mul(speed).mul(0.3)); + vel.y.assign(speed); + vel.z.assign(angle.sin().mul(speed).mul(0.3)); + life.assign(hash(instanceIndex.add(time.mul(1000)).add(2)).mul(2).add(1)); + }); +})().compute(count); + +// Material +const material = new THREE.PointsNodeMaterial(); +material.positionNode = positions.element(instanceIndex); +material.sizeNode = float(3.0); +material.colorNode = vec3(1, 0.5, 0.2); + +// Geometry (dummy positions) +const geometry = new THREE.BufferGeometry(); +geometry.setAttribute('position', new THREE.Float32BufferAttribute(new Float32Array(count * 3), 3)); + +const points = new THREE.Points(geometry, material); +scene.add(points); + +// Init +await renderer.computeAsync(computeInit); + +// Animation loop +function animate() { + dt.value = Math.min(clock.getDelta(), 0.1); + renderer.compute(computeUpdate); + renderer.render(scene, camera); +} +``` diff --git a/.codex/skills/webgpu-threejs-tsl/docs/core-concepts.md b/.codex/skills/webgpu-threejs-tsl/docs/core-concepts.md new file mode 100644 index 0000000..d7aa976 --- /dev/null +++ b/.codex/skills/webgpu-threejs-tsl/docs/core-concepts.md @@ -0,0 +1,453 @@ +# TSL Core Concepts + +## Types and Constructors + +### Scalar Types +```javascript +import { float, int, uint, bool } from 'three/tsl'; + +const f = float(1.0); +const i = int(42); +const u = uint(100); +const b = bool(true); +``` + +### Vector Types +```javascript +import { vec2, vec3, vec4, color } from 'three/tsl'; + +const v2 = vec2(1.0, 2.0); +const v3 = vec3(1.0, 2.0, 3.0); +const v4 = vec4(1.0, 2.0, 3.0, 1.0); + +// Color (RGB, accepts hex or components) +const c = color(0xff0000); // Red +const c2 = color(1, 0.5, 0); // Orange +``` + +### Matrix Types +```javascript +import { mat2, mat3, mat4 } from 'three/tsl'; + +const m3 = mat3(); +const m4 = mat4(); +``` + +### Type Conversion +```javascript +const v = vec3(1, 2, 3); +const v4 = v.toVec4(1.0); // vec4(1, 2, 3, 1) +const f = int(42).toFloat(); // 42.0 +const c = v.toColor(); // Convert to color +``` + +## Vector Swizzling + +Access and reorder vector components using standard notation: + +```javascript +const v = vec3(1.0, 2.0, 3.0); + +// Single component access +v.x // 1.0 +v.y // 2.0 +v.z // 3.0 + +// Multiple components +v.xy // vec2(1.0, 2.0) +v.xyz // vec3(1.0, 2.0, 3.0) + +// Reorder components +v.zyx // vec3(3.0, 2.0, 1.0) +v.xxy // vec3(1.0, 1.0, 2.0) +v.rrr // vec3(1.0, 1.0, 1.0) - same as xxx + +// Alternative accessors (all equivalent) +v.xyz // position +v.rgb // color +v.stp // texture coordinates +``` + +## Uniforms + +Uniforms pass values from JavaScript to shaders: + +```javascript +import { uniform } from 'three/tsl'; +import * as THREE from 'three/webgpu'; + +// Create uniforms +const myColor = uniform(new THREE.Color(0x0066ff)); +const myFloat = uniform(0.5); +const myVec3 = uniform(new THREE.Vector3(1, 2, 3)); + +// Update at runtime +myColor.value.set(0xff0000); +myFloat.value = 0.8; +myVec3.value.set(4, 5, 6); + +// Use in material +material.colorNode = myColor; +``` + +### Auto-Updating Uniforms + +```javascript +// Update every frame +const animatedValue = uniform(0).onFrameUpdate((frame) => { + return Math.sin(frame.time); +}); + +// Update per object render +const perObjectValue = uniform(0).onObjectUpdate((object) => { + return object.userData.customValue; +}); + +// Update once per render cycle +const renderValue = uniform(0).onRenderUpdate((state) => { + return state.delta; +}); +``` + +## Operators + +### Arithmetic +```javascript +// Method chaining (preferred) +const result = a.add(b).mul(c).sub(d).div(e); + +// Individual operations +a.add(b) // a + b +a.sub(b) // a - b +a.mul(b) // a * b +a.div(b) // a / b +a.mod(b) // a % b +a.negate() // -a +``` + +### Comparison +```javascript +a.equal(b) // a == b +a.notEqual(b) // a != b +a.lessThan(b) // a < b +a.greaterThan(b) // a > b +a.lessThanEqual(b) // a <= b +a.greaterThanEqual(b) // a >= b +``` + +### Logical +```javascript +a.and(b) // a && b +a.or(b) // a || b +a.not() // !a +a.xor(b) // a ^ b +``` + +### Bitwise +```javascript +a.bitAnd(b) // a & b +a.bitOr(b) // a | b +a.bitXor(b) // a ^ b +a.bitNot() // ~a +a.shiftLeft(n) // a << n +a.shiftRight(n) // a >> n +``` + +### Assignment (for variables) +```javascript +const v = vec3(0).toVar(); // Create mutable variable + +v.assign(vec3(1, 2, 3)); // v = vec3(1, 2, 3) +v.addAssign(vec3(1)); // v += vec3(1) +v.subAssign(vec3(1)); // v -= vec3(1) +v.mulAssign(2.0); // v *= 2.0 +v.divAssign(2.0); // v /= 2.0 +``` + +## Variables + +### Mutable Variables +```javascript +// Create mutable variable with toVar() +const myVar = vec3(1, 0, 0).toVar(); +myVar.assign(vec3(0, 1, 0)); +myVar.addAssign(vec3(0, 0, 1)); + +// Name the variable (useful for debugging) +const named = vec3(0).toVar('myPosition'); +``` + +### Constants +```javascript +// Create compile-time constant +const PI_HALF = float(Math.PI / 2).toConst(); +``` + +### Properties (named values for shader stages) +```javascript +import { property } from 'three/tsl'; + +// Create named property +const myProp = property('vec3', 'customColor'); +myProp.assign(vec3(1, 0, 0)); +``` + +## Control Flow + +### Conditionals + +```javascript +import { If, select } from 'three/tsl'; + +// If-ElseIf-Else +const result = vec3(0).toVar(); + +If(value.greaterThan(0.5), () => { + result.assign(vec3(1, 0, 0)); // Red +}).ElseIf(value.greaterThan(0.25), () => { + result.assign(vec3(0, 1, 0)); // Green +}).Else(() => { + result.assign(vec3(0, 0, 1)); // Blue +}); + +// Ternary operator (select) +const color = select( + condition, // if true + vec3(1, 0, 0), // return this + vec3(0, 0, 1) // else return this +); +``` + +### Switch-Case + +```javascript +import { Switch } from 'three/tsl'; + +const col = vec3(0).toVar(); + +Switch(intValue) + .Case(0, () => { col.assign(color(1, 0, 0)); }) + .Case(1, () => { col.assign(color(0, 1, 0)); }) + .Case(2, () => { col.assign(color(0, 0, 1)); }) + .Default(() => { col.assign(color(1, 1, 1)); }); +``` + +### Loops + +```javascript +import { Loop, Break, Continue } from 'three/tsl'; + +// Simple loop (0 to 9) +const sum = float(0).toVar(); +Loop(10, ({ i }) => { + sum.addAssign(float(i)); +}); + +// Ranged loop with options +Loop({ start: int(0), end: int(count), type: 'int' }, ({ i }) => { + // Loop body +}); + +// Nested loops +Loop(width, height, ({ i, j }) => { + // i = outer loop index + // j = inner loop index +}); + +// Loop control +Loop(100, ({ i }) => { + If(shouldStop, () => { + Break(); // Exit loop + }); + If(shouldSkip, () => { + Continue(); // Skip to next iteration + }); +}); +``` + +### Flow Control + +```javascript +import { Discard, Return } from 'three/tsl'; + +// Discard fragment (make transparent) +If(alpha.lessThan(0.5), () => { + Discard(); +}); + +// Return from function +const myFn = Fn(() => { + If(condition, () => { + Return(vec3(1, 0, 0)); + }); + return vec3(0, 0, 1); +}); +``` + +## Custom Functions with Fn() + +### Basic Function +```javascript +import { Fn } from 'three/tsl'; + +const addVectors = Fn(([a, b]) => { + return a.add(b); +}); + +// Usage +const result = addVectors(vec3(1, 0, 0), vec3(0, 1, 0)); +``` + +### Default Parameters +```javascript +const oscillate = Fn(([frequency = 1.0, amplitude = 1.0]) => { + return time.mul(frequency).sin().mul(amplitude); +}); + +// Call variations +oscillate(); // Uses defaults +oscillate(2.0); // frequency = 2.0 +oscillate(2.0, 0.5); // frequency = 2.0, amplitude = 0.5 +``` + +### Named Parameters (Object Style) +```javascript +const createGradient = Fn(({ colorA = vec3(0), colorB = vec3(1), t = 0.5 }) => { + return mix(colorA, colorB, t); +}); + +// Call with named parameters +createGradient({ colorA: vec3(1, 0, 0), t: uv().x }); +``` + +### Function with Context +```javascript +// Access shader context +const customShader = Fn(({ material, geometry, object }) => { + if (material.userData.customColor) { + return uniform(material.userData.customColor); + } + return vec3(1); +}); +``` + +## Time and Animation + +```javascript +import { time, deltaTime } from 'three/tsl'; + +// time - seconds since start +const rotation = time.mul(0.5); // Half rotation per second + +// deltaTime - time since last frame +const velocity = speed.mul(deltaTime); +``` + +### Oscillators + +```javascript +import { oscSine, oscSquare, oscTriangle, oscSawtooth } from 'three/tsl'; + +// All oscillators return 0-1 range +oscSine(time) // Smooth sine wave +oscSquare(time) // Square wave (0 or 1) +oscTriangle(time) // Triangle wave +oscSawtooth(time) // Sawtooth wave + +// Custom frequency +oscSine(time.mul(2.0)) // 2Hz oscillation +``` + +## Math Functions + +### Basic Math +```javascript +import { abs, sign, floor, ceil, fract, mod, min, max, clamp } from 'three/tsl'; + +abs(x) // Absolute value +sign(x) // -1, 0, or 1 +floor(x) // Round down +ceil(x) // Round up +fract(x) // Fractional part (x - floor(x)) +mod(x, y) // Modulo +min(x, y) // Minimum +max(x, y) // Maximum +clamp(x, 0, 1) // Clamp to range +``` + +### Trigonometry +```javascript +import { sin, cos, tan, asin, acos, atan, atan2 } from 'three/tsl'; + +sin(x) +cos(x) +tan(x) +asin(x) +acos(x) +atan(x) +atan2(y, x) +``` + +### Exponential +```javascript +import { pow, exp, log, sqrt, inverseSqrt } from 'three/tsl'; + +pow(x, 2.0) // x^2 +exp(x) // e^x +log(x) // Natural log +sqrt(x) // Square root +inverseSqrt(x) // 1 / sqrt(x) +``` + +### Interpolation +```javascript +import { mix, step, smoothstep } from 'three/tsl'; + +mix(a, b, 0.5) // Linear interpolation +step(0.5, x) // 0 if x < 0.5, else 1 +smoothstep(0.0, 1.0, x) // Smooth 0-1 transition +``` + +### Vector Math +```javascript +import { length, distance, dot, cross, normalize, reflect, refract } from 'three/tsl'; + +length(v) // Vector length +distance(a, b) // Distance between points +dot(a, b) // Dot product +cross(a, b) // Cross product (vec3 only) +normalize(v) // Unit vector +reflect(incident, normal) +refract(incident, normal, eta) +``` + +### Constants +```javascript +import { PI, TWO_PI, HALF_PI, EPSILON } from 'three/tsl'; + +PI // 3.14159... +TWO_PI // 6.28318... +HALF_PI // 1.57079... +EPSILON // Very small number +``` + +## Utility Functions + +```javascript +import { hash, checker, remap, range, rotate } from 'three/tsl'; + +// Pseudo-random hash +hash(seed) // Returns 0-1 + +// Checkerboard pattern +checker(uv()) // Returns 0 or 1 + +// Remap value from one range to another +remap(x, 0, 1, -1, 1) // Map 0-1 to -1 to 1 + +// Generate value in range +range(min, max) // Random in range (per instance) + +// Rotate 2D vector +rotate(vec2(1, 0), angle) +``` diff --git a/.codex/skills/webgpu-threejs-tsl/docs/device-loss.md b/.codex/skills/webgpu-threejs-tsl/docs/device-loss.md new file mode 100644 index 0000000..bf4a5ce --- /dev/null +++ b/.codex/skills/webgpu-threejs-tsl/docs/device-loss.md @@ -0,0 +1,359 @@ +# WebGPU Device Loss Handling + +## What Is Device Loss? + +Device loss occurs when the GPU driver cannot continue processing commands. Causes include: + +- Driver crashes +- Extreme resource pressure +- Long-running shaders (GPU watchdog triggers after ~10 seconds) +- Driver updates +- Significant device configuration changes + +When a device is lost, the `GPUDevice` object and **all objects created with it become unusable**. All buffers, textures, pipelines, and GPU memory are discarded. + +## Listening for Device Loss + +Detect loss by attaching a callback to the device's `lost` promise: + +```javascript +const adapter = await navigator.gpu.requestAdapter(); +if (!adapter) { return; } +const device = await adapter.requestDevice(); + +device.lost.then((info) => { + console.error('WebGPU device lost:', info.message); + // Handle recovery +}); +``` + +**Important:** Don't `await` this promise directly - it will block indefinitely if loss never occurs. + +### Device Loss Information + +The `GPUDeviceLostInfo` object provides: + +| Property | Description | +|----------|-------------| +| `reason` | `'destroyed'` (intentional via `destroy()`) or `'unknown'` (unexpected) | +| `message` | Human-readable debugging info (don't parse programmatically) | + +```javascript +device.lost.then((info) => { + if (info.reason === 'unknown') { + // Unexpected loss - attempt recovery + handleUnexpectedDeviceLoss(); + } else { + // Intentional destruction - expected behavior + } +}); +``` + +### Devices Starting Lost + +`adapter.requestDevice()` always returns a `GPUDevice`, but it may already be lost if creation failed. This occurs when the adapter was "consumed" (used previously) or "expired." + +**Best practice:** Always get a new adapter right before requesting a device. + +## Recovery Strategies + +### Minimal Recovery (Page Reload) + +For simple applications: + +```javascript +device.lost.then((info) => { + if (info.reason === 'unknown') { + // Warn user before reload + alert('Graphics error occurred. The page will reload.'); + location.reload(); + } +}); +``` + +### Restart GPU Content Only (Recommended for Three.js) + +Recreate the device and reconfigure the canvas without full page reload: + +```javascript +import * as THREE from 'three/webgpu'; + +let renderer; +let scene, camera; + +async function initWebGPU() { + renderer = new THREE.WebGPURenderer(); + await renderer.init(); + + // Access the underlying WebGPU device + const device = renderer.backend.device; + + device.lost.then((info) => { + console.error('Device lost:', info.message); + if (info.reason === 'unknown') { + // Dispose current renderer + renderer.dispose(); + // Reinitialize + initWebGPU(); + } + }); + + // Configure canvas + renderer.setSize(window.innerWidth, window.innerHeight); + document.body.appendChild(renderer.domElement); + + // Recreate scene content + setupScene(); +} + +function setupScene() { + scene = new THREE.Scene(); + camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); + // ... add meshes, lights, etc. +} + +initWebGPU(); +``` + +### Restore with Application State + +For applications with user progress or configuration: + +```javascript +let appState = { + cameraPosition: { x: 0, y: 5, z: 10 }, + settings: {}, + // Don't save transient data like particle positions +}; + +// Save state periodically +function saveState() { + appState.cameraPosition = { + x: camera.position.x, + y: camera.position.y, + z: camera.position.z + }; + localStorage.setItem('appState', JSON.stringify(appState)); +} + +// Restore on recovery +async function initWebGPU() { + renderer = new THREE.WebGPURenderer(); + await renderer.init(); + + const savedState = localStorage.getItem('appState'); + if (savedState) { + appState = JSON.parse(savedState); + } + + setupScene(); + + // Restore camera position + camera.position.set( + appState.cameraPosition.x, + appState.cameraPosition.y, + appState.cameraPosition.z + ); + + renderer.backend.device.lost.then((info) => { + if (info.reason === 'unknown') { + saveState(); + renderer.dispose(); + initWebGPU(); + } + }); +} +``` + +## When Recovery Fails + +If `requestAdapter()` returns `null` after device loss, the OS or browser has blocked GPU access: + +```javascript +async function initWebGPU() { + const adapter = await navigator.gpu.requestAdapter(); + + if (!adapter) { + // Check if this is initial failure or post-loss failure + if (hadPreviousDevice) { + showMessage('GPU access lost. Please restart your browser.'); + } else { + showMessage('WebGPU is not supported on this device.'); + } + return; + } + + // Continue with device creation... +} +``` + +## Testing Device Loss + +### Using destroy() + +Call `device.destroy()` to simulate loss: + +```javascript +let simulatedLoss = false; + +function simulateDeviceLoss() { + simulatedLoss = true; + renderer.backend.device.destroy(); +} + +// In your device.lost handler: +device.lost.then((info) => { + if (info.reason === 'unknown' || simulatedLoss) { + simulatedLoss = false; + // Treat as unexpected loss for testing + handleDeviceLoss(); + } +}); + +// Add debug keybinding +window.addEventListener('keydown', (e) => { + if (e.key === 'L' && e.ctrlKey && e.shiftKey) { + simulateDeviceLoss(); + } +}); +``` + +**Limitations of destroy():** +- Unmaps buffers immediately (real loss doesn't) +- Always allows device recovery (real loss may not) + +### Chrome GPU Process Crash Testing + +Navigate to `about:gpucrash` in a **separate tab** to crash the GPU process. + +Chrome enforces escalating restrictions: + +| Crash | Effect | +|-------|--------| +| 1st | New adapters allowed | +| 2nd within 2 min | Adapter requests fail (resets on page refresh) | +| 3rd within 2 min | All pages blocked (reset after 2 min or browser restart) | +| 3-6 within 5 min | GPU process stops restarting; browser restart required | + +### Chrome Testing Flags + +Bypass crash limits for development: + +```bash +# macOS +/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \ + --disable-domain-blocking-for-3d-apis \ + --disable-gpu-process-crash-limit + +# Windows +chrome.exe --disable-domain-blocking-for-3d-apis --disable-gpu-process-crash-limit + +# Linux +google-chrome --disable-domain-blocking-for-3d-apis --disable-gpu-process-crash-limit +``` + +## Complete Example + +```javascript +import * as THREE from 'three/webgpu'; +import { color, time, oscSine } from 'three/tsl'; + +let renderer, scene, camera, mesh; +let hadPreviousDevice = false; + +async function init() { + // Check WebGPU support + if (!navigator.gpu) { + showError('WebGPU not supported'); + return; + } + + // Create renderer + renderer = new THREE.WebGPURenderer({ antialias: true }); + + try { + await renderer.init(); + } catch (e) { + if (hadPreviousDevice) { + showError('GPU recovery failed. Please restart browser.'); + } else { + showError('Failed to initialize WebGPU.'); + } + return; + } + + hadPreviousDevice = true; + + // Setup device loss handler + const device = renderer.backend.device; + device.lost.then(handleDeviceLoss); + + // Setup scene + renderer.setSize(window.innerWidth, window.innerHeight); + document.body.appendChild(renderer.domElement); + + scene = new THREE.Scene(); + camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); + camera.position.z = 5; + + const geometry = new THREE.BoxGeometry(); + const material = new THREE.MeshStandardNodeMaterial(); + material.colorNode = color(0x00ff00).mul(oscSine(time)); + + mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + + const light = new THREE.DirectionalLight(0xffffff, 1); + light.position.set(5, 5, 5); + scene.add(light); + scene.add(new THREE.AmbientLight(0x404040)); + + animate(); +} + +function handleDeviceLoss(info) { + console.error('Device lost:', info.reason, info.message); + + if (info.reason === 'unknown') { + // Cleanup + if (renderer) { + renderer.domElement.remove(); + renderer.dispose(); + } + + // Attempt recovery after short delay + setTimeout(() => { + init(); + }, 100); + } +} + +function animate() { + if (!renderer) return; + + requestAnimationFrame(animate); + mesh.rotation.x += 0.01; + mesh.rotation.y += 0.01; + renderer.render(scene, camera); +} + +function showError(message) { + const div = document.createElement('div'); + div.textContent = message; + div.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);padding:20px;background:#f44;color:#fff;border-radius:8px;'; + document.body.appendChild(div); +} + +init(); +``` + +## Best Practices + +1. **Always listen for device loss** - Even if you just show an error message +2. **Get a fresh adapter before each device request** - The GPU hardware may have changed +3. **Don't parse the message field** - It's implementation-specific and changes between browsers +4. **Save critical application state** - Restore user progress after recovery +5. **Don't save transient state** - Particle positions, physics state can be reset +6. **Test your recovery path** - Use `destroy()` and Chrome's `about:gpucrash` +7. **Handle adapter failure gracefully** - Distinguish between initial failure and post-loss failure +8. **Add a short delay before recovery** - Give the system time to stabilize diff --git a/.codex/skills/webgpu-threejs-tsl/docs/materials.md b/.codex/skills/webgpu-threejs-tsl/docs/materials.md new file mode 100644 index 0000000..32f1287 --- /dev/null +++ b/.codex/skills/webgpu-threejs-tsl/docs/materials.md @@ -0,0 +1,353 @@ +# TSL Node Materials + +## Available Material Types + +| Material | Description | +|----------|-------------| +| `MeshBasicNodeMaterial` | Unlit, no lighting calculations | +| `MeshStandardNodeMaterial` | PBR material with metalness/roughness | +| `MeshPhysicalNodeMaterial` | Advanced PBR with clearcoat, transmission, etc. | +| `MeshPhongNodeMaterial` | Classic Phong shading | +| `MeshToonNodeMaterial` | Cel/toon shading | +| `MeshLambertNodeMaterial` | Diffuse-only lighting | +| `MeshNormalNodeMaterial` | Visualize normals | +| `MeshMatcapNodeMaterial` | Matcap texture shading | +| `PointsNodeMaterial` | For point clouds | +| `LineBasicNodeMaterial` | For lines | +| `LineDashedNodeMaterial` | For dashed lines | +| `SpriteNodeMaterial` | For sprites/billboards | + +## Creating Node Materials + +```javascript +import * as THREE from 'three/webgpu'; + +// Standard PBR material +const material = new THREE.MeshStandardNodeMaterial(); + +// Physical material with advanced features +const physicalMat = new THREE.MeshPhysicalNodeMaterial(); + +// Unlit material +const basicMat = new THREE.MeshBasicNodeMaterial(); +``` + +## Material Properties + +### Color and Opacity + +```javascript +import { texture, color, float } from 'three/tsl'; + +// Color from texture +material.colorNode = texture(diffuseMap); + +// Solid color +material.colorNode = color(0xff0000); + +// Computed color +material.colorNode = positionLocal.normalize(); + +// Opacity (requires material.transparent = true) +material.opacityNode = float(0.8); +material.transparent = true; + +// Alpha test threshold +material.alphaTestNode = float(0.5); +``` + +### PBR Properties (MeshStandardNodeMaterial) + +```javascript +import { texture, float, color } from 'three/tsl'; + +// Metalness (0 = dielectric, 1 = metal) +material.metalnessNode = texture(metalMap).r; +material.metalnessNode = float(0.0); + +// Roughness (0 = smooth/mirror, 1 = rough) +material.roughnessNode = texture(roughnessMap).r; +material.roughnessNode = float(0.5); + +// Emissive (self-illumination) +material.emissiveNode = color(0xff0000).mul(2.0); +material.emissiveNode = texture(emissiveMap); +``` + +### Normal Mapping + +```javascript +import { texture, normalMap, bumpMap } from 'three/tsl'; + +// Normal map +material.normalNode = normalMap(texture(normalMapTexture)); + +// Normal map with strength +material.normalNode = normalMap(texture(normalMapTexture), float(0.5)); + +// Bump map (height to normal) +material.normalNode = bumpMap(texture(heightMap), 0.05); +``` + +### Physical Properties (MeshPhysicalNodeMaterial) + +```javascript +const material = new THREE.MeshPhysicalNodeMaterial(); + +// Clearcoat (car paint effect) +material.clearcoatNode = float(1.0); +material.clearcoatRoughnessNode = float(0.1); +material.clearcoatNormalNode = normalMap(texture(clearcoatNormalMap)); + +// Transmission (glass/translucency) +material.transmissionNode = float(0.9); +material.thicknessNode = float(0.5); +material.attenuationDistanceNode = float(1.0); +material.attenuationColorNode = color(0xffffff); + +// Iridescence (soap bubble effect) +material.iridescenceNode = float(1.0); +material.iridescenceIORNode = float(1.3); +material.iridescenceThicknessNode = float(400); + +// Sheen (fabric effect) +material.sheenNode = float(1.0); +material.sheenRoughnessNode = float(0.5); +material.sheenColorNode = color(0xffffff); + +// Anisotropy (brushed metal) +material.anisotropyNode = float(1.0); +material.anisotropyRotationNode = float(0); + +// Specular +material.specularIntensityNode = float(1.0); +material.specularColorNode = color(0xffffff); + +// Index of Refraction +material.iorNode = float(1.5); + +// Dispersion (rainbow effect in glass) +material.dispersionNode = float(0.0); +``` + +### Environment and Lighting + +```javascript +import { cubeTexture, envMap } from 'three/tsl'; + +// Environment map reflection +material.envMapNode = cubeTexture(envCubeMap); + +// Custom lights +material.lightsNode = lights(); +``` + +## Vertex Manipulation + +### Position Displacement + +```javascript +import { positionLocal, normalLocal, texture } from 'three/tsl'; + +// Displace vertices along normals +const displacement = texture(heightMap).r.mul(0.1); +material.positionNode = positionLocal.add(normalLocal.mul(displacement)); + +// Wave displacement +const wave = positionLocal.x.add(time).sin().mul(0.1); +material.positionNode = positionLocal.add(vec3(0, wave, 0)); +``` + +### Custom Vertex Shader + +```javascript +// Complete vertex position override +material.vertexNode = customVertexPosition; +``` + +## Fragment Override + +```javascript +// Complete fragment output override +material.fragmentNode = vec4(finalColor, 1.0); + +// Output node (respects lighting) +material.outputNode = outputStruct; +``` + +## Geometry Attributes + +### Position Nodes + +```javascript +import { + positionGeometry, // Original mesh position + positionLocal, // Position in model space + positionWorld, // Position in world space + positionView // Position in camera space +} from 'three/tsl'; +``` + +### Normal Nodes + +```javascript +import { + normalGeometry, // Original mesh normal + normalLocal, // Normal in model space + normalWorld, // Normal in world space (use for lighting) + normalView // Normal in camera space +} from 'three/tsl'; +``` + +### Tangent/Bitangent + +```javascript +import { + tangentLocal, tangentWorld, tangentView, + bitangentLocal, bitangentWorld, bitangentView +} from 'three/tsl'; +``` + +### UV Coordinates + +```javascript +import { uv } from 'three/tsl'; + +uv() // Primary UV set (UV0) +uv(1) // Secondary UV set (UV1) +uv(2) // Tertiary UV set (UV2) +``` + +### Other Attributes + +```javascript +import { vertexColor, instanceIndex, vertexIndex } from 'three/tsl'; + +vertexColor() // Vertex colors (if present) +instanceIndex // Index for instanced meshes +vertexIndex // Current vertex index +``` + +## Camera Nodes + +```javascript +import { + cameraPosition, // Camera world position + cameraNear, // Near plane distance + cameraFar, // Far plane distance + cameraViewMatrix, // View matrix + cameraProjectionMatrix, // Projection matrix + cameraWorldMatrix // Camera world matrix +} from 'three/tsl'; +``` + +## Screen Space Nodes + +```javascript +import { + screenUV, // Screen UV (0-1) + screenCoordinate, // Pixel coordinates + screenSize, // Screen dimensions + viewportUV, // Viewport UV + viewport, // Viewport dimensions + depth // Fragment depth +} from 'three/tsl'; +``` + +## Examples + +### Animated Color Material + +```javascript +import * as THREE from 'three/webgpu'; +import { color, time, oscSine, mix } from 'three/tsl'; + +const material = new THREE.MeshStandardNodeMaterial(); + +const colorA = color(0xff0000); +const colorB = color(0x0000ff); +const t = oscSine(time.mul(0.5)); + +material.colorNode = mix(colorA, colorB, t); +material.roughnessNode = float(0.5); +material.metalnessNode = float(0.0); +``` + +### Triplanar Mapping Material + +```javascript +import * as THREE from 'three/webgpu'; +import { texture, triplanarTexture, float } from 'three/tsl'; + +const material = new THREE.MeshStandardNodeMaterial(); + +// Apply texture from all three axes +material.colorNode = triplanarTexture( + texture(diffuseMap), + null, // Y-axis texture (optional) + null, // Z-axis texture (optional) + float(0.1) // Blend sharpness +); +``` + +### Glass Material + +```javascript +import * as THREE from 'three/webgpu'; +import { float, color } from 'three/tsl'; + +const material = new THREE.MeshPhysicalNodeMaterial(); + +material.colorNode = color(0xffffff); +material.transmissionNode = float(0.95); +material.roughnessNode = float(0.0); +material.metalnessNode = float(0.0); +material.iorNode = float(1.5); +material.thicknessNode = float(0.5); +``` + +### Fresnel Rim Material + +```javascript +import * as THREE from 'three/webgpu'; +import { + color, float, normalWorld, positionWorld, + cameraPosition, Fn +} from 'three/tsl'; + +const fresnel = Fn(() => { + const viewDir = cameraPosition.sub(positionWorld).normalize(); + const nDotV = normalWorld.dot(viewDir).saturate(); + return float(1.0).sub(nDotV).pow(3.0); +}); + +const material = new THREE.MeshStandardNodeMaterial(); +material.colorNode = color(0x222222); +material.emissiveNode = color(0x00ffff).mul(fresnel()); +``` + +### Dissolve Effect Material + +```javascript +import * as THREE from 'three/webgpu'; +import { + color, float, hash, positionLocal, uniform, + If, Discard, smoothstep +} from 'three/tsl'; + +const threshold = uniform(0.5); + +const material = new THREE.MeshStandardNodeMaterial(); + +const noise = hash(positionLocal.mul(50)); + +// Discard fragments below threshold +If(noise.lessThan(threshold), () => { + Discard(); +}); + +// Edge glow +const edge = smoothstep(threshold, threshold.add(0.1), noise); +material.colorNode = color(0x333333); +material.emissiveNode = color(0xff5500).mul(float(1.0).sub(edge)); +``` diff --git a/.codex/skills/webgpu-threejs-tsl/docs/post-processing.md b/.codex/skills/webgpu-threejs-tsl/docs/post-processing.md new file mode 100644 index 0000000..c615647 --- /dev/null +++ b/.codex/skills/webgpu-threejs-tsl/docs/post-processing.md @@ -0,0 +1,475 @@ +# TSL Post-Processing + +Post-processing applies effects to the rendered image. TSL provides both built-in effects and the ability to create custom effects. + +## Basic Setup + +```javascript +import * as THREE from 'three/webgpu'; +import { pass } from 'three/tsl'; + +// Create renderer +const renderer = new THREE.WebGPURenderer(); +await renderer.init(); + +// Create post-processing +const postProcessing = new THREE.PostProcessing(renderer); + +// Create scene pass +const scenePass = pass(scene, camera); +const scenePassColor = scenePass.getTextureNode('output'); + +// Output (passthrough) +postProcessing.outputNode = scenePassColor; + +// Render with post-processing +function animate() { + postProcessing.render(); // Not renderer.render() +} +``` + +## Built-in Effects + +### Bloom + +```javascript +import { bloom } from 'three/addons/tsl/display/BloomNode.js'; + +const scenePass = pass(scene, camera); +const scenePassColor = scenePass.getTextureNode('output'); + +// Add bloom +const bloomPass = bloom(scenePassColor); + +// Configure +bloomPass.threshold.value = 0.5; // Brightness threshold +bloomPass.strength.value = 1.0; // Bloom intensity +bloomPass.radius.value = 0.5; // Blur radius + +// Combine original + bloom +postProcessing.outputNode = scenePassColor.add(bloomPass); +``` + +### Gaussian Blur + +```javascript +import { gaussianBlur } from 'three/addons/tsl/display/GaussianBlurNode.js'; + +const blurred = gaussianBlur(scenePassColor, vec2(2.0)); // Blur strength +postProcessing.outputNode = blurred; +``` + +### FXAA (Anti-aliasing) + +```javascript +import { fxaa } from 'three/addons/tsl/display/FXAANode.js'; + +postProcessing.outputNode = fxaa(scenePassColor); +``` + +### SMAA (Anti-aliasing) + +```javascript +import { smaa } from 'three/addons/tsl/display/SMAANode.js'; + +postProcessing.outputNode = smaa(scenePassColor); +``` + +### Depth of Field + +```javascript +import { dof } from 'three/addons/tsl/display/DepthOfFieldNode.js'; + +const scenePass = pass(scene, camera); +const colorNode = scenePass.getTextureNode('output'); +const depthNode = scenePass.getTextureNode('depth'); + +const dofPass = dof(colorNode, depthNode, { + focus: 5.0, // Focus distance + aperture: 0.025, // Aperture size + maxblur: 0.01 // Maximum blur +}); + +postProcessing.outputNode = dofPass; +``` + +### Motion Blur + +```javascript +import { motionBlur } from 'three/addons/tsl/display/MotionBlurNode.js'; + +const scenePass = pass(scene, camera); +const velocityPass = scenePass.getTextureNode('velocity'); + +const motionBlurPass = motionBlur(scenePassColor, velocityPass); +postProcessing.outputNode = motionBlurPass; +``` + +### Screen Space Reflections (SSR) + +```javascript +import { ssr } from 'three/addons/tsl/display/SSRNode.js'; + +const scenePass = pass(scene, camera); +const colorNode = scenePass.getTextureNode('output'); +const depthNode = scenePass.getTextureNode('depth'); +const normalNode = scenePass.getTextureNode('normal'); + +const ssrPass = ssr(colorNode, depthNode, normalNode, camera); +postProcessing.outputNode = ssrPass; +``` + +### Ambient Occlusion (SSAO) + +```javascript +import { ao } from 'three/addons/tsl/display/AmbientOcclusionNode.js'; + +const scenePass = pass(scene, camera); +const depthNode = scenePass.getTextureNode('depth'); +const normalNode = scenePass.getTextureNode('normal'); + +const aoPass = ao(depthNode, normalNode, camera); +postProcessing.outputNode = scenePassColor.mul(aoPass); +``` + +### Film Grain + +```javascript +import { film } from 'three/addons/tsl/display/FilmNode.js'; + +const filmPass = film(scenePassColor, { + intensity: 0.5, + grayscale: false +}); +postProcessing.outputNode = filmPass; +``` + +### Outline + +```javascript +import { outline } from 'three/addons/tsl/display/OutlineNode.js'; + +const outlinePass = outline(scene, camera, selectedObjects, { + edgeStrength: 3.0, + edgeGlow: 0.0, + edgeThickness: 1.0, + visibleEdgeColor: new THREE.Color(0xffffff), + hiddenEdgeColor: new THREE.Color(0x190a05) +}); + +postProcessing.outputNode = scenePassColor.add(outlinePass); +``` + +### Chromatic Aberration + +```javascript +import { chromaticAberration } from 'three/addons/tsl/display/ChromaticAberrationNode.js'; + +const caPass = chromaticAberration(scenePassColor, { + offset: vec2(0.002, 0.002) +}); +postProcessing.outputNode = caPass; +``` + +## Color Adjustments + +### Grayscale + +```javascript +import { grayscale } from 'three/tsl'; + +postProcessing.outputNode = grayscale(scenePassColor); +``` + +### Saturation + +```javascript +import { saturation } from 'three/tsl'; + +// 0 = grayscale, 1 = normal, 2 = oversaturated +postProcessing.outputNode = saturation(scenePassColor, 1.5); +``` + +### Hue Shift + +```javascript +import { hue } from 'three/tsl'; + +// Shift hue by radians +postProcessing.outputNode = hue(scenePassColor, time.mul(0.5)); +``` + +### Vibrance + +```javascript +import { vibrance } from 'three/tsl'; + +postProcessing.outputNode = vibrance(scenePassColor, 0.5); +``` + +### Posterize + +```javascript +import { posterize } from 'three/tsl'; + +// Reduce color levels +postProcessing.outputNode = posterize(scenePassColor, 8); +``` + +### Sepia + +```javascript +import { sepia } from 'three/addons/tsl/display/SepiaNode.js'; + +postProcessing.outputNode = sepia(scenePassColor); +``` + +### 3D LUT + +```javascript +import { lut3D } from 'three/addons/tsl/display/Lut3DNode.js'; + +const lutTexture = new THREE.Data3DTexture(lutData, size, size, size); +postProcessing.outputNode = lut3D(scenePassColor, lutTexture, size); +``` + +## Custom Post-Processing + +### Basic Custom Effect + +```javascript +import { Fn, screenUV, float, vec4 } from 'three/tsl'; + +const customEffect = Fn(() => { + const color = scenePassColor.toVar(); + + // Invert colors + color.rgb.assign(float(1.0).sub(color.rgb)); + + return color; +}); + +postProcessing.outputNode = customEffect(); +``` + +### Vignette Effect + +```javascript +const vignette = Fn(() => { + const color = scenePassColor.toVar(); + + // Distance from center + const uv = screenUV; + const dist = uv.sub(0.5).length(); + + // Vignette falloff + const vignette = float(1.0).sub(dist.mul(1.5)).clamp(0, 1); + + color.rgb.mulAssign(vignette); + return color; +}); + +postProcessing.outputNode = vignette(); +``` + +### CRT/Scanline Effect + +```javascript +import { viewportSharedTexture } from 'three/tsl'; + +const crtEffect = Fn(() => { + const uv = screenUV; + + // Sample scene at offset UVs for RGB separation (chromatic aberration) + const uvR = uv.add(vec2(0.002, 0)); + const uvG = uv; + const uvB = uv.sub(vec2(0.002, 0)); + + // Use viewportSharedTexture to sample at different UV coordinates + const r = viewportSharedTexture(uvR).r; + const g = viewportSharedTexture(uvG).g; + const b = viewportSharedTexture(uvB).b; + + const color = vec4(r, g, b, 1.0).toVar(); + + // Scanlines + const scanline = uv.y.mul(screenSize.y).mul(0.5).sin().mul(0.1).add(0.9); + color.rgb.mulAssign(scanline); + + // Vignette + const dist = uv.sub(0.5).length(); + color.rgb.mulAssign(float(1.0).sub(dist.mul(0.5))); + + return color; +}); + +// Note: For this effect, apply after scene rendering +postProcessing.outputNode = crtEffect(); +``` + +### Pixelate Effect + +```javascript +const pixelSize = uniform(8.0); + +const pixelate = Fn(() => { + const uv = screenUV; + const pixelUV = uv.mul(screenSize).div(pixelSize).floor().mul(pixelSize).div(screenSize); + return texture(scenePassColor, pixelUV); +}); + +postProcessing.outputNode = pixelate(); +``` + +### Edge Detection (Sobel) + +```javascript +const sobelEdge = Fn(() => { + const uv = screenUV; + const texelSize = vec2(1.0).div(screenSize); + + // Sample 3x3 kernel + const tl = luminance(texture(scenePassColor, uv.add(texelSize.mul(vec2(-1, -1))))); + const tc = luminance(texture(scenePassColor, uv.add(texelSize.mul(vec2(0, -1))))); + const tr = luminance(texture(scenePassColor, uv.add(texelSize.mul(vec2(1, -1))))); + const ml = luminance(texture(scenePassColor, uv.add(texelSize.mul(vec2(-1, 0))))); + const mr = luminance(texture(scenePassColor, uv.add(texelSize.mul(vec2(1, 0))))); + const bl = luminance(texture(scenePassColor, uv.add(texelSize.mul(vec2(-1, 1))))); + const bc = luminance(texture(scenePassColor, uv.add(texelSize.mul(vec2(0, 1))))); + const br = luminance(texture(scenePassColor, uv.add(texelSize.mul(vec2(1, 1))))); + + // Sobel operators + const gx = tl.add(ml.mul(2)).add(bl).sub(tr).sub(mr.mul(2)).sub(br); + const gy = tl.add(tc.mul(2)).add(tr).sub(bl).sub(bc.mul(2)).sub(br); + + const edge = sqrt(gx.mul(gx).add(gy.mul(gy))); + + return vec4(vec3(edge), 1.0); +}); + +postProcessing.outputNode = sobelEdge(); +``` + +## Multiple Render Targets (MRT) + +Access multiple buffers from the scene pass: + +```javascript +import { mrt, output } from 'three/tsl'; + +const scenePass = pass(scene, camera); + +// Set up MRT +scenePass.setMRT(mrt({ + output: output, // Color output + normal: normalView, // View-space normals + depth: depth // Depth buffer +})); + +// Access individual targets +const colorTexture = scenePass.getTextureNode('output'); +const normalTexture = scenePass.getTextureNode('normal'); +const depthTexture = scenePass.getTextureNode('depth'); +``` + +### Selective Bloom with MRT + +Bloom only emissive objects by rendering emissive to a separate target: + +```javascript +import { pass, mrt, output, emissive } from 'three/tsl'; +import { bloom } from 'three/addons/tsl/display/BloomNode.js'; + +const postProcessing = new THREE.PostProcessing(renderer); +const scenePass = pass(scene, camera); + +// Render both color and emissive to separate targets +scenePass.setMRT(mrt({ + output: output, + emissive: emissive +})); + +// Get the texture nodes +const colorTexture = scenePass.getTextureNode('output'); +const emissiveTexture = scenePass.getTextureNode('emissive'); + +// Apply bloom only to emissive +const bloomPass = bloom(emissiveTexture); +bloomPass.threshold.value = 0.0; // Bloom all emissive +bloomPass.strength.value = 1.5; +bloomPass.radius.value = 0.5; + +// Combine: original color + bloomed emissive +postProcessing.outputNode = colorTexture.add(bloomPass); +``` + +This approach prevents non-emissive bright areas (like white surfaces) from blooming. + +## Chaining Effects + +```javascript +const scenePass = pass(scene, camera); +const color = scenePass.getTextureNode('output'); + +// Chain multiple effects +let output = color; + +// 1. Apply bloom +const bloomPass = bloom(output); +output = output.add(bloomPass.mul(0.5)); + +// 2. Apply color grading +output = saturation(output, 1.2); + +// 3. Apply vignette +const dist = screenUV.sub(0.5).length(); +const vignette = float(1.0).sub(dist.mul(0.5)); +output = output.mul(vignette); + +// 4. Apply FXAA +output = fxaa(output); + +postProcessing.outputNode = output; +``` + +## Conditional Effects + +```javascript +const effectEnabled = uniform(true); + +const conditionalEffect = Fn(() => { + const color = scenePassColor; + return select(effectEnabled, grayscale(color), color); +}); + +postProcessing.outputNode = conditionalEffect(); + +// Toggle at runtime +effectEnabled.value = false; +``` + +## Transitions + +```javascript +import { transition } from 'three/addons/tsl/display/TransitionNode.js'; + +const scenePassA = pass(sceneA, camera); +const scenePassB = pass(sceneB, camera); + +const transitionProgress = uniform(0); + +const transitionPass = transition( + scenePassA.getTextureNode('output'), + scenePassB.getTextureNode('output'), + transitionProgress, + texture(transitionTexture) // Optional transition texture +); + +postProcessing.outputNode = transitionPass; + +// Animate transition +function animate() { + transitionProgress.value = Math.sin(time) * 0.5 + 0.5; + postProcessing.render(); +} +``` diff --git a/.codex/skills/webgpu-threejs-tsl/docs/wgsl-integration.md b/.codex/skills/webgpu-threejs-tsl/docs/wgsl-integration.md new file mode 100644 index 0000000..c4dc7f2 --- /dev/null +++ b/.codex/skills/webgpu-threejs-tsl/docs/wgsl-integration.md @@ -0,0 +1,324 @@ +# WGSL Integration + +TSL allows embedding raw WGSL (WebGPU Shading Language) code when you need direct GPU control. + +## wgslFn - Custom WGSL Functions + +### Basic Usage + +```javascript +import { wgslFn, float, vec3 } from 'three/tsl'; + +// Define WGSL function +const gammaCorrect = wgslFn(` + fn gammaCorrect(color: vec3, gamma: f32) -> vec3 { + return pow(color, vec3(1.0 / gamma)); + } +`); + +// Use in TSL +material.colorNode = gammaCorrect(inputColor, float(2.2)); +``` + +### Function with Multiple Parameters + +```javascript +const blendColors = wgslFn(` + fn blendColors(a: vec3, b: vec3, t: f32) -> vec3 { + return mix(a, b, t); + } +`); + +material.colorNode = blendColors(colorA, colorB, blendFactor); +``` + +### Advanced Math Functions + +```javascript +const fresnelSchlick = wgslFn(` + fn fresnelSchlick(cosTheta: f32, F0: vec3) -> vec3 { + return F0 + (vec3(1.0) - F0) * pow(1.0 - cosTheta, 5.0); + } +`); + +const GGX = wgslFn(` + fn distributionGGX(N: vec3, H: vec3, roughness: f32) -> f32 { + let a = roughness * roughness; + let a2 = a * a; + let NdotH = max(dot(N, H), 0.0); + let NdotH2 = NdotH * NdotH; + + let num = a2; + let denom = (NdotH2 * (a2 - 1.0) + 1.0); + denom = 3.14159265359 * denom * denom; + + return num / denom; + } +`); +``` + +### Noise Functions + +```javascript +const simplexNoise = wgslFn(` + fn mod289(x: vec3) -> vec3 { + return x - floor(x * (1.0 / 289.0)) * 289.0; + } + + fn permute(x: vec3) -> vec3 { + return mod289(((x * 34.0) + 1.0) * x); + } + + fn snoise(v: vec2) -> f32 { + let C = vec4( + 0.211324865405187, + 0.366025403784439, + -0.577350269189626, + 0.024390243902439 + ); + + var i = floor(v + dot(v, C.yy)); + let x0 = v - i + dot(i, C.xx); + + var i1: vec2; + if (x0.x > x0.y) { + i1 = vec2(1.0, 0.0); + } else { + i1 = vec2(0.0, 1.0); + } + + var x12 = x0.xyxy + C.xxzz; + x12 = vec4(x12.xy - i1, x12.zw); + + i = mod289(vec3(i, 0.0)).xy; + let p = permute(permute(i.y + vec3(0.0, i1.y, 1.0)) + i.x + vec3(0.0, i1.x, 1.0)); + + var m = max(vec3(0.5) - vec3(dot(x0, x0), dot(x12.xy, x12.xy), dot(x12.zw, x12.zw)), vec3(0.0)); + m = m * m; + m = m * m; + + let x = 2.0 * fract(p * C.www) - 1.0; + let h = abs(x) - 0.5; + let ox = floor(x + 0.5); + let a0 = x - ox; + + m = m * (1.79284291400159 - 0.85373472095314 * (a0 * a0 + h * h)); + + let g = vec3( + a0.x * x0.x + h.x * x0.y, + a0.y * x12.x + h.y * x12.y, + a0.z * x12.z + h.z * x12.w + ); + + return 130.0 * dot(m, g); + } +`); + +// Use noise +const noiseValue = simplexNoise(uv().mul(10.0)); +``` + +### FBM (Fractal Brownian Motion) + +```javascript +const fbm = wgslFn(` + fn fbm(p: vec2, octaves: i32) -> f32 { + var value = 0.0; + var amplitude = 0.5; + var frequency = 1.0; + var pos = p; + + for (var i = 0; i < octaves; i = i + 1) { + value = value + amplitude * snoise(pos * frequency); + amplitude = amplitude * 0.5; + frequency = frequency * 2.0; + } + + return value; + } +`); +``` + +## WGSL Types Reference + +### Scalar Types +```wgsl +bool // Boolean +i32 // 32-bit signed integer +u32 // 32-bit unsigned integer +f32 // 32-bit float +f16 // 16-bit float (if enabled) +``` + +### Vector Types +```wgsl +vec2 // 2D float vector +vec3 // 3D float vector +vec4 // 4D float vector +vec2 // 2D integer vector +vec2 // 2D unsigned integer vector +``` + +### Matrix Types +```wgsl +mat2x2 // 2x2 matrix +mat3x3 // 3x3 matrix +mat4x4 // 4x4 matrix +mat2x3 // 2 columns, 3 rows +``` + +### Texture Types +```wgsl +texture_2d +texture_3d +texture_cube +texture_storage_2d +``` + +## WGSL Syntax Reference + +### Variables +```wgsl +let x = 1.0; // Immutable +var y = 2.0; // Mutable +const PI = 3.14159; // Compile-time constant +``` + +### Control Flow +```wgsl +// If-else +if (condition) { + // ... +} else if (other) { + // ... +} else { + // ... +} + +// For loop +for (var i = 0; i < 10; i = i + 1) { + // ... +} + +// While loop +while (condition) { + // ... +} + +// Switch +switch (value) { + case 0: { /* ... */ } + case 1, 2: { /* ... */ } + default: { /* ... */ } +} +``` + +### Built-in Functions +```wgsl +// Math +abs(x), sign(x), floor(x), ceil(x), round(x) +fract(x), trunc(x) +min(a, b), max(a, b), clamp(x, lo, hi) +mix(a, b, t), step(edge, x), smoothstep(lo, hi, x) +sin(x), cos(x), tan(x), asin(x), acos(x), atan(x), atan2(y, x) +pow(x, y), exp(x), log(x), exp2(x), log2(x) +sqrt(x), inverseSqrt(x) + +// Vector +length(v), distance(a, b) +dot(a, b), cross(a, b) +normalize(v), faceForward(n, i, nref) +reflect(i, n), refract(i, n, eta) + +// Matrix +transpose(m), determinant(m) + +// Texture +textureSample(t, s, coord) +textureLoad(t, coord, level) +textureStore(t, coord, value) +textureDimensions(t) +``` + +## Combining TSL and WGSL + +### TSL Wrapper for WGSL + +```javascript +import { Fn, wgslFn, float, vec2, vec3 } from 'three/tsl'; + +// WGSL implementation +const wgslNoise = wgslFn(` + fn noise2d(p: vec2) -> f32 { + return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453); + } +`); + +// TSL wrapper with nice API +const noise = Fn(([position, scale = 1.0]) => { + return wgslNoise(position.xy.mul(scale)); +}); + +// Use +material.colorNode = vec3(noise(positionWorld, 10.0)); +``` + +### Hybrid Approach + +```javascript +// Complex math in WGSL +const complexMath = wgslFn(` + fn complexOperation(a: vec3, b: vec3, t: f32) -> vec3 { + let blended = mix(a, b, t); + let rotated = vec3( + blended.x * cos(t) - blended.y * sin(t), + blended.x * sin(t) + blended.y * cos(t), + blended.z + ); + return normalize(rotated); + } +`); + +// Simple logic in TSL +const finalColor = Fn(() => { + const base = texture(diffuseMap).rgb; + const processed = complexMath(base, vec3(1, 0, 0), time); + return mix(base, processed, oscSine(time)); +}); + +material.colorNode = finalColor(); +``` + +## Performance Tips + +### Avoid Branching When Possible +```wgsl +// Instead of: +if (x > 0.5) { + result = a; +} else { + result = b; +} + +// Use: +result = mix(b, a, step(0.5, x)); +``` + +### Use Local Variables +```wgsl +fn compute(p: vec2) -> f32 { + // Cache repeated calculations + let p2 = p * p; + let p4 = p2 * p2; + return p2.x + p2.y + p4.x * p4.y; +} +``` + +### Minimize Texture Samples +```wgsl +// Sample once, use multiple times +let sample = textureSample(tex, sampler, uv); +let r = sample.r; +let g = sample.g; +let b = sample.b; +``` diff --git a/.codex/skills/webgpu-threejs-tsl/examples/basic-setup.js b/.codex/skills/webgpu-threejs-tsl/examples/basic-setup.js new file mode 100644 index 0000000..272287b --- /dev/null +++ b/.codex/skills/webgpu-threejs-tsl/examples/basic-setup.js @@ -0,0 +1,87 @@ +/** + * Basic WebGPU Three.js Setup + * + * Minimal example showing WebGPU renderer initialization + * with a simple animated mesh using TSL. + * + * Based on Three.js examples (MIT License) + * https://github.com/mrdoob/three.js + */ + +import * as THREE from 'three/webgpu'; +import { color, time, oscSine, positionLocal, normalWorld } from 'three/tsl'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +let camera, scene, renderer, controls; + +async function init() { + // Camera + camera = new THREE.PerspectiveCamera( + 70, + window.innerWidth / window.innerHeight, + 0.1, + 100 + ); + camera.position.z = 4; + + // Scene + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x111111); + + // Lighting + const ambientLight = new THREE.AmbientLight(0x404040); + scene.add(ambientLight); + + const directionalLight = new THREE.DirectionalLight(0xffffff, 1); + directionalLight.position.set(5, 5, 5); + scene.add(directionalLight); + + // Create mesh with TSL material + const geometry = new THREE.TorusKnotGeometry(1, 0.3, 128, 32); + const material = new THREE.MeshStandardNodeMaterial(); + + // Animated color using TSL + material.colorNode = color(0x0088ff).mul( + oscSine(time.mul(0.5)).mul(0.5).add(0.5) + ); + + // Add slight position wobble + material.positionNode = positionLocal.add( + normalWorld.mul(oscSine(time.mul(2.0).add(positionLocal.y)).mul(0.05)) + ); + + const mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + + // Renderer + renderer = new THREE.WebGPURenderer({ antialias: true }); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setPixelRatio(window.devicePixelRatio); + document.body.appendChild(renderer.domElement); + + // Initialize WebGPU + await renderer.init(); + + // Controls + controls = new OrbitControls(camera, renderer.domElement); + controls.enableDamping = true; + + // Handle resize + window.addEventListener('resize', onWindowResize); + + // Start animation loop + renderer.setAnimationLoop(animate); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + controls.update(); + renderer.render(scene, camera); +} + +init(); diff --git a/.codex/skills/webgpu-threejs-tsl/examples/custom-material.js b/.codex/skills/webgpu-threejs-tsl/examples/custom-material.js new file mode 100644 index 0000000..563c566 --- /dev/null +++ b/.codex/skills/webgpu-threejs-tsl/examples/custom-material.js @@ -0,0 +1,170 @@ +/** + * Custom TSL Material Example + * + * Demonstrates creating custom shader effects using TSL: + * - Fresnel rim lighting + * - Animated patterns + * - Dynamic displacement + * + * Based on Three.js examples (MIT License) + * https://github.com/mrdoob/three.js + */ + +import * as THREE from 'three/webgpu'; +import { + Fn, + color, + float, + vec2, + vec3, + uniform, + texture, + uv, + time, + mix, + smoothstep, + sin, + cos, + positionLocal, + positionWorld, + normalLocal, + normalWorld, + cameraPosition +} from 'three/tsl'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +let camera, scene, renderer, controls; +let rimColor, patternScale, displacementStrength; + +async function init() { + // Setup + camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.z = 3; + + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x000000); + + // Uniforms for runtime control + rimColor = uniform(new THREE.Color(0x00ffff)); + patternScale = uniform(5.0); + displacementStrength = uniform(0.1); + + // Create custom material + const material = createCustomMaterial(); + + // Mesh + const geometry = new THREE.IcosahedronGeometry(1, 64); + const mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + + // Renderer + renderer = new THREE.WebGPURenderer({ antialias: true }); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setPixelRatio(window.devicePixelRatio); + document.body.appendChild(renderer.domElement); + await renderer.init(); + + // Controls + controls = new OrbitControls(camera, renderer.domElement); + controls.enableDamping = true; + + // Events + window.addEventListener('resize', onWindowResize); + + // GUI (optional - requires lil-gui) + setupGUI(); + + renderer.setAnimationLoop(animate); +} + +function createCustomMaterial() { + const material = new THREE.MeshStandardNodeMaterial(); + + // --- Fresnel Rim Effect --- + const fresnel = Fn(() => { + const viewDir = cameraPosition.sub(positionWorld).normalize(); + const nDotV = normalWorld.dot(viewDir).saturate(); + return float(1.0).sub(nDotV).pow(3.0); + }); + + // --- Animated Pattern --- + const animatedPattern = Fn(() => { + const uvCoord = uv().mul(patternScale); + const t = time.mul(0.5); + + // Create animated wave pattern + const wave1 = sin(uvCoord.x.mul(10.0).add(t)).mul(0.5).add(0.5); + const wave2 = sin(uvCoord.y.mul(10.0).sub(t.mul(1.3))).mul(0.5).add(0.5); + const wave3 = sin(uvCoord.x.add(uvCoord.y).mul(7.0).add(t.mul(0.7))).mul(0.5).add(0.5); + + return wave1.mul(wave2).mul(wave3); + }); + + // --- Displacement --- + const displacement = Fn(() => { + const pattern = animatedPattern(); + return normalLocal.mul(pattern.mul(displacementStrength)); + }); + + // Apply displacement + material.positionNode = positionLocal.add(displacement()); + + // --- Color --- + const baseColor = color(0x222244); + const highlightColor = color(0x4444ff); + + // Mix colors based on pattern + const pattern = animatedPattern(); + const surfaceColor = mix(baseColor, highlightColor, pattern); + + material.colorNode = surfaceColor; + + // --- Rim lighting --- + material.emissiveNode = rimColor.mul(fresnel()); + + // --- PBR properties --- + material.roughnessNode = float(0.3).add(pattern.mul(0.4)); + material.metalnessNode = float(0.1); + + return material; +} + +function setupGUI() { + // Only setup if lil-gui is available + if (typeof window.GUI === 'undefined') { + console.log('Add lil-gui for interactive controls'); + return; + } + + const gui = new GUI(); + const params = { + rimColor: '#00ffff', + patternScale: 5.0, + displacementStrength: 0.1 + }; + + gui.addColor(params, 'rimColor').onChange((value) => { + rimColor.value.set(value); + }); + + gui.add(params, 'patternScale', 1, 20).onChange((value) => { + patternScale.value = value; + }); + + gui.add(params, 'displacementStrength', 0, 0.5).onChange((value) => { + displacementStrength.value = value; + }); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + controls.update(); + renderer.render(scene, camera); +} + +init(); diff --git a/.codex/skills/webgpu-threejs-tsl/examples/earth-shader.js b/.codex/skills/webgpu-threejs-tsl/examples/earth-shader.js new file mode 100644 index 0000000..17f3c94 --- /dev/null +++ b/.codex/skills/webgpu-threejs-tsl/examples/earth-shader.js @@ -0,0 +1,292 @@ +/** + * Earth Shader Example + * + * Complete procedural Earth with: + * - Day/night texture blending + * - Atmospheric glow (fresnel) + * - Cloud layer + * - City lights at night + * - Bump mapping + * + * Based on Three.js webgpu_tsl_earth example (MIT License) + * https://github.com/mrdoob/three.js + */ + +import * as THREE from 'three/webgpu'; +import { + Fn, + If, + float, + vec2, + vec3, + vec4, + color, + uniform, + texture, + uv, + time, + mix, + smoothstep, + pow, + clamp, + normalize, + dot, + max, + positionWorld, + normalWorld, + normalLocal, + cameraPosition, + bumpMap +} from 'three/tsl'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +let camera, scene, renderer, controls; +let earth, clouds, atmosphere; + +// Uniforms +const sunDirection = uniform(new THREE.Vector3(1, 0.2, 0.5).normalize()); +const atmosphereDayColor = uniform(new THREE.Color(0x4db2ff)); +const atmosphereTwilightColor = uniform(new THREE.Color(0xbd5f1b)); +const cloudSpeed = uniform(0.01); +const cityLightIntensity = uniform(1.5); + +async function init() { + // Camera + camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.set(0, 0, 4); + + // Scene + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x000011); + + // Load textures + const loader = new THREE.TextureLoader(); + + // Note: Replace with actual texture paths + const earthDayTexture = loader.load('textures/earth_day.jpg'); + const earthNightTexture = loader.load('textures/earth_night.jpg'); + const earthCloudsTexture = loader.load('textures/earth_clouds.jpg'); + const earthBumpTexture = loader.load('textures/earth_bump.jpg'); + + // Set texture properties + [earthDayTexture, earthNightTexture, earthCloudsTexture, earthBumpTexture].forEach((tex) => { + tex.colorSpace = THREE.SRGBColorSpace; + tex.wrapS = THREE.RepeatWrapping; + tex.wrapT = THREE.ClampToEdgeWrapping; + }); + + // Create Earth + earth = createEarth(earthDayTexture, earthNightTexture, earthBumpTexture); + scene.add(earth); + + // Create cloud layer + clouds = createClouds(earthCloudsTexture); + scene.add(clouds); + + // Create atmosphere glow + atmosphere = createAtmosphere(); + scene.add(atmosphere); + + // Stars background + createStars(); + + // Renderer + renderer = new THREE.WebGPURenderer({ antialias: true }); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setPixelRatio(window.devicePixelRatio); + document.body.appendChild(renderer.domElement); + await renderer.init(); + + // Controls + controls = new OrbitControls(camera, renderer.domElement); + controls.enableDamping = true; + controls.minDistance = 2; + controls.maxDistance = 10; + + // Events + window.addEventListener('resize', onWindowResize); + + renderer.setAnimationLoop(animate); +} + +function createEarth(dayTex, nightTex, bumpTex) { + const geometry = new THREE.SphereGeometry(1, 64, 64); + const material = new THREE.MeshStandardNodeMaterial(); + + // Sun illumination factor + const sunOrientation = Fn(() => { + return normalWorld.dot(sunDirection).mul(0.5).add(0.5); + }); + + // Day/night color mixing + material.colorNode = Fn(() => { + const dayColor = texture(dayTex, uv()); + const nightColor = texture(nightTex, uv()); + + const orientation = sunOrientation(); + const dayNight = smoothstep(0.4, 0.6, orientation); + + // Add city lights on night side + const cityLights = nightColor.mul(cityLightIntensity).mul( + float(1.0).sub(dayNight) + ); + + const baseColor = mix(nightColor, dayColor, dayNight); + return baseColor.add(cityLights.mul(float(1.0).sub(orientation).pow(2.0))); + })(); + + // Bump mapping for terrain + material.normalNode = bumpMap(texture(bumpTex, uv()), 0.03); + + // PBR properties vary with day/night + material.roughnessNode = Fn(() => { + const orientation = sunOrientation(); + return mix(float(0.8), float(0.4), smoothstep(0.3, 0.7, orientation)); + })(); + + material.metalnessNode = float(0.0); + + // Subtle atmospheric rim on day side + material.emissiveNode = Fn(() => { + const viewDir = normalize(cameraPosition.sub(positionWorld)); + const fresnel = pow(float(1.0).sub(normalWorld.dot(viewDir).saturate()), 4.0); + + const orientation = sunOrientation(); + const atmosphereColor = mix(atmosphereTwilightColor, atmosphereDayColor, orientation); + + return atmosphereColor.mul(fresnel).mul(orientation).mul(0.3); + })(); + + return new THREE.Mesh(geometry, material); +} + +function createClouds(cloudsTex) { + const geometry = new THREE.SphereGeometry(1.01, 64, 64); + const material = new THREE.MeshStandardNodeMaterial(); + + // Animated UV for cloud movement + const cloudUV = Fn(() => { + const baseUV = uv(); + const offset = time.mul(cloudSpeed); + return vec2(baseUV.x.add(offset), baseUV.y); + }); + + // Cloud color (white with transparency) + material.colorNode = color(0xffffff); + + // Cloud opacity from texture + material.opacityNode = Fn(() => { + const cloudAlpha = texture(cloudsTex, cloudUV()).r; + + // Fade clouds on night side + const sunOrientation = normalWorld.dot(sunDirection).mul(0.5).add(0.5); + const dayFactor = smoothstep(0.2, 0.5, sunOrientation); + + return cloudAlpha.mul(0.8).mul(dayFactor.mul(0.5).add(0.5)); + })(); + + material.transparent = true; + material.depthWrite = false; + material.side = THREE.DoubleSide; + + // Slight self-illumination + material.emissiveNode = Fn(() => { + const sunOrientation = normalWorld.dot(sunDirection).mul(0.5).add(0.5); + return color(0xffffff).mul(sunOrientation.mul(0.1)); + })(); + + return new THREE.Mesh(geometry, material); +} + +function createAtmosphere() { + const geometry = new THREE.SphereGeometry(1.15, 64, 64); + const material = new THREE.MeshBasicNodeMaterial(); + + material.colorNode = Fn(() => { + const viewDir = normalize(cameraPosition.sub(positionWorld)); + const fresnel = pow(float(1.0).sub(normalWorld.dot(viewDir).abs()), 3.0); + + const sunOrientation = normalWorld.dot(sunDirection).mul(0.5).add(0.5); + const atmosphereColor = mix(atmosphereTwilightColor, atmosphereDayColor, sunOrientation); + + return atmosphereColor; + })(); + + material.opacityNode = Fn(() => { + const viewDir = normalize(cameraPosition.sub(positionWorld)); + const fresnel = pow(float(1.0).sub(normalWorld.dot(viewDir).abs()), 2.5); + + // Stronger on day side + const sunOrientation = normalWorld.dot(sunDirection).mul(0.5).add(0.5); + + return fresnel.mul(sunOrientation.mul(0.5).add(0.3)); + })(); + + material.transparent = true; + material.depthWrite = false; + material.side = THREE.BackSide; + + return new THREE.Mesh(geometry, material); +} + +function createStars() { + const starsGeometry = new THREE.BufferGeometry(); + const starCount = 2000; + + const positions = new Float32Array(starCount * 3); + const colors = new Float32Array(starCount * 3); + + for (let i = 0; i < starCount; i++) { + // Random position on sphere + const theta = Math.random() * Math.PI * 2; + const phi = Math.acos(Math.random() * 2 - 1); + const radius = 50 + Math.random() * 50; + + positions[i * 3] = radius * Math.sin(phi) * Math.cos(theta); + positions[i * 3 + 1] = radius * Math.sin(phi) * Math.sin(theta); + positions[i * 3 + 2] = radius * Math.cos(phi); + + // Slight color variation + const brightness = 0.5 + Math.random() * 0.5; + colors[i * 3] = brightness; + colors[i * 3 + 1] = brightness; + colors[i * 3 + 2] = brightness + Math.random() * 0.2; + } + + starsGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); + starsGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)); + + const starsMaterial = new THREE.PointsNodeMaterial(); + starsMaterial.colorNode = Fn(() => { + return vec3(1.0); + })(); + starsMaterial.sizeNode = float(2.0); + starsMaterial.vertexColors = true; + + const stars = new THREE.Points(starsGeometry, starsMaterial); + scene.add(stars); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + // Rotate Earth slowly + earth.rotation.y += 0.001; + clouds.rotation.y += 0.0012; + + // Animate sun direction (optional - creates day/night cycle) + // const angle = time.value * 0.1; + // sunDirection.value.set(Math.cos(angle), 0.2, Math.sin(angle)).normalize(); + + controls.update(); + renderer.render(scene, camera); +} + +init(); + +// Export for external control +export { sunDirection, atmosphereDayColor, atmosphereTwilightColor, cloudSpeed, cityLightIntensity }; diff --git a/.codex/skills/webgpu-threejs-tsl/examples/particle-system.js b/.codex/skills/webgpu-threejs-tsl/examples/particle-system.js new file mode 100644 index 0000000..38783b8 --- /dev/null +++ b/.codex/skills/webgpu-threejs-tsl/examples/particle-system.js @@ -0,0 +1,259 @@ +/** + * GPU Particle System with Compute Shaders + * + * Demonstrates TSL compute shaders for particle simulation: + * - Instanced array buffers + * - Physics simulation on GPU + * - Mouse interaction + * + * Based on Three.js webgpu_compute_particles example (MIT License) + * https://github.com/mrdoob/three.js + */ + +import * as THREE from 'three/webgpu'; +import { + Fn, + If, + uniform, + float, + vec3, + color, + instancedArray, + instanceIndex, + hash, + time +} from 'three/tsl'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +let camera, scene, renderer, controls; +let computeInit, computeUpdate, computeHit; + +// Particle count +const PARTICLE_COUNT = 100000; + +// Storage buffers +const positions = instancedArray(PARTICLE_COUNT, 'vec3'); +const velocities = instancedArray(PARTICLE_COUNT, 'vec3'); + +// Uniforms +const gravity = uniform(-9.8); +const bounce = uniform(0.7); +const friction = uniform(0.98); +const deltaTimeUniform = uniform(0); +const clickPosition = uniform(new THREE.Vector3()); +const hitStrength = uniform(5.0); + +async function init() { + // Camera + camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.set(0, 5, 15); + + // Scene + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x111122); + + // Create compute shaders + createComputeShaders(); + + // Create particle mesh + createParticleMesh(); + + // Floor + const floorGeometry = new THREE.PlaneGeometry(30, 30); + const floorMaterial = new THREE.MeshStandardNodeMaterial({ + color: 0x333333 + }); + const floor = new THREE.Mesh(floorGeometry, floorMaterial); + floor.rotation.x = -Math.PI / 2; + floor.receiveShadow = true; + scene.add(floor); + + // Lights + const ambientLight = new THREE.AmbientLight(0x404040); + scene.add(ambientLight); + + const pointLight = new THREE.PointLight(0xffffff, 100); + pointLight.position.set(5, 10, 5); + scene.add(pointLight); + + // Renderer + renderer = new THREE.WebGPURenderer({ antialias: true }); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setPixelRatio(window.devicePixelRatio); + document.body.appendChild(renderer.domElement); + await renderer.init(); + + // Initialize particles + await renderer.computeAsync(computeInit); + + // Controls + controls = new OrbitControls(camera, renderer.domElement); + controls.enableDamping = true; + controls.target.set(0, 2, 0); + + // Events + window.addEventListener('resize', onWindowResize); + renderer.domElement.addEventListener('click', onClick); + + renderer.setAnimationLoop(animate); +} + +function createComputeShaders() { + // Grid dimensions for initialization + const gridSize = Math.ceil(Math.sqrt(PARTICLE_COUNT)); + const spacing = 0.15; + const offset = (gridSize * spacing) / 2; + + // Initialize particles in a grid + computeInit = Fn(() => { + const position = positions.element(instanceIndex); + const velocity = velocities.element(instanceIndex); + + // Calculate grid position + const x = instanceIndex.mod(gridSize); + const z = instanceIndex.div(gridSize); + + // Set position + position.x.assign(x.toFloat().mul(spacing).sub(offset)); + position.y.assign(float(5.0).add(hash(instanceIndex).mul(2.0))); + position.z.assign(z.toFloat().mul(spacing).sub(offset)); + + // Random initial velocity + velocity.x.assign(hash(instanceIndex.add(1)).sub(0.5).mul(2.0)); + velocity.y.assign(hash(instanceIndex.add(2)).mul(-2.0)); + velocity.z.assign(hash(instanceIndex.add(3)).sub(0.5).mul(2.0)); + })().compute(PARTICLE_COUNT); + + // Physics update + computeUpdate = Fn(() => { + const position = positions.element(instanceIndex); + const velocity = velocities.element(instanceIndex); + const dt = deltaTimeUniform; + + // Apply gravity + velocity.y.addAssign(gravity.mul(dt)); + + // Update position + position.addAssign(velocity.mul(dt)); + + // Apply friction + velocity.mulAssign(friction); + + // Ground collision + If(position.y.lessThan(0), () => { + position.y.assign(0); + velocity.y.assign(velocity.y.abs().mul(bounce)); // Reverse and dampen + + // Extra friction on ground + velocity.x.mulAssign(0.9); + velocity.z.mulAssign(0.9); + }); + + // Boundary walls + If(position.x.abs().greaterThan(15), () => { + position.x.assign(position.x.sign().mul(15)); + velocity.x.assign(velocity.x.negate().mul(bounce)); + }); + + If(position.z.abs().greaterThan(15), () => { + position.z.assign(position.z.sign().mul(15)); + velocity.z.assign(velocity.z.negate().mul(bounce)); + }); + })().compute(PARTICLE_COUNT); + + // Hit/explosion effect + computeHit = Fn(() => { + const position = positions.element(instanceIndex); + const velocity = velocities.element(instanceIndex); + + // Distance to click + const toClick = position.sub(clickPosition); + const distance = toClick.length(); + + // Apply force within radius + If(distance.lessThan(3.0), () => { + const direction = toClick.normalize(); + const force = float(3.0).sub(distance).div(3.0).mul(hitStrength); + + // Add randomness + const randomForce = force.mul(hash(instanceIndex.add(time.mul(1000))).mul(0.5).add(0.75)); + + velocity.addAssign(direction.mul(randomForce)); + velocity.y.addAssign(randomForce.mul(0.5)); + }); + })().compute(PARTICLE_COUNT); +} + +function createParticleMesh() { + // Simple sphere geometry for each particle + const geometry = new THREE.SphereGeometry(0.08, 8, 8); + + // Material using computed positions + const material = new THREE.MeshStandardNodeMaterial(); + + // Position from compute buffer + material.positionNode = positions.element(instanceIndex); + + // Color based on velocity + material.colorNode = Fn(() => { + const velocity = velocities.element(instanceIndex); + const speed = velocity.length(); + + // Color gradient: blue (slow) -> orange (fast) + const t = speed.div(10.0).saturate(); + return color(0x0066ff).mix(color(0xff6600), t); + })(); + + material.roughnessNode = float(0.5); + material.metalnessNode = float(0.2); + + // Create instanced mesh + const mesh = new THREE.InstancedMesh(geometry, material, PARTICLE_COUNT); + scene.add(mesh); +} + +function onClick(event) { + // Raycast to find click position on floor + const raycaster = new THREE.Raycaster(); + const mouse = new THREE.Vector2( + (event.clientX / window.innerWidth) * 2 - 1, + -(event.clientY / window.innerHeight) * 2 + 1 + ); + + raycaster.setFromCamera(mouse, camera); + + // Intersect with floor plane (y = 0) + const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0); + const intersection = new THREE.Vector3(); + raycaster.ray.intersectPlane(plane, intersection); + + if (intersection) { + // Raise click position slightly + intersection.y = 0.5; + clickPosition.value.copy(intersection); + + // Run hit compute shader + renderer.compute(computeHit); + } +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + renderer.setSize(window.innerWidth, window.innerHeight); +} + +const clock = new THREE.Clock(); + +function animate() { + // Update delta time + deltaTimeUniform.value = Math.min(clock.getDelta(), 0.1); + + // Run physics compute + renderer.compute(computeUpdate); + + controls.update(); + renderer.render(scene, camera); +} + +init(); diff --git a/.codex/skills/webgpu-threejs-tsl/examples/post-processing.js b/.codex/skills/webgpu-threejs-tsl/examples/post-processing.js new file mode 100644 index 0000000..5addf52 --- /dev/null +++ b/.codex/skills/webgpu-threejs-tsl/examples/post-processing.js @@ -0,0 +1,199 @@ +/** + * Post-Processing Pipeline Example + * + * Demonstrates TSL post-processing: + * - Bloom effect + * - Custom vignette + * - Color grading + * - Effect chaining + * + * Based on Three.js webgpu_postprocessing examples (MIT License) + * https://github.com/mrdoob/three.js + */ + +import * as THREE from 'three/webgpu'; +import { + Fn, + float, + vec2, + vec3, + vec4, + color, + uniform, + pass, + screenUV, + screenSize, + time, + oscSine, + mix, + smoothstep, + texture, + grayscale, + saturation +} from 'three/tsl'; +import { bloom } from 'three/addons/tsl/display/BloomNode.js'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +let camera, scene, renderer, controls; +let postProcessing; + +// Effect uniforms +const bloomStrength = uniform(1.0); +const bloomThreshold = uniform(0.5); +const vignetteIntensity = uniform(0.5); +const saturationAmount = uniform(1.2); +const colorTint = uniform(new THREE.Color(1.0, 0.95, 0.9)); + +async function init() { + // Camera + camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.set(0, 2, 8); + + // Scene + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x111111); + + // Add objects with emissive materials (for bloom) + createScene(); + + // Renderer + renderer = new THREE.WebGPURenderer({ antialias: true }); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setPixelRatio(window.devicePixelRatio); + document.body.appendChild(renderer.domElement); + await renderer.init(); + + // Create post-processing pipeline + setupPostProcessing(); + + // Controls + controls = new OrbitControls(camera, renderer.domElement); + controls.enableDamping = true; + controls.target.set(0, 1, 0); + + // Events + window.addEventListener('resize', onWindowResize); + + renderer.setAnimationLoop(animate); +} + +function createScene() { + // Floor + const floorGeometry = new THREE.PlaneGeometry(20, 20); + const floorMaterial = new THREE.MeshStandardNodeMaterial({ + color: 0x222222 + }); + const floor = new THREE.Mesh(floorGeometry, floorMaterial); + floor.rotation.x = -Math.PI / 2; + scene.add(floor); + + // Emissive spheres (will bloom) + const sphereGeometry = new THREE.SphereGeometry(0.5, 32, 32); + + const colors = [0xff0044, 0x00ff88, 0x4488ff, 0xffaa00, 0xff00ff]; + + for (let i = 0; i < 5; i++) { + const material = new THREE.MeshStandardNodeMaterial(); + + // Base color + material.colorNode = color(colors[i]).mul(0.3); + + // Animated emissive + material.emissiveNode = Fn(() => { + const pulse = oscSine(time.mul(1.0 + i * 0.2)).mul(0.5).add(0.5); + return color(colors[i]).mul(pulse.mul(2.0).add(0.5)); + })(); + + material.roughnessNode = float(0.2); + material.metalnessNode = float(0.8); + + const sphere = new THREE.Mesh(sphereGeometry, material); + sphere.position.set( + Math.cos((i / 5) * Math.PI * 2) * 3, + 1 + Math.sin(i) * 0.5, + Math.sin((i / 5) * Math.PI * 2) * 3 + ); + scene.add(sphere); + } + + // Central reflective sphere + const centerMaterial = new THREE.MeshStandardNodeMaterial(); + centerMaterial.colorNode = color(0x888888); + centerMaterial.roughnessNode = float(0.1); + centerMaterial.metalnessNode = float(1.0); + + const centerSphere = new THREE.Mesh(new THREE.SphereGeometry(1, 64, 64), centerMaterial); + centerSphere.position.y = 1; + scene.add(centerSphere); + + // Lights + const ambientLight = new THREE.AmbientLight(0x404040, 0.5); + scene.add(ambientLight); + + const pointLight = new THREE.PointLight(0xffffff, 50); + pointLight.position.set(5, 10, 5); + scene.add(pointLight); +} + +function setupPostProcessing() { + // Create post-processing instance + postProcessing = new THREE.PostProcessing(renderer); + + // Create scene pass + const scenePass = pass(scene, camera); + const sceneColor = scenePass.getTextureNode('output'); + + // --- Effect Chain --- + + // 1. Bloom + const bloomPass = bloom(sceneColor); + bloomPass.threshold = bloomThreshold; + bloomPass.strength = bloomStrength; + bloomPass.radius = uniform(0.5); + + // Add bloom to scene + let output = sceneColor.add(bloomPass); + + // 2. Color Grading + output = saturation(output, saturationAmount); + output = output.mul(colorTint); + + // 3. Vignette (custom effect) + const vignette = Fn(() => { + const uv = screenUV; + const dist = uv.sub(0.5).length(); + return float(1.0).sub(dist.mul(vignetteIntensity).pow(2.0)).saturate(); + }); + + output = output.mul(vignette()); + + // 4. Optional: Scanlines + const scanlines = Fn(() => { + const scanline = screenUV.y.mul(screenSize.y).mul(0.5).sin().mul(0.05).add(0.95); + return scanline; + }); + + // Uncomment for CRT effect: + // output = output.mul(scanlines()); + + // Set final output + postProcessing.outputNode = output; +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + renderer.setSize(window.innerWidth, window.innerHeight); +} + +function animate() { + controls.update(); + + // Render with post-processing + postProcessing.render(); +} + +init(); + +// Export uniforms for external control (e.g., GUI) +export { bloomStrength, bloomThreshold, vignetteIntensity, saturationAmount, colorTint }; diff --git a/.codex/skills/webgpu-threejs-tsl/templates/compute-shader.js b/.codex/skills/webgpu-threejs-tsl/templates/compute-shader.js new file mode 100644 index 0000000..690af6f --- /dev/null +++ b/.codex/skills/webgpu-threejs-tsl/templates/compute-shader.js @@ -0,0 +1,305 @@ +/** + * Compute Shader Template + * + * A template for GPU compute shaders with: + * - Storage buffer setup + * - Initialize and update shaders + * - Visualization with instanced mesh + * + * Usage: + * 1. Modify PARTICLE_COUNT and buffer types + * 2. Implement your initialization logic + * 3. Implement your update logic + * 4. Customize visualization + */ + +import * as THREE from 'three/webgpu'; +import { + Fn, + If, + Loop, + float, + int, + vec2, + vec3, + vec4, + color, + uniform, + instancedArray, + instanceIndex, + hash, + time, + deltaTime +} from 'three/tsl'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +// ============================================ +// CONFIGURATION +// ============================================ + +const PARTICLE_COUNT = 50000; + +// ============================================ +// STORAGE BUFFERS +// ============================================ + +// Define your storage buffers here +// Available types: 'float', 'vec2', 'vec3', 'vec4', 'int', 'uint' + +const positions = instancedArray(PARTICLE_COUNT, 'vec3'); +const velocities = instancedArray(PARTICLE_COUNT, 'vec3'); +// Add more buffers as needed: +// const colors = instancedArray(PARTICLE_COUNT, 'vec3'); +// const lifetimes = instancedArray(PARTICLE_COUNT, 'float'); +// const states = instancedArray(PARTICLE_COUNT, 'uint'); + +// ============================================ +// UNIFORMS +// ============================================ + +const dt = uniform(0); +// Add your uniforms here: +// const gravity = uniform(-9.8); +// const attractorPosition = uniform(new THREE.Vector3()); +// const forceStrength = uniform(1.0); + +// ============================================ +// COMPUTE SHADERS +// ============================================ + +/** + * Initialize particles + * Called once at startup + */ +const computeInit = Fn(() => { + const position = positions.element(instanceIndex); + const velocity = velocities.element(instanceIndex); + + // ======================================== + // IMPLEMENT YOUR INITIALIZATION HERE + // ======================================== + + // Example: Random positions in a cube + position.x.assign(hash(instanceIndex).sub(0.5).mul(10)); + position.y.assign(hash(instanceIndex.add(1)).sub(0.5).mul(10)); + position.z.assign(hash(instanceIndex.add(2)).sub(0.5).mul(10)); + + // Example: Zero velocity + velocity.assign(vec3(0)); +})().compute(PARTICLE_COUNT); + +/** + * Update particles each frame + * Called every frame in animation loop + */ +const computeUpdate = Fn(() => { + const position = positions.element(instanceIndex); + const velocity = velocities.element(instanceIndex); + + // ======================================== + // IMPLEMENT YOUR UPDATE LOGIC HERE + // ======================================== + + // Example: Simple gravity + velocity.y.addAssign(float(-9.8).mul(dt)); + + // Example: Update position + position.addAssign(velocity.mul(dt)); + + // Example: Ground bounce + If(position.y.lessThan(0), () => { + position.y.assign(0); + velocity.y.assign(velocity.y.negate().mul(0.8)); + }); + + // Example: Boundary wrapping + // If(position.x.abs().greaterThan(5), () => { + // position.x.assign(position.x.negate()); + // }); +})().compute(PARTICLE_COUNT); + +/** + * Optional: Additional compute pass (e.g., for interactions) + */ +const computeInteraction = Fn(() => { + const position = positions.element(instanceIndex); + const velocity = velocities.element(instanceIndex); + + // ======================================== + // IMPLEMENT INTERACTION LOGIC HERE + // ======================================== + + // Example: Attract to point + // const toTarget = attractorPosition.sub(position); + // const dist = toTarget.length(); + // const force = toTarget.normalize().mul(forceStrength).div(dist.add(0.1)); + // velocity.addAssign(force.mul(dt)); +})().compute(PARTICLE_COUNT); + +// ============================================ +// VISUALIZATION +// ============================================ + +function createVisualization(scene) { + // Choose visualization type: + // - Points (fastest, simplest) + // - Instanced Mesh (more control) + + // Option 1: Points + // return createPointsVisualization(scene); + + // Option 2: Instanced Mesh + return createInstancedVisualization(scene); +} + +function createPointsVisualization(scene) { + const geometry = new THREE.BufferGeometry(); + geometry.setAttribute( + 'position', + new THREE.Float32BufferAttribute(new Float32Array(PARTICLE_COUNT * 3), 3) + ); + + const material = new THREE.PointsNodeMaterial(); + + // Position from compute buffer + material.positionNode = positions.element(instanceIndex); + + // ======================================== + // CUSTOMIZE POINT APPEARANCE HERE + // ======================================== + + material.sizeNode = float(3.0); + + material.colorNode = Fn(() => { + // Example: Color based on velocity + const velocity = velocities.element(instanceIndex); + const speed = velocity.length(); + return mix(color(0x0066ff), color(0xff6600), speed.div(5).saturate()); + })(); + + const points = new THREE.Points(geometry, material); + scene.add(points); + return points; +} + +function createInstancedVisualization(scene) { + // Geometry for each instance + const geometry = new THREE.SphereGeometry(0.05, 8, 8); + // Or use simpler geometry for better performance: + // const geometry = new THREE.IcosahedronGeometry(0.05, 0); + + const material = new THREE.MeshStandardNodeMaterial(); + + // Position from compute buffer + material.positionNode = positions.element(instanceIndex); + + // ======================================== + // CUSTOMIZE MESH APPEARANCE HERE + // ======================================== + + material.colorNode = Fn(() => { + // Example: Color based on position + const position = positions.element(instanceIndex); + return color(0x0088ff).add(position.mul(0.05)); + })(); + + material.roughnessNode = float(0.5); + material.metalnessNode = float(0.2); + + const mesh = new THREE.InstancedMesh(geometry, material, PARTICLE_COUNT); + scene.add(mesh); + return mesh; +} + +// ============================================ +// MAIN SETUP +// ============================================ + +let camera, scene, renderer, controls; +let visualization; + +async function init() { + // Scene + scene = new THREE.Scene(); + scene.background = new THREE.Color(0x111122); + + // Camera + camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 100); + camera.position.set(0, 5, 15); + + // Lights + const ambientLight = new THREE.AmbientLight(0x404040); + scene.add(ambientLight); + + const directionalLight = new THREE.DirectionalLight(0xffffff, 1); + directionalLight.position.set(5, 10, 5); + scene.add(directionalLight); + + // Optional: Ground plane + const ground = new THREE.Mesh( + new THREE.PlaneGeometry(20, 20), + new THREE.MeshStandardNodeMaterial({ color: 0x333333 }) + ); + ground.rotation.x = -Math.PI / 2; + scene.add(ground); + + // Renderer + renderer = new THREE.WebGPURenderer({ antialias: true }); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); + document.body.appendChild(renderer.domElement); + await renderer.init(); + + // Initialize particles + await renderer.computeAsync(computeInit); + + // Create visualization + visualization = createVisualization(scene); + + // Controls + controls = new OrbitControls(camera, renderer.domElement); + controls.enableDamping = true; + controls.target.set(0, 2, 0); + + // Events + window.addEventListener('resize', onWindowResize); + + // Start + renderer.setAnimationLoop(animate); +} + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + renderer.setSize(window.innerWidth, window.innerHeight); +} + +const clock = new THREE.Clock(); + +function animate() { + // Update delta time uniform + dt.value = Math.min(clock.getDelta(), 0.1); + + // Run compute shaders + renderer.compute(computeUpdate); + // renderer.compute(computeInteraction); + + // Update controls + controls.update(); + + // Render + renderer.render(scene, camera); +} + +init().catch(console.error); + +// Export for external control +export { + positions, + velocities, + dt, + computeInit, + computeUpdate, + computeInteraction +}; diff --git a/.codex/skills/webgpu-threejs-tsl/templates/webgpu-project.js b/.codex/skills/webgpu-threejs-tsl/templates/webgpu-project.js new file mode 100644 index 0000000..b23e6b5 --- /dev/null +++ b/.codex/skills/webgpu-threejs-tsl/templates/webgpu-project.js @@ -0,0 +1,276 @@ +/** + * WebGPU Three.js Project Template + * + * A complete starter template with: + * - WebGPU renderer setup + * - TSL material example + * - Post-processing ready + * - Responsive design + * - Animation loop + * + * Usage: + * 1. Copy this file to your project + * 2. Install Three.js: npm install three + * 3. Replace placeholder content with your scene + */ + +import * as THREE from 'three/webgpu'; +import { + // Types + float, + vec2, + vec3, + vec4, + color, + uniform, + + // Geometry + positionLocal, + positionWorld, + normalLocal, + normalWorld, + uv, + + // Camera + cameraPosition, + + // Time + time, + deltaTime, + + // Math + mix, + smoothstep, + clamp, + sin, + cos, + + // Texture + texture, + + // Functions + Fn, + If, + Loop, + + // Post-processing + pass +} from 'three/tsl'; + +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +// ============================================ +// CONFIGURATION +// ============================================ + +const CONFIG = { + // Renderer + antialias: true, + pixelRatio: Math.min(window.devicePixelRatio, 2), + + // Camera + fov: 60, + near: 0.1, + far: 1000, + position: new THREE.Vector3(0, 2, 5), + + // Scene + backgroundColor: 0x111111, + + // Controls + enableDamping: true, + dampingFactor: 0.05 +}; + +// ============================================ +// GLOBALS +// ============================================ + +let camera, scene, renderer, controls; +let clock; + +// Add your uniforms here +const uniforms = { + // Example: myColor: uniform(new THREE.Color(0xff0000)) +}; + +// ============================================ +// INITIALIZATION +// ============================================ + +async function init() { + // Clock + clock = new THREE.Clock(); + + // Scene + scene = new THREE.Scene(); + scene.background = new THREE.Color(CONFIG.backgroundColor); + + // Camera + camera = new THREE.PerspectiveCamera( + CONFIG.fov, + window.innerWidth / window.innerHeight, + CONFIG.near, + CONFIG.far + ); + camera.position.copy(CONFIG.position); + + // Renderer + renderer = new THREE.WebGPURenderer({ antialias: CONFIG.antialias }); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setPixelRatio(CONFIG.pixelRatio); + document.body.appendChild(renderer.domElement); + + // Initialize WebGPU + await renderer.init(); + + // Controls + controls = new OrbitControls(camera, renderer.domElement); + controls.enableDamping = CONFIG.enableDamping; + controls.dampingFactor = CONFIG.dampingFactor; + + // Setup scene content + setupLights(); + setupScene(); + + // Optional: Setup post-processing + // setupPostProcessing(); + + // Events + window.addEventListener('resize', onWindowResize); + + // Start animation loop + renderer.setAnimationLoop(animate); +} + +// ============================================ +// SCENE SETUP +// ============================================ + +function setupLights() { + // Ambient light + const ambientLight = new THREE.AmbientLight(0x404040, 0.5); + scene.add(ambientLight); + + // Directional light + const directionalLight = new THREE.DirectionalLight(0xffffff, 1); + directionalLight.position.set(5, 10, 5); + directionalLight.castShadow = true; + scene.add(directionalLight); + + // Add more lights as needed +} + +function setupScene() { + // ======================================== + // ADD YOUR SCENE CONTENT HERE + // ======================================== + + // Example: Create a mesh with TSL material + const geometry = new THREE.BoxGeometry(1, 1, 1); + const material = createExampleMaterial(); + const mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + + // Example: Add a floor + const floorGeometry = new THREE.PlaneGeometry(10, 10); + const floorMaterial = new THREE.MeshStandardNodeMaterial({ + color: 0x333333 + }); + const floor = new THREE.Mesh(floorGeometry, floorMaterial); + floor.rotation.x = -Math.PI / 2; + floor.position.y = -0.5; + scene.add(floor); +} + +function createExampleMaterial() { + const material = new THREE.MeshStandardNodeMaterial(); + + // ======================================== + // CUSTOMIZE YOUR MATERIAL HERE + // ======================================== + + // Example: Animated color + material.colorNode = Fn(() => { + const t = time.mul(0.5).sin().mul(0.5).add(0.5); + return mix(color(0x0066ff), color(0xff6600), t); + })(); + + // Example: PBR properties + material.roughnessNode = float(0.5); + material.metalnessNode = float(0.0); + + // Example: Simple fresnel rim + material.emissiveNode = Fn(() => { + const viewDir = cameraPosition.sub(positionWorld).normalize(); + const fresnel = float(1.0).sub(normalWorld.dot(viewDir).saturate()).pow(3.0); + return color(0x00ffff).mul(fresnel).mul(0.5); + })(); + + return material; +} + +// ============================================ +// POST-PROCESSING (Optional) +// ============================================ + +let postProcessing; + +function setupPostProcessing() { + // Uncomment and customize as needed + + // postProcessing = new THREE.PostProcessing(renderer); + // const scenePass = pass(scene, camera); + // const sceneColor = scenePass.getTextureNode('output'); + // + // // Add effects here + // postProcessing.outputNode = sceneColor; +} + +// ============================================ +// ANIMATION LOOP +// ============================================ + +function animate() { + const delta = clock.getDelta(); + const elapsed = clock.getElapsedTime(); + + // ======================================== + // UPDATE YOUR SCENE HERE + // ======================================== + + // Example: Rotate mesh + const mesh = scene.children.find((child) => child.type === 'Mesh'); + if (mesh) { + mesh.rotation.y += delta * 0.5; + } + + // Update controls + controls.update(); + + // Render + if (postProcessing) { + postProcessing.render(); + } else { + renderer.render(scene, camera); + } +} + +// ============================================ +// EVENT HANDLERS +// ============================================ + +function onWindowResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + renderer.setSize(window.innerWidth, window.innerHeight); +} + +// ============================================ +// START +// ============================================ + +init().catch(console.error); + +// Export for external access if needed +export { scene, camera, renderer, uniforms };