Project Ref // 10

ABOUT PROCEDURAL WIREFRAME INSECTS

BACK

When considering the implementation of 3D objects in the browser, many developers immediately gravitate towards importing pre-compiled .gltf or .obj files generated in external software like Blender. While this approach is standard and often necessary for high-fidelity assets, there remains immense creative potential in constructing complex, organic structures programmatically using purely primitive geometries.

In this article, we will examine how to build intricate, organically structured insects entirely from scratch using mathematical placement, THREE.Group nested hierarchies, and a basic selection of three-dimensional primitives directly within the browser natively.


Model I: The Dragonfly

Our first subject implements a slender, dragonfly-esque form. Notice the subtle rotation loops and the procedural background canvas supplementing the primary WebGL canvas structure.

Architectural Breakdown

The secret to building such a structurally defined entity involves decoupling the insect into logical anatomical components (the head, thorax, abdomen, legs, and wings) and treating each as an independent series of mathematical loops.

To maintain a consistent, stylised aesthetic, we strictly enforce a wireframe material (THREE.MeshBasicMaterial({ wireframe: true })) cast across a limited, icy cyan and deep navy colour palette.

Grouping and the Head Assembly

Everything begins by instantiating a central THREE.Group(), which acts as the coordinate container for our insect. This ensures that any rotational or positional adjustments applied in the animation loop automatically cascade down to every internal limb.

const group = new THREE.Group();

// ── HEAD ── small, round, forward-facing
const headGeo = new THREE.SphereGeometry(0.22, 24, 24);
const headMesh = new THREE.Mesh(headGeo, mat(colors.dark1));
headMesh.position.set(0, 0.08, 1.35);
headMesh.scale.set(1, 0.95, 1);
group.add(headMesh);

The Abdomen: Segmented Loops

An insect’s abdomen is inherently segmented. Instead of plotting individual spheres, we can utilise a simple for loop to extrude interconnected cylinders extending backward along the Z-axis.

// ── ABDOMEN ── long, segmented, tapered
const abdSegs = 8;
for (let i = 0; i < abdSegs; i++) {
    const t = i / (abdSegs - 1);
    
    // Calculate the tapering radius as it reaches the tail
    const r = 0.17 * (1 - t * 0.72);
    const segGeo = new THREE.CylinderGeometry(r * 0.88, r, 0.32, 14, 2);
    
    // Continually alternate the materials to build segmented depth
    const isAlt = i % 2 === 0;
    const seg = new THREE.Mesh(segGeo, mat(isAlt ? colors.primary : colors.dark1));
    
    // Apply a slight downward curve using quadratic logic
    const z = -0.12 - i * 0.34;
    const y = 0.08 - i * i * 0.012;
    seg.position.set(0, y, z);
    seg.rotation.x = Math.PI * 0.5 + i * 0.018;
    
    group.add(seg);
}

Notice how r = 0.17 * (1 - t * 0.72) mathematically dictates the narrowing of the tail, guaranteeing a smooth and organic descent without needing to manually define eight separate bounding boxes.

Complete Dragonfly Source Code

If you wish to experiment with this programmatically generated dragonfly in your own local environment, here is the complete, self-contained HTML payload used for the demonstration above.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Insect</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { background: #000; overflow: hidden; height: 100vh; }
        #bgCanvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; }
        #animationCanvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 2; }
    </style>
</head>
<body>
    <canvas id="bgCanvas"></canvas>
    <canvas id="animationCanvas"></canvas>

    <script>
        const colors = {
            primary: 0x4db8d4,   // light blue
            dark1:   0x0d2a3a,   // deep navy
            dark2:   0x0a1e2a,
            dark3:   0x000000,
            light:   0xaee8f5    // pale ice blue
        };

        // Background pixel grid
        const bgCanvas = document.getElementById("bgCanvas");
        const bgCtx = bgCanvas.getContext("2d");
        const pixelSize = 8;
        let cols, rows, pixels = [];

        function initBackground() {
            bgCanvas.width = window.innerWidth;
            bgCanvas.height = window.innerHeight;
            cols = Math.ceil(bgCanvas.width / pixelSize);
            rows = Math.ceil(bgCanvas.height / pixelSize);
            pixels = [];
            for (let i = 0; i < cols * rows; i++) {
                pixels.push({ brightness: Math.random() * 0.3, speed: 0.001 + Math.random() * 0.003 });
            }
        }

        function animateBackground() {
            for (let y = 0; y < rows; y++) {
                for (let x = 0; x < cols; x++) {
                    const pixel = pixels[y * cols + x];
                    pixel.brightness += pixel.speed;
                    if (pixel.brightness > 0.4) { pixel.brightness = 0; }
                    bgCtx.fillStyle = `rgb(${Math.floor(pixel.brightness*10)},${Math.floor(pixel.brightness*30)},${Math.floor(pixel.brightness*50)})`;
                    bgCtx.fillRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize);
                }
            }
            requestAnimationFrame(animateBackground);
        }

        // Three.js
        const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('animationCanvas'), antialias: true, alpha: true });
        let scene, camera, insect;

        function updateSize() {
            renderer.setSize(window.innerWidth, window.innerHeight);
            renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
        }

        function updateCamera() {
            const w = window.innerWidth;
            if (w < 480) { camera.position.set(0, 0.5, 7); camera.fov = 60; }
            else if (w < 768) { camera.position.set(0, 0.3, 6.5); camera.fov = 55; }
            else { camera.position.set(0, 0.3, 5.5); camera.fov = 50; }
            camera.updateProjectionMatrix();
        }

        function mat(color) {
            return new THREE.MeshBasicMaterial({ color, wireframe: true });
        }

        function createInsect() {
            const group = new THREE.Group();

            // ── HEAD ── small, round, forward-facing
            const headGeo = new THREE.SphereGeometry(0.22, 24, 24);
            const headMesh = new THREE.Mesh(headGeo, mat(colors.dark1));
            headMesh.position.set(0, 0.08, 1.35);
            headMesh.scale.set(1, 0.95, 1);
            group.add(headMesh);

            // Large compound eyes wrapping the sides of the head
            const eyeGeo = new THREE.SphereGeometry(0.18, 20, 20, 0, Math.PI);
            [-1, 1].forEach(side => {
                const eye = new THREE.Mesh(eyeGeo, mat(colors.light));
                eye.position.set(side * 0.2, 0.1, 1.38);
                eye.rotation.y = side * Math.PI * 0.35;
                eye.scale.set(1, 0.9, 0.7);
                group.add(eye);
            });

            // Thin filiform antennae — long, tapering, natural curve
            [-1, 1].forEach(side => {
                const pts = [];
                const segs = 14;
                for (let i = 0; i < segs; i++) {
                    const t = i / (segs - 1);
                    pts.push(new THREE.Vector3(
                        side * (0.12 + t * 0.35),
                        0.18 + t * 0.55 - t * t * 0.15,
                        1.35 + t * 0.18 - t * t * 0.3
                    ));
                }
                const curve = new THREE.CatmullRomCurve3(pts);
                const antGeo = new THREE.TubeGeometry(curve, 12, 0.013 - 0, 6, false);
                // Taper: rebuild with varying radius
                const antTaper = new THREE.TubeGeometry(curve, 12, 0.008, 6, false);
                group.add(new THREE.Mesh(antTaper, mat(colors.dark1)));
            });

            // ── THORAX ── slightly flattened, with dorsal hump (mesothorax)
            const thoraxGeo = new THREE.SphereGeometry(0.3, 28, 28);
            const thorax = new THREE.Mesh(thoraxGeo, mat(colors.primary));
            thorax.position.set(0, 0.12, 0.72);
            thorax.scale.set(0.95, 1.05, 1.5);
            group.add(thorax);

            // Dorsal scutellum bump
            const scutGeo = new THREE.SphereGeometry(0.14, 16, 16);
            const scut = new THREE.Mesh(scutGeo, mat(colors.primary));
            scut.position.set(0, 0.33, 0.55);
            scut.scale.set(0.9, 0.7, 1.1);
            group.add(scut);

            // Prothorax neck ring connecting head to thorax
            const protoGeo = new THREE.CylinderGeometry(0.12, 0.18, 0.18, 16);
            const proto = new THREE.Mesh(protoGeo, mat(colors.dark1));
            proto.position.set(0, 0.1, 1.05);
            proto.rotation.x = Math.PI * 0.5;
            group.add(proto);

            // ── ABDOMEN ── long, segmented, tapered — dragonfly/damselfly style
            const abdSegs = 8;
            for (let i = 0; i < abdSegs; i++) {
                const t = i / (abdSegs - 1);
                const r = 0.17 * (1 - t * 0.72);
                const segGeo = new THREE.CylinderGeometry(r * 0.88, r, 0.32, 14, 2);
                const isAlt = i % 2 === 0;
                const seg = new THREE.Mesh(segGeo, mat(isAlt ? colors.primary : colors.dark1));
                // Slight downward curve in abdomen
                const z = -0.12 - i * 0.34;
                const y = 0.08 - i * i * 0.012;
                seg.position.set(0, y, z);
                seg.rotation.x = Math.PI * 0.5 + i * 0.018;
                group.add(seg);

                // Lateral cerci nubs on each segment
                if (i > 1 && i < abdSegs - 1) {
                    [-1, 1].forEach(side => {
                        const nubGeo = new THREE.SphereGeometry(r * 0.4, 8, 8);
                        const nub = new THREE.Mesh(nubGeo, mat(colors.light));
                        nub.position.set(side * r * 1.1, y, z);
                        group.add(nub);
                    });
                }
            }

            // Terminal cerci (tail appendages)
            [-1, 1].forEach(side => {
                const cerciPts = [
                    new THREE.Vector3(side * 0.04, 0.08 - 7*7*0.012, -0.12 - 7*0.34),
                    new THREE.Vector3(side * 0.1, 0.02 - 7*7*0.012 - 0.04, -0.12 - 7*0.34 - 0.2),
                    new THREE.Vector3(side * 0.13, -0.1, -2.75)
                ];
                const cerciCurve = new THREE.CatmullRomCurve3(cerciPts);
                const cerciGeo = new THREE.TubeGeometry(cerciCurve, 8, 0.018, 5, false);
                group.add(new THREE.Mesh(cerciGeo, mat(colors.light)));
            });

            // ── LEGS ── 6 legs, slender & jointed, naturalistic placement
            // Leg configs: [side, thoraxZ, coxaAngleZ, spread, dropY]
            const legDefs = [
                { side: -1, z: 0.88, az: 0.55, sp: 0.9, drop: -0.18 },
                { side:  1, z: 0.88, az:-0.55, sp: 0.9, drop: -0.18 },
                { side: -1, z: 0.68, az: 0.75, sp: 1.05, drop: -0.22 },
                { side:  1, z: 0.68, az:-0.75, sp: 1.05, drop: -0.22 },
                { side: -1, z: 0.48, az: 0.95, sp: 1.12, drop: -0.26 },
                { side:  1, z: 0.48, az:-0.95, sp: 1.12, drop: -0.26 },
            ];

            legDefs.forEach(def => {
                const baseX = def.side * 0.28;
                const baseY = 0.0;
                const baseZ = def.z;

                // Coxa — short, stubby
                const coxGeo = new THREE.CylinderGeometry(0.028, 0.032, 0.1, 8);
                const cox = new THREE.Mesh(coxGeo, mat(colors.dark1));
                cox.position.set(baseX, baseY, baseZ);
                cox.rotation.z = def.az;
                group.add(cox);

                // Femur — longer
                const femX = baseX + def.side * 0.22;
                const femY = baseY + def.drop * 0.4;
                const femGeo = new THREE.CylinderGeometry(0.028, 0.022, 0.42, 8);
                const fem = new THREE.Mesh(femGeo, mat(colors.primary));
                fem.position.set(femX, femY, baseZ);
                fem.rotation.z = def.az * 0.55;
                fem.rotation.y = def.side * 0.25;
                group.add(fem);

                // Tibia — thinner, angled
                const tibX = femX + def.side * 0.26 * def.sp;
                const tibY = femY + def.drop * 0.7;
                const tibGeo = new THREE.CylinderGeometry(0.018, 0.013, 0.46, 7);
                const tib = new THREE.Mesh(tibGeo, mat(colors.light));
                tib.position.set(tibX, tibY, baseZ + 0.02);
                tib.rotation.z = def.az * 0.25 + def.side * 0.6;
                tib.rotation.y = def.side * 0.2;
                group.add(tib);

                // Tarsus — 3 fine segments, reaching down
                for (let t = 0; t < 3; t++) {
                    const tarX = tibX + def.side * (0.08 + t * 0.06);
                    const tarY = tibY + def.drop * 0.55 - t * 0.06;
                    const tarGeo = new THREE.CylinderGeometry(0.011, 0.008, 0.1, 5);
                    const tar = new THREE.Mesh(tarGeo, mat(colors.dark1));
                    tar.position.set(tarX, tarY, baseZ + t * 0.02);
                    tar.rotation.z = def.az * 0.1 + def.side * 0.9;
                    group.add(tar);
                }
            });

            // ── WINGS ── 4 wings — 2 per side, long narrow dragonfly style
            // Wings are built extending outward from the root; left wings are mirrored via scale.x = -1
            const wingDefs = [
                // Right forewing
                { side:  1, fore: true,  x:  0.3,  y: 0.38, z: 0.75, rx: -0.08, ry: -0.18, rz:  0.05 },
                // Right hindwing
                { side:  1, fore: false, x:  0.32, y: 0.3,  z: 0.42, rx: -0.12, ry: -0.28, rz:  0.08 },
                // Left forewing  (mirrored)
                { side: -1, fore: true,  x: -0.3,  y: 0.38, z: 0.75, rx: -0.08, ry:  0.18, rz: -0.05 },
                // Left hindwing  (mirrored)
                { side: -1, fore: false, x: -0.32, y: 0.3,  z: 0.42, rx: -0.12, ry:  0.28, rz: -0.08 },
            ];

            wingDefs.forEach(def => {
                const len = def.fore ? 1.85 : 1.55;
                const wid = def.fore ? 0.42 : 0.38;

                // Wing membrane shape — elongated, narrowing at tip, always extends in +X
                const wShape = new THREE.Shape();
                wShape.moveTo(0, 0);
                wShape.bezierCurveTo(len * 0.25, wid * 0.6, len * 0.55, wid, len * 0.75, wid * 0.85);
                wShape.bezierCurveTo(len * 0.9, wid * 0.65, len, wid * 0.2, len, 0);
                wShape.bezierCurveTo(len * 0.9, -wid * 0.18, len * 0.5, -wid * 0.25, len * 0.2, -wid * 0.1);
                wShape.bezierCurveTo(len * 0.05, -wid * 0.05, 0, 0, 0, 0);

                const wGeo = new THREE.ShapeGeometry(wShape, 8);
                const wing = new THREE.Mesh(wGeo, new THREE.MeshBasicMaterial({ color: colors.light, wireframe: true, side: THREE.DoubleSide, transparent: true, opacity: 0.85 }));
                wing.position.set(def.x, def.y, def.z);
                wing.rotation.set(def.rx, def.ry, def.rz);
                // Mirror left wings so they extend leftward
                if (def.side === -1) wing.scale.x = -1;
                group.add(wing);

                // Wing venation
                const veinData = [
                    { p0: [0,0,0],            p1: [len*0.5, 0.02, 0],      p2: [len, 0, 0] },
                    { p0: [0,0,0],            p1: [len*0.4, wid*0.4, 0],   p2: [len*0.8, wid*0.6, 0] },
                    { p0: [len*0.1,wid*0.08,0], p1: [len*0.5, wid*0.55, 0], p2: [len*0.72, wid*0.78, 0] },
                    { p0: [len*0.2, 0.01, 0], p1: [len*0.22, wid*0.25, 0], p2: [len*0.25, wid*0.48, 0] },
                    { p0: [len*0.42, 0.01, 0],p1: [len*0.44, wid*0.3, 0],  p2: [len*0.47, wid*0.58, 0] },
                    { p0: [len*0.62, 0.01, 0],p1: [len*0.63, wid*0.25, 0], p2: [len*0.65, wid*0.45, 0] },
                    { p0: [len*0.78,wid*0.05,0],p1:[len*0.88,wid*0.08,0],  p2: [len*0.96, wid*0.05, 0] },
                ];

                veinData.forEach(v => {
                    const vc = new THREE.QuadraticBezierCurve3(
                        new THREE.Vector3(...v.p0),
                        new THREE.Vector3(...v.p1),
                        new THREE.Vector3(...v.p2)
                    );
                    const vGeo = new THREE.TubeGeometry(vc, 8, 0.006, 4, false);
                    const vMesh = new THREE.Mesh(vGeo, mat(colors.primary));
                    vMesh.position.set(def.x, def.y, def.z);
                    vMesh.rotation.set(def.rx, def.ry, def.rz);
                    if (def.side === -1) vMesh.scale.x = -1;
                    group.add(vMesh);
                });
            });

            // ── SPIRACLES ── small oval breathing pores along thorax/abdomen sides
            for (let i = 0; i < 5; i++) {
                [-1, 1].forEach(side => {
                    const spGeo = new THREE.TorusGeometry(0.025, 0.008, 6, 12);
                    const sp = new THREE.Mesh(spGeo, mat(colors.light));
                    sp.position.set(side * 0.19, 0.05 - i * i * 0.01, 0.35 - i * 0.34);
                    sp.rotation.y = side * Math.PI * 0.4;
                    group.add(sp);
                });
            }

            return group;
        }

        function init() {
            initBackground();
            animateBackground();
            updateSize();

            scene = new THREE.Scene();
            scene.background = new THREE.Color(colors.dark3);

            camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 1000);
            updateCamera();

            insect = createInsect();
            scene.add(insect);

            const amb = new THREE.AmbientLight(colors.light, 0.5);
            scene.add(amb);
            const pl1 = new THREE.PointLight(colors.primary, 1.5, 100);
            pl1.position.set(5, 5, 5); scene.add(pl1);
            const pl2 = new THREE.PointLight(colors.light, 0.8, 100);
            pl2.position.set(-5, -3, 3); scene.add(pl2);
            const pl3 = new THREE.PointLight(colors.primary, 0.6, 100);
            pl3.position.set(0, 2, -8); scene.add(pl3);

            animate();
        }

        function animate() {
            requestAnimationFrame(animate);
            const t = Date.now();
            if (insect) {
                insect.rotation.y += 0.004;
                // Gentle hovering bob
                insect.position.y = Math.sin(t * 0.0015) * 0.09;
                // Subtle pitch
                insect.rotation.x = Math.sin(t * 0.001) * 0.05;
                // Slight roll sway
                insect.rotation.z = Math.sin(t * 0.0018) * 0.018;
            }
            renderer.render(scene, camera);
        }

        window.addEventListener('resize', () => {
            updateSize();
            initBackground();
            if (camera) { camera.aspect = window.innerWidth / window.innerHeight; updateCamera(); }
        });

        window.addEventListener('load', init);
    </script>
