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.
This commit is contained in:
commit
d844455afc
2 files changed
+191
No files matched your search
+188
@@ -0,0 +1,188 @@
|
||||
<!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>
|
||||
Reference in new issue
Block a user