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---5
6# WebGL 3D Object
7
8## Use When
9- 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.
13
14## Rules
151. 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.
22
23## HTML And CSS
24
25```html
26<div class="webgl-object-shell">
27 <canvas class="webgl-object-canvas" data-webgl-3d-object></canvas>
28</div>
29```
30
31```css
32.webgl-object-shell {
33 position: relative;
34 width: min(100%, 720px);
35 aspect-ratio: 1 / 1;
36}
37
38.webgl-object-canvas {
39 position: absolute;
40 inset: 0;
41 width: 100%;
42 height: 100%;
43 display: block;
44}
45```
46
47## Three.js Object Recipe
48
49```js
50import * as THREE from "three";
51
52function initWebGL3DObject(canvas, options = {}) {
53 if (!canvas) return () => {};
54
55 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;
67
68 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);
71
72 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 });
81
82 const object = new THREE.Mesh(geometry, material);
83 object.castShadow = true;
84 object.receiveShadow = true;
85 scene.add(object);
86
87 const ambient = new THREE.AmbientLight(0xffffff, 0.38);
88 scene.add(ambient);
89
90 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);
95
96 const rim = new THREE.DirectionalLight(options.rimColor || 0x7dd3fc, 0.82);
97 rim.position.set(-4.2, 1.2, -2.8);
98 scene.add(rim);
99
100 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);
108
109 const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
110 let rafId = 0;
111
112 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 }
120
121 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;
127
128 renderer.render(scene, camera);
129 if (!reduceMotion) rafId = requestAnimationFrame(render);
130 }
131
132 function handleResize() {
133 cancelAnimationFrame(rafId);
134 resize();
135 render();
136 }
137
138 resize();
139 render();
140 window.addEventListener("resize", handleResize);
141
142 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}
152
153const 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```
164
165## Material Defaults
166- 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`.
170
171## Lighting Defaults
172- 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.
176
177## Motion Defaults
178- 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.
182
183## Avoid
184- 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.
190
191## Quick Checks
192- 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.