</body>
</html>

Model II: The Spider

Our second implementation expands on these techniques to model a much broader arachnid. Spiders possess drastically different anatomies (cephalothorax and abdomen instead of a tripartite body), requiring an altered approach to procedural generation.

Constructing the Aracnid

While the dragonfly relies heavily on CylinderGeometry, the spider requires complex, elongated, multi-jointed limbs and curved mandibles.

CatmullRomCurve3 for Articulated Limbs

Traditional geometries are insufficient for organic, curving lines passing through multiple joints. To solve this, we rely intensely on THREE.CatmullRomCurve3. This allows us to define an array of THREE.Vector3 points and interpolate a sweeping path across them—perfect for things like the pedipalps and chelicerae.

We encapsulate this logic into a simple helper function:

function tube(p0, p1, radius, color, mid) {
    const pts = mid ? [p0, mid, p1] : [p0, p1];
    const curve = new THREE.CatmullRomCurve3(pts);
    return new THREE.Mesh(new THREE.TubeGeometry(curve, 8, radius, 6, false), mat(color));
}

Fanning the Eight Legs

The true complexity of the spider is rendering eight mathematically distinct legs that sprout radially from the cephalothorax, extend outward, and drop downwards at the tibial joint. We accomplish this by mapping an arcing fan angle and programmatically drawing interconnected tube() meshes for the Coxa, Femur, Tibia, Metatarsus, and Tarsus.

// Leg pairs map out the core attachment Z-index and outward fan angle
const pairs = [
    { attachZ:  0.58, fanAngle: Math.PI * 0.20 }, // Pair 1: Sweeps forward
    { attachZ:  0.30, fanAngle: Math.PI * 0.38 }, // Pair 2
    { attachZ:  0.04, fanAngle: Math.PI * 0.62 }, // Pair 3
    { attachZ: -0.20, fanAngle: Math.PI * 0.80 }, // Pair 4: Sweeps backward
];

pairs.forEach((pair, pi) => {
    [-1, 1].forEach(side => {
        // Calculate the outward and forward unit vectors using basic trigonometry
        const fa = pair.fanAngle;
        const outX =  side * Math.sin(fa);
        const outZ = -Math.cos(fa);

        // ... Calculate vector joints linking the Coxa sequentially down to the Tarsus
    });
});

These techniques demonstrate the significant flexibility built directly into Three.js primitives. By applying a combination of modular geometry helpers and procedural iteration loops, incredibly complex anatomical forms can be orchestrated without ever leaving the code editor.


Complete Spider Source Code

If you wish to experiment with the arachnid model programmatically in your own local environment, here is the complete, self-contained HTML payload used for the spider demonstration above.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Aether — Spider</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { background: #000; overflow: hidden; height: 100vh; }
        #bgCanvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; }
        #animationCanvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 2; }
    </style>
</head>
<body>
    <canvas id="bgCanvas"></canvas>
    <canvas id="animationCanvas"></canvas>

    <script>
        const colors = {
            primary: 0x4db8d4,
            dark1:   0x0d2a3a,
            dark3:   0x000000,
            light:   0xaee8f5
        };

        // ── Background ──
        const bgCanvas = document.getElementById('bgCanvas');
        const bgCtx = bgCanvas.getContext('2d');
        const pixelSize = 8;
        let cols, rows, pixels = [];

        function initBackground() {
            bgCanvas.width = window.innerWidth;
            bgCanvas.height = window.innerHeight;
            cols = Math.ceil(bgCanvas.width / pixelSize);
            rows = Math.ceil(bgCanvas.height / pixelSize);
            pixels = [];
            for (let i = 0; i < cols * rows; i++)
                pixels.push({ brightness: Math.random() * 0.3, speed: 0.001 + Math.random() * 0.003 });
        }

        function animateBackground() {
            for (let y = 0; y < rows; y++) {
                for (let x = 0; x < cols; x++) {
                    const p = pixels[y * cols + x];
                    p.brightness += p.speed;
                    if (p.brightness > 0.4) p.brightness = 0;
                    bgCtx.fillStyle = `rgb(${Math.floor(p.brightness*10)},${Math.floor(p.brightness*30)},${Math.floor(p.brightness*50)})`;
                    bgCtx.fillRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize);
                }
            }
            requestAnimationFrame(animateBackground);
        }

        // ── Three.js ──
        const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('animationCanvas'), antialias: true, alpha: true });
        let scene, camera, spider;

        function updateSize() {
            renderer.setSize(window.innerWidth, window.innerHeight);
            renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
        }

        function updateCamera() {
            const w = window.innerWidth;
            if (w < 480)      { camera.position.set(0, 4.0, 5.0); camera.fov = 58; }
            else if (w < 768) { camera.position.set(0, 3.6, 4.5); camera.fov = 54; }
            else              { camera.position.set(0, 3.2, 4.0); camera.fov = 50; }
            camera.lookAt(0, 0, 0);
            camera.updateProjectionMatrix();
        }

        function mat(color) {
            return new THREE.MeshBasicMaterial({ color, wireframe: true });
        }

        function tube(p0, p1, radius, color, mid) {
            const pts = mid ? [p0, mid, p1] : [p0, p1];
            const curve = new THREE.CatmullRomCurve3(pts);
            return new THREE.Mesh(new THREE.TubeGeometry(curve, 8, radius, 6, false), mat(color));
        }

        function ball(r, color, x, y, z, sx=1, sy=1, sz=1) {
            const m = new THREE.Mesh(new THREE.SphereGeometry(r, 20, 20), mat(color));
            m.position.set(x, y, z);
            m.scale.set(sx, sy, sz);
            return m;
        }

        function createSpider() {
            const g = new THREE.Group();

            // ── ABDOMEN ── large ovoid, rear section
            g.add(ball(0.72, colors.primary,  0, 0.05, -0.95,  0.88, 0.76, 1.22));

            // Dorsal chevron markings
            for (let i = 0; i < 3; i++) {
                const z = -0.72 - i * 0.28;
                const w = 0.32 - i * 0.07;
                [-1,1].forEach(s => {
                    const mk = new THREE.Mesh(new THREE.SphereGeometry(0.1, 10, 10), mat(colors.dark1));
                    mk.position.set(s * w * 0.55, 0.48, z);
                    mk.scale.set(0.5, 0.22, 0.7);
                    g.add(mk);
                });
            }

            // Spinnerets
            [-1,1].forEach(s => {
                const sp = new THREE.Mesh(new THREE.CylinderGeometry(0.022, 0.03, 0.12, 8), mat(colors.light));
                sp.position.set(s * 0.07, -0.08, -1.74);
                sp.rotation.x = Math.PI * 0.38;
                g.add(sp);
            });

            // ── PEDICEL ── narrow waist
            const pedCurve = new THREE.CatmullRomCurve3([
                new THREE.Vector3(0, 0.02, -0.28),
                new THREE.Vector3(0, 0.05, -0.08),
                new THREE.Vector3(0, 0.02,  0.1),
            ]);
            g.add(new THREE.Mesh(new THREE.TubeGeometry(pedCurve, 8, 0.068, 8, false), mat(colors.dark1)));

            // ── CEPHALOTHORAX ── forward section, slightly raised
            g.add(ball(0.48, colors.primary,  0, 0.06, 0.4,  0.95, 0.7, 1.12));

            // Carapace ridge line
            const ridgeCurve = new THREE.CatmullRomCurve3([
                new THREE.Vector3(0, 0.37, 0.68),
                new THREE.Vector3(0, 0.40, 0.38),
                new THREE.Vector3(0, 0.34, 0.08),
            ]);
            g.add(new THREE.Mesh(new THREE.TubeGeometry(ridgeCurve, 8, 0.02, 6, false), mat(colors.light)));

            // Fovea diamond
            const fov = new THREE.Mesh(new THREE.OctahedronGeometry(0.065, 0), mat(colors.light));
            fov.position.set(0, 0.38, 0.28); fov.scale.set(0.8, 0.35, 1.1);
            g.add(fov);

            // ── EYES ── 8 eyes on front face
            const eyeDefs = [
                [-0.09, 0.28, 0.84, 0.056],  // AME L (large)
                [ 0.09, 0.28, 0.84, 0.056],  // AME R (large)
                [-0.23, 0.21, 0.77, 0.036],  // ALE L
                [ 0.23, 0.21, 0.77, 0.036],  // ALE R
                [-0.12, 0.36, 0.66, 0.030],  // PME L
                [ 0.12, 0.36, 0.66, 0.030],  // PME R
                [-0.26, 0.30, 0.60, 0.026],  // PLE L
                [ 0.26, 0.30, 0.60, 0.026],  // PLE R
            ];
            eyeDefs.forEach(([x,y,z,r], i) => {
                g.add(ball(r, i < 2 ? colors.light : colors.primary, x, y, z));
            });

            // ── CHELICERAE ──
            [-1,1].forEach(s => {
                const c0 = new THREE.Vector3(s*0.11, 0.1,  0.83);
                const c1 = new THREE.Vector3(s*0.12, -0.05, 0.90);
                const c2 = new THREE.Vector3(s*0.11, -0.18, 0.86);
                g.add(tube(c0, c1, 0.052, colors.dark1));
                g.add(tube(c1, c2, 0.024, colors.light));
            });

            // ── PEDIPALPS ──
            [-1,1].forEach(s => {
                const p0 = new THREE.Vector3(s*0.21, 0.1,  0.75);
                const p1 = new THREE.Vector3(s*0.34, 0.03, 0.86);
                const p2 = new THREE.Vector3(s*0.44,-0.04, 0.96);
                const curve = new THREE.CatmullRomCurve3([p0,p1,p2]);
                g.add(new THREE.Mesh(new THREE.TubeGeometry(curve, 6, 0.018, 6, false), mat(colors.primary)));
                g.add(ball(0.036, colors.light, s*0.44, -0.04, 0.96));
            });

            // ── LEGS ──
            // 4 pairs. Each leg fans outward from the side of the cephalothorax.
            // Legs are built in the XZ plane (horizontal) then drop at tibia.
            // attachZ: where on the body the leg connects (front=+Z, back=-Z)
            // fanAngle: angle outward from the body's forward axis (radians)
            //           front legs fan forward, rear legs fan backward
            const pairs = [
                { attachZ:  0.58, fanAngle: Math.PI * 0.20 }, // pair 1 — sweeps forward
                { attachZ:  0.30, fanAngle: Math.PI * 0.38 }, // pair 2
                { attachZ:  0.04, fanAngle: Math.PI * 0.62 }, // pair 3
                { attachZ: -0.20, fanAngle: Math.PI * 0.80 }, // pair 4 — sweeps back
            ];

            pairs.forEach((pair, pi) => {
                [-1, 1].forEach(side => {
                    // All angles measured from +Z axis (forward), rotating toward ±X
                    // side=1 → right side, side=-1 → left side
                    const fa = pair.fanAngle;

                    // Outward unit vector in XZ plane for this leg
                    const outX =  side * Math.sin(fa);
                    const outZ = -Math.cos(fa);          // negative = forward component

                    // ── Coxa: body edge → first joint
                    const cox0 = new THREE.Vector3(side * 0.44, 0.0, pair.attachZ);
                    const cox1 = new THREE.Vector3(
                        cox0.x + side * 0.20,
                        0.02,
                        cox0.z + outZ * 0.14
                    );
                    g.add(tube(cox0, cox1, 0.030, colors.dark1));

                    // Trochanter node
                    g.add(ball(0.038, colors.light, cox1.x, cox1.y, cox1.z));

                    // ── Femur: rises up and out
                    const femLen = 0.80;
                    const fem1 = new THREE.Vector3(
                        cox1.x + outX * femLen * 0.85,
                        cox1.y + 0.24,
                        cox1.z + outZ * femLen * 0.45
                    );
                    g.add(tube(cox1, fem1, 0.026, colors.primary));

                    // Knee node
                    g.add(ball(0.044, colors.light, fem1.x, fem1.y, fem1.z));

                    // ── Tibia: angles strongly downward and continues outward
                    const tibLen = 0.78;
                    const tib1 = new THREE.Vector3(
                        fem1.x + outX * tibLen * 0.7,
                        fem1.y - 0.52,
                        fem1.z + outZ * tibLen * 0.35
                    );
                    g.add(tube(fem1, tib1, 0.020, colors.primary));

                    // ── Metatarsus: continues downward
                    const metLen = 0.55;
                    const met1 = new THREE.Vector3(
                        tib1.x + outX * metLen * 0.5,
                        tib1.y - 0.40,
                        tib1.z + outZ * metLen * 0.25
                    );
                    g.add(tube(tib1, met1, 0.014, colors.dark1));

                    // ── Tarsus + claws
                    const tarLen = 0.28;
                    const tar1 = new THREE.Vector3(
                        met1.x + outX * tarLen * 0.4,
                        met1.y - 0.22,
                        met1.z + outZ * tarLen * 0.2
                    );
                    g.add(tube(met1, tar1, 0.009, colors.dark1));

                    // Two claw tips
                    [-1,1].forEach(claw => {
                        const cl = new THREE.Vector3(
                            tar1.x + claw * 0.04,
                            tar1.y - 0.055,
                            tar1.z + 0.02
                        );
                        g.add(tube(tar1, cl, 0.006, colors.light));
                    });
                });
            });

            return g;
        }

        function init() {
            initBackground();
            animateBackground();
            updateSize();

            scene = new THREE.Scene();
            scene.background = new THREE.Color(colors.dark3);

            camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 1000);
            updateCamera();

            spider = createSpider();
            scene.add(spider);

            scene.add(new THREE.AmbientLight(colors.light, 0.5));
            const pl1 = new THREE.PointLight(colors.primary, 1.5, 100);
            pl1.position.set(4, 6, 4); scene.add(pl1);
            const pl2 = new THREE.PointLight(colors.light, 0.8, 100);
            pl2.position.set(-5, 2, 3); scene.add(pl2);
            const pl3 = new THREE.PointLight(colors.primary, 0.5, 100);
            pl3.position.set(0, -3, -6); scene.add(pl3);

            animate();
        }

        function animate() {
            requestAnimationFrame(animate);
            const t = Date.now();
            if (spider) {
                spider.rotation.y += 0.003;
                spider.position.y = Math.sin(t * 0.0012) * 0.06;
                spider.rotation.x = Math.sin(t * 0.0008) * 0.03;
                spider.rotation.z = Math.sin(t * 0.0015) * 0.01;
            }
            renderer.render(scene, camera);
        }

        window.addEventListener('resize', () => {
            updateSize(); initBackground();
            if (camera) { camera.aspect = window.innerWidth / window.innerHeight; updateCamera(); }
        });

        window.addEventListener('load', init);
    </script>
</body>
</html>

Model III: The Scorpion

Our third model introduces articulated animation to a completely new body plan. The scorpion features segmented armor plating on the mesosoma, an imposing metasoma (the tail) culminating in a venom gland and stinger, alongside complex joint hierarchies for its signature claws (pedipalps).

Segmented Carapace and Tail

Unlike the rigid structures of the dragonfly or spider, the scorpion requires distinct segments that move independently yet connectedly. Its tail consists of tapering cylinders joined at dynamic pivot groups:

const tailSegsCount = 5;
let prevPivot = group;
let prevPos = new THREE.Vector3(0, tailBaseY, tailBaseZ);

for (let i = 0; i < tailSegsCount; i++) {
    const pivot = new THREE.Group();
    pivot.position.copy(prevPos);
    // ... create segment mesh and add to pivot ...
    prevPivot.add(pivot);
    tailSegments.push(pivot);
    
    prevPivot = pivot;
    prevPos = new THREE.Vector3(0, 0, -0.30);
}

This nested THREE.Group architecture allows animation routines to recursively apply sine-wave rotations down the tail, creating a natural curling and striking motion.

Articulated Claws

The pedipalps, ending in large manus assemblies, feature movable parts to simulate pincers. By isolating the inner claw on a separate pivot point, we can apply an oscillating rotation matrix directly within the render loop:

const clawAngle = (Math.sin(t * 1.5) * 0.5 + 0.5) * 0.35;
if (leftClawMovable) leftClawMovable.rotation.x = -clawAngle;
if (rightClawMovable) rightClawMovable.rotation.x = -clawAngle;

These techniques demonstrate how simple hierarchical groupings transformed by trigonometric functions can yield convincing procedural lifeforms.


Complete Scorpion Source Code

