Skip to content
Kiln1.3Get Kiln

Eight articulated legs with named contact points, original pixel atlas and a closed scuttle animation. Scene contact solving is separate.

Cabin in the Woods: PSX Horror pack
Eight articulated legs with named contact points, original pixel atlas and a closed scuttle animation. Scene contact solving is separate.

Drag to orbit after opening. The model starts stationary.

For your engine. The download is a standard glTF 2.0 binary (GLB) with PBR metallic-roughness materials. glTF stores materials, not lighting or tone mapping, so your engine decides how they read. This 3D view tone-maps with Review Neutral, the Khronos PBR Neutral construction with a smaller glare offset (0.015 instead of 0.04), at exposure 0.9. Its Tone mapping control also shows ACES and Linear, for comparison.

Triangles
578
Meshes
2
Materials
2
Dimensions (m)
1.24 × 0.63 × 1.76

Runtime assets

Models, textures and animations prepared for use in a scene or application.

23,077 bytes (0.02 MB)

Download runtime assets

Full Kiln assets (remixable)

Models plus Kiln source, editing metadata, materials and included revisions, so you can reopen and continue editing.

100,797 bytes (0.10 MB)

Download full kiln assets (remixable)

Runtime includes the GLB and metadata sidecar. The editable ZIP contains this selected revision and its complete material closure. Parent revision identifiers are retained; earlier authoring history is deliberately not distributed in this initial pack.

Import this ZIP into Kiln’s Library, or run node kiln.mjs import asset.zip in an authoring workspace. The full model, exact source, editing metadata, preview and material resources are included so you can reopen and remix it.

Use an importing Kiln build that supports the included material schemas. Use Kiln 1.2.3 or later.

Collection / asset / revision
project / a_c80528f46ba24acd948cab5eb439e09b / r_7f2e355bcb2e49919a434cfb62778cf9
Source SHA-256
5430593717810a293a95e18f0d781d63c7b07236322962d97cddf16a0a6fa32c

Native clips: Scuttle.

Woodland spider: exact saved source
const meta={name:"Woodland spider"};

function shapeGeo(center,rings,n=7){
 const P=[],UV=[];const pts=rings.map((r,j)=>Array.from({length:n},(_,i)=>{const a=i*2*Math.PI/n;return [center[0]+(r[3]||0)+Math.cos(a)*r[1],center[1]+r[0],center[2]+Math.sin(a)*r[2]];}));
 function tri(a,b,c){P.push(...a,...b,...c);const normal=new THREE.Vector3(...b).sub(new THREE.Vector3(...a)).cross(new THREE.Vector3(...c).sub(new THREE.Vector3(...a)));const abs=[Math.abs(normal.x),Math.abs(normal.y),Math.abs(normal.z)],d=abs.indexOf(Math.max(...abs));for(const p of [a,b,c])UV.push(.5+p[(d+1)%3]-center[(d+1)%3],.5+p[(d+2)%3]-center[(d+2)%3]);}
 for(let j=0;j<rings.length-1;j++)for(let i=0;i<n;i++){const k=(i+1)%n;tri(pts[j][i],pts[j+1][i],pts[j+1][k]);tri(pts[j][i],pts[j+1][k],pts[j][k]);}
 const low=[center[0]+(rings[0][3]||0),center[1]+rings[0][0],center[2]],r=rings[rings.length-1],high=[center[0]+(r[3]||0),center[1]+r[0],center[2]];
 for(let i=0;i<n;i++){const k=(i+1)%n;tri(low,pts[0][i],pts[0][k]);tri(high,pts[pts.length-1][k],pts[pts.length-1][i]);}return meshGeo({positions:P,uvs:UV});
}
function skinChunk(name,geometry,material,region,bones,boneIndex,root){
 const g=remapUVToAtlas(geometry,material.map,region,{inputOrigin:'top-left',mode:'texel-center'}),indices=[],weights=[];
 for(let i=0;i<g.getAttribute('position').count;i++){indices.push(boneIndex,0,0,0);weights.push(1,0,0,0);}
 return createSkinnedPart(name,g,material,{parent:root,bones,indices,weights});
}
function limbGeo(a,b,r0,r1){const A=new THREE.Vector3(...a),B=new THREE.Vector3(...b),L=A.distanceTo(B),axis=B.clone().sub(A).normalize(),u=new THREE.Vector3(0,1,0).cross(axis).normalize(),v=axis.clone().cross(u).normalize(),P=[],UV=[],N=6;
 function at(t,ang,r){return A.clone().lerp(B,t).addScaledVector(u,Math.cos(ang)*r).addScaledVector(v,Math.sin(ang)*r);}
 function tri(p,q,r,uv){P.push(...p.toArray(),...q.toArray(),...r.toArray());UV.push(...uv);}
 for(let i=0;i<N;i++){const circumference=2*Math.PI*(r0+r1)/2,x=i/N*circumference,y=(i+1)/N*circumference,a=i*2*Math.PI/N,b=(i+1)*2*Math.PI/N,p=at(0,a,r0),q=at(0,b,r0),s=at(1,a,r1),t=at(1,b,r1);tri(p,q,t,[x,0,y,0,y,Math.min(L,.98)]);if(r1>0)tri(p,t,s,[x,0,y,Math.min(L,.98),x,Math.min(L,.98)]);tri(A,q,p,[.5,.5,.5+Math.cos(b)*r0,.5+Math.sin(b)*r0,.5+Math.cos(a)*r0,.5+Math.sin(a)*r0]);if(r1>0)tri(B,s,t,[.5,.5,.5+Math.cos(a)*r1,.5+Math.sin(a)*r1,.5+Math.cos(b)*r1,.5+Math.sin(b)*r1]);}return meshGeo({positions:P,uvs:UV});}
