A rock made from a noise sphere still looks like a noise sphere. This one starts as an icosphere, gets stretched and cracked, then loses a few chunks to random planes. The shading is a measurement of how concave each vertex is.
This is one HTML file on Three.js 0.186.1 and WebGPU. The sculpt runs on the CPU. The colour is TSL. The steps follow Alen Vejzovic's rock workflow: block the shape out, sculpt it, break the silhouette, then paint the crevices. I use AI to write a lot of the node graph. How hard the underside sits in the sand, and that the moss stays off the sharp edges, come from 25+ years of JavaScript, game development, modding and level design.
The sculpt
The mesh is an icosphere with its vertices welded, so neighbouring triangles share a point and the normals can stay smooth. Three's own icosahedron keeps a separate vertex on every face, and the lighting then facets a surface you wanted worn.
Each vertex is pushed along its direction. The large octaves set the silhouette, a tighter pass puts the medium forms on, and ridged noise cuts the cracks. A last bit of high-frequency noise is the grain. The radii are scaled on x, y and z, so the rock is never a ball. After that, a handful of random planes clip anything that sticks past them. That is the step that stops it reading as a lumpy sphere. The underside is squashed, so it sits in the ground.
The cavity
Once the normals exist, each vertex looks at the average position of its neighbours. The offset from the vertex to that average, dotted with the normal, is the cavity. Positive is a crevice. Negative is a worn edge. One smoothing pass keeps it from sparkling, and it is stored as a vertex attribute named cavity.
The colour
One MeshStandardNodeMaterial is shared by the hero and the pebbles. World-space noise mixes two greys and a warmer patch, then a fine grain multiplies over the top. The cavity darkens the crevices and lightens the edges. Moss shows up where the normal faces up, in patches, and it fades on the convex edges. Dust gathers where world y is near the ground. Roughness drops a little on the worn edges, which is the polish.
Around it
Fourteen smaller rocks use the same builder, with fewer cuts, scattered in a ring. The grass is an InstancedMesh of cones. The colour drifts from dry gold to green across the meadow, the tips bleach, and a position node bends each blade by the square of its height, so the root stays put. New rock rolls another seed through the same function.
The orbit, and the mesh you can actually build
Drag right and the rock turns with your hand. The camera uses the same spherical mapping as OrbitControls: sine of theta on x, cosine of theta on z, and theta decreases as the pointer moves right. The first version had those swapped, so left and right went backwards.
The file I started from asked for 64 subdivisions on the hero and 10 on every pebble. An icosphere multiplies its faces by four at each step, so those numbers never finish. This copy uses 5 on the hero and 3 on the pebbles. Same sculpt, a mesh the page can carve.
The frame is that file. The line in the corner is the seed. While it says it is carving, the icosphere is still being displaced on the CPU.
WebGPU only, on Three.js 0.186.1, with no WebGL fallback. If the browser has no WebGPU, the page stops. Open it through a server, because file:// will not load the modules. php -S localhost:8000 or npx serve in the folder is enough.
- Open it on its own
rock.html, the project-
SKILL.md
If you add another cut, keep it planar and random. A radial push on its own will walk you back to the lumpy sphere. And if the drag feels backwards again, check that x is sine of theta and z is cosine, with theta decreasing as the pointer moves right.