If you wish to experiment with the scorpion model programmatically in your own local environment, here is the complete, self-contained HTML payload used for the scorpion demonstration above.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ThreeJS Scorpion</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { background: #000; overflow: hidden; height: 100vh; }
        #bgCanvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; }
        #animationCanvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 2; }
    </style>
</head>
<body>
    <canvas id="bgCanvas"></canvas>
    <canvas id="animationCanvas"></canvas>
    <script>
        const colors = {
            primary: 0xe8a020,
            dark1:   0x3a1a00,
            dark2:   0x1e0d00,
            dark3:   0x000000,
            light:   0xffd080
        };

        const bgCanvas = document.getElementById("bgCanvas");
        const bgCtx = bgCanvas.getContext("2d");
        const pixelSize = 8;
        let cols, rows, pixels = [];

        function initBackground() {
            bgCanvas.width = window.innerWidth;
            bgCanvas.height = window.innerHeight;
            cols = Math.ceil(bgCanvas.width / pixelSize);
            rows = Math.ceil(bgCanvas.height / pixelSize);
            pixels = [];
            for (let i = 0; i < cols * rows; i++) {
                pixels.push({ brightness: Math.random() * 0.3, speed: 0.001 + Math.random() * 0.003 });
            }
        }

        function animateBackground() {
            for (let y = 0; y < rows; y++) {
                for (let x = 0; x < cols; x++) {
                    const pixel = pixels[y * cols + x];
                    pixel.brightness += pixel.speed;
                    if (pixel.brightness > 0.4) pixel.brightness = 0;
                    bgCtx.fillStyle = `rgb(${Math.floor(pixel.brightness*40)},${Math.floor(pixel.brightness*20)},${Math.floor(pixel.brightness*5)})`;
                    bgCtx.fillRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize);
                }
            }
            requestAnimationFrame(animateBackground);
        }

        const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('animationCanvas'), antialias: true, alpha: true });
        let scene, camera, scorpion;
        let tailSegments = [];
        let stingerGroup;
        let leftClaw, rightClaw;
        let leftClawMovable, rightClawMovable;
        let legGroups = [];

        function updateSize() {
            renderer.setSize(window.innerWidth, window.innerHeight);
            renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
        }

        function updateCamera() {
            const w = window.innerWidth;
            if (w < 480) { camera.position.set(0, 0, 9); camera.fov = 60; }
            else if (w < 768) { camera.position.set(0, 0, 8); camera.fov = 55; }
            else { camera.position.set(0, 0, 7); camera.fov = 50; }
            camera.lookAt(0, 0, 0);
            camera.updateProjectionMatrix();
        }

        function mat(color) {
            return new THREE.MeshBasicMaterial({ color, wireframe: true });
        }

        function createScorpion() {
            const group = new THREE.Group();
            tailSegments = [];
            legGroups = [];

            const headGeo = new THREE.SphereGeometry(0.28, 20, 20);
            const head = new THREE.Mesh(headGeo, mat(colors.dark1));
            head.position.set(0, 0.05, 1.5);
            head.scale.set(1.2, 0.7, 1.1);
            group.add(head);

            [[-0.08,0],[0.08,0],[-0.22,0],[0.22,0]].forEach(([ex]) => {
                const eGeo = new THREE.SphereGeometry(0.045, 8, 8);
                const eye = new THREE.Mesh(eGeo, mat(colors.light));
                eye.position.set(ex, 0.12, 1.6);
                group.add(eye);
            });

            [-1,1].forEach(side => {
                const pts = [
                    new THREE.Vector3(side*0.12, 0.0, 1.65),
                    new THREE.Vector3(side*0.18, -0.05, 1.85),
                    new THREE.Vector3(side*0.22, -0.08, 2.0)
                ];
                const curve = new THREE.CatmullRomCurve3(pts);
                const cGeo = new THREE.TubeGeometry(curve, 6, 0.025, 5, false);
                group.add(new THREE.Mesh(cGeo, mat(colors.dark1)));
                const tipGeo = new THREE.SphereGeometry(0.04, 6, 6);
                const tip = new THREE.Mesh(tipGeo, mat(colors.light));
                tip.position.set(side*0.22, -0.08, 2.0);
                group.add(tip);
            });

            [-1,1].forEach(side => {
                const armPts = [
                    new THREE.Vector3(side*0.3, 0.0, 1.4),
                    new THREE.Vector3(side*0.7, 0.05, 1.55),
                    new THREE.Vector3(side*1.0, 0.0, 1.7)
                ];
                const armCurve = new THREE.CatmullRomCurve3(armPts);
                const armGeo = new THREE.TubeGeometry(armCurve, 8, 0.045, 6, false);
                group.add(new THREE.Mesh(armGeo, mat(colors.primary)));

                const manusGeo = new THREE.SphereGeometry(0.1, 12, 12);
                const manus = new THREE.Mesh(manusGeo, mat(colors.primary));
                manus.position.set(side*1.1, 0.0, 1.82);
                manus.scale.set(1.0, 0.75, 1.2);
                group.add(manus);

                const fxPivot = new THREE.Group();
                fxPivot.position.set(side*1.1, 0.0, 1.82);
                const fxPts = [
                    new THREE.Vector3(0, 0, 0),
                    new THREE.Vector3(side*0.05, 0.07, 0.22),
                    new THREE.Vector3(side*0.0, 0.05, 0.40)
                ];
                const fxCurve = new THREE.CatmullRomCurve3(fxPts);
                const fxGeo = new THREE.TubeGeometry(fxCurve, 6, 0.022, 5, false);
                fxPivot.add(new THREE.Mesh(fxGeo, mat(colors.dark1)));
                group.add(fxPivot);

                const mvPivot = new THREE.Group();
                mvPivot.position.set(side*1.1, 0.0, 1.82);
                const mvPts = [
                    new THREE.Vector3(0, 0, 0),
                    new THREE.Vector3(side*0.05, -0.07, 0.22),
                    new THREE.Vector3(side*0.0, -0.05, 0.40)
                ];
                const mvCurve = new THREE.CatmullRomCurve3(mvPts);
                const mvGeo = new THREE.TubeGeometry(mvCurve, 6, 0.020, 5, false);
                mvPivot.add(new THREE.Mesh(mvGeo, mat(colors.light)));
                group.add(mvPivot);

                if (side === -1) { leftClaw = fxPivot; leftClawMovable = mvPivot; }
                else { rightClaw = fxPivot; rightClawMovable = mvPivot; }
            });

            const carapaceGeo = new THREE.SphereGeometry(0.52, 24, 24);
            const carapace = new THREE.Mesh(carapaceGeo, mat(colors.primary));
            carapace.position.set(0, 0.08, 0.7);
            carapace.scale.set(1.1, 0.6, 1.6);
            group.add(carapace);

            const mesoSegs = 6;
            for (let i = 0; i < mesoSegs; i++) {
                const t = i / (mesoSegs - 1);
                const w = 0.44 - t * 0.12;
                const segGeo = new THREE.CylinderGeometry(w * 0.88, w, 0.28, 14, 2);
                const isAlt = i % 2 === 0;
                const seg = new THREE.Mesh(segGeo, mat(isAlt ? colors.primary : colors.dark1));
                const z = -0.1 - i * 0.3;
                const y = 0.06 - i * 0.01;
                seg.position.set(0, y, z);
                seg.rotation.x = Math.PI * 0.5;
                group.add(seg);
                [-1,1].forEach(side => {
                    const nubGeo = new THREE.SphereGeometry(w * 0.28, 7, 7);
                    const nub = new THREE.Mesh(nubGeo, mat(colors.light));
                    nub.position.set(side * w * 1.05, y, z);
                    group.add(nub);
                });
            }

            const tailSegsCount = 5;
            const tailBaseZ = -0.1 - mesoSegs * 0.3;
            const tailBaseY = 0.06 - mesoSegs * 0.01;
            let prevPivot = group;
            let prevPos = new THREE.Vector3(0, tailBaseY, tailBaseZ);

            for (let i = 0; i < tailSegsCount; i++) {
                const r = 0.14 - i * 0.018;
                const pivot = new THREE.Group();
                pivot.position.copy(prevPos);
                const isAlt = i % 2 === 0;
                const segGeo = new THREE.CylinderGeometry(r * 0.9, r, 0.30, 12, 2);
                const seg = new THREE.Mesh(segGeo, mat(isAlt ? colors.dark1 : colors.primary));
                seg.position.set(0, 0, -0.15);
                seg.rotation.x = Math.PI * 0.5;
                pivot.add(seg);
                prevPivot.add(pivot);
                tailSegments.push(pivot);
                prevPivot = pivot;
                prevPos = new THREE.Vector3(0, 0, -0.30);
            }

            stingerGroup = new THREE.Group();
            stingerGroup.position.set(0, 0, -0.30);

            const bulbGeo = new THREE.SphereGeometry(0.14, 12, 12);
            const bulb = new THREE.Mesh(bulbGeo, mat(colors.primary));
            bulb.scale.set(0.85, 1.1, 1.0);
            stingerGroup.add(bulb);

            const stingerPts = [
                new THREE.Vector3(0, 0.0, 0),
                new THREE.Vector3(0, 0.22, 0.12),
                new THREE.Vector3(0, 0.38, 0.28)
            ];
            const stingerCurve = new THREE.CatmullRomCurve3(stingerPts);
            const stingerGeo = new THREE.TubeGeometry(stingerCurve, 8, 0.022, 5, false);
            stingerGroup.add(new THREE.Mesh(stingerGeo, mat(colors.light)));

            const tipGeo2 = new THREE.ConeGeometry(0.022, 0.12, 6);
            const stingerTip = new THREE.Mesh(tipGeo2, mat(colors.light));
            stingerTip.position.set(0, 0.44, 0.34);
            stingerTip.rotation.x = -0.7;
            stingerGroup.add(stingerTip);

            prevPivot.add(stingerGroup);

            const legPairs = [
                { z: 1.15, spread: 1.0 },
                { z: 0.82, spread: 1.05 },
                { z: 0.50, spread: 1.08 },
                { z: 0.18, spread: 1.12 },
            ];

            legPairs.forEach((pair, pi) => {
                [-1,1].forEach(side => {
                    const legGroup = new THREE.Group();
                    legGroup.position.set(side * 0.48, -0.02, pair.z);
                    group.add(legGroup);

                    const coxGeo = new THREE.CylinderGeometry(0.03, 0.035, 0.12, 7);
                    const cox = new THREE.Mesh(coxGeo, mat(colors.dark1));
                    cox.rotation.z = side * 1.1;
                    legGroup.add(cox);

                    const femGroup = new THREE.Group();
                    femGroup.position.set(side * 0.18, -0.06, 0);
                    const femGeo = new THREE.CylinderGeometry(0.025, 0.020, 0.45, 7);
                    const fem = new THREE.Mesh(femGeo, mat(colors.primary));
                    fem.rotation.z = side * 0.6;
                    femGroup.add(fem);
                    legGroup.add(femGroup);

                    const tibGroup = new THREE.Group();
                    tibGroup.position.set(side * 0.32, -0.18, 0);
                    const tibGeo = new THREE.CylinderGeometry(0.018, 0.013, 0.42, 6);
                    const tib = new THREE.Mesh(tibGeo, mat(colors.light));
                    tib.rotation.z = side * 0.9;
                    tibGroup.add(tib);
                    legGroup.add(tibGroup);

                    for (let t = 0; t < 2; t++) {
                        const tarGeo = new THREE.CylinderGeometry(0.011, 0.008, 0.10, 5);
                        const tar = new THREE.Mesh(tarGeo, mat(colors.dark1));
                        tar.position.set(side * (0.42 + t * 0.07), -0.26 - t * 0.06, 0);
                        tar.rotation.z = side * 0.92;
                        legGroup.add(tar);
                    }

                    const clawGeo = new THREE.ConeGeometry(0.012, 0.07, 5);
                    const claw = new THREE.Mesh(clawGeo, mat(colors.light));
                    claw.position.set(side * 0.57, -0.35, 0);
                    claw.rotation.z = side * -0.4;
                    legGroup.add(claw);

                    legGroups.push({ group: legGroup, side, pi });
                });
            });

            [-1,1].forEach(side => {
                for (let t = 0; t < 5; t++) {
                    const torusGeo = new THREE.TorusGeometry(0.018, 0.006, 5, 8);
                    const pec = new THREE.Mesh(torusGeo, mat(colors.light));
                    pec.position.set(side * (0.06 + t * 0.07), -0.12, 0.0);
                    pec.rotation.x = Math.PI * 0.5;
                    group.add(pec);
                }
            });

            group.position.set(0, 0.6, -0.5);
            group.rotation.x = 0.08;
            return group;
        }

        function init() {
            initBackground();
            animateBackground();
            updateSize();

            scene = new THREE.Scene();
            scene.background = new THREE.Color(colors.dark3);

            camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 1000);
            updateCamera();

            scorpion = createScorpion();
            scene.add(scorpion);

            const amb = new THREE.AmbientLight(colors.light, 0.5);
            scene.add(amb);
            const pl1 = new THREE.PointLight(colors.primary, 1.5, 100);
            pl1.position.set(5, 5, 5); scene.add(pl1);
            const pl2 = new THREE.PointLight(colors.light, 0.8, 100);
            pl2.position.set(-5, -3, 3); scene.add(pl2);
            const pl3 = new THREE.PointLight(colors.primary, 0.6, 100);
            pl3.position.set(0, 2, -8); scene.add(pl3);

            animate();
        }

        function animate() {
            requestAnimationFrame(animate);
            const t = Date.now() * 0.001;

            if (scorpion) {
                scorpion.rotation.y += 0.004;
                scorpion.position.y = 0.6 + Math.sin(t * 1.5) * 0.07;
                scorpion.rotation.z = Math.sin(t * 1.8) * 0.015;

                tailSegments.forEach((seg, i) => {
                    const phase = i * 0.4;
                    const base = 0.28 + i * 0.18;
                    seg.rotation.x = base + Math.sin(t * 1.2 + phase) * 0.06;
                });

                if (stingerGroup) {
                    stingerGroup.rotation.x = Math.sin(t * 2.0) * 0.08;
                }

                const clawAngle = (Math.sin(t * 1.5) * 0.5 + 0.5) * 0.35;
                if (leftClawMovable) leftClawMovable.rotation.x = -clawAngle;
                if (rightClawMovable) rightClawMovable.rotation.x = -clawAngle;
                if (leftClaw) leftClaw.rotation.x = clawAngle * 0.5;
                if (rightClaw) rightClaw.rotation.x = clawAngle * 0.5;

                legGroups.forEach(({ group: lg, side, pi }) => {
                    const phase = pi * 0.8 + (side > 0 ? Math.PI : 0);
                    const swing = Math.sin(t * 2.2 + phase) * 0.12;
                    lg.rotation.x = swing;
                    lg.position.y = -0.02 + Math.abs(Math.sin(t * 2.2 + phase)) * 0.04;
                });
            }

            renderer.render(scene, camera);
        }

        window.addEventListener('resize', () => {
            updateSize();
            initBackground();
            if (camera) { camera.aspect = window.innerWidth / window.innerHeight; updateCamera(); }
        });

        window.addEventListener('load', init);
    </script>
</body>
</html>

Model IV: The Mosquito

Our final addition brings to life a delicate, hovering mosquito. This model contrasts with the heavily armored creatures by focusing on thin, elongated structures: an extended proboscis, wispy antennae, and translucent, fast-beating wings with intricate venation.

Interactive Orbital Mechanics

Unlike the preceding models, the mosquito introduces interactive camera orbital mechanics. A custom orbit control state manages spherical coordinate rotation (theta and phi). Viewers can drag to rotate the camera, seamlessly blending with an auto-rotation loop when inactive.

const orbit = {
    theta: 0, phi: 1.1, radius: 8,
    isDragging: false, lastX: 0, lastY: 0,
    autoRotate: true, autoRotateSpeed: 0.003,
    velTheta: 0, velPhi: 0,
};

function setCameraFromOrbit() {
    const x = orbit.radius * Math.sin(orbit.phi) * Math.sin(orbit.theta);
    const y = orbit.radius * Math.cos(orbit.phi);
    const z = orbit.radius * Math.sin(orbit.phi) * Math.cos(orbit.theta);
    camera.position.set(x, y, z);
    camera.lookAt(0, 0, 0);
}

High-Frequency Wing Animation

To simulate a mosquito’s high-frequency wing beat, the wings are detached into secondary groups. Within the animation loop, the wing rotation vectors are driven by sine waves with aggressive scalar parameters—such as Math.sin(t * 18.0) * 0.55.

if (wingL) {
    wingL.rotation.z =  Math.sin(t * 18.0) * 0.55;
    wingL.rotation.x =  Math.sin(t * 17.0 + 0.4) * 0.12;
}
if (wingR) {
    wingR.rotation.z = -Math.sin(t * 18.5 + 0.2) * 0.55;
    wingR.rotation.x =  Math.sin(t * 17.5 + 1.1) * 0.12;
}

By layering these oscillating offsets, we create a chaotic, natural buzz that prevents the model from feeling static.


Complete Mosquito Source Code

If you wish to experiment with the mosquito model programmatically in your own local environment, here is the complete, self-contained HTML payload used for the mosquito demonstration above.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ThreeJS: Mosquito</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { background: #000; overflow: hidden; height: 100vh; }
        #bgCanvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; }
        #animationCanvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 2; cursor: grab; }
        #animationCanvas.dragging { cursor: grabbing; }
        #hint { position: absolute; bottom: 18px; left: 50%; transform: translateX(-50%); z-index: 10;
            color: rgba(180,140,255,0.5); font-family: monospace; font-size: 12px; letter-spacing: 0.12em;
            pointer-events: none; transition: opacity 2s; user-select: none; }
    </style>
