@marble.fun/track-catalog 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +15 -0
- package/assets.json +346 -0
- package/package.json +32 -0
- package/release.json +135 -0
- package/src/audio/game-audio-preference.ts +9 -0
- package/src/authoritative.ts +6 -0
- package/src/catalog.ts +1 -0
- package/src/index.ts +14 -0
- package/src/legacy-stock-catalog.ts +130 -0
- package/src/pages/lobby/lobby-types.ts +140 -0
- package/src/pages/lobby/marble-factory.ts +931 -0
- package/src/pages/play/race/egypt-elevator-blender.ts +159 -0
- package/src/pages/play/race/egypt-portal-frame.ts +382 -0
- package/src/pages/play/race/finish-detection.ts +1 -0
- package/src/pages/play/race/japanese-sakura-castle-support-blender.ts +510 -0
- package/src/pages/play/race/japanese-sakura-kawara-roof.ts +201 -0
- package/src/pages/play/race/japanese-sakura-portal-frame.ts +325 -0
- package/src/pages/play/race/jungle-portal-frame.ts +308 -0
- package/src/pages/play/race/jungle-temple-lift.ts +366 -0
- package/src/pages/play/race/marble-entity.ts +257 -0
- package/src/pages/play/race/marble-material.ts +20 -0
- package/src/pages/play/race/marble-race-engine.ts +2652 -0
- package/src/pages/play/race/path-pattern-layout.ts +142 -0
- package/src/pages/play/race/peg-obstacle-visual.ts +171 -0
- package/src/pages/play/race/physics-debug.ts +1 -0
- package/src/pages/play/race/physics-dry-run.ts +90 -0
- package/src/pages/play/race/portal-derived-elevator-skins.ts +729 -0
- package/src/pages/play/race/portal-pair-color.ts +20 -0
- package/src/pages/play/race/portal-proportions.ts +25 -0
- package/src/pages/play/race/portal-visual.ts +1108 -0
- package/src/pages/play/race/race-camera-director.ts +586 -0
- package/src/pages/play/race/race-camera-obstruction.ts +37 -0
- package/src/pages/play/race/race-camera-tour-planner.ts +274 -0
- package/src/pages/play/race/race-camera.ts +206 -0
- package/src/pages/play/race/race-completion.ts +5 -0
- package/src/pages/play/race/race-configuration.ts +57 -0
- package/src/pages/play/race/race-marble-focus-renderer.ts +134 -0
- package/src/pages/play/race/race-physics-profile.ts +7 -0
- package/src/pages/play/race/race-physics.ts +16 -0
- package/src/pages/play/race/race-playback.ts +41 -0
- package/src/pages/play/race/race-rolling-audio.ts +616 -0
- package/src/pages/play/race/race-simulation-renderer.ts +221 -0
- package/src/pages/play/race/race-terminal-checker-strip.ts +226 -0
- package/src/pages/play/race/race-transform-interpolation.ts +50 -0
- package/src/pages/play/race/race-types.ts +12 -0
- package/src/pages/play/race/rebound-visual.ts +70 -0
- package/src/pages/play/race/seeded-random.ts +23 -0
- package/src/pages/play/race/space-station-portal-frame.ts +275 -0
- package/src/pages/play/race/track-appearance-canopy.ts +946 -0
- package/src/pages/play/race/track-appearance-floor-runner.ts +206 -0
- package/src/pages/play/race/track-appearance-jungle-flower-hedge.ts +483 -0
- package/src/pages/play/race/track-appearance-rope-wall.ts +443 -0
- package/src/pages/play/race/track-appearance-sakura-bridge-fence.ts +305 -0
- package/src/pages/play/race/track-appearance-sakura-castle-wall.ts +1031 -0
- package/src/pages/play/race/track-appearance-space-station-wall.ts +497 -0
- package/src/pages/play/race/track-appearance-stone-block-shell.ts +517 -0
- package/src/pages/play/race/track-appearance-support.ts +983 -0
- package/src/pages/play/race/track-appearance-vampire-carved-wall.ts +545 -0
- package/src/pages/play/race/track-appearance-wall-terminal.ts +167 -0
- package/src/pages/play/race/track-course.ts +271 -0
- package/src/pages/play/race/track-definition.ts +443 -0
- package/src/pages/play/race/track-fence-architecture.ts +33 -0
- package/src/pages/play/race/track-floor-material-rules.ts +51 -0
- package/src/pages/play/race/track-mechanisms.ts +1350 -0
- package/src/pages/play/race/track-physics-surface.ts +6 -0
- package/src/pages/play/race/track-route-network.ts +9 -0
- package/src/pages/play/race/track-shadow-batching.ts +760 -0
- package/src/pages/play/race/track-surface-basis.ts +1 -0
- package/src/pages/play/race/track-surface-topology.ts +326 -0
- package/src/pages/play/race/track-world.ts +10181 -0
- package/src/pages/play/race/under-track-support-architecture.ts +122 -0
- package/src/pages/play/race/vortex-profile.ts +5 -0
- package/src/pages/play/race/vortex-water-effect.ts +863 -0
- package/src/pages/play/race/wild-west-mine-lift.ts +281 -0
- package/src/pages/play/race/wild-west-vortex-housing.ts +572 -0
- package/src/pages/track-lab/atlas-vortex-layout.ts +1 -0
- package/src/pages/track-lab/cone-field-layout.ts +1 -0
- package/src/pages/track-lab/conveyor-layout.ts +1 -0
- package/src/pages/track-lab/engine/canonical-track-assembly.ts +64 -0
- package/src/pages/track-lab/engine/editor-object-geometry.ts +170 -0
- package/src/pages/track-lab/engine/track-lab-connection-state.ts +123 -0
- package/src/pages/track-lab/engine/track-piece-visual.ts +1420 -0
- package/src/pages/track-lab/flipper-bank-layout.ts +1 -0
- package/src/pages/track-lab/flipper-component.ts +127 -0
- package/src/pages/track-lab/rising-wall-layout.ts +1 -0
- package/src/pages/track-lab/simple-obstacle-layout.ts +1 -0
- package/src/pages/track-lab/track-piece-layout.ts +28 -0
- package/src/pages/track-lab/track-runtime-adapter.ts +1152 -0
- package/src/three/geometry-attribute-aliases.ts +24 -0
- package/src/three/geometry-detail.ts +36 -0
- package/src/three/homepage-marble-geometry.ts +15 -0
- package/src/three/japanese-sakura-appearance.ts +252 -0
- package/src/three/jungle-flower-hedge-appearance.ts +7 -0
- package/src/three/jungle-temple-appearance.ts +5 -0
- package/src/three/low-quality-scene-renderer.ts +132 -0
- package/src/three/marble-sphere-geometry.ts +13 -0
- package/src/three/mobile-rendering-budget.ts +11 -0
- package/src/three/painted-valley-appearance.ts +270 -0
- package/src/three/railroad-appearance.ts +71 -0
- package/src/three/renderer-resize.ts +32 -0
- package/src/three/rope-wall-appearance.ts +91 -0
- package/src/three/rugged-track-appearance.ts +2 -0
- package/src/three/scene-lighting.ts +183 -0
- package/src/three/scene-shader-preparation.ts +64 -0
- package/src/three/scene-theme-controller.ts +461 -0
- package/src/three/shadow-shader-preparation.ts +162 -0
- package/src/three/shared-obstacle-metal.ts +44 -0
- package/src/three/soft-additive-particles.ts +57 -0
- package/src/three/space-station-appearance.ts +213 -0
- package/src/three/static-track-batched-mesh.ts +132 -0
- package/src/three/stone-track-shell-appearance.ts +12 -0
- package/src/three/theme-ambience-controller.ts +226 -0
- package/src/three/theme-asset-cache.ts +120 -0
- package/src/three/theme-audio-preference.ts +43 -0
- package/src/three/theme-catalog.ts +1457 -0
- package/src/three/theme-material-assets.ts +258 -0
- package/src/three/theme-material-ktx2-manifest.ts +190 -0
- package/src/three/theme-material-registry.ts +1952 -0
- package/src/three/theme-world-factory.ts +590 -0
- package/src/three/track-connector-batch.ts +125 -0
- package/src/three/track-material-continuity.ts +442 -0
- package/src/three/track-theme.ts +535 -0
- package/src/three/vampire-castle-appearance.ts +258 -0
- package/src/three/visual-frame-pacing.ts +19 -0
- package/src/three/visual-geometry.ts +120 -0
- package/src/three/visual-quality.ts +83 -0
- package/src/three/water-appearance.ts +29 -0
- package/src/three/wild-west-sand-appearance.ts +2 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
import * as THREE from 'three'
|
|
2
|
+
|
|
3
|
+
import { VisualSphereGeometry, VisualCylinderGeometry } from '../../../three/visual-geometry'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
PAINTED_VALLEY_THEME_ID,
|
|
7
|
+
PAINTED_VALLEY_TROPICAL_FLOWER_HEDGE_WALL_ID,
|
|
8
|
+
} from '../../../three/painted-valley-appearance'
|
|
9
|
+
import {
|
|
10
|
+
canonicalWallFrameAt,
|
|
11
|
+
type CanonicalWallRun,
|
|
12
|
+
type RawWallFrame,
|
|
13
|
+
} from './track-surface-topology'
|
|
14
|
+
|
|
15
|
+
export type JungleFlowerHedgePathFrame = RawWallFrame
|
|
16
|
+
export type JungleFlowerHedgePathRun = CanonicalWallRun
|
|
17
|
+
|
|
18
|
+
type FlowerType = 'orange' | 'pink' | 'magenta' | 'coral' | 'gold'
|
|
19
|
+
|
|
20
|
+
interface HedgeAssets {
|
|
21
|
+
leafGeometry: THREE.BufferGeometry
|
|
22
|
+
petalGeometry: THREE.BufferGeometry
|
|
23
|
+
sphereGeometry: THREE.SphereGeometry
|
|
24
|
+
stemGeometry: THREE.CylinderGeometry
|
|
25
|
+
stamenGeometry: THREE.CylinderGeometry
|
|
26
|
+
trumpetGeometry: THREE.CylinderGeometry
|
|
27
|
+
leafMaterials: THREE.MeshStandardMaterial[]
|
|
28
|
+
leafMatrices: THREE.Matrix4[][]
|
|
29
|
+
flowerMaterials: Record<string, THREE.MeshStandardMaterial>
|
|
30
|
+
centreMaterials: Record<string, THREE.MeshStandardMaterial>
|
|
31
|
+
stemMaterial: THREE.MeshStandardMaterial
|
|
32
|
+
calyxMaterial: THREE.MeshStandardMaterial
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type SampledRun = CanonicalWallRun
|
|
36
|
+
|
|
37
|
+
const HEDGE_DEPTH = 0.82
|
|
38
|
+
const FLOWER_SCHEDULE: readonly FlowerType[] = ['orange', 'coral', 'magenta', 'gold', 'orange', 'pink']
|
|
39
|
+
const REAR_FLOWER_SCHEDULE: readonly FlowerType[] = ['pink', 'gold', 'orange', 'magenta', 'coral', 'orange']
|
|
40
|
+
const CROWN_FLOWER_SCHEDULE: readonly FlowerType[] = ['pink', 'orange', 'magenta', 'gold']
|
|
41
|
+
|
|
42
|
+
function hash01(value: number): number {
|
|
43
|
+
const result = Math.sin(value * 91.3458 + 17.147) * 47_453.5453
|
|
44
|
+
return result - Math.floor(result)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function standardMaterial(color: string, roughness = 0.88, emissive = '#000000', emissiveIntensity = 0): THREE.MeshStandardMaterial {
|
|
48
|
+
return new THREE.MeshStandardMaterial({ color, roughness, metalness: 0, emissive, emissiveIntensity, side: THREE.FrontSide })
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function createLeafGeometry(): THREE.BufferGeometry {
|
|
52
|
+
const steps = 3
|
|
53
|
+
const positions: number[] = []
|
|
54
|
+
const colors: number[] = []
|
|
55
|
+
const indices: number[] = []
|
|
56
|
+
for (let step = 0; step <= steps; step += 1) {
|
|
57
|
+
const t = step / steps
|
|
58
|
+
const serration = 1 + Math.sin(t * Math.PI * 7) * 0.045
|
|
59
|
+
const width = (0.1 * (1 - t) + Math.pow(Math.sin(Math.PI * t), 0.76)) * 0.37 * serration
|
|
60
|
+
const spine = Math.sin(Math.PI * t) * 0.075 + t * t * 0.035
|
|
61
|
+
const edge = spine - width * 0.105
|
|
62
|
+
positions.push(-width, t, edge, 0, t, spine, width, t, edge)
|
|
63
|
+
positions.push(-width, t, edge - 0.028, 0, t, spine - 0.032, width, t, edge - 0.028)
|
|
64
|
+
colors.push(0.7, 0.76, 0.58, 1.26, 1.18, 0.72, 0.7, 0.76, 0.58)
|
|
65
|
+
colors.push(0.5, 0.56, 0.43, 0.72, 0.7, 0.48, 0.5, 0.56, 0.43)
|
|
66
|
+
}
|
|
67
|
+
const stride = 6
|
|
68
|
+
for (let step = 0; step < steps; step += 1) {
|
|
69
|
+
const a = step * stride
|
|
70
|
+
const b = a + stride
|
|
71
|
+
indices.push(a, a + 1, b, a + 1, b + 1, b, a + 1, a + 2, b + 1, a + 2, b + 2, b + 1)
|
|
72
|
+
indices.push(a + 3, b + 3, a + 4, a + 4, b + 3, b + 4, a + 4, b + 4, a + 5, a + 5, b + 4, b + 5)
|
|
73
|
+
indices.push(a, b, a + 3, a + 3, b, b + 3, a + 2, a + 5, b + 2, a + 5, b + 5, b + 2)
|
|
74
|
+
}
|
|
75
|
+
indices.push(0, 3, 1, 1, 3, 4)
|
|
76
|
+
const end = steps * stride
|
|
77
|
+
indices.push(end, end + 1, end + 3, end + 1, end + 4, end + 3)
|
|
78
|
+
const geometry = new THREE.BufferGeometry()
|
|
79
|
+
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3))
|
|
80
|
+
geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3))
|
|
81
|
+
geometry.setIndex(indices)
|
|
82
|
+
geometry.computeVertexNormals()
|
|
83
|
+
return geometry
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function createPetalGeometry(): THREE.BufferGeometry {
|
|
87
|
+
const steps = 3
|
|
88
|
+
const positions: number[] = []
|
|
89
|
+
const indices: number[] = []
|
|
90
|
+
for (let step = 0; step <= steps; step += 1) {
|
|
91
|
+
const t = step / steps
|
|
92
|
+
const width = Math.pow(Math.sin(Math.PI * Math.pow(t, 0.92)), 0.78) * (0.44 + t * 0.1)
|
|
93
|
+
const centerZ = Math.sin(Math.PI * t) * 0.1 + t * t * 0.045
|
|
94
|
+
const edgeZ = centerZ - width * 0.17
|
|
95
|
+
positions.push(-width, t, edgeZ, 0, t, centerZ, width, t, edgeZ)
|
|
96
|
+
positions.push(-width, t, edgeZ - 0.035, 0, t, centerZ - 0.04, width, t, edgeZ - 0.035)
|
|
97
|
+
}
|
|
98
|
+
const stride = 6
|
|
99
|
+
for (let step = 0; step < steps; step += 1) {
|
|
100
|
+
const a = step * stride
|
|
101
|
+
const b = a + stride
|
|
102
|
+
indices.push(a, a + 1, b, a + 1, b + 1, b, a + 1, a + 2, b + 1, a + 2, b + 2, b + 1)
|
|
103
|
+
indices.push(a + 3, b + 3, a + 4, a + 4, b + 3, b + 4, a + 4, b + 4, a + 5, a + 5, b + 4, b + 5)
|
|
104
|
+
indices.push(a, b, a + 3, a + 3, b, b + 3, a + 2, a + 5, b + 2, a + 5, b + 5, b + 2)
|
|
105
|
+
}
|
|
106
|
+
const geometry = new THREE.BufferGeometry()
|
|
107
|
+
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3))
|
|
108
|
+
geometry.setIndex(indices)
|
|
109
|
+
geometry.computeVertexNormals()
|
|
110
|
+
return geometry
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function frameQuaternion(tangent: THREE.Vector3, up: THREE.Vector3, normal: THREE.Vector3): THREE.Quaternion {
|
|
114
|
+
const x = tangent.clone().normalize()
|
|
115
|
+
let z = normal.clone().addScaledVector(x, -normal.dot(x)).normalize()
|
|
116
|
+
if (z.lengthSq() < 0.5) z = new THREE.Vector3().crossVectors(x, up).normalize()
|
|
117
|
+
const y = new THREE.Vector3().crossVectors(z, x).normalize()
|
|
118
|
+
z = new THREE.Vector3().crossVectors(x, y).normalize()
|
|
119
|
+
return new THREE.Quaternion().setFromRotationMatrix(new THREE.Matrix4().makeBasis(x, y, z))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function sampleAt(run: SampledRun, distance: number): JungleFlowerHedgePathFrame {
|
|
123
|
+
return canonicalWallFrameAt(run, distance)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function createSweptCoreGeometry(run: SampledRun, height: number): THREE.BufferGeometry {
|
|
127
|
+
const crossSegments = 32
|
|
128
|
+
const ringCount = Math.max(2, Math.ceil(run.length / 0.12) + 1)
|
|
129
|
+
const positions: number[] = []
|
|
130
|
+
const uvs: number[] = []
|
|
131
|
+
const indices: number[] = []
|
|
132
|
+
const exponent = 0.34
|
|
133
|
+
for (let ring = 0; ring < ringCount; ring += 1) {
|
|
134
|
+
const distance = run.length * ring / (ringCount - 1)
|
|
135
|
+
const frame = sampleAt(run, distance)
|
|
136
|
+
const endScale = ring === 0 || ring === ringCount - 1 ? 0.94 : 1
|
|
137
|
+
for (let segment = 0; segment <= crossSegments; segment += 1) {
|
|
138
|
+
const angle = segment / crossSegments * Math.PI * 2 - Math.PI / 2
|
|
139
|
+
const zProfile = Math.sign(Math.cos(angle)) * Math.pow(Math.abs(Math.cos(angle)), exponent)
|
|
140
|
+
const yProfile = Math.sign(Math.sin(angle)) * Math.pow(Math.abs(Math.sin(angle)), exponent)
|
|
141
|
+
let y = height * (0.5 + yProfile * 0.5)
|
|
142
|
+
if (y < height * 0.075) y *= 0.35
|
|
143
|
+
y = height / 2 + (y - height / 2) * endScale
|
|
144
|
+
const z = zProfile * HEDGE_DEPTH / 2 * endScale
|
|
145
|
+
const broad = Math.sin(distance * 1.47 + z * 3.1) * Math.cos(y * 5.2 - distance * 0.37)
|
|
146
|
+
const fine = Math.sin(distance * 6.7 + y * 9.3 + z * 5.8) * 0.36
|
|
147
|
+
const displacement = y < height * 0.07 ? 0 : (broad + fine) * 0.013
|
|
148
|
+
const point = frame.base.clone().addScaledVector(frame.up, y).addScaledVector(frame.outward, z + displacement * Math.sign(z || 1))
|
|
149
|
+
positions.push(point.x, point.y, point.z)
|
|
150
|
+
uvs.push(run.length > 0 ? distance / run.length : 0, segment / crossSegments)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const stride = crossSegments + 1
|
|
154
|
+
for (let ring = 0; ring < ringCount - 1; ring += 1) {
|
|
155
|
+
for (let segment = 0; segment < crossSegments; segment += 1) {
|
|
156
|
+
const a = ring * stride + segment
|
|
157
|
+
const b = a + stride
|
|
158
|
+
indices.push(a, b, a + 1, a + 1, b, b + 1)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const addCap = (ring: number, reverse: boolean) => {
|
|
162
|
+
const frame = sampleAt(run, ring === 0 ? 0 : run.length)
|
|
163
|
+
const center = positions.length / 3
|
|
164
|
+
positions.push(frame.base.x + frame.up.x * height / 2, frame.base.y + frame.up.y * height / 2, frame.base.z + frame.up.z * height / 2)
|
|
165
|
+
uvs.push(ring === 0 ? 0 : 1, 0.5)
|
|
166
|
+
for (let segment = 0; segment < crossSegments; segment += 1) {
|
|
167
|
+
const a = ring * stride + segment
|
|
168
|
+
if (reverse) indices.push(center, a, a + 1)
|
|
169
|
+
else indices.push(center, a + 1, a)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
addCap(0, true)
|
|
173
|
+
addCap(ringCount - 1, false)
|
|
174
|
+
const geometry = new THREE.BufferGeometry()
|
|
175
|
+
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3))
|
|
176
|
+
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2))
|
|
177
|
+
geometry.setIndex(indices)
|
|
178
|
+
geometry.computeVertexNormals()
|
|
179
|
+
geometry.computeBoundingSphere()
|
|
180
|
+
return geometry
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function createAssets(): HedgeAssets {
|
|
184
|
+
const leafMaterials = [
|
|
185
|
+
standardMaterial('#3b5429', 0.9, '#081307', 0.15), standardMaterial('#4e6931', 0.9, '#0a1708', 0.14),
|
|
186
|
+
standardMaterial('#304823', 0.92, '#071106', 0.16), standardMaterial('#60733a', 0.88, '#0c1908', 0.12),
|
|
187
|
+
]
|
|
188
|
+
leafMaterials.forEach(material => { material.vertexColors = true; material.flatShading = true })
|
|
189
|
+
const flowerMaterials = {
|
|
190
|
+
orange: standardMaterial('#d56a38', 0.76, '#3f1209', 0.18), coral: standardMaterial('#cf6043', 0.78, '#42130b', 0.17),
|
|
191
|
+
coralLight: standardMaterial('#dd805b', 0.76, '#48170d', 0.16), magenta: standardMaterial('#ad3564', 0.78, '#35081c', 0.19),
|
|
192
|
+
orchidPink: standardMaterial('#cf6895', 0.76, '#3b0d28', 0.17), deepPink: standardMaterial('#7d2849', 0.8, '#260512', 0.17),
|
|
193
|
+
gold: standardMaterial('#d2a044', 0.8, '#442807', 0.16), cream: standardMaterial('#e4d0b8', 0.78, '#432718', 0.1),
|
|
194
|
+
}
|
|
195
|
+
Object.values(flowerMaterials).forEach(material => { material.flatShading = true })
|
|
196
|
+
return {
|
|
197
|
+
leafGeometry: createLeafGeometry(), petalGeometry: createPetalGeometry(), sphereGeometry: new VisualSphereGeometry(1, 12, 8),
|
|
198
|
+
stemGeometry: new VisualCylinderGeometry(0.012, 0.016, 0.12, 7), stamenGeometry: new VisualCylinderGeometry(0.007, 0.006, 0.08, 6),
|
|
199
|
+
trumpetGeometry: new VisualCylinderGeometry(0.11, 0.038, 0.21, 10, 3, true), leafMaterials, leafMatrices: [[], [], [], []], flowerMaterials,
|
|
200
|
+
centreMaterials: { amber: standardMaterial('#77311d', 0.82, '#210502', 0.12), gold: standardMaterial('#d8b05a', 0.78, '#402806', 0.14) },
|
|
201
|
+
stemMaterial: standardMaterial('#315716', 0.94), calyxMaterial: standardMaterial('#3c681b', 0.94),
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function pushLeaf(assets: HedgeAssets, frame: JungleFlowerHedgePathFrame, position: THREE.Vector3, normal: THREE.Vector3, length: number, roll: number, seed: number): void {
|
|
206
|
+
const leaf = new THREE.Object3D()
|
|
207
|
+
leaf.position.copy(position)
|
|
208
|
+
leaf.quaternion.copy(frameQuaternion(frame.tangent, frame.up, normal))
|
|
209
|
+
leaf.rotateZ(roll)
|
|
210
|
+
leaf.scale.set(length, length, length * (0.82 + hash01(seed * 7 + 13) * 0.26))
|
|
211
|
+
leaf.updateMatrix()
|
|
212
|
+
assets.leafMatrices[Math.abs(seed) % assets.leafMatrices.length]!.push(leaf.matrix.clone())
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function addRosette(assets: HedgeAssets, frame: JungleFlowerHedgePathFrame, position: THREE.Vector3, normal: THREE.Vector3, radius: number, phase: number, seed: number): void {
|
|
216
|
+
for (let index = 0; index < 5; index += 1) {
|
|
217
|
+
const angle = phase + index * Math.PI * 0.4 + (hash01(seed + index * 3) - 0.5) * 0.24
|
|
218
|
+
pushLeaf(assets, frame, position, normal, radius * (0.78 + hash01(seed + index * 7 + 1) * 0.28), angle, seed + index)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function populateFoliage(assets: HedgeAssets, run: SampledRun, height: number, runSeed: number): void {
|
|
223
|
+
const columns = Math.max(2, Math.ceil(run.length / 0.26))
|
|
224
|
+
for (const face of [-1, 1]) {
|
|
225
|
+
for (let column = 0; column <= columns; column += 1) {
|
|
226
|
+
const distance = run.length * column / columns
|
|
227
|
+
const frame = sampleAt(run, distance)
|
|
228
|
+
for (let row = 0; row < 5; row += 1) {
|
|
229
|
+
const seed = runSeed + column * 17 + row * 41 + face * 113
|
|
230
|
+
const normal = frame.outward.clone().multiplyScalar(face).addScaledVector(frame.up, 0.08 + hash01(seed + 4) * 0.14).normalize()
|
|
231
|
+
const length = 0.21 + hash01(seed + 5) * 0.12
|
|
232
|
+
const position = frame.base.clone().addScaledVector(frame.up, 0.025 + row * height * 0.2 + (hash01(seed + 2) - 0.5) * 0.085)
|
|
233
|
+
.addScaledVector(frame.outward, face * (HEDGE_DEPTH / 2 + 0.018 + hash01(seed + 3) * 0.018))
|
|
234
|
+
.addScaledVector(frame.tangent, (hash01(seed) - 0.5) * 0.12)
|
|
235
|
+
const roll = (hash01(seed + 6) - 0.5) * 1.38 + (row % 2 ? 0.16 : -0.16)
|
|
236
|
+
pushLeaf(assets, frame, position, normal, length, roll, seed)
|
|
237
|
+
if (hash01(seed + 9) > 0.48) pushLeaf(assets, frame, position.clone().addScaledVector(normal, 0.012), normal, length * (0.68 + hash01(seed + 11) * 0.12), roll + (hash01(seed + 12) > 0.5 ? 0.92 : -0.92), seed + 1)
|
|
238
|
+
}
|
|
239
|
+
const shoulderSeed = runSeed + 971 + column * 37 + face * 83
|
|
240
|
+
const shoulderNormal = frame.up.clone().multiplyScalar(0.72).addScaledVector(frame.outward, face * 0.7).normalize()
|
|
241
|
+
const shoulderPosition = frame.base.clone().addScaledVector(frame.up, height - 0.075 + (hash01(shoulderSeed + 1) - 0.5) * 0.055)
|
|
242
|
+
.addScaledVector(frame.outward, face * (HEDGE_DEPTH / 2 - 0.02))
|
|
243
|
+
pushLeaf(assets, frame, shoulderPosition, shoulderNormal, 0.22 + hash01(shoulderSeed + 3) * 0.1, (hash01(shoulderSeed + 4) - 0.5) * 1.5, shoulderSeed)
|
|
244
|
+
}
|
|
245
|
+
const rosettes = Math.max(2, Math.ceil(run.length * 4.5))
|
|
246
|
+
for (let index = 0; index < rosettes; index += 1) {
|
|
247
|
+
const seed = runSeed + 1801 + index * 53 + face * 127
|
|
248
|
+
const frame = sampleAt(run, 0.14 + hash01(seed) * Math.max(0, run.length - 0.28))
|
|
249
|
+
const normal = frame.outward.clone().multiplyScalar(face).addScaledVector(frame.up, 0.1).normalize()
|
|
250
|
+
const position = frame.base.clone().addScaledVector(frame.up, 0.1 + hash01(seed + 1) * (height - 0.22)).addScaledVector(frame.outward, face * (HEDGE_DEPTH / 2 + 0.045))
|
|
251
|
+
addRosette(assets, frame, position, normal, 0.16 + hash01(seed + 2) * 0.07, hash01(seed + 3) * Math.PI * 2, seed)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const crownLeaves = Math.max(4, Math.ceil(run.length * 26.75))
|
|
255
|
+
for (let index = 0; index < crownLeaves; index += 1) {
|
|
256
|
+
const seed = runSeed + 701 + index * 43
|
|
257
|
+
const frame = sampleAt(run, 0.08 + hash01(seed) * Math.max(0, run.length - 0.16))
|
|
258
|
+
const position = frame.base.clone().addScaledVector(frame.up, height + 0.01 + hash01(seed + 2) * 0.024).addScaledVector(frame.outward, -HEDGE_DEPTH * 0.39 + hash01(seed + 1) * HEDGE_DEPTH * 0.78)
|
|
259
|
+
const length = 0.19 + hash01(seed + 3) * 0.12
|
|
260
|
+
pushLeaf(assets, frame, position, frame.up, length, hash01(seed + 4) * Math.PI * 2, seed)
|
|
261
|
+
if (hash01(seed + 8) > 0.58) pushLeaf(assets, frame, position.clone().addScaledVector(frame.up, 0.008), frame.up, length * 0.7, hash01(seed + 4) * Math.PI * 2 + 1, seed + 2)
|
|
262
|
+
}
|
|
263
|
+
for (const end of [0, run.length]) {
|
|
264
|
+
const frame = sampleAt(run, end)
|
|
265
|
+
const normal = frame.tangent.clone().multiplyScalar(end === 0 ? -1 : 1).addScaledVector(frame.up, 0.08).normalize()
|
|
266
|
+
for (let index = 0; index < 12; index += 1) {
|
|
267
|
+
const seed = runSeed + 3209 + index * 71 + (end === 0 ? -101 : 101)
|
|
268
|
+
const position = frame.base.clone().addScaledVector(frame.up, 0.12 + hash01(seed) * (height - 0.25)).addScaledVector(frame.outward, -HEDGE_DEPTH * 0.27 + hash01(seed + 1) * HEDGE_DEPTH * 0.54)
|
|
269
|
+
addRosette(assets, frame, position, normal, 0.15 + hash01(seed + 2) * 0.06, hash01(seed + 3) * Math.PI * 2, seed)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function petal(assets: HedgeAssets, material: THREE.Material, angle: number, width: number, length: number, lift = 0, twist = 0): THREE.Mesh {
|
|
275
|
+
const mesh = new THREE.Mesh(assets.petalGeometry, material)
|
|
276
|
+
mesh.scale.set(width, length, Math.min(width, length) * 1.1)
|
|
277
|
+
mesh.rotation.set(lift, 0, -angle + twist)
|
|
278
|
+
mesh.position.set(Math.sin(angle) * 0.012, Math.cos(angle) * 0.012, 0.018)
|
|
279
|
+
mesh.castShadow = true
|
|
280
|
+
mesh.receiveShadow = true
|
|
281
|
+
return mesh
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function addCentre(group: THREE.Group, assets: HedgeAssets, color: string, radius = 0.035, z = 0.065): void {
|
|
285
|
+
const centre = new THREE.Mesh(assets.sphereGeometry, assets.centreMaterials[color]!)
|
|
286
|
+
centre.scale.set(radius, radius, radius * 0.72)
|
|
287
|
+
centre.position.z = z
|
|
288
|
+
centre.castShadow = true
|
|
289
|
+
group.add(centre)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function makeLily(assets: HedgeAssets): THREE.Group {
|
|
293
|
+
const group = new THREE.Group()
|
|
294
|
+
for (let index = 0; index < 6; index += 1) {
|
|
295
|
+
const angle = index * Math.PI / 3 + 0.08
|
|
296
|
+
group.add(petal(assets, assets.flowerMaterials.orange!, angle, 0.09, 0.19 + (index % 2) * 0.018, (index % 3 - 1) * 0.06))
|
|
297
|
+
}
|
|
298
|
+
addCentre(group, assets, 'amber', 0.039, 0.075)
|
|
299
|
+
for (let index = 0; index < 5; index += 1) {
|
|
300
|
+
const stamen = new THREE.Mesh(assets.stamenGeometry, assets.centreMaterials.gold!)
|
|
301
|
+
const angle = index * Math.PI * 0.4
|
|
302
|
+
stamen.position.set(Math.sin(angle) * 0.04, Math.cos(angle) * 0.04, 0.105)
|
|
303
|
+
stamen.rotation.x = Math.PI / 2
|
|
304
|
+
stamen.castShadow = true
|
|
305
|
+
group.add(stamen)
|
|
306
|
+
}
|
|
307
|
+
return group
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function makePlumeria(assets: HedgeAssets): THREE.Group {
|
|
311
|
+
const group = new THREE.Group()
|
|
312
|
+
for (let index = 0; index < 5; index += 1) {
|
|
313
|
+
const angle = index * Math.PI * 0.4 + 0.16
|
|
314
|
+
group.add(petal(assets, assets.flowerMaterials.cream!, angle, 0.095, 0.17, -0.04, 0.18))
|
|
315
|
+
}
|
|
316
|
+
addCentre(group, assets, 'gold', 0.035, 0.07)
|
|
317
|
+
return group
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function makeOrchid(assets: HedgeAssets): THREE.Group {
|
|
321
|
+
const group = new THREE.Group()
|
|
322
|
+
;[0, 2.08, 4.19].forEach((angle, index) => group.add(petal(assets, assets.flowerMaterials.magenta!, angle, 0.11, 0.18 + index * 0.008, -0.05 + index * 0.04)))
|
|
323
|
+
for (const angle of [-1.05, 1.05]) group.add(petal(assets, assets.flowerMaterials.orchidPink!, angle, 0.075, 0.13, 0.02))
|
|
324
|
+
const lip = new THREE.Mesh(assets.sphereGeometry, assets.flowerMaterials.deepPink!)
|
|
325
|
+
lip.scale.set(0.066, 0.046, 0.045)
|
|
326
|
+
lip.position.set(0, -0.038, 0.085)
|
|
327
|
+
lip.castShadow = true
|
|
328
|
+
group.add(lip)
|
|
329
|
+
addCentre(group, assets, 'gold', 0.026, 0.11)
|
|
330
|
+
return group
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function makeTrumpet(assets: HedgeAssets): THREE.Group {
|
|
334
|
+
const group = new THREE.Group()
|
|
335
|
+
const bell = new THREE.Mesh(assets.trumpetGeometry, assets.flowerMaterials.coral!)
|
|
336
|
+
bell.rotation.x = Math.PI / 2
|
|
337
|
+
bell.position.z = 0.1
|
|
338
|
+
bell.castShadow = true
|
|
339
|
+
group.add(bell)
|
|
340
|
+
for (let index = 0; index < 5; index += 1) {
|
|
341
|
+
const angle = index * Math.PI * 0.4 + 0.18
|
|
342
|
+
const flare = petal(assets, assets.flowerMaterials.coralLight!, angle, 0.045, 0.095, -0.04, 0.08)
|
|
343
|
+
flare.position.set(Math.sin(angle) * 0.035, Math.cos(angle) * 0.035, 0.185)
|
|
344
|
+
group.add(flare)
|
|
345
|
+
}
|
|
346
|
+
addCentre(group, assets, 'amber', 0.038, 0.218)
|
|
347
|
+
group.rotation.x = -0.24
|
|
348
|
+
return group
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function makeGoldCluster(assets: HedgeAssets): THREE.Group {
|
|
352
|
+
const group = new THREE.Group()
|
|
353
|
+
const offsets = [[-0.09, -0.02, 0], [0.085, -0.025, 0.012], [-0.035, 0.085, 0.018], [0.055, 0.075, 0.026], [0, -0.105, 0.02]]
|
|
354
|
+
offsets.forEach(([x, y, z], bloomIndex) => {
|
|
355
|
+
const bloom = new THREE.Group()
|
|
356
|
+
for (let index = 0; index < 5; index += 1) bloom.add(petal(assets, assets.flowerMaterials.gold!, index * Math.PI * 0.4, 0.038, 0.07, 0, 0.1))
|
|
357
|
+
addCentre(bloom, assets, 'amber', 0.016, 0.04)
|
|
358
|
+
bloom.position.set(x!, y!, z!)
|
|
359
|
+
bloom.rotation.z = bloomIndex * 0.48
|
|
360
|
+
group.add(bloom)
|
|
361
|
+
})
|
|
362
|
+
return group
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function makeBotanicalCluster(assets: HedgeAssets, type: FlowerType): THREE.Group {
|
|
366
|
+
const group = new THREE.Group()
|
|
367
|
+
;[[-0.78, 0.14, -0.05], [0.72, 0.13, 0.045], [2.45, 0.105, 0]].forEach(([roll, length, offset]) => {
|
|
368
|
+
const leaf = new THREE.Mesh(assets.leafGeometry, assets.leafMaterials[2]!)
|
|
369
|
+
leaf.scale.setScalar(length!)
|
|
370
|
+
leaf.rotation.z = roll!
|
|
371
|
+
leaf.position.set(offset!, -0.035, -0.045)
|
|
372
|
+
leaf.castShadow = true
|
|
373
|
+
group.add(leaf)
|
|
374
|
+
})
|
|
375
|
+
const stem = new THREE.Mesh(assets.stemGeometry, assets.stemMaterial)
|
|
376
|
+
stem.rotation.x = Math.PI / 2
|
|
377
|
+
stem.position.z = -0.015
|
|
378
|
+
group.add(stem)
|
|
379
|
+
const calyx = new THREE.Mesh(assets.sphereGeometry, assets.calyxMaterial)
|
|
380
|
+
calyx.scale.set(0.075, 0.075, 0.032)
|
|
381
|
+
calyx.position.z = 0.022
|
|
382
|
+
group.add(calyx)
|
|
383
|
+
const flower = type === 'orange' ? makeLily(assets) : type === 'pink' ? makePlumeria(assets) : type === 'magenta' ? makeOrchid(assets) : type === 'coral' ? makeTrumpet(assets) : makeGoldCluster(assets)
|
|
384
|
+
flower.position.z = 0.035
|
|
385
|
+
group.add(flower)
|
|
386
|
+
if (type === 'magenta' || type === 'coral') {
|
|
387
|
+
const maker = type === 'magenta' ? makeOrchid : makeTrumpet
|
|
388
|
+
const left = maker(assets)
|
|
389
|
+
left.scale.setScalar(type === 'magenta' ? 0.58 : 0.62)
|
|
390
|
+
left.position.set(type === 'magenta' ? -0.145 : -0.12, 0.05, 0.05)
|
|
391
|
+
left.rotation.z = type === 'magenta' ? -0.28 : -0.48
|
|
392
|
+
group.add(left)
|
|
393
|
+
const right = maker(assets)
|
|
394
|
+
right.scale.setScalar(type === 'magenta' ? 0.52 : 0.56)
|
|
395
|
+
right.position.set(type === 'magenta' ? 0.145 : 0.115, 0, 0.045)
|
|
396
|
+
right.rotation.z = type === 'magenta' ? 0.32 : 0.52
|
|
397
|
+
group.add(right)
|
|
398
|
+
}
|
|
399
|
+
return group
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function addFlowerCluster(root: THREE.Group, assets: HedgeAssets, type: FlowerType, frame: JungleFlowerHedgePathFrame, position: THREE.Vector3, normal: THREE.Vector3, scale: number, roll: number): void {
|
|
403
|
+
const cluster = makeBotanicalCluster(assets, type)
|
|
404
|
+
cluster.name = `jungle-v4-${type}-cluster`
|
|
405
|
+
cluster.position.copy(position)
|
|
406
|
+
cluster.quaternion.copy(frameQuaternion(frame.tangent, frame.up, normal))
|
|
407
|
+
cluster.rotateZ(roll)
|
|
408
|
+
cluster.scale.setScalar(scale * 1.1)
|
|
409
|
+
cluster.userData.decorative = true
|
|
410
|
+
cluster.userData.visualOnly = true
|
|
411
|
+
root.add(cluster)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function populateFlowers(root: THREE.Group, assets: HedgeAssets, run: SampledRun, height: number, runSeed: number): void {
|
|
415
|
+
const faceSlots = Math.max(1, Math.round(run.length * 0.75))
|
|
416
|
+
for (const face of [-1, 1]) {
|
|
417
|
+
const schedule = face > 0 ? FLOWER_SCHEDULE : REAR_FLOWER_SCHEDULE
|
|
418
|
+
for (let index = 0; index < faceSlots; index += 1) {
|
|
419
|
+
const seed = runSeed + 5003 + index * 89 + face * 137
|
|
420
|
+
const distance = (index + 0.5) * run.length / faceSlots
|
|
421
|
+
const frame = sampleAt(run, distance)
|
|
422
|
+
const type = schedule[index % schedule.length]!
|
|
423
|
+
const y = height * (0.31 + hash01(seed) * 0.36)
|
|
424
|
+
const normal = frame.outward.clone().multiplyScalar(face).addScaledVector(frame.up, 0.08).normalize()
|
|
425
|
+
const position = frame.base.clone().addScaledVector(frame.up, y).addScaledVector(frame.outward, face * (HEDGE_DEPTH / 2 + 0.105))
|
|
426
|
+
addFlowerCluster(root, assets, type, frame, position, normal, 0.88 + hash01(seed + 2) * 0.12, (hash01(seed + 3) - 0.5) * 0.4 * face)
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
const crownSlots = Math.max(1, Math.round(run.length * 0.5))
|
|
430
|
+
for (let index = 0; index < crownSlots; index += 1) {
|
|
431
|
+
const seed = runSeed + 6101 + index * 97
|
|
432
|
+
const frame = sampleAt(run, (index + 0.5) * run.length / crownSlots)
|
|
433
|
+
const position = frame.base.clone().addScaledVector(frame.up, height + 0.09).addScaledVector(frame.outward, (hash01(seed) - 0.5) * HEDGE_DEPTH * 0.28)
|
|
434
|
+
addFlowerCluster(root, assets, CROWN_FLOWER_SCHEDULE[index % CROWN_FLOWER_SCHEDULE.length]!, frame, position, frame.up, 0.86 + hash01(seed + 1) * 0.09, (hash01(seed + 2) - 0.5) * 0.4)
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/** Adds the approved tropical hedge as visual-only geometry over unchanged wall colliders. */
|
|
439
|
+
export function addTropicalFlowerHedgeVisual(root: THREE.Group, id: string, runs: readonly JungleFlowerHedgePathRun[], wallHeight: number, coreMaterial: THREE.Material): THREE.Group | undefined {
|
|
440
|
+
if (wallHeight <= 0.18) return undefined
|
|
441
|
+
const height = Math.min(0.95, wallHeight)
|
|
442
|
+
const group = new THREE.Group()
|
|
443
|
+
group.name = `${id}-jungle-flower-hedge`
|
|
444
|
+
group.userData.themeOnly = PAINTED_VALLEY_THEME_ID
|
|
445
|
+
group.userData.decorative = true
|
|
446
|
+
group.userData.visualOnly = true
|
|
447
|
+
group.userData.appearance = 'tropical-flower-hedge'
|
|
448
|
+
group.userData.approvedVisualContract = 'jungle-short-flower-hedge-v4.2'
|
|
449
|
+
const assets = createAssets()
|
|
450
|
+
runs.forEach((pathRun, runIndex) => {
|
|
451
|
+
if (pathRun.frames.length < 2) return
|
|
452
|
+
const run = pathRun
|
|
453
|
+
if (run.length < 0.02) return
|
|
454
|
+
const core = new THREE.Mesh(createSweptCoreGeometry(run, height), coreMaterial)
|
|
455
|
+
core.name = `${id}-jungle-v4-clipped-hedge-core-${runIndex}`
|
|
456
|
+
core.castShadow = true
|
|
457
|
+
core.receiveShadow = true
|
|
458
|
+
core.userData.visualOnly = true
|
|
459
|
+
group.add(core)
|
|
460
|
+
populateFoliage(assets, run, height, runIndex * 10_007)
|
|
461
|
+
populateFlowers(group, assets, run, height, runIndex * 10_007)
|
|
462
|
+
})
|
|
463
|
+
assets.leafMatrices.forEach((matrices, index) => {
|
|
464
|
+
const mesh = new THREE.InstancedMesh(assets.leafGeometry, assets.leafMaterials[index]!, matrices.length)
|
|
465
|
+
matrices.forEach((matrix, matrixIndex) => mesh.setMatrixAt(matrixIndex, matrix))
|
|
466
|
+
mesh.instanceMatrix.needsUpdate = true
|
|
467
|
+
mesh.name = `${id}-jungle-v4-foliage-family-${index}`
|
|
468
|
+
mesh.castShadow = true
|
|
469
|
+
mesh.receiveShadow = true
|
|
470
|
+
mesh.userData.visualOnly = true
|
|
471
|
+
group.add(mesh)
|
|
472
|
+
})
|
|
473
|
+
group.traverse(object => {
|
|
474
|
+
if (object instanceof THREE.Mesh) {
|
|
475
|
+
object.userData.appearanceMaterialId = PAINTED_VALLEY_TROPICAL_FLOWER_HEDGE_WALL_ID
|
|
476
|
+
}
|
|
477
|
+
})
|
|
478
|
+
root.add(group)
|
|
479
|
+
return group
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/** Backwards-compatible export for existing Track World call sites. */
|
|
483
|
+
export const addJungleFlowerHedgeVisual = addTropicalFlowerHedgeVisual
|