const legSpecs=[{"id": "L0", "hip": [0.26, 0.37, -0.17], "knee": [0.54, 0.57, -0.48], "foot": [0.69, 0, -0.84], "upperLength": 0.4631414470763764, "lowerLength": 0.6906518659932802, "bones": ["Spider_L0_Hip", "Spider_L0_Knee", "Spider_L0_Foot"], "contact": "Spider_L0_Contact", "restPlaneNormal": [-0.7835321761633123, 0.17107276705133856, -0.5973369545659992]}, {"id": "L1", "hip": [0.11, 0.37, -0.17], "knee": [0.24000000000000005, 0.57, -0.515], "foot": [0.31, 0, -0.88], "upperLength": 0.41943414262551393, "lowerLength": 0.6804594036384536, "bones": ["Spider_L1_Hip", "Spider_L1_Knee", "Spider_L1_Foot"], "contact": "Spider_L1_Contact", "restPlaneNormal": [-0.9473621687633991, 0.08185996118000088, -0.3095219991398312]}, {"id": "L2", "hip": [-0.1, 0.37, -0.17], "knee": [-0.11999999999999997, 0.57, -0.48], "foot": [-0.13000000000000003, 0, -0.84], "upperLength": 0.36945906403822326, "lowerLength": 0.6742403132415029, "bones": ["Spider_L2_Hip", "Spider_L2_Knee", "Spider_L2_Foot"], "contact": "Spider_L2_Contact", "restPlaneNormal": [-0.9984163436010449, -0.016459618048911346, 0.05379484923302773]}, {"id": "L3", "hip": [-0.27, 0.37, -0.17], "knee": [-0.43999999999999995, 0.57, -0.515], "foot": [-0.53, 0, -0.88], "upperLength": 0.43350317184537407, "lowerLength": 0.6828067076413353, "bones": ["Spider_L3_Hip", "Spider_L3_Knee", "Spider_L3_Foot"], "contact": "Spider_L3_Contact", "restPlaneNormal": [-0.9148611472882211, -0.10517595240472766, 0.38982957842913624]}, {"id": "R0", "hip": [0.26, 0.37, 0.17], "knee": [0.54, 0.57, 0.48], "foot": [0.69, 0, 0.84], "upperLength": 0.4631414470763764, "lowerLength": 0.6906518659932802, "bones": ["Spider_R0_Hip", "Spider_R0_Knee", "Spider_R0_Foot"], "contact": "Spider_R0_Contact", "restPlaneNormal": [0.7835321761633123, -0.17107276705133856, -0.5973369545659992]}, {"id": "R1", "hip": [0.11, 0.37, 0.17], "knee": [0.24000000000000005, 0.57, 0.515], "foot": [0.31, 0, 0.88], "upperLength": 0.41943414262551393, "lowerLength": 0.6804594036384536, "bones": ["Spider_R1_Hip", "Spider_R1_Knee", "Spider_R1_Foot"], "contact": "Spider_R1_Contact", "restPlaneNormal": [0.9473621687633991, -0.08185996118000088, -0.3095219991398312]}, {"id": "R2", "hip": [-0.1, 0.37, 0.17], "knee": [-0.11999999999999997, 0.57, 0.48], "foot": [-0.13000000000000003, 0, 0.84], "upperLength": 0.36945906403822326, "lowerLength": 0.6742403132415029, "bones": ["Spider_R2_Hip", "Spider_R2_Knee", "Spider_R2_Foot"], "contact": "Spider_R2_Contact", "restPlaneNormal": [0.9984163436010449, 0.016459618048911346, 0.05379484923302773]}, {"id": "R3", "hip": [-0.27, 0.37, 0.17], "knee": [-0.43999999999999995, 0.57, 0.515], "foot": [-0.53, 0, 0.88], "upperLength": 0.43350317184537407, "lowerLength": 0.6828067076413353, "bones": ["Spider_R3_Hip", "Spider_R3_Knee", "Spider_R3_Foot"], "contact": "Spider_R3_Contact", "restPlaneNormal": [0.9148611472882211, 0.10517595240472766, 0.38982957842913624]}];
async function build(){const root=createRoot('Spider');const material=await compilePortableMaterialSpecV3({"schemaVersion":3,"model":"pbrMetallicRoughness","name":"Forest spider-atlas","baseColor":16777215,"roughness":1,"metalness":0,"emissiveIntensity":1,"alphaMode":"opaque","alphaCutoff":0.5,"doubleSided":false,"textures":{"baseColor":{"kind":"resource","resourceId":"kiln.library.17bce067cd782389fdf4510d6d70cd0bedcd8b836bcae732c38c16f4cf6d4c92.base-color","sampler":{"minFilter":"nearest","magFilter":"nearest","wrapS":"clamp-to-edge","wrapT":"clamp-to-edge"}}}});const body=createBoneChain('SpiderBody',[{role:'body',offset:[0,.38,0]}],{parent:root});const all=[body.root],rigs=[];
 for(const l of legSpecs){const chain=createBoneChain('SpiderLeg_'+l.id,[{role:'leg.'+l.id.toLowerCase()+'.hip',offset:l.hip.map((v,i)=>v-[0,.38,0][i])},{role:'leg.'+l.id.toLowerCase()+'.knee',offset:l.knee.map((v,i)=>v-l.hip[i])},{role:'leg.'+l.id.toLowerCase()+'.foot',endEffector:true,contact:true,offset:l.foot.map((v,i)=>v-l.knee[i])}],{parent:body.root});all.push(...chain.nodes);rigs.push(chain);const a=createPivot(l.contact,[0,0,0],chain.end);a.name=l.contact;}
 body.root.name='Spider_Body';rigs.forEach((chain,j)=>chain.nodes.forEach((b,i)=>b.name=legSpecs[j].bones[i]));skinChunk('Abdomen',shapeGeo([-.24,.40,0],[[-.15,.20,.20],[0,.33,.27],[.18,.25,.22],[.23,.12,.14]],9),material,'body',all,0,root);skinChunk('Thorax',shapeGeo([.14,.36,0],[[-.13,.18,.16],[0,.24,.22],[.14,.19,.17]],8),material,'body',all,0,root);skinChunk('Head',shapeGeo([.34,.34,0],[[-.10,.13,.12],[.02,.17,.16],[.12,.11,.12]],7),material,'body',all,0,root);
 for(let i=0;i<legSpecs.length;i++){const l=legSpecs[i];skinChunk('Thigh_'+l.id,limbGeo(l.hip,l.knee,.052,.037),material,'legs',all,1+i*3,root);skinChunk('Shin_'+l.id,limbGeo(l.knee,l.foot,.037,0),material,'legs',all,2+i*3,root);}
 for(const z of [-.065,.065])skinChunk('Fang_'+(z<0?'L':'R'),limbGeo([.45,.30,z],[.52,.21,z*.7],.034,.007),material,'legs',all,0,root);
 const eye=gameMaterial(0x15211a,{roughness:.35});for(let i=0;i<4;i++)createPart('SpiderEye_'+i,shapeGeo([0,0,0],[[-.014,.013,.013],[.014,.016,.016]],5),eye,{position:[.474,.385+(i%2)*.038,-.066+i*.044],parent:root});
 return consolidateRenderParts(root);}