</head>
<body>
    <canvas id="bgCanvas"></canvas>
    <canvas id="animationCanvas"></canvas>
    <script>
        const colors = {
            body:   0x8060aa,
            thorax: 0xa080cc,
            wing:   0xc8b0ff,
            vein:   0xf0e8ff,
            leg:    0x604888,
            eye:    0xff3060,
            probos: 0xd0a8ff,
            spot:   0x40ff90,
            dark:   0x1a0a2e,
        };

        const bgCanvas = document.getElementById('bgCanvas');
        const bgCtx = bgCanvas.getContext('2d');
        const px = 7;
        let cols, rows, pixels = [];

        function initBackground() {
            bgCanvas.width = window.innerWidth;
            bgCanvas.height = window.innerHeight;
            cols = Math.ceil(bgCanvas.width / px);
            rows = Math.ceil(bgCanvas.height / px);
            pixels = [];
            for (let i = 0; i < cols * rows; i++) {
                pixels.push({ b: Math.random() * 0.3, speed: 0.001 + Math.random() * 0.003 });
            }
        }

        function animateBackground() {
            for (let y = 0; y < rows; y++) {
                for (let x = 0; x < cols; x++) {
                    const p = pixels[y * cols + x];
                    p.b += p.speed;
                    if (p.b > 0.4) p.b = 0;
                    bgCtx.fillStyle = `rgb(${Math.floor(p.b*20)},${Math.floor(p.b*10)},${Math.floor(p.b*45)})`;
                    bgCtx.fillRect(x * px, y * px, px, px);
                }
            }
            requestAnimationFrame(animateBackground);
        }

        const renderer = new THREE.WebGLRenderer({
            canvas: document.getElementById('animationCanvas'),
            antialias: true, alpha: true
        });

        let scene, camera, mosquito;
        let wingL, wingR;
        let legGroups = [];

        const orbit = {
            theta: 0, phi: 1.1, radius: 8,
            isDragging: false, lastX: 0, lastY: 0,
            autoRotate: true, autoRotateSpeed: 0.003,
            velTheta: 0, velPhi: 0,
        };
        const PHI_MIN = 0.05, PHI_MAX = Math.PI * 0.95;
        const RADIUS_MIN = 3, RADIUS_MAX = 20;

        function setCameraFromOrbit() {
            const x = orbit.radius * Math.sin(orbit.phi) * Math.sin(orbit.theta);
            const y = orbit.radius * Math.cos(orbit.phi);
            const z = orbit.radius * Math.sin(orbit.phi) * Math.cos(orbit.theta);
            camera.position.set(x, y, z);
            camera.lookAt(0, 0, 0);
            camera.updateProjectionMatrix();
        }

        function updateSize() {
            renderer.setSize(window.innerWidth, window.innerHeight);
            renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
        }

        function updateCamera() {
            const w = window.innerWidth;
            orbit.radius = w < 480 ? 12 : w < 768 ? 10 : 8;
            camera.fov = 52;
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            setCameraFromOrbit();
        }

        function mat(color) {
            return new THREE.MeshBasicMaterial({ color, wireframe: true });
        }
        function lineMat(color) {
            return new THREE.LineBasicMaterial({ color });
        }

        function makeWing(side) {
            const group = new THREE.Group();
            const s = side;

            const shape = new THREE.Shape();
            shape.moveTo(0, 0);
            shape.bezierCurveTo(s*0.3, 0.18, s*1.0, 0.32, s*1.8, 0.18);
            shape.bezierCurveTo(s*2.3, 0.08, s*2.4, -0.04, s*2.2, -0.10);
            shape.bezierCurveTo(s*1.8, -0.22, s*0.9, -0.28, s*0.2, -0.12);
            shape.bezierCurveTo(s*0.05, -0.06, 0, 0, 0, 0);

            const geo = new THREE.ShapeGeometry(shape, 14);
            group.add(new THREE.Mesh(geo, mat(colors.wing)));

            const edgeGeo = new THREE.EdgesGeometry(geo);
            group.add(new THREE.LineSegments(edgeGeo, lineMat(colors.vein)));

            const veinDefs = [
                { pts: [[0,0,0.01],[s*0.8,0.18,0.01],[s*1.8,0.16,0.01],[s*2.2,0.05,0.01]], r: 0.009 },
                { pts: [[0,0,0.01],[s*0.7,0.10,0.01],[s*1.6,0.08,0.01],[s*2.1,-0.02,0.01]], r: 0.007 },
                { pts: [[s*0.1,-0.02,0.01],[s*0.8,0.02,0.01],[s*1.7,-0.01,0.01],[s*2.1,-0.08,0.01]], r: 0.006 },
                { pts: [[s*0.2,-0.06,0.01],[s*0.7,-0.06,0.01],[s*1.4,-0.09,0.01],[s*2.0,-0.14,0.01]], r: 0.005 },
            ];
            veinDefs.forEach(v => {
                const pts = v.pts.map(p => new THREE.Vector3(p[0], p[1], p[2]));
                const curve = new THREE.CatmullRomCurve3(pts);
                const vGeo = new THREE.TubeGeometry(curve, 8, v.r, 4, false);
                group.add(new THREE.Mesh(vGeo, mat(colors.vein)));
            });

            for (let i = 0; i < 3; i++) {
                const cx = s * (0.6 + i * 0.5);
                const cvPts = [
                    new THREE.Vector3(cx, 0.12 - i*0.03, 0.01),
                    new THREE.Vector3(cx + s*0.05, -0.04 - i*0.02, 0.01),
                ];
                const cvCurve = new THREE.CatmullRomCurve3(cvPts);
                const cvGeo = new THREE.TubeGeometry(cvCurve, 4, 0.004, 4, false);
                group.add(new THREE.Mesh(cvGeo, mat(colors.vein)));
            }

            return group;
        }

        function makeAntenna(side) {
            const group = new THREE.Group();
            const s = side;
            const segments = 14;

            for (let i = 0; i < segments; i++) {
                const t = i / segments;
                const y = 0.15 + t * 1.1;
                const x = s * (0.05 + t * 0.08);

                if (i < segments - 1) {
                    const sPts = [
                        new THREE.Vector3(x, y, 0),
                        new THREE.Vector3(x + s*0.006, y + 0.09, 0),
                    ];
                    const sCurve = new THREE.CatmullRomCurve3(sPts);
                    group.add(new THREE.Mesh(
                        new THREE.TubeGeometry(sCurve, 3, 0.008 - t*0.005, 4, false),
                        mat(colors.probos)
                    ));
                }

                if (i > 1 && i < segments - 2) {
                    const hairLen = 0.12 - t * 0.07;
                    for (let h = -1; h <= 1; h += 2) {
                        const hPts = [
                            new THREE.Vector3(x, y, 0),
                            new THREE.Vector3(x + s*hairLen*0.4, y + hairLen*0.3, h*hairLen*0.6),
                            new THREE.Vector3(x + s*hairLen*0.7, y + hairLen*0.1, h*hairLen),
                        ];
                        const hCurve = new THREE.CatmullRomCurve3(hPts);
                        group.add(new THREE.Mesh(
                            new THREE.TubeGeometry(hCurve, 3, 0.003, 3, false),
                            mat(colors.vein)
                        ));
                    }
                }
            }
            return group;
        }

        function makeLeg(side, index) {
            const group = new THREE.Group();
            const s = side;

            const zOff = (index - 1) * 0.28;

            const coxPts = [
                new THREE.Vector3(0, 0, 0),
                new THREE.Vector3(s*0.18, -0.08, 0.04),
            ];
            group.add(new THREE.Mesh(
                new THREE.TubeGeometry(new THREE.CatmullRomCurve3(coxPts), 4, 0.025, 5, false),
                mat(colors.leg)
            ));

            const femStart = new THREE.Vector3(s*0.18, -0.08, 0.04);
            const femEnd   = new THREE.Vector3(s*0.65, -0.55, 0.06);
            const femPts = [femStart, new THREE.Vector3(s*0.40, -0.30, 0.05), femEnd];
            group.add(new THREE.Mesh(
                new THREE.TubeGeometry(new THREE.CatmullRomCurve3(femPts), 6, 0.018, 5, false),
                mat(colors.body)
            ));

            const tibEnd = new THREE.Vector3(s*1.05, -1.05, -0.05);
            const tibPts = [femEnd, new THREE.Vector3(s*0.85, -0.80, 0.0), tibEnd];
            group.add(new THREE.Mesh(
                new THREE.TubeGeometry(new THREE.CatmullRomCurve3(tibPts), 6, 0.013, 5, false),
                mat(colors.leg)
            ));

            const tarsSegs = 3;
            let tStart = tibEnd.clone();
            for (let ti = 0; ti < tarsSegs; ti++) {
                const tEnd = new THREE.Vector3(
                    tStart.x + s*0.22,
                    tStart.y - 0.22 + ti*0.06,
                    tStart.z - 0.04
                );
                group.add(new THREE.Mesh(
                    new THREE.TubeGeometry(
                        new THREE.CatmullRomCurve3([tStart, tEnd]), 4,
                        0.009 - ti*0.002, 4, false
                    ),
                    mat(ti % 2 === 0 ? colors.dark : colors.vein)
                ));
                tStart = tEnd;
            }

            return group;
        }

        function createMosquito() {
            const group = new THREE.Group();
            legGroups = [];

            const abdSegs = 8;
            for (let i = 0; i < abdSegs; i++) {
                const t = i / (abdSegs - 1);
                const r = 0.13 - t * 0.07;
                const z = 0.1 + i * 0.30;
                const isAlt = i % 2 === 0;
                const geo = new THREE.CylinderGeometry(r * 0.85, r, 0.28, 12, 1);
                const seg = new THREE.Mesh(geo, mat(isAlt ? colors.body : colors.dark));
                seg.rotation.x = Math.PI * 0.5;
                seg.position.set(0, 0, z);
                group.add(seg);

                const ringGeo = new THREE.TorusGeometry(r, 0.008, 6, 14);
                const ring = new THREE.Mesh(ringGeo, mat(colors.vein));
                ring.position.set(0, 0, z + 0.13);
                group.add(ring);
            }

            const tailGeo = new THREE.ConeGeometry(0.04, 0.18, 8);
            const tail = new THREE.Mesh(tailGeo, mat(colors.dark));
            tail.rotation.x = Math.PI * 0.5;
            tail.position.set(0, 0, 0.1 + abdSegs * 0.30 + 0.09);
            group.add(tail);

            const thoraxGeo = new THREE.SphereGeometry(0.30, 16, 14);
            const thorax = new THREE.Mesh(thoraxGeo, mat(colors.thorax));
            thorax.scale.set(0.9, 1.15, 1.1);
            thorax.position.set(0, 0.12, -0.08);
            group.add(thorax);

            const scuGeo = new THREE.SphereGeometry(0.12, 10, 8);
            const scu = new THREE.Mesh(scuGeo, mat(colors.body));
            scu.scale.set(1, 0.6, 0.8);
            scu.position.set(0, 0.22, 0.18);
            group.add(scu);

            const headGeo = new THREE.SphereGeometry(0.18, 14, 12);
            const head = new THREE.Mesh(headGeo, mat(colors.thorax));
            head.scale.set(0.95, 0.9, 1.0);
            head.position.set(0, -0.04, -0.42);
            group.add(head);

            [-1, 1].forEach(side => {
                const eyeGeo = new THREE.SphereGeometry(0.11, 12, 10);
                const eye = new THREE.Mesh(eyeGeo, mat(colors.eye));
                eye.scale.set(0.75, 0.85, 0.9);
                eye.position.set(side * 0.15, 0.02, -0.44);
                group.add(eye);

                for (let fi = 0; fi < 4; fi++) {
                    const fGeo = new THREE.CircleGeometry(0.04, 6);
                    const facet = new THREE.Mesh(fGeo, mat(colors.vein));
                    const ang = (fi / 4) * Math.PI * 1.2 - 0.3;
                    facet.position.set(
                        side * (0.17 + Math.cos(ang)*0.05),
                        0.02 + Math.sin(ang)*0.05,
                        -0.42
                    );
                    group.add(facet);
                }
            });

            const probPts = [
                new THREE.Vector3(0, -0.08, -0.54),
                new THREE.Vector3(0, -0.20, -0.90),
                new THREE.Vector3(0, -0.28, -1.30),
                new THREE.Vector3(0, -0.32, -1.70),
            ];
            const probCurve = new THREE.CatmullRomCurve3(probPts);
            const probGeo = new THREE.TubeGeometry(probCurve, 12, 0.012, 6, false);
            group.add(new THREE.Mesh(probGeo, mat(colors.probos)));

            const tipGeo = new THREE.ConeGeometry(0.008, 0.06, 5);
            const tip = new THREE.Mesh(tipGeo, mat(colors.vein));
            tip.rotation.x = Math.PI * 0.5;
            tip.position.set(0, -0.34, -1.73);
            group.add(tip);

            [-1, 1].forEach(side => {
                const lpPts = [
                    new THREE.Vector3(side*0.03, -0.06, -0.52),
                    new THREE.Vector3(side*0.05, -0.14, -0.72),
                    new THREE.Vector3(side*0.04, -0.20, -0.90),
                ];
                group.add(new THREE.Mesh(
                    new THREE.TubeGeometry(new THREE.CatmullRomCurve3(lpPts), 6, 0.007, 4, false),
                    mat(colors.leg)
                ));
            });

            [-1, 1].forEach(side => {
                const antGroup = makeAntenna(side);
                antGroup.position.set(side * 0.06, 0.06, -0.52);
                group.add(antGroup);
            });

            wingL = new THREE.Group();
            wingL.position.set(0, 0.14, -0.05);
            const wL = makeWing(-1);
            wL.rotation.x = -0.18;
            wL.rotation.z = 0.10;
            wingL.add(wL);
            group.add(wingL);

            wingR = new THREE.Group();
            wingR.position.set(0, 0.14, -0.05);
            const wR = makeWing(1);
            wR.rotation.x = -0.18;
            wR.rotation.z = -0.10;
            wingR.add(wR);
            group.add(wingR);

            [-1, 1].forEach(side => {
                for (let li = 0; li < 3; li++) {
                    const legGroup = makeLeg(side, li);
                    legGroup.position.set(side * 0.28, -0.05, -0.15 + li * 0.28);
                    group.add(legGroup);
                    legGroups.push({ g: legGroup, side, li });
                }
            });

            group.position.set(0, 0.3, 0.4);
            return group;
        }

        function initOrbitControls() {
            const canvas = document.getElementById('animationCanvas');
            const hint   = document.getElementById('hint');

            function onDragStart(x, y) {
                orbit.isDragging = true;
                orbit.autoRotate = false;
                orbit.lastX = x; orbit.lastY = y;
                orbit.velTheta = 0; orbit.velPhi = 0;
                canvas.classList.add('dragging');
                hint.style.opacity = '0';
            }
            function onDragMove(x, y) {
                if (!orbit.isDragging) return;
                const dx = x - orbit.lastX;
                const dy = y - orbit.lastY;
                const speed = 0.005;
                orbit.velTheta = -dx * speed;
                orbit.velPhi   = -dy * speed;
                orbit.theta += orbit.velTheta;
                orbit.phi = Math.max(PHI_MIN, Math.min(PHI_MAX, orbit.phi + orbit.velPhi));
                orbit.lastX = x; orbit.lastY = y;
                setCameraFromOrbit();
            }
            function onDragEnd() {
                orbit.isDragging = false;
                canvas.classList.remove('dragging');
                setTimeout(() => { orbit.autoRotate = true; }, 2000);
            }

            canvas.addEventListener('mousedown', e => onDragStart(e.clientX, e.clientY));
            window.addEventListener('mousemove', e => onDragMove(e.clientX, e.clientY));
            window.addEventListener('mouseup',   () => onDragEnd());

            canvas.addEventListener('touchstart', e => {
                e.preventDefault();
                onDragStart(e.touches[0].clientX, e.touches[0].clientY);
            }, { passive: false });
            window.addEventListener('touchmove', e => {
                e.preventDefault();
                onDragMove(e.touches[0].clientX, e.touches[0].clientY);
            }, { passive: false });
            window.addEventListener('touchend', () => onDragEnd());

            canvas.addEventListener('wheel', e => {
                e.preventDefault();
                orbit.radius = Math.max(RADIUS_MIN, Math.min(RADIUS_MAX, orbit.radius + e.deltaY * 0.01));
                setCameraFromOrbit();
            }, { passive: false });

            setTimeout(() => { hint.style.opacity = '0'; }, 3000);
        }

        function init() {
            initBackground();
            animateBackground();
            updateSize();

            scene = new THREE.Scene();
            scene.background = new THREE.Color(0x000000);

            camera = new THREE.PerspectiveCamera(52, window.innerWidth / window.innerHeight, 0.1, 1000);
            updateCamera();

            mosquito = createMosquito();
            scene.add(mosquito);

            scene.add(new THREE.AmbientLight(0xc0a0ff, 0.5));
            const pl1 = new THREE.PointLight(0xa070ff, 1.6, 100);
            pl1.position.set(4, 5, 3);
            scene.add(pl1);
            const pl2 = new THREE.PointLight(0xff3060, 0.7, 100);
            pl2.position.set(-4, -2, 2);
            scene.add(pl2);
            const pl3 = new THREE.PointLight(0x40d0ff, 0.5, 100);
            pl3.position.set(0, 3, -8);
            scene.add(pl3);

            animate();
            initOrbitControls();
        }

        function animate() {
            requestAnimationFrame(animate);
            const t = Date.now() * 0.001;

            if (!orbit.isDragging) {
                if (orbit.autoRotate) {
                    orbit.theta += orbit.autoRotateSpeed;
                } else {
                    orbit.velTheta *= 0.92;
                    orbit.velPhi   *= 0.92;
                    orbit.theta += orbit.velTheta;
                    orbit.phi = Math.max(PHI_MIN, Math.min(PHI_MAX, orbit.phi + orbit.velPhi));
                }
                setCameraFromOrbit();
            }

            if (mosquito) {
                mosquito.position.y = 0.3 + Math.sin(t * 1.8) * 0.06;
                mosquito.rotation.x = Math.sin(t * 2.3) * 0.03;
                mosquito.rotation.z = Math.sin(t * 1.5) * 0.02;

                if (wingL) {
                    wingL.rotation.z =  Math.sin(t * 18.0)               * 0.55;
                    wingL.rotation.x =  Math.sin(t * 17.0 + 0.4)         * 0.12;
                    wingL.rotation.y =  Math.sin(t * 19.0 + 0.8)         * 0.08;
                }
                if (wingR) {
                    wingR.rotation.z = -Math.sin(t * 18.5 + 0.2)         * 0.55;
                    wingR.rotation.x =  Math.sin(t * 17.5 + 1.1)         * 0.12;
                    wingR.rotation.y = -Math.sin(t * 19.5 + 0.5)         * 0.08;
                }

                legGroups.forEach(({ g, side, li }) => {
                    const phase = li * 0.7 + (side > 0 ? 1.2 : 0);
                    g.rotation.z = side * (Math.sin(t * 2.2 + phase) * 0.04);
                    g.rotation.x = Math.sin(t * 1.8 + phase * 0.5) * 0.03;
                });
            }

            renderer.render(scene, camera);
        }

        window.addEventListener('resize', () => {
            updateSize();
            initBackground();
            if (camera) updateCamera();
        });

        window.addEventListener('load', init);
    </script>
