Text Generation
MLX
Safetensors
qwen3_5
quantized
mixed-precision
4bit
8bit
optiq
apple-silicon
qwen3.5
three.js
3d
game-assets
conversational
4-bit precision
Instructions to use mlx-community/Qwen3.5-4B-3D-Coder-OptiQ-4bit with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- MLX
How to use mlx-community/Qwen3.5-4B-3D-Coder-OptiQ-4bit with MLX:
# Make sure mlx-lm is installed # pip install --upgrade mlx-lm # Generate text with mlx-lm from mlx_lm import load, generate model, tokenizer = load("mlx-community/Qwen3.5-4B-3D-Coder-OptiQ-4bit") prompt = "Write a story about Einstein" messages = [{"role": "user", "content": prompt}] prompt = tokenizer.apply_chat_template( messages, add_generation_prompt=True ) text = generate(model, tokenizer, prompt=prompt, verbose=True) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- LM Studio
- Pi
How to use mlx-community/Qwen3.5-4B-3D-Coder-OptiQ-4bit with Pi:
Start the MLX server
# Install MLX LM: uv tool install mlx-lm # Start a local OpenAI-compatible server: mlx_lm.server --model "mlx-community/Qwen3.5-4B-3D-Coder-OptiQ-4bit"
Configure the model in Pi
# Install Pi: npm install -g @earendil-works/pi-coding-agent # Add to ~/.pi/agent/models.json: { "providers": { "mlx-lm": { "baseUrl": "http://localhost:8080/v1", "api": "openai-completions", "apiKey": "none", "models": [ { "id": "mlx-community/Qwen3.5-4B-3D-Coder-OptiQ-4bit" } ] } } }Run Pi
# Start Pi in your project directory: pi
- MLX LM
How to use mlx-community/Qwen3.5-4B-3D-Coder-OptiQ-4bit with MLX LM:
Generate or start a chat session
# Install MLX LM uv tool install mlx-lm # Interactive chat REPL mlx_lm.chat --model "mlx-community/Qwen3.5-4B-3D-Coder-OptiQ-4bit"
Run an OpenAI-compatible server
# Install MLX LM uv tool install mlx-lm # Start the server mlx_lm.server --model "mlx-community/Qwen3.5-4B-3D-Coder-OptiQ-4bit" # Calling the OpenAI-compatible server with curl curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "mlx-community/Qwen3.5-4B-3D-Coder-OptiQ-4bit", "messages": [ {"role": "user", "content": "Hello"} ] }' - Hermes Agent
How to use mlx-community/Qwen3.5-4B-3D-Coder-OptiQ-4bit with Hermes Agent:
Start the MLX server
# Install MLX LM: uv tool install mlx-lm # Start a local OpenAI-compatible server: mlx_lm.server --model "mlx-community/Qwen3.5-4B-3D-Coder-OptiQ-4bit"
Configure Hermes
# Install Hermes: curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash hermes setup # Point Hermes at the local server: hermes config set model.provider custom hermes config set model.base_url http://127.0.0.1:8080/v1 hermes config set model.default mlx-community/Qwen3.5-4B-3D-Coder-OptiQ-4bit
Run Hermes
hermes
- Atomic Chat
- OpenClaw
How to use mlx-community/Qwen3.5-4B-3D-Coder-OptiQ-4bit with OpenClaw:
Start the MLX server
# Install MLX LM: uv tool install mlx-lm # Start a local OpenAI-compatible server: mlx_lm.server --model "mlx-community/Qwen3.5-4B-3D-Coder-OptiQ-4bit"
Configure OpenClaw
# Install OpenClaw: npm install -g openclaw@latest # Register the local server and set it as the default model: openclaw onboard --non-interactive --mode local \ --auth-choice custom-api-key \ --custom-base-url http://127.0.0.1:8080/v1 \ --custom-model-id "mlx-community/Qwen3.5-4B-3D-Coder-OptiQ-4bit" \ --custom-provider-id mlx-lm \ --custom-compatibility openai \ --custom-text-input \ --accept-risk \ --skip-health
Run OpenClaw
openclaw agent --local --agent main --message "Hello from Hugging Face"
Download examples/golem.html from mlx-community/Qwen3.5-4B-3D-Coder-OptiQ-4bit: direct link, hf CLI and curl.
- Browser
- Download file 23 kB
-
https://huggingface.co/mlx-community/Qwen3.5-4B-3D-Coder-OptiQ-4bit/resolve/main/examples/golem.html
- Command line
-
hf download hf://mlx-community/Qwen3.5-4B-3D-Coder-OptiQ-4bit/examples/golem.html
-
curl -L -o golem.html https://huggingface.co/mlx-community/Qwen3.5-4B-3D-Coder-OptiQ-4bit/resolve/main/examples/golem.html
23 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>Stone Golem Boss</title> | |
| <style> | |
| :root { | |
| color-scheme: dark; | |
| font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, | |
| "Segoe UI", sans-serif; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| } | |
| html, | |
| body { | |
| width: 100%; | |
| height: 100%; | |
| margin: 0; | |
| overflow: hidden; | |
| background: | |
| radial-gradient(circle at 50% 35%, #172126 0%, #080c0e 48%, #030506 100%); | |
| } | |
| #stage { | |
| position: fixed; | |
| inset: 0; | |
| } | |
| canvas { | |
| display: block; | |
| width: 100%; | |
| height: 100%; | |
| touch-action: none; | |
| cursor: grab; | |
| } | |
| canvas:active { | |
| cursor: grabbing; | |
| } | |
| .hud { | |
| position: fixed; | |
| z-index: 10; | |
| pointer-events: none; | |
| user-select: none; | |
| } | |
| .title { | |
| top: 28px; | |
| left: 30px; | |
| max-width: min(420px, calc(100vw - 60px)); | |
| } | |
| .eyebrow { | |
| margin-bottom: 8px; | |
| color: #75e58c; | |
| font-size: 11px; | |
| font-weight: 700; | |
| letter-spacing: 0.24em; | |
| text-transform: uppercase; | |
| } | |
| h1 { | |
| margin: 0; | |
| color: #edf4f1; | |
| font-size: clamp(25px, 4vw, 48px); | |
| font-weight: 800; | |
| line-height: 0.98; | |
| letter-spacing: -0.04em; | |
| text-transform: uppercase; | |
| text-shadow: 0 0 30px rgba(47, 225, 116, 0.22); | |
| } | |
| .subtitle { | |
| margin-top: 12px; | |
| color: #8d9b9b; | |
| font-size: 12px; | |
| line-height: 1.5; | |
| letter-spacing: 0.06em; | |
| } | |
| .controls { | |
| right: 24px; | |
| bottom: 24px; | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| pointer-events: auto; | |
| } | |
| .hint { | |
| color: #899595; | |
| font-size: 11px; | |
| letter-spacing: 0.05em; | |
| } | |
| button { | |
| border: 1px solid rgba(117, 229, 140, 0.35); | |
| border-radius: 999px; | |
| padding: 9px 14px; | |
| color: #bfe9a3; | |
| background: rgba(12, 22, 22, 0.72); | |
| font: inherit; | |
| font-size: 11px; | |
| font-weight: 700; | |
| letter-spacing: 0.08em; | |
| text-transform: uppercase; | |
| cursor: pointer; | |
| backdrop-filter: blur(10px); | |
| transition: background 160ms ease, border-color 160ms ease, transform 160ms ease; | |
| } | |
| button:hover { | |
| border-color: rgba(117, 229, 140, 0.75); | |
| background: rgba(20, 38, 35, 0.88); | |
| transform: translateY(-1px); | |
| } | |
| .status { | |
| top: 30px; | |
| right: 28px; | |
| display: flex; | |
| align-items: center; | |
| gap: 9px; | |
| color: #7d8b8b; | |
| font-size: 10px; | |
| font-weight: 700; | |
| letter-spacing: 0.16em; | |
| text-transform: uppercase; | |
| } | |
| .status::before { | |
| width: 7px; | |
| height: 7px; | |
| border-radius: 50%; | |
| background: #55ff91; | |
| box-shadow: 0 0 12px #55ff91; | |
| content: ""; | |
| } | |
| #loading { | |
| position: fixed; | |
| inset: 0; | |
| z-index: 20; | |
| display: grid; | |
| place-items: center; | |
| color: #7ce38c; | |
| background: #070b0c; | |
| font-size: 11px; | |
| font-weight: 700; | |
| letter-spacing: 0.2em; | |
| text-transform: uppercase; | |
| transition: opacity 500ms ease; | |
| } | |
| #loading.hidden { | |
| opacity: 0; | |
| pointer-events: none; | |
| } | |
| @media (max-width: 620px) { | |
| .title { | |
| top: 20px; | |
| left: 20px; | |
| } | |
| .subtitle { | |
| max-width: 270px; | |
| } | |
| .status { | |
| display: none; | |
| } | |
| .controls { | |
| right: 16px; | |
| bottom: 16px; | |
| } | |
| .hint { | |
| display: none; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="stage"></div> | |
| <div class="hud title"> | |
| <div class="eyebrow">Sector 07 · Final Guardian</div> | |
| <h1>Stone Golem</h1> | |
| <div class="subtitle"> | |
| An ancient guardian awakened by a surge of corrupted bio-luminescent energy. | |
| </div> | |
| </div> | |
| <div class="hud status">Turntable active</div> | |
| <div class="hud controls"> | |
| <span class="hint">Drag to orbit · Scroll to zoom</span> | |
| <button id="rotateButton" type="button">Pause rotation</button> | |
| </div> | |
| <div id="loading">Forging the guardian…</div> | |
| <script type="module"> | |
| import * as THREE from "https://cdnjs.cloudflare.com/ajax/libs/three.js/0.160.0/three.module.min.js"; | |
| const stage = document.getElementById("stage"); | |
| const loading = document.getElementById("loading"); | |
| const rotateButton = document.getElementById("rotateButton"); | |
| // --------------------------------------------------------------------- | |
| // Renderer and scene | |
| // --------------------------------------------------------------------- | |
| const scene = new THREE.Scene(); | |
| scene.background = new THREE.Color(0x070b0c); | |
| scene.fog = new THREE.Fog(0x070b0c, 17, 43); | |
| const camera = new THREE.PerspectiveCamera( | |
| 42, | |
| window.innerWidth / window.innerHeight, | |
| 0.1, | |
| 100 | |
| ); | |
| const renderer = new THREE.WebGLRenderer({ | |
| antialias: true, | |
| powerPreference: "high-performance" | |
| }); | |
| renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| renderer.outputColorSpace = THREE.SRGBColorSpace; | |
| renderer.toneMapping = THREE.ACESFilmicToneMapping; | |
| renderer.toneMappingExposure = 1.12; | |
| renderer.shadowMap.enabled = true; | |
| renderer.shadowMap.type = THREE.PCFSoftShadowMap; | |
| stage.appendChild(renderer.domElement); | |
| const hemi = new THREE.HemisphereLight(0x9ab4c4, 0x17120b, 1.25); | |
| scene.add(hemi); | |
| const keyLight = new THREE.DirectionalLight(0xffe1b0, 3.2); | |
| keyLight.position.set(6, 11, 8); | |
| keyLight.castShadow = true; | |
| keyLight.shadow.mapSize.set(2048, 2048); | |
| keyLight.shadow.camera.left = -8; | |
| keyLight.shadow.camera.right = 8; | |
| keyLight.shadow.camera.top = 10; | |
| keyLight.shadow.camera.bottom = -4; | |
| keyLight.shadow.camera.near = 1; | |
| keyLight.shadow.camera.far = 28; | |
| keyLight.shadow.bias = -0.0005; | |
| scene.add(keyLight); | |
| const rimLight = new THREE.DirectionalLight(0x43d99a, 2.1); | |
| rimLight.position.set(-7, 5, -6); | |
| scene.add(rimLight); | |
| const frontFill = new THREE.PointLight(0x677b78, 12, 18, 2); | |
| frontFill.position.set(0, 4, 7); | |
| scene.add(frontFill); | |
| // --------------------------------------------------------------------- | |
| // Materials | |
| // --------------------------------------------------------------------- | |
| const stoneMaterial = new THREE.MeshStandardMaterial({ | |
| color: 0x667073, | |
| roughness: 0.92, | |
| metalness: 0.08 | |
| }); | |
| const darkStoneMaterial = new THREE.MeshStandardMaterial({ | |
| color: 0x343d40, | |
| roughness: 0.98, | |
| metalness: 0.04 | |
| }); | |
| const wornStoneMaterial = new THREE.MeshStandardMaterial({ | |
| color: 0x858b87, | |
| roughness: 0.95, | |
| metalness: 0.03 | |
| }); | |
| const armorMaterial = new THREE.MeshStandardMaterial({ | |
| color: 0x414b4d, | |
| roughness: 0.68, | |
| metalness: 0.48 | |
| }); | |
| const ironMaterial = new THREE.MeshStandardMaterial({ | |
| color: 0x20282b, | |
| roughness: 0.58, | |
| metalness: 0.72 | |
| }); | |
| const crackMaterial = new THREE.LineBasicMaterial({ | |
| color: 0x171b1c, | |
| transparent: true, | |
| opacity: 0.72 | |
| }); | |
| const crystalMaterial = new THREE.MeshStandardMaterial({ | |
| color: 0x8dffa5, | |
| emissive: 0x2dff65, | |
| emissiveIntensity: 1.7, | |
| roughness: 0.28, | |
| metalness: 0.12, | |
| flatShading: true | |
| }); | |
| const crystalCoreMaterial = new THREE.MeshStandardMaterial({ | |
| color: 0xd6ffbf, | |
| emissive: 0x75ff9d, | |
| emissiveIntensity: 2.4, | |
| roughness: 0.2, | |
| metalness: 0.08, | |
| flatShading: true | |
| }); | |
| const eyeMaterial = new THREE.MeshStandardMaterial({ | |
| color: 0x82e8a3, | |
| emissive: 0x35ff83, | |
| emissiveIntensity: 1.5, | |
| roughness: 0.25 | |
| }); | |
| const edgeMaterial = new THREE.LineBasicMaterial({ | |
| color: 0x252d2e, | |
| transparent: true, | |
| opacity: 0.65 | |
| }); | |
| // --------------------------------------------------------------------- | |
| // Helpers | |
| // --------------------------------------------------------------------- | |
| const boss = new THREE.Group(); | |
| boss.position.y = 0.02; | |
| scene.add(boss); | |
| function box( | |
| width, | |
| height, | |
| depth, | |
| material, | |
| x, | |
| y, | |
| z, | |
| parent = boss, | |
| name = "stone-part" | |
| ) { | |
| const mesh = new THREE.Mesh( | |
| new THREE.BoxGeometry(width, height, depth), | |
| material | |
| ); | |
| mesh.position.set(x, y, z); | |
| mesh.castShadow = true; | |
| mesh.receiveShadow = true; | |
| mesh.name = name; | |
| parent.add(mesh); | |
| return mesh; | |
| } | |
| function cylinder( | |
| radiusTop, | |
| radiusBottom, | |
| height, | |
| material, | |
| x, | |
| y, | |
| z, | |
| parent = boss, | |
| name = "stone-part" | |
| ) { | |
| const mesh = new THREE.Mesh( | |
| new THREE.CylinderGeometry(radiusTop, radiusBottom, height, 12), | |
| material | |
| ); | |
| mesh.position.set(x, y, z); | |
| mesh.castShadow = true; | |
| mesh.receiveShadow = true; | |
| mesh.name = name; | |
| parent.add(mesh); | |
| return mesh; | |
| } | |
| function addEdges(mesh, opacity = 0.55) { | |
| const edges = new THREE.LineSegments( | |
| new THREE.EdgesGeometry(mesh.geometry), | |
| new THREE.LineBasicMaterial({ | |
| color: 0x1c2527, | |
| transparent: true, | |
| opacity | |
| }) | |
| ); | |
| edges.position.copy(mesh.position); | |
| mesh.parent.add(edges); | |
| return edges; | |
| } | |
| // --------------------------------------------------------------------- | |
| // Ground and subtle environmental marker | |
| // --------------------------------------------------------------------- | |
| const ground = new THREE.Mesh( | |
| new THREE.PlaneGeometry(80, 80), | |
| new THREE.MeshStandardMaterial({ | |
| color: 0x111719, | |
| roughness: 1, | |
| metalness: 0 | |
| }) | |
| ); | |
| ground.rotation.x = -Math.PI / 2; | |
| ground.position.y = -0.015; | |
| ground.receiveShadow = true; | |
| scene.add(ground); | |
| const halo = new THREE.Mesh( | |
| new THREE.RingGeometry(3.6, 3.63, 96), | |
| new THREE.MeshBasicMaterial({ | |
| color: 0x55ff8a, | |
| transparent: true, | |
| opacity: 0.18, | |
| side: THREE.DoubleSide, | |
| depthWrite: false | |
| }) | |
| ); | |
| halo.rotation.x = -Math.PI / 2; | |
| halo.position.y = 0.008; | |
| scene.add(halo); | |
| const innerHalo = new THREE.Mesh( | |
| new THREE.RingGeometry(2.25, 2.265, 96), | |
| new THREE.MeshBasicMaterial({ | |
| color: 0x85ff9d, | |
| transparent: true, | |
| opacity: 0.3, | |
| side: THREE.DoubleSide, | |
| depthWrite: false | |
| }) | |
| ); | |
| innerHalo.rotation.x = -Math.PI / 2; | |
| innerHalo.position.y = 0.012; | |
| scene.add(innerHalo); | |
| // --------------------------------------------------------------------- | |
| // Legs and feet | |
| // --------------------------------------------------------------------- | |
| const leftLeg = box( | |
| 1.55, 2.65, 1.75, | |
| stoneMaterial, | |
| -1.02, 1.55, 0, | |
| boss, | |
| "left-thigh" | |
| ); | |
| addEdges(leftLeg, 0.4); | |
| const rightLeg = box( | |
| 1.55, 2.65, 1.75, | |
| stoneMaterial, | |
| 1.02, 1.55, 0, | |
| boss, | |
| "right-thigh" | |
| ); | |
| addEdges(rightLeg, 0.4); | |
| const leftFoot = box( | |
| 2.05, 0.55, 2.45, | |
| darkStoneMaterial, | |
| -1.02, 0.32, 0.1, | |
| boss, | |
| "left-foot" | |
| ); | |
| const rightFoot = box( | |
| 2.05, 0.55, 2.45, | |
| darkStoneMaterial, | |
| 1.02, 0.32, 0.1, | |
| boss, | |
| "right-foot" | |
| ); | |
| // --------------------------------------------------------------------- | |
| // Torso, chest, neck, and head | |
| // --------------------------------------------------------------------- | |
| const lowerTorso = box( | |
| 3.15, 4.15, 3.0, | |
| stoneMaterial, | |
| 0, 2.68, 0, | |
| boss, | |
| "lower-torso" | |
| ); | |
| addEdges(lowerTorso, 0.32); | |
| const chest = box( | |
| 3.45, 1.75, 3.25, | |
| wornStoneMaterial, | |
| 0, 4.25, 0, | |
| boss, | |
| "chest-plate" | |
| ); | |
| addEdges(chest, 0.38); | |
| const upperTorso = box( | |
| 2.75, 1.1, 2.65, | |
| stoneMaterial, | |
| 0, 5.3, 0, | |
| boss, | |
| "upper-torso" | |
| ); | |
| const neck = box( | |
| 1.35, 1.1, 1.5, | |
| darkStoneMaterial, | |
| 0, 5.7, 0, | |
| boss, | |
| "neck" | |
| ); | |
| addEdges(neck, 0.4); | |
| const skull = box( | |
| 2.35, 1.65, 2.05, | |
| stoneMaterial, | |
| 0, 6.45, 0, | |
| boss, | |
| "skull" | |
| ); | |
| addEdges(skull, 0.45); | |
| const jaw = box( | |
| 2.65, 0.65, 2.18, | |
| darkStoneMaterial, | |
| 0, 5.55, 0.04, | |
| boss, | |
| "jaw" | |
| ); | |
| // Brow ridges. | |
| box(0.85, 0.34, 0.42, darkStoneMaterial, -0.78, 6.94, 0.72, boss, "left-brow"); | |
| box(0.85, 0.34, 0.42, darkStoneMaterial, 0.78, 6.94, 0.72, boss, "right-brow"); | |
| // Eye sockets and luminous cores. | |
| box(0.34, 0.43, 0.18, ironMaterial, -0.57, 6.55, 1.02, boss, "left-eye-socket"); | |
| box(0.34, 0.43, 0.18, ironMaterial, 0.57, 6.55, 1.02, boss, "right-eye-socket"); | |
| box(0.18, 0.25, 0.12, eyeMaterial, -0.57, 6.55, 1.13, boss, "left-eye"); | |
| box(0.18, 0.25, 0.12, eyeMaterial, 0.57, 6.55, 1.13, boss, "right-eye"); | |
| // Mouth and cheek stones. | |
| box(1.45, 0.16, 0.12, ironMaterial, 0, 5.92, 1.06, boss, "mouth"); | |
| box(0.45, 0.55, 0.4, darkStoneMaterial, -0.9, 6.08, 0.85, boss, "left-cheek"); | |
| box(0.45, 0.55, 0.4, darkStoneMaterial, 0.9, 6.08, 0.85, boss, "right-cheek"); | |
| // --------------------------------------------------------------------- | |
| // Arms, gauntlets, and shoulder armor | |
| // --------------------------------------------------------------------- | |
| for (const side of [-1, 1]) { | |
| const upperArm = box( | |
| 1.35, 2.15, 1.45, | |
| stoneMaterial, | |
| side * 2.35, 3.2, 0, | |
| boss, | |
| `${side === -1 ? "left" : "right"}-upper-arm` | |
| ); | |
| addEdges(upperArm, 0.3); | |
| const forearm = box( | |
| 1.0, 2.45, 1.18, | |
| wornStoneMaterial, | |
| side * 3.08, 2.0, 0.05, | |
| boss, | |
| `${side === -1 ? "left" : "right"}-forearm` | |
| ); | |
| addEdges(forearm, 0.3); | |
| const hand = box( | |
| 1.05, 1.05, 1.15, | |
| ironMaterial, | |
| side * 3.08, 1.05, 0.08, | |
| boss, | |
| `${side === -1 ? "left" : "right"}-hand` | |
| ); | |
| addEdges(hand, 0.35); | |
| const shoulderPad = box( | |
| 1.7, 0.72, 1.55, | |
| armorMaterial, | |
| side * 2.2, 4.18, 0.05, | |
| boss, | |
| `${side === -1 ? "left" : "right"}-shoulder-pad` | |
| ); | |
| addEdges(shoulderPad, 0.35); | |
| const gauntlet = box( | |
| 1.12, 0.75, 1.22, | |
| ironMaterial, | |
| side * 3.05, 1.67, 0.08, | |
| boss, | |
| `${side === -1 ? "left" : "right"}-gauntlet` | |
| ); | |
| addEdges(gauntlet, 0.3); | |
| } | |
| // Belt and chest trim. | |
| const belt = new THREE.Mesh( | |
| new THREE.TorusGeometry(1.62, 0.16, 8, 40), | |
| ironMaterial | |
| ); | |
| belt.rotation.x = Math.PI / 2; | |
| belt.position.y = 2.45; | |
| belt.castShadow = true; | |
| boss.add(belt); | |
| box(0.8, 0.22, 0.12, ironMaterial, 0, 4.9, 1.68, boss, "chest-trim"); | |
| // --------------------------------------------------------------------- | |
| // Glowing green shoulder crystal clusters | |
| // --------------------------------------------------------------------- | |
| const animatedCrystals = []; | |
| function createShoulderCrystals(side) { | |
| const cluster = new THREE.Group(); | |
| cluster.position.set(side * 2.18, 4.38, 0.52); | |
| cluster.rotation.z = side * 0.035; | |
| cluster.name = `${side === -1 ? "left" : "right"}-crystal-cluster`; | |
| boss.add(cluster); | |
| const stem = box( | |
| 0.22, 0.55, 0.22, | |
| ironMaterial, | |
| 0, 0.25, 0, | |
| cluster, | |
| "crystal-stem" | |
| ); | |
| addEdges(stem, 0.25); | |
| const centralCrystal = new THREE.Mesh( | |
| new THREE.ConeGeometry(0.42, 1.55, 5, 1), | |
| crystalMaterial | |
| ); | |
| centralCrystal.position.set(0, 1.13, 0); | |
| centralCrystal.castShadow = true; | |
| centralCrystal.name = "central-shoulder-crystal"; | |
| cluster.add(centralCrystal); | |
| const sideCrystals = [ | |
| { x: -0.46, y: 0.86, scale: 0.72 }, | |
| { x: 0.46, y: 0.86, scale: 0.72 }, | |
| { x: 0, y: 0.68, scale: 0.48 } | |
| ]; | |
| for (const shard of sideCrystals) { | |
| const crystal = new THREE.Mesh( | |
| new THREE.OctahedronGeometry(0.34, 0), | |
| crystalMaterial | |
| ); | |
| crystal.position.set(shard.x, shard.y, 0.02); | |
| crystal.scale.setScalar(shard.scale); | |
| crystal.rotation.y = shard.x * 0.7; | |
| crystal.castShadow = true; | |
| crystal.name = `shoulder-${side === -1 ? "left" : "right"}-crystal`; | |
| cluster.add(crystal); | |
| } | |
| const clusterLight = new THREE.PointLight(0x4dff78, 4.2, 5, 2); | |
| clusterLight.position.set(0, 1.15, 0.2); | |
| cluster.add(clusterLight); | |
| animatedCrystals.push({ | |
| group: cluster, | |
| phase: side * 1.7, | |
| baseScale: 1 | |
| }); | |
| } | |
| createShoulderCrystals(-1); | |
| createShoulderCrystals(1); | |
| // A smaller crystal at the center of the chest. | |
| const chestCrystal = new THREE.Mesh( | |
| new THREE.OctahedronGeometry(0.38, 0), | |
| crystalCoreMaterial | |
| ); | |
| chestCrystal.position.set(0, 4.75, 1.2); | |
| chestCrystal.castShadow = true; | |
| boss.add(chestCrystal); | |
| const chestLight = new THREE.PointLight(0x3dff83, 2.8, 4, 2); | |
| chestLight.position.set(0, 4.75, 1.35); | |
| boss.add(chestLight); | |
| // --------------------------------------------------------------------- | |
| // Small environmental particles | |
| // --------------------------------------------------------------------- | |
| const particleCount = 28; | |
| const particlePositions = new Float32Array(particleCount * 3); | |
| for (let i = 0; i < particleCount; i++) { | |
| const angle = Math.random() * Math.PI * 2; | |
| const radius = 2.5 + Math.random() * 5; | |
| particlePositions[i * 3] = Math.cos(angle) * radius; | |
| particlePositions[i * 3 + 1] = 0.3 + Math.random() * 4.5; | |
| particlePositions[i * 3 + 2] = Math.sin(angle) * radius; | |
| } | |
| const particleGeometry = new THREE.BufferGeometry(); | |
| particleGeometry.setAttribute( | |
| "position", | |
| new THREE.BufferAttribute(particlePositions, 3) | |
| ); | |
| const particles = new THREE.Points( | |
| particleGeometry, | |
| new THREE.PointsMaterial({ | |
| color: 0x65ff91, | |
| size: 0.045, | |
| transparent: true, | |
| opacity: 0.65, | |
| depthWrite: false, | |
| blending: THREE.AdditiveBlending | |
| }) | |
| ); | |
| scene.add(particles); | |
| // --------------------------------------------------------------------- | |
| // Custom camera controls | |
| // --------------------------------------------------------------------- | |
| const cameraState = { | |
| radius: 12.2, | |
| theta: 0.72, | |
| phi: 1.17, | |
| target: new THREE.Vector3(0, 3.25, 0) | |
| }; | |
| const defaultCamera = { ...cameraState }; | |
| function updateCamera() { | |
| const sinPhi = Math.sin(cameraState.phi); | |
| camera.position.set( | |
| cameraState.target.x + | |
| cameraState.radius * sinPhi * Math.sin(cameraState.theta), | |
| cameraState.target.y + | |
| cameraState.radius * Math.cos(cameraState.phi), | |
| cameraState.target.z + | |
| cameraState.radius * sinPhi * Math.cos(cameraState.theta) | |
| ); | |
| camera.lookAt(cameraState.target); | |
| } | |
| updateCamera(); | |
| let dragging = false; | |
| let pointerX = 0; | |
| let pointerY = 0; | |
| renderer.domElement.addEventListener("pointerdown", (event) => { | |
| dragging = true; | |
| pointerX = event.clientX; | |
| pointerY = event.clientY; | |
| renderer.domElement.setPointerCapture(event.pointerId); | |
| }); | |
| renderer.domElement.addEventListener("pointermove", (event) => { | |
| if (!dragging) return; | |
| const deltaX = event.clientX - pointerX; | |
| const deltaY = event.clientY - pointerY; | |
| pointerX = event.clientX; | |
| pointerY = event.clientY; | |
| cameraState.theta -= deltaX * 0.006; | |
| cameraState.phi = THREE.MathUtils.clamp( | |
| cameraState.phi - deltaY * 0.005, | |
| 0.55, | |
| 1.48 | |
| ); | |
| updateCamera(); | |
| }); | |
| function endDrag(event) { | |
| dragging = false; | |
| if (renderer.domElement.hasPointerCapture(event.pointerId)) { | |
| renderer.domElement.releasePointerCapture(event.pointerId); | |
| } | |
| } | |
| renderer.domElement.addEventListener("pointerup", endDrag); | |
| renderer.domElement.addEventListener("pointercancel", endDrag); | |
| renderer.domElement.addEventListener( | |
| "wheel", | |
| (event) => { | |
| event.preventDefault(); | |
| cameraState.radius = THREE.MathUtils.clamp( | |
| cameraState.radius * Math.exp(event.deltaY * 0.001), | |
| 7.2, | |
| 20 | |
| ); | |
| updateCamera(); | |
| }, | |
| { passive: false } | |
| ); | |
| // --------------------------------------------------------------------- | |
| // Turntable and animation | |
| // --------------------------------------------------------------------- | |
| let turning = true; | |
| let elapsed = 0; | |
| rotateButton.addEventListener("click", () => { | |
| turning = !turning; | |
| rotateButton.textContent = turning ? "Pause rotation" : "Resume rotation"; | |
| rotateButton.setAttribute("aria-pressed", String(!turning)); | |
| }); | |
| const clock = new THREE.Clock(); | |
| renderer.setAnimationLoop(() => { | |
| const delta = Math.min(clock.getDelta(), 0.05); | |
| elapsed += delta; | |
| // Slow 360-degree turntable rotation. | |
| if (turning) { | |
| boss.rotation.y += delta * 0.15; | |
| } | |
| // Pulsing crystal growth. | |
| for (const crystal of animatedCrystals) { | |
| const pulse = 1 + Math.sin(elapsed * 1.7 + crystal.phase) * 0.075; | |
| crystal.group.scale.setScalar(pulse); | |
| } | |
| crystalMaterial.emissiveIntensity = 1.65 + Math.sin(elapsed * 2.1) * 0.22; | |
| crystalCoreMaterial.emissiveIntensity = 2.25 + Math.sin(elapsed * 2.8) * 0.35; | |
| chestCrystal.scale.setScalar(1 + Math.sin(elapsed * 2.3) * 0.08); | |
| particles.rotation.y = elapsed * 0.08; | |
| particles.position.y = Math.sin(elapsed * 0.7) * 0.12; | |
| halo.rotation.z = elapsed * 0.025; | |
| halo.material.opacity = 0.16 + Math.sin(elapsed * 0.8) * 0.035; | |
| renderer.render(scene, camera); | |
| }); | |
| // --------------------------------------------------------------------- | |
| // Resize handling | |
| // --------------------------------------------------------------------- | |
| window.addEventListener("resize", () => { | |
| camera.aspect = window.innerWidth / window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| }); | |
| requestAnimationFrame(() => { | |
| loading.classList.add("hidden"); | |
| setTimeout(() => loading.remove(), 600); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |