Files
dualfroz d844455afc feat: add 3D pipeline research POC
Single-file Three.js test of a procedurally generated low-poly rock
(no assets) alongside a primitive-built rig with tweened rotation
animation, to scope the 3D tooling pipeline before committing to it.
2026-07-26 23:18:52 +02:00

189 lines
6.4 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>3D pipeline POC — procedural low-poly + free CC0 asset + tween animation</title>
<style>
html, body { margin: 0; height: 100%; background: #0d1117; overflow: hidden; font-family: system-ui, sans-serif; }
#info { position: fixed; top: 10px; left: 12px; color: #c9d1d9; font-size: 13px; line-height: 1.5; z-index: 10; max-width: 480px; text-shadow: 0 1px 3px #000; }
#info b { color: #58a6ff; }
canvas { display: block; }
</style>
</head>
<body>
<div id="info">
<b>3D pipeline POC</b><br/>
Left: procedurally generated rock (Three.js BufferGeometry + noise displacement, zero assets).<br/>
Right: simple rig built from primitives, animated with tweened rotations (no skeletal export needed).<br/>
Ground plane: free CC0 texture would slot in here (Kenney/Poly Haven) — using a generated checker for this test.<br/>
Drag to orbit, scroll to zoom.
</div>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.165.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.165.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0d1117);
scene.fog = new THREE.Fog(0x0d1117, 10, 40);
const camera = new THREE.PerspectiveCamera(50, innerWidth / innerHeight, 0.1, 100);
camera.position.set(6, 5, 9);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
document.body.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 1, 0);
controls.enableDamping = true;
scene.add(new THREE.HemisphereLight(0x88aaff, 0x223311, 0.6));
const sun = new THREE.DirectionalLight(0xffffff, 1.6);
sun.position.set(5, 10, 4);
sun.castShadow = true;
sun.shadow.mapSize.set(1024, 1024);
scene.add(sun);
// ---- Ground: generated checker (stand-in for a CC0 texture from Kenney/Poly Haven) ----
const size = 64, data = new Uint8Array(size * size * 4);
for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) {
const i = (y * size + x) * 4;
const c = ((x >> 3) + (y >> 3)) % 2 ? 40 : 55;
data[i] = data[i+1] = c; data[i+2] = c + 10; data[i+3] = 255;
}
const checkerTex = new THREE.DataTexture(data, size, size);
checkerTex.wrapS = checkerTex.wrapT = THREE.RepeatWrapping;
checkerTex.repeat.set(6, 6);
checkerTex.needsUpdate = true;
const ground = new THREE.Mesh(
new THREE.PlaneGeometry(30, 30),
new THREE.MeshStandardMaterial({ map: checkerTex, roughness: 0.9 })
);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
scene.add(ground);
// ---- Procedural low-poly rock: icosahedron + per-vertex noise displacement ----
function hash(x, y, z) {
let s = Math.sin(x * 127.1 + y * 311.7 + z * 74.7) * 43758.5453;
return s - Math.floor(s);
}
function makeRock(radius, detail, roughnessAmt, seed) {
const geo = new THREE.IcosahedronGeometry(radius, detail);
const pos = geo.attributes.position;
const v = new THREE.Vector3();
for (let i = 0; i < pos.count; i++) {
v.fromBufferAttribute(pos, i);
const n = hash(v.x * 2 + seed, v.y * 2 + seed, v.z * 2 + seed);
const disp = 1 + (n - 0.5) * roughnessAmt;
v.multiplyScalar(disp);
pos.setXYZ(i, v.x, v.y * 0.8, v.z);
}
geo.computeVertexNormals();
return geo;
}
const rock = new THREE.Mesh(
makeRock(1.6, 1, 0.55, 3.1),
new THREE.MeshStandardMaterial({ color: 0x6b6458, flatShading: true, roughness: 1 })
);
rock.position.set(-3.5, 1.1, 0);
rock.castShadow = rock.receiveShadow = true;
scene.add(rock);
// scatter a few smaller rocks generated from the same function, different seeds — shows reusability
for (let i = 0; i < 5; i++) {
const s = 0.2 + Math.random() * 0.35;
const r = new THREE.Mesh(
makeRock(s, 0, 0.6, i * 5.7 + 1),
new THREE.MeshStandardMaterial({ color: 0x5a5348, flatShading: true, roughness: 1 })
);
const a = Math.random() * Math.PI * 2, d = 3 + Math.random() * 2;
r.position.set(-3.5 + Math.cos(a) * d, s * 0.6, Math.sin(a) * d);
r.castShadow = r.receiveShadow = true;
scene.add(r);
}
// ---- Simple primitive-rig creature: no skeleton, just a hierarchy of meshes + tweened local rotations ----
const rig = new THREE.Group();
rig.position.set(3.2, 0, 0);
scene.add(rig);
const mat = new THREE.MeshStandardMaterial({ color: 0xd97757, flatShading: true });
const jointMat = new THREE.MeshStandardMaterial({ color: 0x8a4a33, flatShading: true });
function box(w, h, d, m = mat) {
const mesh = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), m);
mesh.castShadow = true;
return mesh;
}
const torso = box(0.7, 0.9, 0.45);
torso.position.y = 1.35;
rig.add(torso);
const head = box(0.45, 0.45, 0.45);
head.position.y = 1.0;
torso.add(head);
function makeLimb(side, isLeg) {
const hip = new THREE.Group();
hip.position.set(side * 0.3, isLeg ? -0.45 : 0.4, 0);
const upper = box(0.2, isLeg ? 0.55 : 0.5, 0.2, jointMat);
upper.position.y = isLeg ? -0.27 : -0.25;
hip.add(upper);
const knee = new THREE.Group();
knee.position.y = isLeg ? -0.55 : -0.5;
upper.add(knee);
const lower = box(0.17, isLeg ? 0.5 : 0.45, 0.17, mat);
lower.position.y = isLeg ? -0.25 : -0.22;
knee.add(lower);
torso.add(hip);
return { hip, knee };
}
const legL = makeLimb(-1, true);
const legR = makeLimb(1, true);
const armL = makeLimb(-1, false);
const armR = makeLimb(1, false);
// tweened walk cycle — pure JS, no animation clips/skeleton needed for this fidelity level
const clock = new THREE.Clock();
function animate() {
const t = clock.getElapsedTime();
const walk = Math.sin(t * 3);
legL.hip.rotation.x = walk * 0.6;
legR.hip.rotation.x = -walk * 0.6;
legL.knee.rotation.x = Math.max(0, -walk) * 0.8;
legR.knee.rotation.x = Math.max(0, walk) * 0.8;
armL.hip.rotation.x = -walk * 0.5;
armR.hip.rotation.x = walk * 0.5;
torso.position.y = 1.35 + Math.abs(Math.sin(t * 6)) * 0.03;
rig.rotation.y = t * 0.4;
rock.rotation.y = t * 0.05;
controls.update();
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
animate();
addEventListener('resize', () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
</script>
</body>
</html>