</body>
</html>

Model V: The Bumble Bee

To conclude our exploration of procedurally generated insectoids, we approach the Bumble Bee. This model is thicker, softer, and more textured than its predecessors, pushing our primitive geometry architecture to accommodate organic bulk, body fuzz, and specialized anatomy like pollen baskets.

Algorithmic Fuzz and Texturing

Unlike the slick carapaces of the spider or scorpion, a bumble bee is famously fuzzy. In the absence of complex shader materials, we can achieve a striking stylized fur effect by algorithmically scattering mismatched THREE.SphereGeometry meshes across the thorax and abdomen segments.

function addFuzz(group, cx, cy, cz, radius, count, color) {
    for (let i = 0; i < count; i++) {
        // Distribute spherically using random angles (phi and theta)
        const phi   = Math.acos(2 * Math.random() - 1);
        const theta = Math.random() * Math.PI * 2;
        
        // Push slightly outward past the base radius
        const r     = radius + 0.015 + Math.random() * 0.04;
        const fGeo  = new THREE.SphereGeometry(0.018 + Math.random()*0.014, 4, 4);
        
        const fuzz  = new THREE.Mesh(fGeo, mat(color));
        fuzz.position.set(
            cx + r * Math.sin(phi) * Math.cos(theta),
            cy + r * Math.sin(phi) * Math.sin(theta),
            cz + r * Math.cos(phi)
        );
        group.add(fuzz);
    }
}

By scattering these low-poly fuzz particles dynamically, the model immediately gains physical volume and a soft, scattered light-catching silhouette.

Specialized Limb Structures

Procedural generation excels at easily mutating existing patterns based on conditional loops. The bee’s six legs function via identical parametric logic, but we inject a conditional branch (isHind) during the leg generation loop. When the index matches the hind legs, it drastically thickens the tibia geometry and injects bright yellow pollen basket meshes directly onto the limb.

if (isHind) {
    const pbGeo = new THREE.SphereGeometry(0.10, 10, 8);
    const pb = new THREE.Mesh(pbGeo, mat(colors.pollen));
    pb.scale.set(0.6, 1.1, 0.55);
    pb.position.set(s*0.92, -0.80, -0.02);
    group.add(pb);
    // ... algorithmic distribution of inner pollen strands ...
}

These layered techniques—combining trigonometric scattering with nested pivots and simple materials—demonstrate that you do not always need sophisticated 3D modeling suites to create compelling, living scenes within WebGL.


Complete Bumble Bee Source Code

If you wish to experiment with the bumble bee model programmatically in your own local environment, here is the complete, self-contained HTML payload used for the bumble bee demonstration above.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ThreeJS: Bumble Bee</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { background: #000; overflow: hidden; height: 100vh; }
        #bgCanvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; }
        #animationCanvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 2; cursor: grab; }
        #animationCanvas.dragging { cursor: grabbing; }
        #hint { position: absolute; bottom: 18px; left: 50%; transform: translateX(-50%); z-index: 10;
            color: rgba(255, 200, 40, 0.5); font-family: monospace; font-size: 12px; letter-spacing: 0.12em;
            pointer-events: none; transition: opacity 2s; user-select: none; }
    </style>
</head>
<body>
    <canvas id="bgCanvas"></canvas>
    <canvas id="animationCanvas"></canvas>
    <script>
        const colors = {
            yellow:  0xffc020,
            black:   0x1a1200,
            thorax:  0xe8a000,
            wing:    0xffe090,
            vein:    0xfff4c0,
            eye:     0x203000,
            leg:     0x2a1a00,
            pollen:  0xffdd00,
            stinger: 0x3a2800,
            fuzz:    0xffd060,
            stripe:  0xfff0a0,
        };

        const bgCanvas = document.getElementById('bgCanvas');
        const bgCtx = bgCanvas.getContext('2d');
        const px = 7;
        let cols, rows, pixels = [];

        function initBackground() {
            bgCanvas.width = window.innerWidth;
            bgCanvas.height = window.innerHeight;
            cols = Math.ceil(bgCanvas.width / px);
            rows = Math.ceil(bgCanvas.height / px);
            pixels = [];
            for (let i = 0; i < cols * rows; i++) {
                pixels.push({ b: Math.random() * 0.3, speed: 0.001 + Math.random() * 0.003 });
            }
        }

        function animateBackground() {
            for (let y = 0; y < rows; y++) {
                for (let x = 0; x < cols; x++) {
                    const p = pixels[y * cols + x];
                    p.b += p.speed;
                    if (p.b > 0.4) p.b = 0;
                    bgCtx.fillStyle = `rgb(${Math.floor(p.b*50)},${Math.floor(p.b*32)},${Math.floor(p.b*5)})`;
                    bgCtx.fillRect(x * px, y * px, px, px);
                }
            }
            requestAnimationFrame(animateBackground);
        }

        const renderer = new THREE.WebGLRenderer({
            canvas: document.getElementById('animationCanvas'),
            antialias: true, alpha: true
        });

        let scene, camera, bee;
        let wingFL, wingFR, wingHL, wingHR;
        let legGroups = [];

        const orbit = {
            theta: 0.4, phi: 1.0, radius: 8,
            isDragging: false, lastX: 0, lastY: 0,
            autoRotate: true, autoRotateSpeed: 0.003,
            velTheta: 0, velPhi: 0,
        };
        const PHI_MIN = 0.05, PHI_MAX = Math.PI * 0.95;
        const RADIUS_MIN = 3, RADIUS_MAX = 20;

        function setCameraFromOrbit() {
            const x = orbit.radius * Math.sin(orbit.phi) * Math.sin(orbit.theta);
            const y = orbit.radius * Math.cos(orbit.phi);
            const z = orbit.radius * Math.sin(orbit.phi) * Math.cos(orbit.theta);
            camera.position.set(x, y, z);
            camera.lookAt(0, 0, 0);
            camera.updateProjectionMatrix();
        }

        function updateSize() {
            renderer.setSize(window.innerWidth, window.innerHeight);
            renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
        }

        function updateCamera() {
            const w = window.innerWidth;
            orbit.radius = w < 480 ? 12 : w < 768 ? 10 : 8;
            camera.fov = 52;
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            setCameraFromOrbit();
        }

        function mat(color) {
            return new THREE.MeshBasicMaterial({ color, wireframe: true });
        }

        function addFuzz(group, cx, cy, cz, radius, count, color) {
            for (let i = 0; i < count; i++) {
                const phi   = Math.acos(2 * Math.random() - 1);
                const theta = Math.random() * Math.PI * 2;
                const r     = radius + 0.015 + Math.random() * 0.04;
                const fGeo  = new THREE.SphereGeometry(0.018 + Math.random()*0.014, 4, 4);
                const fuzz  = new THREE.Mesh(fGeo, mat(color));
                fuzz.position.set(
                    cx + r * Math.sin(phi) * Math.cos(theta),
                    cy + r * Math.sin(phi) * Math.sin(theta),
                    cz + r * Math.cos(phi)
                );
                group.add(fuzz);
            }
        }

        function makeWing(side, isFore) {
            const group = new THREE.Group();
            const s = side;

            if (isFore) {
                const shape = new THREE.Shape();
                shape.moveTo(0, 0);
                shape.bezierCurveTo(s*0.4, 0.30, s*1.1, 0.55, s*1.7, 0.40);
                shape.bezierCurveTo(s*2.1, 0.28, s*2.2, 0.05, s*2.0, -0.18);
                shape.bezierCurveTo(s*1.7, -0.38, s*1.0, -0.45, s*0.4, -0.28);
                shape.bezierCurveTo(s*0.12, -0.16, 0, -0.06, 0, 0);
                const geo = new THREE.ShapeGeometry(shape, 14);
                group.add(new THREE.Mesh(geo, mat(colors.wing)));
                group.add(new THREE.LineSegments(new THREE.EdgesGeometry(geo),
                    new THREE.LineBasicMaterial({ color: colors.vein })));

                [
                    { pts: [[0,0],[s*0.7,0.36],[s*1.5,0.36],[s*2.0,0.16]], r: 0.012 },
                    { pts: [[s*0.05,-0.05],[s*0.6,0.22],[s*1.3,0.22],[s*1.85,0.04]], r: 0.009 },
                    { pts: [[s*0.08,-0.08],[s*0.5,0.06],[s*1.1,0.02],[s*1.7,-0.12]], r: 0.008 },
                    { pts: [[s*0.1,-0.12],[s*0.5,-0.10],[s*1.0,-0.18],[s*1.6,-0.30]], r: 0.007 },
                ].forEach(v => {
                    const pts = v.pts.map(p => new THREE.Vector3(p[0], p[1], 0.01));
                    const geo2 = new THREE.TubeGeometry(new THREE.CatmullRomCurve3(pts), 8, v.r, 4, false);
                    group.add(new THREE.Mesh(geo2, mat(colors.vein)));
                });
                [[s*0.55,0.22,s*0.60,-0.04],[s*1.0,0.20,s*1.05,-0.10],[s*1.4,0.14,s*1.45,-0.20]].forEach(cv => {
                    const pts = [new THREE.Vector3(cv[0],cv[1],0.01), new THREE.Vector3(cv[2],cv[3],0.01)];
                    group.add(new THREE.Mesh(
                        new THREE.TubeGeometry(new THREE.CatmullRomCurve3(pts), 3, 0.005, 4, false),
                        mat(colors.vein)
                    ));
                });

            } else {
                const shape2 = new THREE.Shape();
                shape2.moveTo(0, 0);
                shape2.bezierCurveTo(s*0.3, 0.18, s*0.9, 0.32, s*1.3, 0.20);
                shape2.bezierCurveTo(s*1.6, 0.10, s*1.6, -0.10, s*1.4, -0.22);
                shape2.bezierCurveTo(s*1.1, -0.34, s*0.5, -0.30, s*0.15, -0.14);
                shape2.bezierCurveTo(s*0.04, -0.06, 0, 0, 0, 0);
                const geo3 = new THREE.ShapeGeometry(shape2, 10);
                group.add(new THREE.Mesh(geo3, mat(colors.wing)));
                group.add(new THREE.LineSegments(new THREE.EdgesGeometry(geo3),
                    new THREE.LineBasicMaterial({ color: colors.vein })));

                [[s*0.05,-0.03,s*0.8,0.18,s*1.25,0.06],[s*0.08,-0.08,s*0.6,0.00,s*1.1,-0.14]].forEach(v => {
                    const pts = [
                        new THREE.Vector3(v[0],v[1],0.01),
                        new THREE.Vector3(v[2],v[3],0.01),
                        new THREE.Vector3(v[4],v[5],0.01)
                    ];
                    group.add(new THREE.Mesh(
                        new THREE.TubeGeometry(new THREE.CatmullRomCurve3(pts), 6, 0.007, 4, false),
                        mat(colors.vein)
                    ));
                });
            }
            return group;
        }

        function makeLeg(side, index) {
            const group = new THREE.Group();
            const s = side;
            const isHind = index === 2;

            const cPts = [new THREE.Vector3(0,0,0), new THREE.Vector3(s*0.22,-0.10,0.02)];
            group.add(new THREE.Mesh(
                new THREE.TubeGeometry(new THREE.CatmullRomCurve3(cPts), 4, 0.030, 6, false),
                mat(colors.black)
            ));

            const fPts = [
                new THREE.Vector3(s*0.22,-0.10,0.02),
                new THREE.Vector3(s*0.50,-0.38,0.02),
                new THREE.Vector3(s*0.70,-0.55,0.00)
            ];
            group.add(new THREE.Mesh(
                new THREE.TubeGeometry(new THREE.CatmullRomCurve3(fPts), 6, 0.028, 6, false),
                mat(isHind ? colors.yellow : colors.black)
            ));

            const tPts = [
                new THREE.Vector3(s*0.70,-0.55,0.00),
                new THREE.Vector3(s*0.95,-0.82,-0.02),
                new THREE.Vector3(s*1.10,-1.10,-0.04)
            ];
            group.add(new THREE.Mesh(
                new THREE.TubeGeometry(new THREE.CatmullRomCurve3(tPts), 6, isHind ? 0.032 : 0.022, 6, false),
                mat(isHind ? colors.yellow : colors.black)
            ));

            if (isHind) {
                const pbGeo = new THREE.SphereGeometry(0.10, 10, 8);
                const pb = new THREE.Mesh(pbGeo, mat(colors.pollen));
                pb.scale.set(0.6, 1.1, 0.55);
                pb.position.set(s*0.92, -0.80, -0.02);
                group.add(pb);
                for (let i = 0; i < 5; i++) {
                    const ang = (i / 5) * Math.PI * 0.8 - 0.2;
                    const sPts = [
                        new THREE.Vector3(s*0.92, -0.80, -0.02),
                        new THREE.Vector3(s*(0.92 + Math.cos(ang)*0.08), -0.80 + Math.sin(ang)*0.08, -0.10)
                    ];
                    group.add(new THREE.Mesh(
                        new THREE.TubeGeometry(new THREE.CatmullRomCurve3(sPts), 3, 0.004, 3, false),
                        mat(colors.pollen)
                    ));
                }
            }

            let tStart = new THREE.Vector3(s*1.10,-1.10,-0.04);
            for (let ti = 0; ti < 3; ti++) {
                const tEnd = new THREE.Vector3(
                    tStart.x + s*0.18,
                    tStart.y - 0.18 + ti*0.04,
                    tStart.z - 0.02
                );
                group.add(new THREE.Mesh(
                    new THREE.TubeGeometry(new THREE.CatmullRomCurve3([tStart, tEnd]), 4,
                        0.016 - ti*0.003, 5, false),
                    mat(ti % 2 === 0 ? colors.black : colors.yellow)
                ));
                tStart = tEnd;
            }
            const clGeo = new THREE.ConeGeometry(0.010, 0.06, 5);
            const cl = new THREE.Mesh(clGeo, mat(colors.fuzz));
            cl.rotation.z = s * -0.5;
            cl.position.copy(tStart).addScaledVector(new THREE.Vector3(s,0,-0.5).normalize(), 0.04);
            group.add(cl);

            return group;
        }

        function createBee() {
            const group = new THREE.Group();
            legGroups = [];

            const abdData = [
                { r: 0.30, col: colors.yellow, z: 0.12 },
                { r: 0.36, col: colors.black,  z: 0.42 },
                { r: 0.38, col: colors.yellow, z: 0.72 },
                { r: 0.33, col: colors.black,  z: 1.00 },
                { r: 0.22, col: colors.yellow, z: 1.26 },
                { r: 0.12, col: colors.black,  z: 1.48 },
            ];
            abdData.forEach((s, i) => {
                const next = abdData[i + 1];
                const rTop = next ? next.r : s.r * 0.5;
                const geo = new THREE.CylinderGeometry(rTop * 0.92, s.r, 0.32, 16, 1);
                const seg = new THREE.Mesh(geo, mat(s.col));
                seg.rotation.x = Math.PI * 0.5;
                seg.position.set(0, 0, s.z);
                group.add(seg);

                if (s.col === colors.yellow) {
                    addFuzz(group, 0, 0, s.z, s.r, 18, colors.fuzz);
                }

                const ringGeo = new THREE.TorusGeometry(s.r + 0.01, 0.010, 6, 18);
                const ring = new THREE.Mesh(ringGeo, mat(colors.stripe));
                ring.position.set(0, 0, s.z);
                group.add(ring);
            });

            const stingPts = [
                new THREE.Vector3(0, 0, 1.62),
                new THREE.Vector3(0, -0.04, 1.76),
                new THREE.Vector3(0, -0.06, 1.88),
            ];
            group.add(new THREE.Mesh(
                new THREE.TubeGeometry(new THREE.CatmullRomCurve3(stingPts), 6, 0.016, 5, false),
                mat(colors.stinger)
            ));
            const stingTip = new THREE.Mesh(new THREE.ConeGeometry(0.010, 0.05, 5), mat(colors.stinger));
            stingTip.rotation.x = 0.4;
            stingTip.position.set(0, -0.07, 1.92);
            group.add(stingTip);

            const txGeo = new THREE.SphereGeometry(0.42, 18, 16);
            const thorax = new THREE.Mesh(txGeo, mat(colors.thorax));
            thorax.scale.set(1.0, 1.05, 1.15);
            thorax.position.set(0, 0.06, -0.28);
            group.add(thorax);
            addFuzz(group, 0, 0.06, -0.28, 0.44, 60, colors.fuzz);

            const proGeo = new THREE.TorusGeometry(0.30, 0.040, 8, 20);
            const pro = new THREE.Mesh(proGeo, mat(colors.yellow));
            pro.rotation.x = Math.PI * 0.5;
            pro.position.set(0, 0.08, -0.48);
            group.add(pro);

            const headGeo = new THREE.SphereGeometry(0.26, 16, 14);
            const head = new THREE.Mesh(headGeo, mat(colors.black));
            head.scale.set(1.0, 0.95, 1.0);
            head.position.set(0, 0.00, -0.78);
            group.add(head);
            addFuzz(group, 0, 0.00, -0.78, 0.27, 22, colors.fuzz);

            [-1, 1].forEach(side => {
                const eyeGeo = new THREE.SphereGeometry(0.14, 14, 12);
                const eye = new THREE.Mesh(eyeGeo, mat(colors.eye));
                eye.scale.set(0.70, 0.90, 0.80);
                eye.position.set(side * 0.21, 0.04, -0.80);
                group.add(eye);
                for (let fi = 0; fi < 6; fi++) {
                    const fGeo = new THREE.CircleGeometry(0.035, 6);
                    const facet = new THREE.Mesh(fGeo, mat(colors.fuzz));
                    const ang = (fi / 6) * Math.PI * 1.6 - 0.4;
                    facet.position.set(
                        side * (0.24 + Math.cos(ang)*0.04),
                        0.04 + Math.sin(ang)*0.06,
                        -0.78
                    );
                    group.add(facet);
                }
            });

            [-1, 1].forEach(side => {
                const scPts = [
                    new THREE.Vector3(side*0.08, 0.14, -0.90),
                    new THREE.Vector3(side*0.14, 0.28, -0.96),
                    new THREE.Vector3(side*0.18, 0.40, -0.98),
                ];
                group.add(new THREE.Mesh(
                    new THREE.TubeGeometry(new THREE.CatmullRomCurve3(scPts), 6, 0.016, 5, false),
                    mat(colors.black)
                ));
                const pelGeo = new THREE.SphereGeometry(0.028, 7, 6);
                const pel = new THREE.Mesh(pelGeo, mat(colors.yellow));
                pel.position.set(side*0.18, 0.42, -0.98);
                group.add(pel);
                const flPts = [
                    new THREE.Vector3(side*0.18, 0.42, -0.98),
                    new THREE.Vector3(side*0.32, 0.46, -0.96),
                    new THREE.Vector3(side*0.44, 0.40, -0.90),
                    new THREE.Vector3(side*0.52, 0.32, -0.84),
                ];
                group.add(new THREE.Mesh(
                    new THREE.TubeGeometry(new THREE.CatmullRomCurve3(flPts), 8, 0.011, 5, false),
                    mat(colors.black)
                ));
                const knob = new THREE.Mesh(new THREE.SphereGeometry(0.022, 6, 6), mat(colors.yellow));
                knob.position.set(side*0.52, 0.32, -0.84);
                group.add(knob);
            });

            const probPts = [
                new THREE.Vector3(0, -0.14, -0.90),
                new THREE.Vector3(0, -0.22, -1.00),
                new THREE.Vector3(0, -0.26, -1.12),
            ];
            group.add(new THREE.Mesh(
                new THREE.TubeGeometry(new THREE.CatmullRomCurve3(probPts), 6, 0.018, 5, false),
                mat(colors.yellow)
            ));
            const tongueGeo = new THREE.SphereGeometry(0.025, 7, 6);
            const tongue = new THREE.Mesh(tongueGeo, mat(colors.fuzz));
            tongue.position.set(0, -0.28, -1.14);
            group.add(tongue);

            [-1, 1].forEach(side => {
                const mPts = [
                    new THREE.Vector3(side*0.08, -0.10, -0.92),
                    new THREE.Vector3(side*0.16, -0.18, -1.02),
                ];
                group.add(new THREE.Mesh(
                    new THREE.TubeGeometry(new THREE.CatmullRomCurve3(mPts), 3, 0.014, 4, false),
                    mat(colors.black)
                ));
            });

            wingFL = new THREE.Group();
            wingFL.position.set(0, 0.16, -0.30);
            wingFL.rotation.z =  0.08;
            wingFL.rotation.x = -0.12;
            wingFL.add(makeWing(-1, true));
            group.add(wingFL);

            wingFR = new THREE.Group();
            wingFR.position.set(0, 0.16, -0.30);
            wingFR.rotation.z = -0.08;
            wingFR.rotation.x = -0.12;
            wingFR.add(makeWing(1, true));
            group.add(wingFR);

            wingHL = new THREE.Group();
            wingHL.position.set(0, 0.10, -0.10);
            wingHL.rotation.z =  0.12;
            wingHL.rotation.x = -0.10;
            wingHL.add(makeWing(-1, false));
            group.add(wingHL);

            wingHR = new THREE.Group();
            wingHR.position.set(0, 0.10, -0.10);
            wingHR.rotation.z = -0.12;
            wingHR.rotation.x = -0.10;
            wingHR.add(makeWing(1, false));
            group.add(wingHR);

            [-1, 1].forEach(side => {
                for (let li = 0; li < 3; li++) {
                    const legGroup = makeLeg(side, li);
                    legGroup.position.set(side * 0.38, -0.08, -0.45 + li * 0.30);
                    group.add(legGroup);
                    legGroups.push({ g: legGroup, side, li });
                }
            });

            group.position.set(0, 0.2, 0.2);
            return group;
        }

        function initOrbitControls() {
            const canvas = document.getElementById('animationCanvas');
            const hint   = document.getElementById('hint');

            function onDragStart(x, y) {
                orbit.isDragging = true; orbit.autoRotate = false;
                orbit.lastX = x; orbit.lastY = y;
                orbit.velTheta = 0; orbit.velPhi = 0;
                canvas.classList.add('dragging');
                hint.style.opacity = '0';
            }
            function onDragMove(x, y) {
                if (!orbit.isDragging) return;
                const dx = x - orbit.lastX, dy = y - orbit.lastY;
                const speed = 0.005;
                orbit.velTheta = -dx * speed;
                orbit.velPhi   = -dy * speed;
                orbit.theta += orbit.velTheta;
                orbit.phi = Math.max(PHI_MIN, Math.min(PHI_MAX, orbit.phi + orbit.velPhi));
                orbit.lastX = x; orbit.lastY = y;
                setCameraFromOrbit();
            }
            function onDragEnd() {
                orbit.isDragging = false;
                canvas.classList.remove('dragging');
                setTimeout(() => { orbit.autoRotate = true; }, 2000);
            }

            canvas.addEventListener('mousedown', e => onDragStart(e.clientX, e.clientY));
            window.addEventListener('mousemove', e => onDragMove(e.clientX, e.clientY));
            window.addEventListener('mouseup',   () => onDragEnd());
            canvas.addEventListener('touchstart', e => { e.preventDefault(); onDragStart(e.touches[0].clientX, e.touches[0].clientY); }, { passive: false });
            window.addEventListener('touchmove',  e => { e.preventDefault(); onDragMove(e.touches[0].clientX, e.touches[0].clientY); }, { passive: false });
            window.addEventListener('touchend',   () => onDragEnd());
            canvas.addEventListener('wheel', e => {
                e.preventDefault();
                orbit.radius = Math.max(RADIUS_MIN, Math.min(RADIUS_MAX, orbit.radius + e.deltaY * 0.01));
                setCameraFromOrbit();
            }, { passive: false });

            setTimeout(() => { hint.style.opacity = '0'; }, 3000);
        }

        function init() {
            initBackground();
            animateBackground();
            updateSize();

            scene = new THREE.Scene();
            scene.background = new THREE.Color(0x000000);

            camera = new THREE.PerspectiveCamera(52, window.innerWidth / window.innerHeight, 0.1, 1000);
            updateCamera();

            bee = createBee();
            scene.add(bee);

            scene.add(new THREE.AmbientLight(0xffcc60, 0.55));
            const pl1 = new THREE.PointLight(0xffa020, 1.8, 100);
            pl1.position.set(4, 5, 3);
            scene.add(pl1);
            const pl2 = new THREE.PointLight(0xffd060, 0.9, 100);
            pl2.position.set(-4, -2, 2);
            scene.add(pl2);
            const pl3 = new THREE.PointLight(0xffffff, 0.4, 100);
            pl3.position.set(0, 3, -8);
            scene.add(pl3);

            animate();
            initOrbitControls();
        }

        function animate() {
            requestAnimationFrame(animate);
            const t = Date.now() * 0.001;

            if (!orbit.isDragging) {
                if (orbit.autoRotate) {
                    orbit.theta += orbit.autoRotateSpeed;
                } else {
                    orbit.velTheta *= 0.92; orbit.velPhi *= 0.92;
                    orbit.theta += orbit.velTheta;
                    orbit.phi = Math.max(PHI_MIN, Math.min(PHI_MAX, orbit.phi + orbit.velPhi));
                }
                setCameraFromOrbit();
            }

            if (bee) {
                bee.position.y = 0.2 + Math.sin(t * 2.2) * 0.08;
                bee.rotation.x = Math.sin(t * 1.6) * 0.04;
                bee.rotation.z = Math.sin(t * 2.0 + 0.8) * 0.03;

                const buzz = t * 14.0;
                if (wingFL) {
                    wingFL.rotation.z =  0.08 + Math.sin(buzz)          * 0.60;
                    wingFL.rotation.x = -0.12 + Math.sin(buzz * 0.97)   * 0.10;
                }
                if (wingFR) {
                    wingFR.rotation.z = -0.08 - Math.sin(buzz + 0.05)   * 0.60;
                    wingFR.rotation.x = -0.12 + Math.sin(buzz * 0.97)   * 0.10;
                }
                if (wingHL) {
                    wingHL.rotation.z =  0.12 + Math.sin(buzz + 0.3)    * 0.45;
                    wingHL.rotation.x = -0.10 + Math.sin(buzz * 0.95)   * 0.08;
                }
                if (wingHR) {
                    wingHR.rotation.z = -0.12 - Math.sin(buzz + 0.35)   * 0.45;
                    wingHR.rotation.x = -0.10 + Math.sin(buzz * 0.95)   * 0.08;
                }

                legGroups.forEach(({ g, side, li }) => {
                    const phase = li * 0.9 + (side > 0 ? 1.4 : 0);
                    g.rotation.z = side * (0.05 + Math.sin(t * 2.0 + phase) * 0.05);
                    g.rotation.x = Math.sin(t * 1.7 + phase) * 0.04;
                });
            }

            renderer.render(scene, camera);
        }

        window.addEventListener('resize', () => {
            updateSize();
            initBackground();
            if (camera) updateCamera();
        });

        window.addEventListener('load', init);
    </script>
