Back to Components
Cylinder Creature AI – Procedural Kinematics & Autonomous Patrol using Three.js (2025)
Component

Cylinder Creature AI – Procedural Kinematics & Autonomous Patrol using Three.js (2025)

CodewithLord
December 18, 2025

A fully procedural 3D creature simulation built with Three.js, featuring inverse kinematics, segmented limb systems, autonomous AI patrol behavior, and real-time physics-based motion using cylinder-only geometry.


🧠 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

Love this component?

Explore more components and build amazing UIs.

View All Components