🧠 Description
This project showcases a procedural 3D creature AI system that moves autonomously in a virtual environment using inverse kinematics, segment-based skeletal animation, and AI patrol logic.
The entire creature is built using only cylinders, demonstrating how complex organic motion can emerge from simple geometry.
What Makes This Project Exceptional?
Visual & Motion Features:
- Fully procedural multi-legged creature
- Smooth crawling and walking animations
- Dynamic leg lifting and stepping logic
- Tail and spine follow-through motion
- Real-time shadow casting and lighting
- Auto-follow cinematic camera system
Technical Highlights:
- Inverse Kinematics (IK) for limb control
- Segment-chain physics simulation
- AI-driven autonomous patrol behavior
- Pure cylinder geometry (no models used)
- Three.js scene graph mastery
- OrbitControls with damping
- Real-time performance-friendly math
Core Concept
The creature is composed of:
- A head (cylinder)
- A spinal chain of connected segments
- Multiple leg systems using IK
- A tail with tapering thickness
- An AI target it continuously patrols toward
All movement emerges from math — no keyframe animations, no external models.
💻 Step 1: HTML Structure
Complete HTML Code
1<!DOCTYPE html>
2<html lang="en">
3
4 <head>
5 <meta charset="UTF-8">
6 <title>Creepy Crawly Kinematics 2</title>
7
8
9 </head>
10
11 <body>
12 <html lang="en">
13<head>
14 <meta charset="UTF-8">
15 <title>Cylinder Creature AI</title>
16 <style>
17 body { margin: 0; overflow: hidden; background-color: #050505; }
18 canvas { display: block; }
19 #info {
20 position: absolute;
21 top: 10px;
22 left: 10px;
23 color: #aaaaaa;
24 font-family: monospace;
25 pointer-events: none;
26 user-select: none;
27 }
28 #credit {
29 position: fixed;
30 bottom: 1em;
31 right: 1em;
32 z-index: 990;
33 font-family: sans-serif;
34 color: white;
35 font-size: 14px;
36 opacity: 0.7;
37 pointer-events: none;
38 text-shadow: 1px 1px 2px black;
39 }
40 </style>
41 <!-- Import Three.js and OrbitControls -->
42 <script type="importmap">
43 {
44 "imports": {
45 "three": "https://unpkg.com/three@0.160.0/build/three.module.js",
46 "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
47 }
48 }
49 </script>
50</head>
51<body>
52 <div id="info">AI Patrol Mode.<br>Left Click: Rotate | Right Click: Pan | Scroll: Zoom</div>
53 <div id="credit">Edit by Travon L Lawrence</div>
54
55 <script type="module">
56 import * as THREE from 'three';
57 import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
58
59 // --- 1. SETUP ---
60 const scene = new THREE.Scene();
61 scene.background = new THREE.Color(0x050505);
62 scene.fog = new THREE.Fog(0x050505, 200, 2000);
63
64 const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 5000);
65 camera.position.set(0, 600, 600);
66
67 const renderer = new THREE.WebGLRenderer({ antialias: true });
68 renderer.setSize(window.innerWidth, window.innerHeight);
69 renderer.shadowMap.enabled = true;
70 renderer.shadowMap.type = THREE.PCFSoftShadowMap;
71 document.body.appendChild(renderer.domElement);
72
73 // Controls
74 const controls = new OrbitControls(camera, renderer.domElement);
75 controls.enableDamping = true;
76 controls.dampingFactor = 0.05;
77 controls.maxPolarAngle = Math.PI / 2 - 0.05; // Don't go below floor
78 controls.minDistance = 100;
79 controls.maxDistance = 2000;
80
81 // Lights
82 const ambientLight = new THREE.AmbientLight(0x404040, 2); // Soft white light
83 scene.add(ambientLight);
84
85 const dirLight = new THREE.DirectionalLight(0xffffff, 2);
86 dirLight.position.set(100, 500, 200);
87 dirLight.castShadow = true;
88 dirLight.shadow.mapSize.width = 2048;
89 dirLight.shadow.mapSize.height = 2048;
90 dirLight.shadow.camera.near = 0.5;
91 dirLight.shadow.camera.far = 3000;
92 dirLight.shadow.camera.left = -1000;
93 dirLight.shadow.camera.right = 1000;
94 dirLight.shadow.camera.top = 1000;
95 dirLight.shadow.camera.bottom = -1000;
96 scene.add(dirLight);
97
98 // Floor
99 const planeGeo = new THREE.PlaneGeometry(10000, 10000);
100 const planeMat = new THREE.MeshStandardMaterial({ color: 0x111111, roughness: 0.8 });
101 const floor = new THREE.Mesh(planeGeo, planeMat);
102 floor.rotation.x = -Math.PI / 2;
103 floor.receiveShadow = true;
104 scene.add(floor);
105
106 const gridHelper = new THREE.GridHelper(4000, 40, 0x333333, 0x0a0a0a);
107 scene.add(gridHelper);
108
109 // --- 2. MATERIALS & TARGET ---
110
111 const bodyMat = new THREE.MeshStandardMaterial({
112 color: 0x00ddee,
113 roughness: 0.2,
114 metalness: 0.8
115 });
116 const jointMat = new THREE.MeshStandardMaterial({
117 color: 0x222222,
118 roughness: 0.7
119 });
120 const targetMat = new THREE.MeshStandardMaterial({
121 color: 0xff0000,
122 emissive: 0xaa0000
123 });
124
125 // Target (Cylinder per request)
126 const targetGeo = new THREE.CylinderGeometry(20, 20, 10, 32);
127 const targetMesh = new THREE.Mesh(targetGeo, targetMat);
128 targetMesh.position.set(200, 5, 0);
129 targetMesh.castShadow = true;
130 scene.add(targetMesh);
131
132 // --- 3. GEOMETRY HELPERS ---
133
134 // Pre-rotate cylinder geometry so it points along Z-axis.
135 // This makes lookAt() work correctly for connecting segments.
136 const segmentGeo = new THREE.CylinderGeometry(1, 1, 1, 16);
137 segmentGeo.rotateX(-Math.PI / 2);
138 segmentGeo.translate(0, 0, 0.5); // Move pivot to start of cylinder
139
140 // Joint geometry (Cylinder used as a disc/wheel)
141 const jointGeo = new THREE.CylinderGeometry(1, 1, 1, 16);
142
143 // --- 4. PROCEDURAL CLASSES ---
144
145 var segmentCount = 0;
146
147 class Segment {
148 constructor(parent, size, angle, range, stiffness, thickness = 5) {
149 segmentCount++;
150 this.isSegment = true;
151 this.parent = parent;
152 if (typeof parent.children == "object") {
153 parent.children.push(this);
154 }
155 this.children = [];
156 this.size = size;
157 this.relAngle = angle;
158 this.defAngle = angle;
159 this.absAngle = parent.absAngle + angle;
160 this.range = range;
161 this.stiffness = stiffness;
162
163 // Simulation Position (2D Logic)
164 this.x = 0;
165 this.y = 0;
166
167 // --- 3D MESHES (Cylinders Only) ---
168 this.group = new THREE.Group();
169 scene.add(this.group);
170
171 // The Bone (Length)
172 this.bone = new THREE.Mesh(segmentGeo, bodyMat);
173 this.bone.castShadow = true;
174 this.bone.receiveShadow = true;
175 this.group.add(this.bone);
176
177 // The Joint (Connection point - A flat cylinder/disc)
178 this.joint = new THREE.Mesh(jointGeo, jointMat);
179 this.joint.rotation.z = Math.PI / 2; // Lay flat like a wheel
180 this.joint.castShadow = true;
181 this.joint.receiveShadow = true;
182 this.group.add(this.joint);
183
184 // Initial Size setup
185 this.thickness = thickness;
186 this.joint.scale.set(thickness * 0.8, thickness * 1.2, thickness * 0.8);
187
188 this.updateRelative(false, true);
189 }
190
191 updateRelative(iter, flex) {
192 this.relAngle = this.relAngle - 2 * Math.PI * Math.floor((this.relAngle - this.defAngle) / 2 / Math.PI + 1 / 2);
193 if (flex) {
194 this.relAngle = Math.min(
195 this.defAngle + this.range / 2,
196 Math.max(
197 this.defAngle - this.range / 2,
198 (this.relAngle - this.defAngle) / this.stiffness + this.defAngle
199 )
200 );
201 }
202 this.absAngle = this.parent.absAngle + this.relAngle;
203 this.x = this.parent.x + Math.cos(this.absAngle) * this.size;
204 this.y = this.parent.y + Math.sin(this.absAngle) * this.size;
205
206 if (iter) {
207 for (var i = 0; i < this.children.length; i++) {
208 this.children[i].updateRelative(iter, flex);
209 }
210 }
211 }
212
213 updateVisuals(offsetY = 0) {
214 // Map Logic (X, Y) -> 3D (X, Z)
215 const startX = this.parent.x;
216 const startZ = this.parent.y;
217 const endX = this.x;
218 const endZ = this.y;
219
220 // Move Group to Start
221 this.group.position.set(startX, offsetY, startZ);
222
223 // Rotate entire group to look at End
224 this.group.lookAt(endX, offsetY, endZ);
225
226 // Stretch Bone Cylinder to fit distance
227 const dist = Math.sqrt((endX - startX)**2 + (endZ - startZ)**2);
228 this.bone.scale.set(this.thickness, this.thickness, dist);
229
230 for (var i = 0; i < this.children.length; i++) {
231 this.children[i].updateVisuals(offsetY);
232 }
233 }
234
235 follow(iter) {
236 var x = this.parent.x;
237 var y = this.parent.y;
238 var dist = ((this.x - x) ** 2 + (this.y - y) ** 2) ** 0.5;
239 this.x = x + this.size * (this.x - x) / dist;
240 this.y = y + this.size * (this.y - y) / dist;
241 this.absAngle = Math.atan2(this.y - y, this.x - x);
242 this.relAngle = this.absAngle - this.parent.absAngle;
243 this.updateRelative(false, true);
244
245 if (iter) {
246 for (var i = 0; i < this.children.length; i++) {
247 this.children[i].follow(true);
248 }
249 }
250 }
251 }
252
253 class LimbSystem {
254 constructor(end, length, speed, creature) {
255 this.end = end;
256 this.length = Math.max(1, length);
257 this.creature = creature;
258 this.speed = speed;
259 creature.systems.push(this);
260 this.nodes = [];
261 var node = end;
262 for (var i = 0; i < length; i++) {
263 this.nodes.unshift(node);
264 node = node.parent;
265 if (!node.isSegment) {
266 this.length = i + 1;
267 break;
268 }
269 }
270 this.hip = this.nodes[0].parent;
271 }
272
273 moveTo(x, y) {
274 this.nodes[0].updateRelative(true, true);
275 var dist = ((x - this.end.x) ** 2 + (y - this.end.y) ** 2) ** 0.5;
276 var len = Math.max(0, dist - this.speed);
277 for (var i = this.nodes.length - 1; i >= 0; i--) {
278 var node = this.nodes[i];
279 var ang = Math.atan2(node.y - y, node.x - x);
280 node.x = x + len * Math.cos(ang);
281 node.y = y + len * Math.sin(ang);
282 x = node.x;
283 y = node.y;
284 len = node.size;
285 }
286 for (var i = 0; i < this.nodes.length; i++) {
287 var node = this.nodes[i];
288 node.absAngle = Math.atan2(node.y - node.parent.y, node.x - node.parent.x);
289 node.relAngle = node.absAngle - node.parent.absAngle;
290 for (var ii = 0; ii < node.children.length; ii++) {
291 var childNode = node.children[ii];
292 if (!this.nodes.includes(childNode)) {
293 childNode.updateRelative(true, false);
294 }
295 }
296 }
297 }
298 update(x,y) { this.moveTo(x, y); }
299 }
300
301 class LegSystem extends LimbSystem {
302 constructor(end, length, speed, creature, liftHeight = 10) {
303 super(end, length, speed, creature);
304 this.goalX = end.x;
305 this.goalY = end.y;
306 this.step = 0;
307 this.forwardness = 0;
308 this.liftHeight = liftHeight;
309 this.reach = 0.9 * ((this.end.x - this.hip.x) ** 2 + (this.end.y - this.hip.y) ** 2) ** 0.5;
310 var relAngle = this.creature.absAngle - Math.atan2(this.end.y - this.hip.y, this.end.x - this.hip.x);
311 relAngle -= 2 * Math.PI * Math.floor(relAngle / 2 / Math.PI + 1 / 2);
312 this.swing = -relAngle + (2 * (relAngle < 0) - 1) * Math.PI / 2;
313 this.swingOffset = this.creature.absAngle - this.hip.absAngle;
314 }
315
316 update(x, y) {
317 this.moveTo(this.goalX, this.goalY);
318 let heightOffset = 0;
319 // Leg Lift Logic
320 if (this.step == 0) {
321 var dist = ((this.end.x - this.goalX) ** 2 + (this.end.y - this.goalY) ** 2) ** 0.5;
322 if (dist > 1) {
323 this.step = 1;
324 this.goalX = this.hip.x + this.reach * Math.cos(this.swing + this.hip.absAngle + this.swingOffset) + (2 * Math.random() - 1) * this.reach / 2;
325 this.goalY = this.hip.y + this.reach * Math.sin(this.swing + this.hip.absAngle + this.swingOffset) + (2 * Math.random() - 1) * this.reach / 2;
326 }
327 } else if (this.step == 1) {
328 var theta = Math.atan2(this.end.y - this.hip.y, this.end.x - this.hip.x) - this.hip.absAngle;
329 var dist = ((this.end.x - this.hip.x) ** 2 + (this.end.y - this.hip.y) ** 2) ** 0.5;
330 var forwardness2 = dist * Math.cos(theta);
331 var dF = this.forwardness - forwardness2;
332 this.forwardness = forwardness2;
333 let distToGoal = ((this.end.x - this.goalX) ** 2 + (this.end.y - this.goalY) ** 2) ** 0.5;
334 heightOffset = Math.sin((1 - Math.min(1, distToGoal / 50)) * Math.PI) * this.liftHeight;
335 if (dF * dF < 1) {
336 this.step = 0;
337 this.goalX = this.hip.x + (this.end.x - this.hip.x);
338 this.goalY = this.hip.y + (this.end.y - this.hip.y);
339 }
340 }
341 // Visual Update
342 for(let i=0; i<this.nodes.length; i++) {
343 let factor = (i+1) / this.nodes.length;
344 this.nodes[i].updateVisuals(heightOffset * factor + 5); // +5 to keep off floor
345 }
346 }
347 }
348
349 class Creature {
350 constructor(x, y, angle, fAccel, fFric, fRes, fThresh, rAccel, rFric, rRes, rThresh) {
351 this.x = x;
352 this.y = y;
353 this.absAngle = angle;
354 this.fSpeed = 0;
355 this.fAccel = fAccel;
356 this.fFric = fFric;
357 this.fRes = fRes;
358 this.fThresh = fThresh;
359 this.rSpeed = 0;
360 this.rAccel = rAccel;
361 this.rFric = rFric;
362 this.rRes = rRes;
363 this.rThresh = rThresh;
364 this.children = [];
365 this.systems = [];
366
367 // 3D Head (Cylinder)
368 const headGeo = new THREE.CylinderGeometry(15, 10, 30, 16);
369 headGeo.rotateX(-Math.PI / 2);
370 this.mesh = new THREE.Mesh(headGeo, bodyMat);
371 this.mesh.castShadow = true;
372 this.mesh.position.set(x, 20, y);
373 scene.add(this.mesh);
374 }
375
376 follow(x, y) {
377 var dist = ((this.x - x) ** 2 + (this.y - y) ** 2) ** 0.5;
378 var angle = Math.atan2(y - this.y, x - this.x);
379
380 // Physics
381 var accel = this.fAccel;
382 if (this.systems.length > 0) {
383 var sum = 0;
384 for (var i = 0; i < this.systems.length; i++) {
385 sum += this.systems[i].step == 0;
386 }
387 accel *= sum / this.systems.length;
388 }
389 this.fSpeed += accel * (dist > this.fThresh);
390 this.fSpeed *= 1 - this.fRes;
391 this.speed = Math.max(0, this.fSpeed - this.fFric);
392
393 var dif = this.absAngle - angle;
394 dif -= 2 * Math.PI * Math.floor(dif / (2 * Math.PI) + 1 / 2);
395 if (Math.abs(dif) > this.rThresh && dist > this.fThresh) {
396 this.rSpeed -= this.rAccel * (2 * (dif > 0) - 1);
397 }
398 this.rSpeed *= 1 - this.rRes;
399 if (Math.abs(this.rSpeed) > this.rFric) {
400 this.rSpeed -= this.rFric * (2 * (this.rSpeed > 0) - 1);
401 } else {
402 this.rSpeed = 0;
403 }
404
405 this.absAngle += this.rSpeed;
406 this.absAngle -= 2 * Math.PI * Math.floor(this.absAngle / (2 * Math.PI) + 1 / 2);
407 this.x += this.speed * Math.cos(this.absAngle);
408 this.y += this.speed * Math.sin(this.absAngle);
409
410 // Update Head
411 this.mesh.position.set(this.x, 20, this.y);
412 this.mesh.rotation.y = -this.absAngle;
413
414 this.absAngle += Math.PI;
415 for (var i = 0; i < this.children.length; i++) {
416 this.children[i].follow(true, true);
417 this.children[i].updateVisuals(20); // 20 units off floor
418 }
419 for (var i = 0; i < this.systems.length; i++) {
420 this.systems[i].update(x, y);
421 }
422 this.absAngle -= Math.PI;
423 }
424 }
425
426 // --- 5. INITIALIZATION ---
427
428 var critter;
429
430 function setupLizard(size, legs, tail) {
431 var s = size * 3;
432 critter = new Creature(0, 0, 0, s * 10, s * 2, 0.5, 16, 0.5, 0.085, 0.5, 0.3);
433 var spinal = critter;
434
435 // Neck
436 for (var i = 0; i < 6; i++) {
437 spinal = new Segment(spinal, s * 4, 0, 3.1415 * 2 / 3, 1.1, 12);
438 }
439 // Torso
440 for (var i = 0; i < legs; i++) {
441 if (i > 0) {
442 for (var ii = 0; ii < 6; ii++) {
443 spinal = new Segment(spinal, s * 4, 0, 1.571, 1.5, 15); // Thick body
444 }
445 }
446 // Legs
447 for (var ii = -1; ii <= 1; ii += 2) {
448 var node = new Segment(spinal, s * 12, ii * 0.785, 0, 8, 8);
449 node = new Segment(node, s * 16, -ii * 0.785, 6.28, 1, 6);
450 node = new Segment(node, s * 16, ii * 1.571, 3.1415, 2, 4);
451 new LegSystem(node, 3, s * 12, critter, 30);
452 }
453 }
454 // Tail
455 for (var i = 0; i < tail; i++) {
456 // Taper thickness
457 let thick = 15 * (1 - (i/tail));
458 spinal = new Segment(spinal, s * 4, 0, 3.1415 * 2 / 3, 1.1, Math.max(1, thick));
459 }
460 }
461
462 // Generate
463 var legNum = 4;
464 setupLizard(8 / Math.sqrt(legNum), legNum, 40);
465
466 // AI Logic
467 let aiTargetX = (Math.random() - 0.5) * 2000;
468 let aiTargetY = (Math.random() - 0.5) * 2000;
469 let aiPauseTimer = 0;
470
471 function animate() {
472 requestAnimationFrame(animate);
473
474 if (critter) {
475 // AI Patrol
476 let dx = aiTargetX - critter.x;
477 let dy = aiTargetY - critter.y;
478 let dist = Math.sqrt(dx*dx + dy*dy);
479
480 if (dist < 100) {
481 if (aiPauseTimer++ > 50) {
482 aiTargetX = (Math.random() - 0.5) * 3000;
483 aiTargetY = (Math.random() - 0.5) * 3000;
484 aiPauseTimer = 0;
485 }
486 }
487
488 // Update Target Visual
489 targetMesh.position.set(aiTargetX, 10, aiTargetY);
490 targetMesh.rotation.y += 0.02;
491
492 // Move Creature
493 critter.follow(aiTargetX, aiTargetY);
494
495 // Auto-Follow Camera Logic
496 controls.target.set(critter.x, 0, critter.y);
497 }
498
499 controls.update();
500 renderer.render(scene, camera);
501 }
502
503 window.addEventListener('resize', () => {
504 camera.aspect = window.innerWidth / window.innerHeight;
505 camera.updateProjectionMatrix();
506 renderer.setSize(window.innerWidth, window.innerHeight);
507 });
508
509 animate();
510
511 </script>
512</body>
513</html>
514
515 </body>
516
517</html>
HTML Breakdown
Document Setup
Uses ES Modules (type="module")
Import Maps for clean Three.js imports
Fullscreen canvas rendering
Overlay UI
#info → User instructions
#credit → Attribution
Pointer-events disabled for immersion
🎨 Step 2: CSS – Scene Presentation & UI
CSS Responsibilities
The CSS is intentionally minimal and focuses on:
Fullscreen WebGL rendering
Immersive dark environment
Overlay UI readability
1body { 2 margin: 0; 3 overflow: hidden; 4 background-color: #050505; 5} 6 7canvas { 8 display: block; 9}
Why Minimal CSS?
Performance-first approach
All visuals handled by WebGL
No layout reflows during animation
⚙️ Step 3: JavaScript – Procedural Animation & AI Logic
Scene Initialization
const scene = new THREE.Scene(); scene.background = new THREE.Color(0x050505); scene.fog = new THREE.Fog(0x050505, 200, 2000);
Fog adds depth perception
Dark palette emphasizes silhouette motion