</body>
</html>

Model VI: The Butterfly

To cap off our collection of WebGL insects, we pivot from the earthy and armored towards something lighter: the Butterfly. Rather than focusing on heavy segments or high-frequency buzzing, this model centers on sweeping, graceful arcs and vividly painted, large-surface wings that flap in slow, elegant sine waves.

Generative Wing Profiles

The sweeping profile of a butterfly wing is notoriously complex to model mathematically. Instead of using raw mathematical functions, we construct a 2D bezier-driven THREE.Shape containing multiple connected arcs spanning outward.

const shape = new THREE.Shape();
shape.moveTo(0, 0);
// Upper wing sweeping span
shape.bezierCurveTo(s * 0.8, 0.5, s * 1.6, 1.0, s * 2.0, 0.4);
shape.bezierCurveTo(s * 2.2, 0.0, s * 1.9, -0.5, s * 1.0, -0.7);
shape.bezierCurveTo(s * 0.5, -0.8, s * 0.1, -0.5, 0, 0);

const geo = new THREE.ShapeGeometry(shape, 14);
const mesh = new THREE.Mesh(geo, wireframeMaterial(colors.wing1));

This ShapeGeometry allows the canvas renderer to effortlessly triangulate smooth arcs, giving the butterfly its trademark expansive wing silhouette. Overlaid upon this structure are procedurally driven THREE.CatmullRomCurve3 vein conduits and distinct, isolated glowing spots populated via THREE.CircleGeometry.

Rhythmic Flap Oscillations

While the mosquito wings flutter aggressively using high scalar multiples, a butterfly moves rhythmically. By utilizing nested pivot groups (leftWingPivot and rightWingPivot) fixed around the butterfly’s thorax, we can cleanly isolate the wings’ Y-axis rotation and drive it through a smooth oscillation:

butterfly.position.y = Math.sin(time * 1.4) * 0.12;

// Rhythmic, slow flap
const flapAngle = Math.sin(time * 3.5) * 0.55;
if (leftUpper) leftUpper.rotation.y = flapAngle;
if (rightUpper) rightUpper.rotation.y = -flapAngle;

Coupled with a slow positional sway on the Y-axis and an ongoing ambient rotation, the butterfly feels gentle, floating through the space effortlessly.


Complete Butterfly Source Code

If you wish to experiment with the butterfly model programmatically in your own local environment, here is the complete, self-contained HTML payload used for the butterfly demonstration above.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
    <title>ThreeJS: Butterfly</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body, html {
            width: 100%;
            height: 100%;
            overflow: hidden;
            background: #000000;
        }

        #bfly-wrap {
            position: relative;
            width: 100vw;
            height: 100vh;
            background: #000000;
            overflow: hidden;
        }

        #animC {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            display: block;
            z-index: 2;
        }
    </style>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
</head>
<body>
<div id="bfly-wrap">
    <canvas id="animC"></canvas>
</div>

