[READ-ONLY] Mirror of https://github.com/flo-bit/shapecraft.
flo-bit.dev/shapecraft/
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()