WebGL 3D Object
Use When
- A hero, feature block, or product moment needs one strong 3D object.
- The visual should show real geometry, lighting, highlights, and edges.
- A faceted mesh should float or rotate subtly inside a web layout.
- CSS transforms, SVG illusions, or flat gradients are not enough.
Rules
- Use real 3D geometry:
IcosahedronGeometry, DodecahedronGeometry, BoxGeometry, custom BufferGeometry, or a glTF mesh.
- Use a perspective camera so the object has depth and scale.
- Use PBR material:
MeshStandardMaterial or MeshPhysicalMaterial.
- Tune
metalness, roughness, and emissive to match the brand mood.
- Light the object with at least one directional light plus ambient or hemisphere fill.
- Animate transforms only: subtle rotation, bobbing, or parallax.
- Handle resize and dispose geometry/material/renderer on teardown.
HTML And CSS
<div class="webgl-object-shell">
<canvas class="webgl-object-canvas" data-webgl-3d-object></canvas>
</div>
.webgl-object-shell {
position: relative;
width: min(100%, 720px);
aspect-ratio: 1 / 1;
}
.webgl-object-canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: block;
}
Three.js Object Recipe
import * as THREE from "three";
function initWebGL3DObject(canvas, options = {}) {
if (!canvas) return () => {};
const renderer = new THREE.WebGLRenderer({
canvas,
antialias: true,
alpha: true,
});
renderer.setClearColor(0x000000, 0);
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, options.maxDpr || 1.75));
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = options.exposure || 1.05;
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(38, 1, 0.1, 100);
camera.position.set(0, 0.15, 5.2);
const geometry = new THREE.IcosahedronGeometry(options.radius || 1.35, options.detail || 1);
const material = new THREE.MeshStandardMaterial({
color: options.color || 0x8aa4ff,
metalness: options.metalness ?? 0.48,
roughness: options.roughness ?? 0.34,
emissive: options.emissive || 0x101833,
emissiveIntensity: options.emissiveIntensity ?? 0.22,
flatShading: true,
});
const object = new THREE.Mesh(geometry, material);
object.castShadow = true;
object.receiveShadow = true;
scene.add(object);
const ambient = new THREE.AmbientLight(0xffffff, 0.38);
scene.add(ambient);
const key = new THREE.DirectionalLight(0xffffff, 2.15);
key.position.set(3.4, 4.2, 4.8);
key.castShadow = true;
key.shadow.mapSize.set(1024, 1024);
scene.add(key);
const rim = new THREE.DirectionalLight(options.rimColor || 0x7dd3fc, 0.82);
rim.position.set(-4.2, 1.2, -2.8);
scene.add(rim);
const shadowPlane = new THREE.Mesh(
new THREE.PlaneGeometry(5.2, 5.2),
new THREE.ShadowMaterial({ opacity: 0.18 })
);
shadowPlane.position.set(0, -1.65, 0);
shadowPlane.rotation.x = -Math.PI / 2;
shadowPlane.receiveShadow = true;
scene.add(shadowPlane);
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
let rafId = 0;
function resize() {
const width = Math.max(1, canvas.clientWidth);
const height = Math.max(1, canvas.clientHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, options.maxDpr || 1.75));
renderer.setSize(width, height, false);
camera.aspect = width / height;
camera.updateProjectionMatrix();
}
function render(time = 0) {
const t = time * 0.001;
object.rotation.x = -0.16 + Math.sin(t * 0.45) * 0.06;
object.rotation.y = t * 0.28;
object.rotation.z = Math.sin(t * 0.32) * 0.08;
object.position.y = reduceMotion ? 0 : Math.sin(t * 0.8) * 0.08;
renderer.render(scene, camera);
if (!reduceMotion) rafId = requestAnimationFrame(render);
}
function handleResize() {
cancelAnimationFrame(rafId);
resize();
render();
}
resize();
render();
window.addEventListener("resize", handleResize);
return () => {
cancelAnimationFrame(rafId);
window.removeEventListener("resize", handleResize);
geometry.dispose();
material.dispose();
shadowPlane.geometry.dispose();
shadowPlane.material.dispose();
renderer.dispose();
};
}
const cleanupObject = initWebGL3DObject(
document.querySelector("[data-webgl-3d-object]"),
{
color: 0x8aa4ff,
rimColor: 0x7dd3fc,
metalness: 0.48,
roughness: 0.34,
emissive: 0x101833,
}
);
Material Defaults
- Premium metal:
metalness: 0.45-0.7, roughness: 0.25-0.45.
- Soft ceramic:
metalness: 0.0-0.15, roughness: 0.38-0.62.
- Glow-tinted tech object: low
emissive with emissiveIntensity: 0.12-0.35.
- Faceted object: set
flatShading: true; smooth product object: set it to false.
Lighting Defaults
- Key light: directional, high front-side angle, strongest source.
- Ambient fill: low intensity so shadows stay visible.
- Rim light: brand-tinted or cool light from behind to reveal edges.
- Shadows: enable only when the object needs grounded depth; keep map size moderate.
Motion Defaults
- Rotation: slow, continuous, and secondary to the page content.
- Floating:
0.04 to 0.12 units on Y.
- Reduced motion: render a still frame or only allow direct interaction.
- Avoid camera movement unless the object is the main interaction.
Avoid
- CSS 3D transforms pretending to be WebGL.
- Unlit materials when the ask is real lighting and depth.
- Flat planes with gradients instead of actual geometry.
- Strong bloom or particles that hide the form.
- High DPR, huge shadow maps, or too many lights on mobile.
- Letting the object compete with foreground copy or CTAs.
Quick Checks
- The object has visible form, edges, highlights, and shadows.
- The material uses
metalness, roughness, and optional emissive.
- Directional and ambient lights are both present.
- The camera is perspective, not orthographic by accident.
- Resize does not stretch the object.
- Geometry, material, event listeners, RAF, and renderer are cleaned up.
1---2name: webgl-3d-object3description: Create a real 3D WebGL object with geometric mesh depth, physically based material, directional and ambient lighting, perspective camera, subtle rotation, and floating motion. Use when a page needs a faceted 3D hero object or product-like visual with real lighting instead of CSS transform tricks.4---56# WebGL 3D Object78## Use When9- A hero, feature block, or product moment needs one strong 3D object.10- The visual should show real geometry, lighting, highlights, and edges.11- A faceted mesh should float or rotate subtly inside a web layout.12- CSS transforms, SVG illusions, or flat gradients are not enough.1314## Rules151. Use real 3D geometry: `IcosahedronGeometry`, `DodecahedronGeometry`, `BoxGeometry`, custom `BufferGeometry`, or a glTF mesh.162. Use a perspective camera so the object has depth and scale.173. Use PBR material: `MeshStandardMaterial` or `MeshPhysicalMaterial`.184. Tune `metalness`, `roughness`, and `emissive` to match the brand mood.195. Light the object with at least one directional light plus ambient or hemisphere fill.206. Animate transforms only: subtle rotation, bobbing, or parallax.217. Handle resize and dispose geometry/material/renderer on teardown.2223## HTML And CSS2425```html26<div class="webgl-object-shell">27 <canvas class="webgl-object-canvas" data-webgl-3d-object></canvas>28</div>29```3031```css32.webgl-object-shell {33 position: relative;34 width: min(100%, 720px);35 aspect-ratio: 1 / 1;36}3738.webgl-object-canvas {39 position: absolute;40 inset: 0;41 width: 100%;42 height: 100%;43 display: block;44}45```4647## Three.js Object Recipe4849```js50import * as THREE from "three";5152function initWebGL3DObject(canvas, options = {}) {53 if (!canvas) return () => {};5455 const renderer = new THREE.WebGLRenderer({56 canvas,57 antialias: true,58 alpha: true,59 });60 renderer.setClearColor(0x000000, 0);61 renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, options.maxDpr || 1.75));62 renderer.outputColorSpace = THREE.SRGBColorSpace;63 renderer.toneMapping = THREE.ACESFilmicToneMapping;64 renderer.toneMappingExposure = options.exposure || 1.05;65 renderer.shadowMap.enabled = true;66 renderer.shadowMap.type = THREE.PCFSoftShadowMap;6768 const scene = new THREE.Scene();69 const camera = new THREE.PerspectiveCamera(38, 1, 0.1, 100);70 camera.position.set(0, 0.15, 5.2);7172 const geometry = new THREE.IcosahedronGeometry(options.radius || 1.35, options.detail || 1);73 const material = new THREE.MeshStandardMaterial({74 color: options.color || 0x8aa4ff,75 metalness: options.metalness ?? 0.48,76 roughness: options.roughness ?? 0.34,77 emissive: options.emissive || 0x101833,78 emissiveIntensity: options.emissiveIntensity ?? 0.22,79 flatShading: true,80 });8182 const object = new THREE.Mesh(geometry, material);83 object.castShadow = true;84 object.receiveShadow = true;85 scene.add(object);8687 const ambient = new THREE.AmbientLight(0xffffff, 0.38);88 scene.add(ambient);8990 const key = new THREE.DirectionalLight(0xffffff, 2.15);91 key.position.set(3.4, 4.2, 4.8);92 key.castShadow = true;93 key.shadow.mapSize.set(1024, 1024);94 scene.add(key);9596 const rim = new THREE.DirectionalLight(options.rimColor || 0x7dd3fc, 0.82);97 rim.position.set(-4.2, 1.2, -2.8);98 scene.add(rim);99100 const shadowPlane = new THREE.Mesh(101 new THREE.PlaneGeometry(5.2, 5.2),102 new THREE.ShadowMaterial({ opacity: 0.18 })103 );104 shadowPlane.position.set(0, -1.65, 0);105 shadowPlane.rotation.x = -Math.PI / 2;106 shadowPlane.receiveShadow = true;107 scene.add(shadowPlane);108109 const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;110 let rafId = 0;111112 function resize() {113 const width = Math.max(1, canvas.clientWidth);114 const height = Math.max(1, canvas.clientHeight);115 renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, options.maxDpr || 1.75));116 renderer.setSize(width, height, false);117 camera.aspect = width / height;118 camera.updateProjectionMatrix();119 }120121 function render(time = 0) {122 const t = time * 0.001;123 object.rotation.x = -0.16 + Math.sin(t * 0.45) * 0.06;124 object.rotation.y = t * 0.28;125 object.rotation.z = Math.sin(t * 0.32) * 0.08;126 object.position.y = reduceMotion ? 0 : Math.sin(t * 0.8) * 0.08;127128 renderer.render(scene, camera);129 if (!reduceMotion) rafId = requestAnimationFrame(render);130 }131132 function handleResize() {133 cancelAnimationFrame(rafId);134 resize();135 render();136 }137138 resize();139 render();140 window.addEventListener("resize", handleResize);141142 return () => {143 cancelAnimationFrame(rafId);144 window.removeEventListener("resize", handleResize);145 geometry.dispose();146 material.dispose();147 shadowPlane.geometry.dispose();148 shadowPlane.material.dispose();149 renderer.dispose();150 };151}152153const cleanupObject = initWebGL3DObject(154 document.querySelector("[data-webgl-3d-object]"),155 {156 color: 0x8aa4ff,157 rimColor: 0x7dd3fc,158 metalness: 0.48,159 roughness: 0.34,160 emissive: 0x101833,161 }162);163```164165## Material Defaults166- Premium metal: `metalness: 0.45-0.7`, `roughness: 0.25-0.45`.167- Soft ceramic: `metalness: 0.0-0.15`, `roughness: 0.38-0.62`.168- Glow-tinted tech object: low `emissive` with `emissiveIntensity: 0.12-0.35`.169- Faceted object: set `flatShading: true`; smooth product object: set it to `false`.170171## Lighting Defaults172- Key light: directional, high front-side angle, strongest source.173- Ambient fill: low intensity so shadows stay visible.174- Rim light: brand-tinted or cool light from behind to reveal edges.175- Shadows: enable only when the object needs grounded depth; keep map size moderate.176177## Motion Defaults178- Rotation: slow, continuous, and secondary to the page content.179- Floating: `0.04` to `0.12` units on Y.180- Reduced motion: render a still frame or only allow direct interaction.181- Avoid camera movement unless the object is the main interaction.182183## Avoid184- CSS 3D transforms pretending to be WebGL.185- Unlit materials when the ask is real lighting and depth.186- Flat planes with gradients instead of actual geometry.187- Strong bloom or particles that hide the form.188- High DPR, huge shadow maps, or too many lights on mobile.189- Letting the object compete with foreground copy or CTAs.190191## Quick Checks192- The object has visible form, edges, highlights, and shadows.193- The material uses `metalness`, `roughness`, and optional `emissive`.194- Directional and ambient lights are both present.195- The camera is perspective, not orthographic by accident.196- Resize does not stretch the object.197- Geometry, material, event listeners, RAF, and renderer are cleaned up.