<script>
    (function() {
        const colors = {
            wing1: 0x5bcaaa,
            wing2: 0x1d9e75,
            body: 0x9fe1cb,
            vein: 0xe1f5ee,
            dark: 0x04342c,
            spot: 0xfac775
        };

        const canvasEl = document.getElementById('animC');
        const renderer = new THREE.WebGLRenderer({
            canvas: canvasEl,
            antialias: true,
            alpha: false
        });

        let scene, camera, butterfly;
        let leftUpper, rightUpper;

        function updateSize() {
            const wrap = document.getElementById('bfly-wrap');
            const width = wrap.offsetWidth;
            const height = wrap.offsetHeight;
            renderer.setSize(width, height);
            renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
            if (camera) {
                camera.aspect = width / height;
                camera.updateProjectionMatrix();
            }
        }

        function wireframeMaterial(color) {
            return new THREE.MeshBasicMaterial({
                color: color,
                wireframe: true,
                transparent: false
            });
        }

        function makeWingShape(side, isUpper) {
            const group = new THREE.Group();
            const s = side;

            if (isUpper) {
                const shape = new THREE.Shape();
                shape.moveTo(0, 0);
                shape.bezierCurveTo(s * 0.8, 0.5, s * 1.6, 1.0, s * 2.0, 0.4);
                shape.bezierCurveTo(s * 2.2, 0.0, s * 1.9, -0.5, s * 1.0, -0.7);
                shape.bezierCurveTo(s * 0.5, -0.8, s * 0.1, -0.5, 0, 0);
                const geo = new THREE.ShapeGeometry(shape, 14);
                const mesh = new THREE.Mesh(geo, wireframeMaterial(colors.wing1));
                group.add(mesh);

                const edgeGeo = new THREE.EdgesGeometry(geo);
                const edgeMat = new THREE.LineBasicMaterial({ color: colors.vein });
                const wireEdges = new THREE.LineSegments(edgeGeo, edgeMat);
                group.add(wireEdges);

                for (let i = 0; i < 5; i++) {
                    const pt1 = new THREE.Vector3(0, 0, 0.01);
                    const pt2 = new THREE.Vector3(s * (0.5 + i * 0.25), (0.3 - i * 0.1), 0.01);
                    const pt3 = new THREE.Vector3(s * (0.8 + i * 0.3), (0.1 - i * 0.15), 0.01);
                    const curve = new THREE.CatmullRomCurve3([pt1, pt2, pt3]);
                    const tubeGeo = new THREE.TubeGeometry(curve, 5, 0.012, 4, false);
                    const tubeMat = wireframeMaterial(colors.vein);
                    group.add(new THREE.Mesh(tubeGeo, tubeMat));
                }

                for (let i = 0; i < 3; i++) {
                    const spotGeo = new THREE.CircleGeometry(0.08 + i * 0.03, 8);
                    const spotMat = wireframeMaterial(colors.spot);
                    const spot = new THREE.Mesh(spotGeo, spotMat);
                    spot.position.set(s * (0.9 + i * 0.5), 0.2 - i * 0.15, 0.02);
                    group.add(spot);
                }
            } else {
                const lowerShape = new THREE.Shape();
                lowerShape.moveTo(0, 0);
                lowerShape.bezierCurveTo(s * 0.5, -0.1, s * 1.3, 0.1, s * 1.6, -0.3);
                lowerShape.bezierCurveTo(s * 1.8, -0.6, s * 1.5, -1.1, s * 0.8, -1.2);
                lowerShape.bezierCurveTo(s * 0.3, -1.2, s * 0.1, -0.8, 0, 0);
                const geoLower = new THREE.ShapeGeometry(lowerShape, 12);
                const lowerMesh = new THREE.Mesh(geoLower, wireframeMaterial(colors.wing2));
                group.add(lowerMesh);

                const lowerEdges = new THREE.EdgesGeometry(geoLower);
                const edgeLowerMat = new THREE.LineBasicMaterial({ color: colors.vein });
                group.add(new THREE.LineSegments(lowerEdges, edgeLowerMat));

                for (let i = 0; i < 3; i++) {
                    const spotGeo = new THREE.CircleGeometry(0.07, 7);
                    const spotMat = wireframeMaterial(colors.spot);
                    const spot = new THREE.Mesh(spotGeo, spotMat);
                    spot.position.set(s * (0.5 + i * 0.4), -0.4 - i * 0.2, 0.02);
                    group.add(spot);
                }
            }

            return group;
        }

        function createButterfly() {
            const group = new THREE.Group();

            const bodySegments = 7;
            for (let i = 0; i < bodySegments; i++) {
                const radius = 0.09 - i * 0.008;
                const yPos = 0.5 - i * 0.22;
                const useDark = (i % 2 === 0);
                const bodyMat = wireframeMaterial(useDark ? colors.dark : colors.body);
                const segmentGeo = new THREE.SphereGeometry(radius, 10, 10);
                const segment = new THREE.Mesh(segmentGeo, bodyMat);
                segment.position.set(0, yPos, 0);
                segment.scale.set(0.8, 1.0, 0.8);
                group.add(segment);
            }

            const headGeo = new THREE.SphereGeometry(0.13, 12, 12);
            const headMat = wireframeMaterial(colors.body);
            const head = new THREE.Mesh(headGeo, headMat);
            head.position.set(0, 0.72, 0);
            group.add(head);

            [-1, 1].forEach(side => {
                const eyeGeo = new THREE.SphereGeometry(0.04, 7, 7);
                const eyeMat = wireframeMaterial(colors.vein);
                const eye = new THREE.Mesh(eyeGeo, eyeMat);
                eye.position.set(side * 0.1, 0.76, 0.08);
                group.add(eye);

                const antPoints = [
                    new THREE.Vector3(side * 0.07, 0.82, 0),
                    new THREE.Vector3(side * 0.2, 1.1, 0.05),
                    new THREE.Vector3(side * 0.28, 1.35, 0.02)
                ];
                const antCurve = new THREE.CatmullRomCurve3(antPoints);
                const antTubeGeo = new THREE.TubeGeometry(antCurve, 8, 0.012, 4, false);
                const antMat = wireframeMaterial(colors.vein);
                group.add(new THREE.Mesh(antTubeGeo, antMat));

                const knobGeo = new THREE.SphereGeometry(0.035, 7, 7);
                const knobMat = wireframeMaterial(colors.spot);
                const knob = new THREE.Mesh(knobGeo, knobMat);
                knob.position.set(side * 0.28, 1.38, 0.02);
                group.add(knob);

                const legYPositions = [0.2, 0.0, -0.2];
                legYPositions.forEach(legY => {
                    const legPoints = [
                        new THREE.Vector3(side * 0.08, legY, 0),
                        new THREE.Vector3(side * 0.25, legY - 0.15, 0.05),
                        new THREE.Vector3(side * 0.35, legY - 0.35, 0.0)
                    ];
                    const legCurve = new THREE.CatmullRomCurve3(legPoints);
                    const legGeo = new THREE.TubeGeometry(legCurve, 5, 0.012, 4, false);
                    const legMat = wireframeMaterial(colors.dark);
                    group.add(new THREE.Mesh(legGeo, legMat));
                });
            });

            const leftWingPivot = new THREE.Group();
            const rightWingPivot = new THREE.Group();

            const leftUpperWing = makeWingShape(-1, true);
            const leftLowerWing = makeWingShape(-1, false);
            const rightUpperWing = makeWingShape(1, true);
            const rightLowerWing = makeWingShape(1, false);

            leftWingPivot.add(leftUpperWing);
            leftWingPivot.add(leftLowerWing);
            rightWingPivot.add(rightUpperWing);
            rightWingPivot.add(rightLowerWing);

            group.add(leftWingPivot);
            group.add(rightWingPivot);

            leftUpper = leftWingPivot;
            rightUpper = rightWingPivot;

            group.position.set(0, 0, 0);
            return group;
        }

        function init() {
            updateSize();

            scene = new THREE.Scene();
            scene.background = new THREE.Color(0x000000);

            camera = new THREE.PerspectiveCamera(50, 1, 0.1, 1000);
            const wrap = document.getElementById('bfly-wrap');
            camera.aspect = wrap.offsetWidth / wrap.offsetHeight;
            camera.position.set(0, 0.2, 6);
            camera.lookAt(0, 0.2, 0);
            camera.updateProjectionMatrix();

            butterfly = createButterfly();
            scene.add(butterfly);

            const ambientLight = new THREE.AmbientLight(0x404060, 0.5);
            scene.add(ambientLight);
            const pointLight1 = new THREE.PointLight(0x5bcaaa, 0.9);
            pointLight1.position.set(3, 4, 4);
            scene.add(pointLight1);
            const pointLight2 = new THREE.PointLight(0xfac775, 0.7);
            pointLight2.position.set(-3, 2, 5);
            scene.add(pointLight2);

            animate();
        }

        function animate() {
            requestAnimationFrame(animate);

            const time = Date.now() * 0.001;

            if (butterfly) {
                butterfly.position.y = Math.sin(time * 1.4) * 0.12;
                butterfly.rotation.z = Math.sin(time * 2.0) * 0.02;
                butterfly.rotation.y += 0.004;

                const flapAngle = Math.sin(time * 3.5) * 0.55;
                if (leftUpper) leftUpper.rotation.y = flapAngle;
                if (rightUpper) rightUpper.rotation.y = -flapAngle;
            }

            renderer.render(scene, camera);
        }

        window.addEventListener('resize', () => {
            updateSize();
            if (camera) {
                const wrap = document.getElementById('bfly-wrap');
                camera.aspect = wrap.offsetWidth / wrap.offsetHeight;
                camera.updateProjectionMatrix();
            }
        });
        init();
    })();
</script>
</body>
</html>

Model VII: The Locust

To conclude our exploration of procedurally generated insectoids, we approach the Locust. This model represents the synthesis of everything we’ve built: complex armored plating, delicate anatomical sub-structures like wing vein networks, and highly specialized limb geometry designed for jumping.

Procedural Vein Scaffolding

While the wings are generated via THREE.ShapeGeometry just like the butterfly, the locust’s leathery forewings (tegmina) require structural detail. Instead of manually plotting an infinite series of cylinders, we drive the generation of the costa, radius, and medial veins purely through parametric offsets scaled tightly against the overarching wing metrics:

const veinDefs = [
    { xFrac: 0.95, label: 'costa',    r: 0.010 },
    { xFrac: 0.78, label: 'radius',   r: 0.008 },
    { xFrac: 0.55, label: 'medial',   r: 0.007 },
    { xFrac: 0.30, label: 'cubitus',  r: 0.006 },
    { xFrac: 0.05, label: 'anal',     r: 0.005 },
];

veinDefs.forEach(v => {
    const xBase = s * W * v.xFrac;
    const pts = [
        new THREE.Vector3(xBase * 0.1, 0.005, 0.05),
        // Intermediate points forming the arc
        new THREE.Vector3(xBase * 0.75, 0.005, L * 0.96),
    ];
    group.add(new THREE.Mesh(
        new THREE.TubeGeometry(new THREE.CatmullRomCurve3(pts), 8, v.r, 4),
        mat(colors.vein)
    ));
});

Because the veins are drawn algorithmically as fractions of the wing limits, adjusting the total width W or total length L effortlessly reflows the entire skeletal structure—a key advantage of programmatic generation over strict polygon modeling.

Specialized Jumping Legs

The saltatorial hind legs are the locust’s defining feature. Utilizing THREE.TubeGeometry for the muscular femurs and elongated tibiae, we add aggressive defensive spikes scattered specifically down the trailing edge of the tibia:

for (let si = 0; si < 7; si++) {
    const t = si / 6;
    // Interpolate base position along the tibia's length
    const base = new THREE.Vector3(
        s*0.82 + (s*0.82-s*0.82)*t,
        -0.30 + (-1.40+0.30)*t,
        0.80 + (1.80-0.80)*t
    );
    
    // Add symmetrical spines on both sides of the ridge
    [0.06, -0.06].forEach(offset => {
        const spPts = [
            base.clone(),
            new THREE.Vector3(base.x + s*0.04, base.y + offset, base.z + 0.08),
        ];
        group.add(new THREE.Mesh(
            new THREE.TubeGeometry(new THREE.CatmullRomCurve3(spPts), 3, 0.008, 3),
            mat(colors.spine)
        ));
    });
}

By abstracting these complex anatomical details—veins, spines, overlapping segmented plates—into isolated mathematical matrices, we end up with deeply satisfying, heavily articulated WebGL renders running entirely through code.


Complete Locust Source Code

If you wish to experiment with the locust model programmatically in your own local environment, here is the complete, self-contained HTML payload used for the locust demonstration above.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ThreeJS Locust</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { background: #000; overflow: hidden; height: 100vh; }
        #bgCanvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; }
        #animationCanvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 2; cursor: grab; }
        #animationCanvas.dragging { cursor: grabbing; }
        #hint { position: absolute; bottom: 18px; left: 50%; transform: translateX(-50%); z-index: 10;
            color: rgba(180, 210, 80, 0.55); font-family: monospace; font-size: 12px; letter-spacing: 0.12em;
            pointer-events: none; transition: opacity 2s; user-select: none; }
    </style>
