[READ-ONLY] Mirror of https://github.com/flo-bit/shapecraft. flo-bit.dev/shapecraft/
0

Configure Feed

Select the types of activity you want to include in your feed.

shapecraft / demo / main.ts
7.0 kB 200 lines
1import * as THREE from 'three' 2import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' 3import { toThreeMesh } from '../src/three' 4import { plane } from '../src' 5import { fbm } from '../src/noise' 6import { heightGradient } from '../src/color' 7import { 8 tree, treeSchema, treePresets, 9 pine, pineSchema, pinePresets, 10 palm, palmSchema, palmPresets, 11 bush, bushSchema, bushPresets, 12 grass, grassSchema, grassPresets, 13 fern, fernSchema, fernPresets, 14} from '../src/generators' 15import { createEditor } from './editor/editor' 16import type { OptionSchema } from '../src/core/schema' 17import type { Mesh } from '../src/core/mesh' 18 19// --- Generator registry: add a model here and it appears in the switcher + editor. --- 20interface GenEntry { 21 label: string 22 gen: (opts: Record<string, any>) => Mesh 23 schema: OptionSchema 24 presets: Record<string, Record<string, any>> 25 sizeKey: string // which option the forest-scatter varies per instance 26 defaultSize: number 27} 28const GENERATORS: Record<string, GenEntry> = { 29 common: { label: 'Common', gen: tree, schema: treeSchema, presets: treePresets, sizeKey: 'height', defaultSize: 2.5 }, 30 pine: { label: 'Pine', gen: pine, schema: pineSchema, presets: pinePresets, sizeKey: 'height', defaultSize: 3 }, 31 palm: { label: 'Palm', gen: palm, schema: palmSchema, presets: palmPresets, sizeKey: 'height', defaultSize: 3.5 }, 32 bush: { label: 'Bush', gen: bush, schema: bushSchema, presets: bushPresets, sizeKey: 'size', defaultSize: 0.6 }, 33 grass: { label: 'Grass', gen: grass, schema: grassSchema, presets: grassPresets, sizeKey: 'height', defaultSize: 0.6 }, 34 fern: { label: 'Fern', gen: fern, schema: fernSchema, presets: fernPresets, sizeKey: 'length', defaultSize: 1.0 }, 35} 36 37// --- Renderer --- 38const renderer = new THREE.WebGLRenderer({ antialias: true }) 39renderer.setSize(window.innerWidth, window.innerHeight) 40renderer.setPixelRatio(window.devicePixelRatio) 41renderer.shadowMap.enabled = true 42renderer.shadowMap.type = THREE.PCFSoftShadowMap 43renderer.toneMapping = THREE.ACESFilmicToneMapping 44renderer.toneMappingExposure = 1.1 45document.body.appendChild(renderer.domElement) 46 47// --- Scene --- 48const scene = new THREE.Scene() 49 50// --- Sky dome --- 51const skyGeo = new THREE.SphereGeometry(50, 16, 12) 52const skyColors = new Float32Array(skyGeo.getAttribute('position').count * 3) 53const skyPos = skyGeo.getAttribute('position') 54for (let i = 0; i < skyPos.count; i++) { 55 const t = Math.max(0, skyPos.getY(i) / 50) 56 skyColors[i * 3] = 0.55 + (0.2 - 0.55) * t 57 skyColors[i * 3 + 1] = 0.7 + (0.35 - 0.7) * t 58 skyColors[i * 3 + 2] = 0.85 + (0.65 - 0.85) * t 59} 60skyGeo.setAttribute('color', new THREE.BufferAttribute(skyColors, 3)) 61const sky = new THREE.Mesh(skyGeo, new THREE.MeshBasicMaterial({ vertexColors: true, side: THREE.BackSide })) 62scene.add(sky) 63 64// --- Fog --- 65scene.fog = new THREE.FogExp2(0x8eb3d9, 0.025) 66 67// --- Camera --- 68const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 200) 69camera.position.set(4, 3, 8) 70 71const controls = new OrbitControls(camera, renderer.domElement) 72controls.target.set(0, 1, 0) 73controls.enableDamping = true 74controls.dampingFactor = 0.08 75controls.maxPolarAngle = Math.PI / 2 - 0.05 76controls.update() 77 78// --- Lighting --- 79const hemi = new THREE.HemisphereLight(0x87ceeb, 0x3a5a2a, 0.6) 80scene.add(hemi) 81 82const sun = new THREE.DirectionalLight(0xfff4e0, 1.4) 83sun.position.set(8, 12, 5) 84sun.castShadow = true 85sun.shadow.mapSize.set(4096, 4096) 86sun.shadow.camera.left = -15 87sun.shadow.camera.right = 15 88sun.shadow.camera.top = 15 89sun.shadow.camera.bottom = -15 90sun.shadow.camera.near = 0.01 91sun.shadow.camera.far = 40 92sun.shadow.bias = -0.001 93scene.add(sun) 94 95const fill = new THREE.DirectionalLight(0xb0c4de, 0.3) 96fill.position.set(-5, 4, -3) 97scene.add(fill) 98 99// --- Ground --- 100const groundNoise = fbm({ seed: 7, octaves: 3, scale: 0.15, min: 0, max: 0.3 }) 101const groundMesh = plane({ size: 30, segments: 60 }) 102 .displace((pos) => groundNoise.get(pos[0], pos[2])) 103 .vertexColor(heightGradient([ 104 [-0.05, [0.25, 0.38, 0.12]], 105 [0.1, [0.3, 0.45, 0.15]], 106 [0.25, [0.35, 0.42, 0.18]], 107 ])) 108const groundObj = toThreeMesh(groundMesh) 109groundObj.receiveShadow = true 110scene.add(groundObj) 111 112// --- Tree management --- 113let treeObjects: THREE.Mesh[] = [] 114let activeGenerator: keyof typeof GENERATORS | string = 'common' 115 116function clearTrees() { 117 for (const obj of treeObjects) { 118 scene.remove(obj) 119 obj.geometry.dispose() 120 if (Array.isArray(obj.material)) obj.material.forEach(m => m.dispose()) 121 else obj.material.dispose() 122 } 123 treeObjects = [] 124} 125 126function rebuildTrees(opts: Record<string, any>) { 127 clearTrees() 128 129 let rngSeed = 42 130 function rng() { rngSeed = (rngSeed * 16807) % 2147483647; return (rngSeed & 0x7fffffff) / 2147483647 } 131 132 const entry = GENERATORS[activeGenerator] 133 const { gen, sizeKey, defaultSize } = entry 134 135 for (let i = 0; i < 15; i++) { 136 const x = (rng() - 0.5) * 20 137 const z = (rng() - 0.5) * 20 138 if (Math.sqrt(x * x + z * z) < 1.5) continue 139 140 const t = gen({ 141 ...opts, 142 seed: (opts.seed ?? 1) + i, 143 [sizeKey]: (opts[sizeKey] ?? defaultSize) * (0.8 + rng() * 0.4), 144 }) 145 const obj = toThreeMesh(t) 146 obj.position.set(x, groundNoise.get(x, z), z) 147 obj.castShadow = true 148 obj.receiveShadow = true 149 scene.add(obj) 150 treeObjects.push(obj) 151 } 152} 153 154// --- Editor with generator switcher --- 155let currentEditorEl: HTMLElement | null = null 156 157function mountEditor(type: string) { 158 activeGenerator = type 159 if (currentEditorEl) currentEditorEl.remove() 160 161 const entry = GENERATORS[type] 162 currentEditorEl = createEditor(entry.schema, { 163 onChange: (opts) => { rebuildTrees(opts) }, 164 }, { presets: entry.presets }) 165 166 // Add generator switcher at the top 167 const switcher = document.createElement('div') 168 switcher.style.cssText = 'margin-bottom: 12px; padding-bottom: 10px; border-bottom: 1px solid #333; display: flex; flex-wrap: wrap; gap: 4px;' 169 for (const key of Object.keys(GENERATORS)) { 170 const btn = document.createElement('button') 171 btn.textContent = GENERATORS[key].label 172 btn.style.cssText = `flex: 1 1 30%; padding: 6px; border: 1px solid #444; background: ${key === type ? '#3a5a3a' : '#222'}; color: #ddd; cursor: pointer; font-size: 12px;` 173 btn.addEventListener('click', () => { if (key !== activeGenerator) mountEditor(key) }) 174 switcher.appendChild(btn) 175 } 176 currentEditorEl.prepend(switcher) 177 178 document.body.appendChild(currentEditorEl) 179} 180 181mountEditor('common') 182 183// Expose for screenshot scripts 184;(window as any).__camera = camera 185;(window as any).__controls = controls 186 187// --- Resize --- 188window.addEventListener('resize', () => { 189 camera.aspect = window.innerWidth / window.innerHeight 190 camera.updateProjectionMatrix() 191 renderer.setSize(window.innerWidth, window.innerHeight) 192}) 193 194// --- Animate --- 195function animate() { 196 requestAnimationFrame(animate) 197 controls.update() 198 renderer.render(scene, camera) 199} 200animate()