function animate(root){const tracks=[];for(let i=0;i<legSpecs.length;i++){const l=legSpecs[i],s=(i%2===0?1:-1),bendSign=l.id[0]==='L'?1:-1,lift=i%2===0?0:6,other=6-lift;tracks.push(rotationTrack(l.bones[0],[{time:0,rotation:[0,s*-10,0]},{time:.35,rotation:[0,s*10,0]},{time:.7,rotation:[0,s*-10,0]}]));tracks.push(rotationTrack(l.bones[1],[{time:0,rotation:[0,0,0]},{time:.175,rotation:[bendSign*(i%2===0?6:0),0,0]},{time:.35,rotation:[0,0,0]},{time:.525,rotation:[bendSign*(i%2===0?0:6),0,0]},{time:.7,rotation:[0,0,0]}]));}return [createClip('Scuttle',.7,tracks,{loop:true})];}
// Source-local consolidation only. It keeps the original procedural builders,
// triangle corners, material instances, rig and named rest frames editable.
function consolidateRenderParts(root){
 root.updateWorldMatrix(true,true);const rootInverse=root.matrixWorld.clone().invert(),groups=new Map(),parts=[];
 root.traverse(n=>{if(n.isMesh){if(Array.isArray(n.material))throw new Error('Consolidation requires one material per source part');parts.push(n);}});
 for(const n of parts){let g=groups.get(n.material);if(!g){g={material:n.material,parts:[],skin:!!n.isSkinnedMesh};groups.set(n.material,g);}if(g.skin!==!!n.isSkinnedMesh)throw new Error('Mixed skin/static material requires separate grouping');g.parts.push(n);}
 let groupIndex=0;
 for(const group of groups.values()){
  const positions=[],normals=[],uvs=[],indices=[],weights=[],ranges=[],first=group.parts[0],bones=group.skin?first.skeleton.bones:null;
  for(const n of group.parts){const geometry=n.geometry,p=geometry.getAttribute('position'),normal=geometry.getAttribute('normal'),uv=geometry.getAttribute('uv'),si=geometry.getAttribute('skinIndex'),sw=geometry.getAttribute('skinWeight'),matrix=rootInverse.clone().multiply(n.matrixWorld),normalMatrix=new THREE.Matrix3().getNormalMatrix(matrix),start=positions.length/9;
   if(group.skin&&(!n.bindMatrix.equals(first.bindMatrix)||n.skeleton.bones.length!==bones.length||n.skeleton.bones.some((b,i)=>b!==bones[i])||n.skeleton.boneInverses.some((m,i)=>!m.equals(first.skeleton.boneInverses[i]))))throw new Error('Source skin bind compatibility changed');
   for(let i=0;i<(geometry.index?.count??p.count);i++){const k=geometry.index?geometry.index.getX(i):i,P=new THREE.Vector3().fromBufferAttribute(p,k).applyMatrix4(matrix),N=new THREE.Vector3().fromBufferAttribute(normal,k).applyNormalMatrix(normalMatrix);positions.push(...P.toArray());normals.push(...N.toArray());uvs.push(uv.getX(k),uv.getY(k));if(group.skin){for(let j=0;j<4;j++){indices.push(si.getComponent(k,j));weights.push(sw.getComponent(k,j));}}}
   ranges.push({name:n.name,startTriangle:start,triangleCount:positions.length/9-start});
  }
  const geometry=new THREE.BufferGeometry();geometry.setAttribute('position',new THREE.Float32BufferAttribute(positions,3));geometry.setAttribute('normal',new THREE.Float32BufferAttribute(normals,3));geometry.setAttribute('uv',new THREE.Float32BufferAttribute(uvs,2));geometry.computeBoundingBox();geometry.computeBoundingSphere();
  // Combined body/leg atlas UVs are intentionally manual full-sheet coordinates.
  // No single named-region stamp is copied onto a mixed-region mesh. Exact source
  // mappings remain in the original builders and the editable triangle ranges.
  const name=group.skin?'SpiderSurface':group.material.alphaTest>0?'FoliageSurface':group.material.name==='Pine bark / broken plates'?'BarkSurface':group.material.name==='Cut endgrain'?'CutSurfaceCombined':'EyeSurface';
  const combined=group.skin?createSkinnedPart(name,geometry,group.material,{parent:root,bones,indices,weights}):createPart(name,geometry,group.material,{parent:root});combined.userData.consolidatedParts=ranges;if(group.material.alphaTest>0)markOpenShell(combined,'Original double-sided MASK foliage cards; intentional open sheets');
  for(const n of group.parts){const marker=new THREE.Object3D();marker.name=n.name;marker.position.copy(n.position);marker.quaternion.copy(n.quaternion);marker.scale.copy(n.scale);marker.userData.consolidatedTriangleRange=ranges.find(r=>r.name===n.name);const parent=n.parent;for(const child of [...n.children])marker.add(child);parent.add(marker);n.removeFromParent();}
  groupIndex++;
 }
 return root;
}

Asset content follows the supplied CC0-1.0 licence record. Kiln and scene software retain their own licences.