</head>
<body>
    <canvas id="bgCanvas"></canvas>
    <canvas id="animationCanvas"></canvas>
    <script>
        const colors = {
            body:    0x8a7a30,
            dark:    0x2e2800,
            light:   0xc8b84a,
            tegmen:  0xa09030,
            hindwing:0x70c040,
            vein:    0xd8e878,
            eye:     0xd0e000,
            spine:   0xe8d060,
            pronotum:0x6a5e20,
            stripe:  0xf0e870,
            tarsus:  0x3a3010,
        };

        const bgCanvas = document.getElementById('bgCanvas');
        const bgCtx    = bgCanvas.getContext('2d');
        const px = 7;
        let cols, rows, pixels = [];

        function initBackground() {
            bgCanvas.width  = window.innerWidth;
            bgCanvas.height = window.innerHeight;
            cols = Math.ceil(bgCanvas.width / px);
            rows = Math.ceil(bgCanvas.height / px);
            pixels = [];
            for (let i = 0; i < cols * rows; i++)
                pixels.push({ b: Math.random() * 0.3, speed: 0.001 + Math.random() * 0.003 });
        }

        function animateBackground() {
            for (let y = 0; y < rows; y++) {
                for (let x = 0; x < cols; x++) {
                    const p = pixels[y * cols + x];
                    p.b += p.speed;
                    if (p.b > 0.4) p.b = 0;
                    bgCtx.fillStyle = `rgb(${Math.floor(p.b*44)},${Math.floor(p.b*38)},${Math.floor(p.b*8)})`;
                    bgCtx.fillRect(x * px, y * px, px, px);
                }
            }
            requestAnimationFrame(animateBackground);
        }

        const renderer = new THREE.WebGLRenderer({
            canvas: document.getElementById('animationCanvas'),
            antialias: true, alpha: true
        });

        let scene, camera, locust;
        let tegmenL, tegmenR, hindwingL, hindwingR;
        let hindLegL, hindLegR;
        let legGroups = [];

        const orbit = {
            theta: 0.5, phi: 1.15, radius: 9,
            isDragging: false, lastX: 0, lastY: 0,
            autoRotate: true, autoRotateSpeed: 0.003,
            velTheta: 0, velPhi: 0,
        };
        const PHI_MIN = 0.05, PHI_MAX = Math.PI * 0.95;
        const RADIUS_MIN = 3,  RADIUS_MAX = 22;

        function setCameraFromOrbit() {
            const x = orbit.radius * Math.sin(orbit.phi) * Math.sin(orbit.theta);
            const y = orbit.radius * Math.cos(orbit.phi);
            const z = orbit.radius * Math.sin(orbit.phi) * Math.cos(orbit.theta);
            camera.position.set(x, y, z);
            camera.lookAt(0, 0, 0);
            camera.updateProjectionMatrix();
        }

        function updateSize() {
            renderer.setSize(window.innerWidth, window.innerHeight);
            renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
        }

        function updateCamera() {
            const w = window.innerWidth;
            orbit.radius = w < 480 ? 13 : w < 768 ? 11 : 9;
            camera.fov   = 52;
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            setCameraFromOrbit();
        }

        function mat(color) {
            return new THREE.MeshBasicMaterial({ color, wireframe: true });
        }

        function makeTegmen(side) {
            const s = side, group = new THREE.Group();
            const W = 0.38;
            const L = 2.20;

            const shape = new THREE.Shape();
            shape.moveTo(0, 0);
            shape.bezierCurveTo(s*W*0.55, 0.05,  s*W*0.90, 0.10,  s*W, L*0.35);
            shape.bezierCurveTo(s*W*1.02, L*0.55, s*W*0.98, L*0.80, s*W*0.60, L);
            shape.bezierCurveTo(s*W*0.40, L*1.02, s*W*0.10, L*0.98, 0, L*0.95);
            shape.bezierCurveTo(s*(-W*0.08), L*0.70, s*(-W*0.06), L*0.30, 0, 0);

            const geo = new THREE.ShapeGeometry(shape, 16);
            group.add(new THREE.Mesh(geo, mat(colors.tegmen)));
            group.add(new THREE.LineSegments(
                new THREE.EdgesGeometry(geo),
                new THREE.LineBasicMaterial({ color: colors.vein })
            ));

            const veinDefs = [
                { xFrac: 0.95, label: 'costa',    r: 0.010 },
                { xFrac: 0.78, label: 'radius',   r: 0.008 },
                { xFrac: 0.55, label: 'medial',   r: 0.007 },
                { xFrac: 0.30, label: 'cubitus',  r: 0.006 },
                { xFrac: 0.05, label: 'anal',     r: 0.005 },
            ];
            veinDefs.forEach(v => {
                const xBase = s * W * v.xFrac;
                const pts = [
                    new THREE.Vector3(xBase * 0.1, 0.005, 0.05),
                    new THREE.Vector3(xBase * 0.7, 0.005, L * 0.4),
                    new THREE.Vector3(xBase * 0.9, 0.005, L * 0.75),
                    new THREE.Vector3(xBase * 0.75, 0.005, L * 0.96),
                ];
                group.add(new THREE.Mesh(
                    new THREE.TubeGeometry(new THREE.CatmullRomCurve3(pts), 8, v.r, 4),
                    mat(colors.vein)
                ));
            });

            const cvPositions = [0.30, 0.52, 0.68, 0.80, 0.89, 0.95];
            cvPositions.forEach(zf => {
                const zp = zf * L;
                const pts = [
                    new THREE.Vector3(s*W*0.90, 0.005, zp),
                    new THREE.Vector3(s*W*0.55, 0.005, zp + 0.06),
                    new THREE.Vector3(s*W*0.28, 0.005, zp + 0.04),
                ];
                group.add(new THREE.Mesh(
                    new THREE.TubeGeometry(new THREE.CatmullRomCurve3(pts), 4, 0.005, 4),
                    mat(colors.vein)
                ));
            });

            return group;
        }

        function makeHindwing(side) {
            const s = side, group = new THREE.Group();
            const W = 0.18;
            const L = 2.00;

            const shape = new THREE.Shape();
            shape.moveTo(0, 0);
            shape.bezierCurveTo(s*W*0.6, 0.02, s*W, L*0.3, s*W*0.95, L*0.7);
            shape.bezierCurveTo(s*W*0.85, L*0.9, s*W*0.50, L, s*W*0.15, L*0.96);
            shape.bezierCurveTo(s*(-W*0.05), L*0.65, s*(-W*0.05), L*0.25, 0, 0);
            const geo = new THREE.ShapeGeometry(shape, 12);
            group.add(new THREE.Mesh(geo, mat(colors.hindwing)));
            group.add(new THREE.LineSegments(
                new THREE.EdgesGeometry(geo),
                new THREE.LineBasicMaterial({ color: colors.vein })
            ));
            for (let fi = 0; fi < 4; fi++) {
                const xf = s * W * (0.2 + fi * 0.18);
                const pts = [
                    new THREE.Vector3(xf * 0.2, 0.004, L * 0.05),
                    new THREE.Vector3(xf,        0.004, L * 0.55),
                    new THREE.Vector3(xf * 0.9,  0.004, L * 0.92),
                ];
                group.add(new THREE.Mesh(
                    new THREE.TubeGeometry(new THREE.CatmullRomCurve3(pts), 6, 0.005, 4),
                    mat(colors.vein)
                ));
            }
            return group;
        }

        function makeHindLeg(side) {
            const s = side, group = new THREE.Group();
            group.add(new THREE.Mesh(
                new THREE.TubeGeometry(new THREE.CatmullRomCurve3([
                    new THREE.Vector3(0, 0, 0),
                    new THREE.Vector3(s*0.14, -0.06, 0.04),
                ]), 3, 0.025, 5),
                mat(colors.dark)
            ));
            const femPts = [
                new THREE.Vector3(s*0.14, -0.06, 0.04),
                new THREE.Vector3(s*0.44, -0.10, 0.30),
                new THREE.Vector3(s*0.70, -0.18, 0.60),
                new THREE.Vector3(s*0.82, -0.30, 0.80),
            ];
            group.add(new THREE.Mesh(
                new THREE.TubeGeometry(new THREE.CatmullRomCurve3(femPts), 10, 0.060, 8),
                mat(colors.body)
            ));
            const keelPts = femPts.map((p, i) =>
                new THREE.Vector3(p.x + s*0.01, p.y + 0.07, p.z)
            );
            group.add(new THREE.Mesh(
                new THREE.TubeGeometry(new THREE.CatmullRomCurve3(keelPts), 8, 0.018, 4),
                mat(colors.light)
            ));
            const tibPts = [
                new THREE.Vector3(s*0.82, -0.30, 0.80),
                new THREE.Vector3(s*0.90, -0.65, 1.20),
                new THREE.Vector3(s*0.88, -1.05, 1.55),
                new THREE.Vector3(s*0.82, -1.40, 1.80),
            ];
            group.add(new THREE.Mesh(
                new THREE.TubeGeometry(new THREE.CatmullRomCurve3(tibPts), 10, 0.022, 6),
                mat(colors.light)
            ));
            for (let si = 0; si < 7; si++) {
                const t = si / 6;
                const base = new THREE.Vector3(
                    s*0.82 + (s*0.82-s*0.82)*t,
                    -0.30 + (-1.40+0.30)*t,
                    0.80 + (1.80-0.80)*t
                );
                [0.06, -0.06].forEach(offset => {
                    const spPts = [
                        base.clone(),
                        new THREE.Vector3(base.x + s*0.04, base.y + offset, base.z + 0.08),
                    ];
                    group.add(new THREE.Mesh(
                        new THREE.TubeGeometry(new THREE.CatmullRomCurve3(spPts), 3, 0.008, 3),
                        mat(colors.spine)
                    ));
                });
            }
            let tPos = new THREE.Vector3(s*0.82, -1.40, 1.80);
            for (let ti = 0; ti < 3; ti++) {
                const tEnd = new THREE.Vector3(tPos.x + s*0.08, tPos.y - 0.14 + ti*0.03, tPos.z + 0.14);
                group.add(new THREE.Mesh(
                    new THREE.TubeGeometry(new THREE.CatmullRomCurve3([tPos, tEnd]), 4, 0.018 - ti*0.004, 5),
                    mat(ti % 2 === 0 ? colors.dark : colors.tarsus)
                ));
                const padGeo = new THREE.SphereGeometry(0.020, 6, 5);
                const pad = new THREE.Mesh(padGeo, mat(colors.spine));
                pad.position.copy(tEnd);
                group.add(pad);
                tPos = tEnd;
            }
            const clPts = [tPos.clone(), new THREE.Vector3(tPos.x + s*0.06, tPos.y - 0.06, tPos.z + 0.06)];
            group.add(new THREE.Mesh(
                new THREE.TubeGeometry(new THREE.CatmullRomCurve3(clPts), 3, 0.010, 4),
                mat(colors.dark)
            ));
            return group;
        }

        function makeWalkingLeg(side, index) {
            const s = side, group = new THREE.Group();
            const zOff = index * 0.40;
            group.add(new THREE.Mesh(
                new THREE.TubeGeometry(new THREE.CatmullRomCurve3([
                    new THREE.Vector3(0,0,0),
                    new THREE.Vector3(s*0.15,-0.08,0.02),
                ]), 3, 0.022, 5),
                mat(colors.dark)
            ));
            const fPts = [
                new THREE.Vector3(s*0.15,-0.08,0.02),
                new THREE.Vector3(s*0.38,-0.32,0.04),
                new THREE.Vector3(s*0.52,-0.52,0.02),
            ];
            group.add(new THREE.Mesh(
                new THREE.TubeGeometry(new THREE.CatmullRomCurve3(fPts), 6, 0.026, 6),
                mat(colors.body)
            ));
            const tPts = [
                new THREE.Vector3(s*0.52,-0.52,0.02),
                new THREE.Vector3(s*0.72,-0.82,-0.02),
                new THREE.Vector3(s*0.80,-1.10,-0.06),
            ];
            group.add(new THREE.Mesh(
                new THREE.TubeGeometry(new THREE.CatmullRomCurve3(tPts), 6, 0.018, 6),
                mat(colors.light)
            ));
            for (let si = 0; si < 3; si++) {
                const t = 0.3 + si * 0.25;
                const bx = s*0.52 + (s*0.80 - s*0.52)*t;
                const by = -0.52 + (-1.10+0.52)*t;
                const bz = 0.02 + (-0.06-0.02)*t;
                group.add(new THREE.Mesh(
                    new THREE.TubeGeometry(new THREE.CatmullRomCurve3([
                        new THREE.Vector3(bx, by, bz),
                        new THREE.Vector3(bx + s*0.04, by + 0.06, bz + 0.04),
                    ]), 3, 0.006, 3),
                    mat(colors.spine)
                ));
            }
            let tp = new THREE.Vector3(s*0.80,-1.10,-0.06);
            for (let ti = 0; ti < 3; ti++) {
                const te = new THREE.Vector3(tp.x + s*0.07, tp.y - 0.10 + ti*0.02, tp.z - 0.03);
                group.add(new THREE.Mesh(
                    new THREE.TubeGeometry(new THREE.CatmullRomCurve3([tp,te]), 3, 0.013-ti*0.003, 4),
                    mat(ti%2===0?colors.dark:colors.tarsus)
                ));
                tp = te;
            }
            return group;
        }

        function createLocust() {
            const group = new THREE.Group();
            legGroups = [];

            const abdData = [
                { r:0.22, z:0.05 }, { r:0.24, z:0.35 }, { r:0.23, z:0.64 },
                { r:0.21, z:0.92 }, { r:0.18, z:1.18 }, { r:0.15, z:1.42 },
                { r:0.11, z:1.62 }, { r:0.07, z:1.78 },
            ];
            abdData.forEach((s, i) => {
                const next = abdData[i+1];
                const rTop = next ? next.r : s.r * 0.5;
                const isAlt = i % 2 === 0;
                const geo = new THREE.CylinderGeometry(rTop*0.9, s.r, 0.30, 14, 1);
                const seg = new THREE.Mesh(geo, mat(isAlt ? colors.body : colors.dark));
                seg.rotation.x = Math.PI * 0.5;
                seg.position.set(0, 0, s.z);
                group.add(seg);
                [-1,1].forEach(side => {
                    const spGeo = new THREE.TorusGeometry(0.020, 0.006, 5, 10);
                    const sp = new THREE.Mesh(spGeo, mat(colors.light));
                    sp.position.set(side * s.r * 0.92, 0.04, s.z);
                    group.add(sp);
                });
                if (isAlt) {
                    const ridgeGeo = new THREE.BoxGeometry(0.04, 0.025, 0.28);
                    const ridge = new THREE.Mesh(ridgeGeo, mat(colors.stripe));
                    ridge.position.set(0, s.r + 0.01, s.z);
                    group.add(ridge);
                }
            });
            [-1,1].forEach(side => {
                const cPts = [
                    new THREE.Vector3(side*0.06, 0, 1.90),
                    new THREE.Vector3(side*0.10, 0.04, 2.04),
                    new THREE.Vector3(side*0.12, 0.06, 2.14),
                ];
                group.add(new THREE.Mesh(
                    new THREE.TubeGeometry(new THREE.CatmullRomCurve3(cPts), 5, 0.012, 4),
                    mat(colors.dark)
                ));
            });

            const mGeo = new THREE.CylinderGeometry(0.24, 0.28, 0.72, 14, 2);
            const meso = new THREE.Mesh(mGeo, mat(colors.body));
            meso.rotation.x = Math.PI * 0.5;
            meso.position.set(0, 0.04, -0.42);
            group.add(meso);

            const proGeo = new THREE.CylinderGeometry(0.22, 0.30, 0.55, 14, 3);
            const pro = new THREE.Mesh(proGeo, mat(colors.pronotum));
            pro.rotation.x = Math.PI * 0.5;
            pro.position.set(0, 0.06, -0.86);
            group.add(pro);
            const keelPts = [
                new THREE.Vector3(0, 0.30, -0.62),
                new THREE.Vector3(0, 0.32, -0.86),
                new THREE.Vector3(0, 0.30, -1.10),
            ];
            group.add(new THREE.Mesh(
                new THREE.TubeGeometry(new THREE.CatmullRomCurve3(keelPts), 6, 0.022, 5),
                mat(colors.stripe)
            ));
            [-1,1].forEach(side => {
                const cPts = [
                    new THREE.Vector3(side*0.20, 0.14, -0.64),
                    new THREE.Vector3(side*0.24, 0.10, -0.86),
                    new THREE.Vector3(side*0.20, 0.14, -1.08),
                ];
                group.add(new THREE.Mesh(
                    new THREE.TubeGeometry(new THREE.CatmullRomCurve3(cPts), 5, 0.012, 4),
                    mat(colors.light)
                ));
            });

            const headGeo = new THREE.SphereGeometry(0.24, 16, 14);
            const head = new THREE.Mesh(headGeo, mat(colors.body));
            head.scale.set(0.85, 1.10, 0.95);
            head.position.set(0, 0.04, -1.28);
            group.add(head);

            const fastGeo = new THREE.BoxGeometry(0.28, 0.08, 0.16);
            const fast = new THREE.Mesh(fastGeo, mat(colors.pronotum));
            fast.position.set(0, 0.24, -1.28);
            group.add(fast);

            [-1,1].forEach(side => {
                const eyeGeo = new THREE.SphereGeometry(0.13, 14, 12);
                const eye = new THREE.Mesh(eyeGeo, mat(colors.eye));
                eye.scale.set(0.65, 1.0, 0.80);
                eye.position.set(side*0.20, 0.08, -1.28);
                group.add(eye);
                for (let fi = 0; fi < 8; fi++) {
                    const ang = (fi/8) * Math.PI * 2;
                    const fGeo = new THREE.CircleGeometry(0.028, 6);
                    const facet = new THREE.Mesh(fGeo, mat(colors.stripe));
                    facet.position.set(
                        side*(0.22 + Math.cos(ang)*0.04),
                        0.08 + Math.sin(ang)*0.06,
                        -1.26
                    );
                    group.add(facet);
                }
            });

            const fronsGeo = new THREE.BoxGeometry(0.26, 0.28, 0.08);
            const frons = new THREE.Mesh(fronsGeo, mat(colors.light));
            frons.position.set(0, 0.00, -1.48);
            group.add(frons);

            const clyGeo = new THREE.BoxGeometry(0.22, 0.12, 0.06);
            const cly = new THREE.Mesh(clyGeo, mat(colors.pronotum));
            cly.position.set(0, -0.14, -1.50);
            group.add(cly);

            [-1,1].forEach(side => {
                const mPts = [
                    new THREE.Vector3(side*0.08, -0.18, -1.50),
                    new THREE.Vector3(side*0.16, -0.24, -1.60),
                    new THREE.Vector3(side*0.18, -0.28, -1.68),
                ];
                group.add(new THREE.Mesh(
                    new THREE.TubeGeometry(new THREE.CatmullRomCurve3(mPts), 5, 0.020, 5),
                    mat(colors.dark)
                ));
            });

            [-1,1].forEach(side => {
                const antSegs = 20;
                let aPos = new THREE.Vector3(side*0.12, 0.26, -1.38);
                for (let ai = 0; ai < antSegs; ai++) {
                    const t = ai / antSegs;
                    const spread = side * (0.04 + t * 0.06);
                    const aEnd = new THREE.Vector3(
                        aPos.x + spread,
                        aPos.y + 0.05 - t*0.02,
                        aPos.z - 0.12 - t*0.02
                    );
                    const r = 0.010 - t*0.007;
                    if (r > 0.002) {
                        group.add(new THREE.Mesh(
                            new THREE.TubeGeometry(new THREE.CatmullRomCurve3([aPos.clone(), aEnd.clone()]), 3, r, 4),
                            mat(ai%2===0 ? colors.dark : colors.light)
                        ));
                    }
                    if (ai % 3 === 0) {
                        const nodeGeo = new THREE.SphereGeometry(0.014 - t*0.008, 5, 4);
                        const node = new THREE.Mesh(nodeGeo, mat(colors.body));
                        node.position.copy(aEnd);
                        group.add(node);
                    }
                    aPos = aEnd;
                }
            });

            tegmenL = new THREE.Group();
            tegmenL.position.set(-0.10, 0.24, -0.72);
            tegmenL.rotation.z =  0.08;
            tegmenL.rotation.x =  0.04;
            tegmenL.add(makeTegmen(-1));
            group.add(tegmenL);

            tegmenR = new THREE.Group();
            tegmenR.position.set( 0.10, 0.24, -0.72);
            tegmenR.rotation.z = -0.08;
            tegmenR.rotation.x =  0.04;
            tegmenR.add(makeTegmen(1));
            group.add(tegmenR);

            hindwingL = new THREE.Group();
            hindwingL.position.set(-0.08, 0.21, -0.70);
            hindwingL.rotation.z =  0.06;
            hindwingL.rotation.x =  0.05;
            hindwingL.add(makeHindwing(-1));
            group.add(hindwingL);

            hindwingR = new THREE.Group();
            hindwingR.position.set( 0.08, 0.21, -0.70);
            hindwingR.rotation.z = -0.06;
            hindwingR.rotation.x =  0.05;
            hindwingR.add(makeHindwing(1));
            group.add(hindwingR);

            [-1,1].forEach(side => {
                const fl = makeWalkingLeg(side, 0);
                fl.position.set(side*0.28, -0.06, -0.92);
                group.add(fl);
                legGroups.push({ g: fl, side, li: 0, isHind: false });
                const ml = makeWalkingLeg(side, 1);
                ml.position.set(side*0.28, -0.06, -0.50);
                group.add(ml);
                legGroups.push({ g: ml, side, li: 1, isHind: false });
            });

            hindLegL = makeHindLeg(-1);
            hindLegL.position.set(-0.26, 0.00, -0.08);
            group.add(hindLegL);
            hindLegR = makeHindLeg(1);
            hindLegR.position.set( 0.26, 0.00, -0.08);
            group.add(hindLegR);

            group.position.set(0, 0.4, 0.6);
            return group;
        }

        function initOrbitControls() {
            const canvas = document.getElementById('animationCanvas');
            const hint   = document.getElementById('hint');

            function onDragStart(x, y) {
                orbit.isDragging = true; orbit.autoRotate = false;
                orbit.lastX = x; orbit.lastY = y;
                orbit.velTheta = 0; orbit.velPhi = 0;
                canvas.classList.add('dragging');
                hint.style.opacity = '0';
            }
            function onDragMove(x, y) {
                if (!orbit.isDragging) return;
                const dx = x - orbit.lastX, dy = y - orbit.lastY;
                orbit.velTheta = -dx * 0.005; orbit.velPhi = -dy * 0.005;
                orbit.theta += orbit.velTheta;
                orbit.phi = Math.max(PHI_MIN, Math.min(PHI_MAX, orbit.phi + orbit.velPhi));
                orbit.lastX = x; orbit.lastY = y;
                setCameraFromOrbit();
            }
            function onDragEnd() {
                orbit.isDragging = false;
                canvas.classList.remove('dragging');
                setTimeout(() => { orbit.autoRotate = true; }, 2000);
            }

            canvas.addEventListener('mousedown', e => onDragStart(e.clientX, e.clientY));
            window.addEventListener('mousemove', e => onDragMove(e.clientX, e.clientY));
            window.addEventListener('mouseup',   () => onDragEnd());
            canvas.addEventListener('touchstart', e => { e.preventDefault(); onDragStart(e.touches[0].clientX, e.touches[0].clientY); }, { passive: false });
            window.addEventListener('touchmove',  e => { e.preventDefault(); onDragMove(e.touches[0].clientX, e.touches[0].clientY); }, { passive: false });
            window.addEventListener('touchend',   () => onDragEnd());
            canvas.addEventListener('wheel', e => {
                e.preventDefault();
                orbit.radius = Math.max(RADIUS_MIN, Math.min(RADIUS_MAX, orbit.radius + e.deltaY * 0.01));
                setCameraFromOrbit();
            }, { passive: false });

            setTimeout(() => { hint.style.opacity = '0'; }, 3000);
        }

        function init() {
            initBackground();
            animateBackground();
            updateSize();

            scene = new THREE.Scene();
            scene.background = new THREE.Color(0x000000);

            camera = new THREE.PerspectiveCamera(52, window.innerWidth / window.innerHeight, 0.1, 1000);
            updateCamera();

            locust = createLocust();
            scene.add(locust);

            scene.add(new THREE.AmbientLight(0xd0c870, 0.5));
            const pl1 = new THREE.PointLight(0xffe060, 1.8, 100);
            pl1.position.set(5, 6, 3);  scene.add(pl1);
            const pl2 = new THREE.PointLight(0x80d040, 0.7, 100);
            pl2.position.set(-4, -2, 2); scene.add(pl2);
            const pl3 = new THREE.PointLight(0xfff0a0, 0.5, 100);
            pl3.position.set(0, 3, -10); scene.add(pl3);

            animate();
            initOrbitControls();
        }

        function animate() {
            requestAnimationFrame(animate);
            const t = Date.now() * 0.001;

            if (!orbit.isDragging) {
                if (orbit.autoRotate) {
                    orbit.theta += orbit.autoRotateSpeed;
                } else {
                    orbit.velTheta *= 0.92; orbit.velPhi *= 0.92;
                    orbit.theta += orbit.velTheta;
                    orbit.phi = Math.max(PHI_MIN, Math.min(PHI_MAX, orbit.phi + orbit.velPhi));
                }
                setCameraFromOrbit();
            }

            if (locust) {
                legGroups.forEach(({ g, side, li }) => {
                    const phase = li * 1.2 + (side > 0 ? 0.8 : 0);
                    g.rotation.x = Math.sin(t * 0.8 + phase) * 0.012;
                    g.rotation.z = side * (0.04 + Math.sin(t * 0.6 + phase) * 0.010);
                });
                if (hindLegL) hindLegL.rotation.x = Math.sin(t * 0.7)        * 0.010;
                if (hindLegR) hindLegR.rotation.x = Math.sin(t * 0.7 + 0.4)  * 0.010;
            }

            renderer.render(scene, camera);
        }

        window.addEventListener('resize', () => {
            updateSize(); initBackground();
            if (camera) updateCamera();
        });

        window.addEventListener('load', init);
    </script>
</body>
</html>