1. Minimal Boilerplate Scene
scene-setup.js
import * as THREE from 'three';
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2)); // avoid oversampling on Retina
renderer.setSize(innerWidth, innerHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.1;
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 0.1, 1000);
camera.position.set(0, 5, 15);
// Resize handler
const onResize = () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
};
window.addEventListener('resize', onResize);
2. rAF Loop with delta time
let prevTime = performance.now();
function animate(time) {
requestAnimationFrame(animate);
const dt = Math.min((time - prevTime) / 1000, 0.05);
prevTime = time;
update(dt);
renderer.render(scene, camera);
}
requestAnimationFrame(animate);
3. OrbitControls (lazy import)
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.minDistance = 2;
controls.maxDistance = 200;
// In animate(): controls.update();
4. InstancedMesh for Particles
const N = 10000;
const geo = new THREE.SphereGeometry(0.05, 4, 4);
const mat = new THREE.MeshBasicMaterial({ color: 0x00ffff });
const mesh = new THREE.InstancedMesh(geo, mat, N);
mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
scene.add(mesh);
const dummy = new THREE.Object3D();
// Update each frame:
for (let i = 0; i < N; i++) {
dummy.position.set(particles[i].x, particles[i].y, particles[i].z);
dummy.updateMatrix();
mesh.setMatrixAt(i, dummy.matrix);
}
mesh.instanceMatrix.needsUpdate = true;
5. Custom ShaderMaterial
const mat = new THREE.ShaderMaterial({
uniforms: {
uTime: { value: 0 },
uColor: { value: new THREE.Color('#00ffcc') },
},
vertexShader: /* glsl */`
uniform float uTime;
varying vec2 vUv;
void main() {
vUv = uv;
vec3 pos = position;
pos.y += sin(pos.x * 4.0 + uTime) * 0.2;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
`,
fragmentShader: /* glsl */`
uniform vec3 uColor;
varying vec2 vUv;
void main() {
gl_FragColor = vec4(uColor * (0.5 + 0.5 * vUv.y), 1.0);
}
`,
side: THREE.DoubleSide,
});
// In animate: mat.uniforms.uTime.value = time * 0.001;
6. Ping-Pong FBO (GPGPU)
const opts = { type: THREE.FloatType, depthBuffer: false };
let rtA = new THREE.WebGLRenderTarget(W, H, opts);
let rtB = new THREE.WebGLRenderTarget(W, H, opts);
function pingPong() {
computeMat.uniforms.uState.value = rtA.texture;
renderer.setRenderTarget(rtB);
renderer.render(computeScene, computeCamera);
renderer.setRenderTarget(null);
[rtA, rtB] = [rtB, rtA]; // swap
}
// Read back data (slow — avoid if possible):
// renderer.readRenderTargetPixels(rtA, 0, 0, W, H, buffer);
7. Post-processing Bloom
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 { OutputPass } from 'three/addons/postprocessing/OutputPass.js';
renderer.outputColorSpace = THREE.LinearSRGBColorSpace; // composer handles output
const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
composer.addPass(new UnrealBloomPass(
new THREE.Vector2(innerWidth, innerHeight),
1.2, // strength
0.4, // radius
0.85 // threshold
));
composer.addPass(new OutputPass());
// In animate: composer.render() instead of renderer.render()
8. Dispose Properly
// Preventing memory leaks when removing objects
function disposeMesh(mesh) {
mesh.geometry?.dispose();
if (Array.isArray(mesh.material)) mesh.material.forEach(m => m.dispose());
else mesh.material?.dispose();
scene.remove(mesh);
}
// Renderer cleanup on unmount
renderer.dispose();
renderer.domElement.remove();