@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,308 @@
|
|
|
1
|
+
import * as THREE from 'three'
|
|
2
|
+
|
|
3
|
+
import { VisualConeGeometry, VisualTubeGeometry, VisualBoxGeometry } from '../../../three/visual-geometry'
|
|
4
|
+
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
|
5
|
+
|
|
6
|
+
import { PORTAL_APERTURE_HALF_WIDTH, PORTAL_THEME_FRAME_X_SCALE } from './portal-proportions'
|
|
7
|
+
|
|
8
|
+
export const JUNGLE_PORTAL_FRAME_THEME_ID = 'low-poly-jungle'
|
|
9
|
+
|
|
10
|
+
export interface JunglePortalFrameMaterials {
|
|
11
|
+
/** Exact `jungle.temple-stone` appearance from the active registry. */
|
|
12
|
+
stone: THREE.Material
|
|
13
|
+
/** Retained for shared material-bundle compatibility; portals use stone courses on both pillars. */
|
|
14
|
+
facade?: THREE.Material
|
|
15
|
+
/** Exact `jungle.temple-bronze` appearance. */
|
|
16
|
+
bronze: THREE.Material
|
|
17
|
+
/** Existing Jungle world foliage appearance; no portal-private plant recipe. */
|
|
18
|
+
foliage: THREE.Material
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface JunglePortalFrameOptions {
|
|
22
|
+
root: THREE.Group
|
|
23
|
+
name: string
|
|
24
|
+
color: string
|
|
25
|
+
materials: JunglePortalFrameMaterials
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const FRONT_Z = 0.46
|
|
29
|
+
const IDENTITY_QUATERNION = new THREE.Quaternion()
|
|
30
|
+
const SHARED_SCALE = new THREE.Vector3(1, 1, 1)
|
|
31
|
+
|
|
32
|
+
function markVisualOnly(object: THREE.Object3D, frameStyle = 'jungle-serpent-temple'): void {
|
|
33
|
+
object.userData.themeOnly = JUNGLE_PORTAL_FRAME_THEME_ID
|
|
34
|
+
object.userData.decorative = true
|
|
35
|
+
object.userData.visualOnly = true
|
|
36
|
+
object.userData.portalFrameStyle = frameStyle
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Give repeatable stone/metal recipes stable world-sized planar coordinates. */
|
|
40
|
+
function metricPlanarUvs(geometry: THREE.BufferGeometry): THREE.BufferGeometry {
|
|
41
|
+
const prepared = geometry.index ? geometry.toNonIndexed() : geometry
|
|
42
|
+
if (prepared !== geometry) geometry.dispose()
|
|
43
|
+
const position = prepared.getAttribute('position')
|
|
44
|
+
const normal = prepared.getAttribute('normal')
|
|
45
|
+
const uv = new Float32Array(position.count * 2)
|
|
46
|
+
const themeNetUv = new Float32Array(position.count * 4)
|
|
47
|
+
for (let index = 0; index < position.count; index += 1) {
|
|
48
|
+
const x = position.getX(index)
|
|
49
|
+
const y = position.getY(index)
|
|
50
|
+
const z = position.getZ(index)
|
|
51
|
+
const nx = Math.abs(normal.getX(index))
|
|
52
|
+
const ny = Math.abs(normal.getY(index))
|
|
53
|
+
const nz = Math.abs(normal.getZ(index))
|
|
54
|
+
if (nx >= ny && nx >= nz) {
|
|
55
|
+
uv[index * 2] = z
|
|
56
|
+
uv[index * 2 + 1] = y
|
|
57
|
+
} else if (ny >= nz) {
|
|
58
|
+
uv[index * 2] = x
|
|
59
|
+
uv[index * 2 + 1] = z
|
|
60
|
+
} else {
|
|
61
|
+
uv[index * 2] = x
|
|
62
|
+
uv[index * 2 + 1] = y
|
|
63
|
+
}
|
|
64
|
+
themeNetUv[index * 4 + 2] = 1
|
|
65
|
+
}
|
|
66
|
+
prepared.setAttribute('uv', new THREE.BufferAttribute(uv, 2))
|
|
67
|
+
prepared.setAttribute('themeNetUv', new THREE.BufferAttribute(themeNetUv, 4))
|
|
68
|
+
return prepared
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function transformGeometry(
|
|
72
|
+
geometry: THREE.BufferGeometry,
|
|
73
|
+
position: THREE.Vector3,
|
|
74
|
+
rotation = IDENTITY_QUATERNION,
|
|
75
|
+
scale = SHARED_SCALE,
|
|
76
|
+
): THREE.BufferGeometry {
|
|
77
|
+
geometry.applyMatrix4(new THREE.Matrix4().compose(position, rotation, scale))
|
|
78
|
+
return geometry
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function roundedStone(size: THREE.Vector3, position: THREE.Vector3, radius = 0.07): THREE.BufferGeometry {
|
|
82
|
+
return transformGeometry(metricPlanarUvs(new VisualBoxGeometry(size.x, size.y, size.z, 2, radius)), position)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function mergeBatch(
|
|
86
|
+
frame: THREE.Group,
|
|
87
|
+
name: string,
|
|
88
|
+
material: THREE.Material,
|
|
89
|
+
parts: THREE.BufferGeometry[],
|
|
90
|
+
): THREE.Mesh {
|
|
91
|
+
const geometry = mergeGeometries(parts, false)
|
|
92
|
+
parts.forEach(part => part.dispose())
|
|
93
|
+
if (!geometry) throw new Error(`Unable to merge Jungle portal batch "${name}".`)
|
|
94
|
+
geometry.name = `${name}-geometry`
|
|
95
|
+
geometry.computeBoundingBox()
|
|
96
|
+
geometry.computeBoundingSphere()
|
|
97
|
+
geometry.userData.visualOnly = true
|
|
98
|
+
const mesh = new THREE.Mesh(geometry, material)
|
|
99
|
+
mesh.name = name
|
|
100
|
+
mesh.castShadow = true
|
|
101
|
+
mesh.receiveShadow = true
|
|
102
|
+
markVisualOnly(mesh)
|
|
103
|
+
frame.add(mesh)
|
|
104
|
+
return mesh
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function addPairCrystal(frame: THREE.Group, name: string, color: string, bronze: THREE.Material): THREE.Group {
|
|
108
|
+
const crown = new THREE.Group()
|
|
109
|
+
crown.name = `${name}-jungle-pair-crystal`
|
|
110
|
+
crown.position.set(0, 1.78, 0.68)
|
|
111
|
+
crown.userData.portalPairColor = color
|
|
112
|
+
crown.userData.portalLightRole = 'pair-color-temple-crystal'
|
|
113
|
+
markVisualOnly(crown)
|
|
114
|
+
|
|
115
|
+
const pairColor = new THREE.Color(color)
|
|
116
|
+
const crystalMaterial = new THREE.MeshPhysicalMaterial({
|
|
117
|
+
color: pairColor.clone().lerp(new THREE.Color('#ffffff'), 0.16),
|
|
118
|
+
emissive: pairColor,
|
|
119
|
+
emissiveIntensity: 2.1,
|
|
120
|
+
roughness: 0.14,
|
|
121
|
+
metalness: 0,
|
|
122
|
+
// A tiny decorative gem must not trigger a full-scene refraction pass.
|
|
123
|
+
transmission: 0,
|
|
124
|
+
thickness: 0.22,
|
|
125
|
+
transparent: true,
|
|
126
|
+
opacity: 0.94,
|
|
127
|
+
toneMapped: false,
|
|
128
|
+
})
|
|
129
|
+
const crystal = new THREE.Mesh(new THREE.OctahedronGeometry(0.25, 0), crystalMaterial)
|
|
130
|
+
crystal.name = `${name}-jungle-pair-crystal-gem`
|
|
131
|
+
crystal.scale.set(0.76, 1.25, 0.7)
|
|
132
|
+
crystal.rotation.y = Math.PI / 4
|
|
133
|
+
crystal.castShadow = false
|
|
134
|
+
crystal.userData.ownsMaterial = true
|
|
135
|
+
crystal.userData.portalPairColor = color
|
|
136
|
+
markVisualOnly(crystal)
|
|
137
|
+
crown.add(crystal)
|
|
138
|
+
|
|
139
|
+
const haloMaterial = new THREE.MeshBasicMaterial({
|
|
140
|
+
color,
|
|
141
|
+
transparent: true,
|
|
142
|
+
opacity: 0.2,
|
|
143
|
+
blending: THREE.AdditiveBlending,
|
|
144
|
+
depthWrite: false,
|
|
145
|
+
toneMapped: false,
|
|
146
|
+
})
|
|
147
|
+
const halo = new THREE.Mesh(new THREE.OctahedronGeometry(0.31, 1), haloMaterial)
|
|
148
|
+
halo.name = `${name}-jungle-pair-crystal-halo`
|
|
149
|
+
halo.scale.set(0.76, 1.25, 0.7)
|
|
150
|
+
halo.rotation.y = Math.PI / 4
|
|
151
|
+
halo.userData.ownsMaterial = true
|
|
152
|
+
halo.userData.portalPairColor = color
|
|
153
|
+
markVisualOnly(halo)
|
|
154
|
+
crown.add(halo)
|
|
155
|
+
|
|
156
|
+
const holderParts = [
|
|
157
|
+
roundedStone(new THREE.Vector3(0.56, 0.12, 0.34), new THREE.Vector3(0, -0.3, -0.03), 0.035),
|
|
158
|
+
roundedStone(new THREE.Vector3(0.12, 0.38, 0.28), new THREE.Vector3(-0.3, -0.12, -0.03), 0.025),
|
|
159
|
+
roundedStone(new THREE.Vector3(0.12, 0.38, 0.28), new THREE.Vector3(0.3, -0.12, -0.03), 0.025),
|
|
160
|
+
]
|
|
161
|
+
mergeBatch(crown, `${name}-jungle-crystal-bronze-setting`, bronze, holderParts)
|
|
162
|
+
|
|
163
|
+
const light = new THREE.PointLight(color, 0.82, 3.2, 2)
|
|
164
|
+
light.name = `${name}-jungle-pair-crystal-light`
|
|
165
|
+
light.position.set(0, -0.04, 0.18)
|
|
166
|
+
light.userData.portalPairColor = color
|
|
167
|
+
light.userData.portalLightRole = 'pair-color-temple-crystal'
|
|
168
|
+
markVisualOnly(light)
|
|
169
|
+
crown.add(light)
|
|
170
|
+
frame.add(crown)
|
|
171
|
+
return crown
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function addOuterPlants(frame: THREE.Group, name: string, foliage: THREE.Material): void {
|
|
175
|
+
const leafGeometry = metricPlanarUvs(new VisualConeGeometry(0.12, 0.48, 5, 1))
|
|
176
|
+
leafGeometry.translate(0, 0.24, 0)
|
|
177
|
+
leafGeometry.name = `${name}-jungle-outer-leaf-geometry`
|
|
178
|
+
const placements = [
|
|
179
|
+
[-2.58, -1.46, 0.48, -0.72, 1.16],
|
|
180
|
+
[-2.43, -1.42, 0.57, -0.32, 1.34],
|
|
181
|
+
[-2.28, -1.5, 0.51, 0.2, 1.04],
|
|
182
|
+
[-2.69, -1.51, 0.36, -1.02, 0.92],
|
|
183
|
+
[2.58, -1.47, 0.48, 0.64, 1.2],
|
|
184
|
+
[2.42, -1.43, 0.58, 0.22, 1.36],
|
|
185
|
+
[2.27, -1.51, 0.5, -0.28, 1.02],
|
|
186
|
+
[2.68, -1.5, 0.36, 0.96, 0.9],
|
|
187
|
+
[-2.42, 1.08, 0.5, -0.9, 0.84],
|
|
188
|
+
[-2.2, 1.34, 0.52, -0.34, 0.72],
|
|
189
|
+
[-2.22, 1.84, 0.48, -0.84, 0.68],
|
|
190
|
+
[2.42, 1.06, 0.5, 0.78, 0.82],
|
|
191
|
+
[2.18, 1.34, 0.52, 0.3, 0.72],
|
|
192
|
+
[2.22, 1.84, 0.48, 0.82, 0.68],
|
|
193
|
+
] as const
|
|
194
|
+
const leaves = new THREE.InstancedMesh(leafGeometry, foliage, placements.length)
|
|
195
|
+
leaves.name = `${name}-jungle-outer-plants`
|
|
196
|
+
leaves.castShadow = true
|
|
197
|
+
leaves.receiveShadow = true
|
|
198
|
+
const matrix = new THREE.Matrix4()
|
|
199
|
+
const quaternion = new THREE.Quaternion()
|
|
200
|
+
const scale = new THREE.Vector3()
|
|
201
|
+
placements.forEach(([x, y, z, angle, size], index) => {
|
|
202
|
+
quaternion.setFromAxisAngle(new THREE.Vector3(0, 0, 1), angle)
|
|
203
|
+
scale.set(size, size, size * 0.72)
|
|
204
|
+
matrix.compose(new THREE.Vector3(x, y, z), quaternion, scale)
|
|
205
|
+
leaves.setMatrixAt(index, matrix)
|
|
206
|
+
})
|
|
207
|
+
leaves.instanceMatrix.needsUpdate = true
|
|
208
|
+
leaves.computeBoundingBox()
|
|
209
|
+
leaves.computeBoundingSphere()
|
|
210
|
+
leaves.userData.plantPlacement = 'outer-frame-only'
|
|
211
|
+
markVisualOnly(leaves)
|
|
212
|
+
frame.add(leaves)
|
|
213
|
+
|
|
214
|
+
const vineCurves = [
|
|
215
|
+
[
|
|
216
|
+
new THREE.Vector3(-2.28, 1.65, FRONT_Z + 0.08),
|
|
217
|
+
new THREE.Vector3(-2.47, 1.04, FRONT_Z + 0.12),
|
|
218
|
+
new THREE.Vector3(-2.36, 0.38, FRONT_Z + 0.14),
|
|
219
|
+
new THREE.Vector3(-2.5, -0.28, FRONT_Z + 0.1),
|
|
220
|
+
],
|
|
221
|
+
[
|
|
222
|
+
new THREE.Vector3(2.32, 1.58, FRONT_Z + 0.07),
|
|
223
|
+
new THREE.Vector3(2.48, 1.08, FRONT_Z + 0.12),
|
|
224
|
+
new THREE.Vector3(2.36, 0.56, FRONT_Z + 0.13),
|
|
225
|
+
],
|
|
226
|
+
[
|
|
227
|
+
new THREE.Vector3(-1.62, 1.92, FRONT_Z + 0.04),
|
|
228
|
+
new THREE.Vector3(-0.82, 1.78, FRONT_Z + 0.1),
|
|
229
|
+
new THREE.Vector3(0.12, 1.9, FRONT_Z + 0.08),
|
|
230
|
+
new THREE.Vector3(1.02, 1.74, FRONT_Z + 0.12),
|
|
231
|
+
new THREE.Vector3(1.72, 1.9, FRONT_Z + 0.05),
|
|
232
|
+
],
|
|
233
|
+
]
|
|
234
|
+
const vines = vineCurves.map(points =>
|
|
235
|
+
metricPlanarUvs(new VisualTubeGeometry(new THREE.CatmullRomCurve3(points), 18, 0.04, 6, false)),
|
|
236
|
+
)
|
|
237
|
+
mergeBatch(frame, `${name}-jungle-outer-vines`, foliage, vines)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Adds only the Jungle portal's theme-local architectural frame and light source.
|
|
242
|
+
* The black aperture, live camera, animated rift, glow, and particles stay owned by
|
|
243
|
+
* the shared portal visual. This function creates no physics bodies or colliders.
|
|
244
|
+
*/
|
|
245
|
+
export function addJunglePortalFrame(options: JunglePortalFrameOptions): THREE.Group {
|
|
246
|
+
const { root, name, color, materials } = options
|
|
247
|
+
const frame = new THREE.Group()
|
|
248
|
+
frame.name = `${name}-jungle-temple-frame`
|
|
249
|
+
frame.userData.portalPairColor = color
|
|
250
|
+
frame.scale.x = PORTAL_THEME_FRAME_X_SCALE
|
|
251
|
+
frame.userData.portalApertureClearHalfWidth = PORTAL_APERTURE_HALF_WIDTH
|
|
252
|
+
frame.userData.drawCallBudget = 7
|
|
253
|
+
frame.userData.triangleBudget = 5_500
|
|
254
|
+
frame.userData.materialReuse = {
|
|
255
|
+
stone: 'jungle.temple-stone',
|
|
256
|
+
bronze: 'jungle.temple-bronze',
|
|
257
|
+
plants: 'world.foliage',
|
|
258
|
+
}
|
|
259
|
+
markVisualOnly(frame)
|
|
260
|
+
|
|
261
|
+
const stoneParts: THREE.BufferGeometry[] = []
|
|
262
|
+
// Deliberately chunky, slightly offset courses read as an old temple without
|
|
263
|
+
// spending a draw call per block. They sit outside the shared clear aperture.
|
|
264
|
+
for (const side of [-1, 1] as const) {
|
|
265
|
+
const x = side * 2.16
|
|
266
|
+
stoneParts.push(
|
|
267
|
+
roundedStone(new THREE.Vector3(0.76, 0.54, 0.82), new THREE.Vector3(x + side * 0.03, -1.33, 0), 0.095),
|
|
268
|
+
roundedStone(new THREE.Vector3(0.68, 0.52, 0.86), new THREE.Vector3(x - side * 0.035, -0.82, 0.02), 0.09),
|
|
269
|
+
roundedStone(new THREE.Vector3(0.75, 0.5, 0.84), new THREE.Vector3(x + side * 0.025, -0.32, 0), 0.09),
|
|
270
|
+
roundedStone(new THREE.Vector3(0.69, 0.5, 0.88), new THREE.Vector3(x - side * 0.03, 0.18, 0.025), 0.09),
|
|
271
|
+
roundedStone(new THREE.Vector3(0.76, 0.5, 0.84), new THREE.Vector3(x + side * 0.025, 0.68, 0), 0.095),
|
|
272
|
+
roundedStone(new THREE.Vector3(0.72, 0.42, 0.86), new THREE.Vector3(x, 1.13, 0.015), 0.085),
|
|
273
|
+
roundedStone(new THREE.Vector3(1.02, 0.34, 1.0), new THREE.Vector3(side * 2.2, -1.66, -0.02), 0.08),
|
|
274
|
+
roundedStone(new THREE.Vector3(0.86, 0.28, 0.94), new THREE.Vector3(side * 2.16, 1.38, 0), 0.07),
|
|
275
|
+
roundedStone(new THREE.Vector3(0.68, 2.36, 0.18), new THREE.Vector3(side * 2.165, -0.08, 0.46), 0.055),
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
stoneParts.push(
|
|
279
|
+
roundedStone(new THREE.Vector3(1.18, 0.54, 0.88), new THREE.Vector3(-1.72, 1.65, 0), 0.1),
|
|
280
|
+
roundedStone(new THREE.Vector3(1.08, 0.57, 0.9), new THREE.Vector3(-0.62, 1.63, 0.02), 0.1),
|
|
281
|
+
roundedStone(new THREE.Vector3(1.08, 0.57, 0.9), new THREE.Vector3(0.62, 1.64, 0), 0.1),
|
|
282
|
+
roundedStone(new THREE.Vector3(1.18, 0.54, 0.88), new THREE.Vector3(1.72, 1.65, 0.015), 0.1),
|
|
283
|
+
roundedStone(new THREE.Vector3(0.76, 0.7, 0.96), new THREE.Vector3(0, 1.65, 0.03), 0.1),
|
|
284
|
+
roundedStone(new THREE.Vector3(5.08, 0.16, 0.98), new THREE.Vector3(0, 1.96, -0.02), 0.045),
|
|
285
|
+
)
|
|
286
|
+
mergeBatch(frame, `${name}-jungle-temple-stone`, materials.stone, stoneParts)
|
|
287
|
+
|
|
288
|
+
const bronzeParts: THREE.BufferGeometry[] = []
|
|
289
|
+
for (const side of [-1, 1] as const) {
|
|
290
|
+
for (const y of [-0.72, 0.63]) {
|
|
291
|
+
bronzeParts.push(
|
|
292
|
+
roundedStone(new THREE.Vector3(0.82, 0.15, 0.94), new THREE.Vector3(side * 2.16, y, 0.05), 0.04),
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
for (const x of [-1.48, 1.48]) {
|
|
297
|
+
bronzeParts.push(roundedStone(new THREE.Vector3(0.16, 0.72, 0.98), new THREE.Vector3(x, 1.64, 0.045), 0.04))
|
|
298
|
+
}
|
|
299
|
+
mergeBatch(frame, `${name}-jungle-temple-bronze`, materials.bronze, bronzeParts)
|
|
300
|
+
|
|
301
|
+
addPairCrystal(frame, name, color, materials.bronze)
|
|
302
|
+
addOuterPlants(frame, name, materials.foliage)
|
|
303
|
+
|
|
304
|
+
// Reassert invariant tags on every descendant, including meshes created by helpers.
|
|
305
|
+
frame.traverse(object => markVisualOnly(object))
|
|
306
|
+
root.add(frame)
|
|
307
|
+
return frame
|
|
308
|
+
}
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import * as THREE from 'three'
|
|
2
|
+
|
|
3
|
+
import { VisualTorusGeometry, VisualTubeGeometry, VisualBoxGeometry } from '../../../three/visual-geometry'
|
|
4
|
+
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
|
5
|
+
|
|
6
|
+
export const JUNGLE_TEMPLE_THEME_ID = 'low-poly-jungle'
|
|
7
|
+
|
|
8
|
+
const STONE_UV_SPAN = new THREE.Vector2(4.8, 4.8)
|
|
9
|
+
const TIMBER_UV_SPAN = new THREE.Vector2(3.6, 3.6)
|
|
10
|
+
const BRONZE_UV_SPAN = new THREE.Vector2(3.2, 3.2)
|
|
11
|
+
|
|
12
|
+
export interface JungleTempleLiftMaterials {
|
|
13
|
+
stone: THREE.Material
|
|
14
|
+
facade: THREE.Material
|
|
15
|
+
timber: THREE.Material
|
|
16
|
+
platform: THREE.Material
|
|
17
|
+
moss: THREE.Material
|
|
18
|
+
accent: THREE.Material
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface JungleTempleLiftOptions {
|
|
22
|
+
id: string
|
|
23
|
+
bottom: THREE.Vector3
|
|
24
|
+
rotation: THREE.Quaternion
|
|
25
|
+
width: number
|
|
26
|
+
height: number
|
|
27
|
+
depth: number
|
|
28
|
+
releaseHeight: number
|
|
29
|
+
materials: JungleTempleLiftMaterials
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface GeometryBatch {
|
|
33
|
+
readonly name: string
|
|
34
|
+
readonly material: THREE.Material
|
|
35
|
+
readonly parts: THREE.BufferGeometry[]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function authoredUvGeometry(geometry: THREE.BufferGeometry, span: THREE.Vector2): THREE.BufferGeometry {
|
|
39
|
+
const prepared = geometry.index ? geometry.toNonIndexed() : geometry
|
|
40
|
+
if (prepared !== geometry) geometry.dispose()
|
|
41
|
+
const uv = prepared.getAttribute('uv')
|
|
42
|
+
if (uv) {
|
|
43
|
+
for (let index = 0; index < uv.count; index += 1) {
|
|
44
|
+
uv.setXY(index, uv.getX(index) * span.x, uv.getY(index) * span.y)
|
|
45
|
+
}
|
|
46
|
+
uv.needsUpdate = true
|
|
47
|
+
}
|
|
48
|
+
const position = prepared.getAttribute('position')
|
|
49
|
+
const metricUvs = new Float32Array(position.count * 4)
|
|
50
|
+
for (let index = 0; index < position.count; index += 1) metricUvs[index * 4 + 2] = 1
|
|
51
|
+
prepared.setAttribute('themeNetUv', new THREE.BufferAttribute(metricUvs, 4))
|
|
52
|
+
return prepared
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function metricPlanarUvGeometry(geometry: THREE.BufferGeometry): THREE.BufferGeometry {
|
|
56
|
+
const prepared = geometry.index ? geometry.toNonIndexed() : geometry
|
|
57
|
+
if (prepared !== geometry) geometry.dispose()
|
|
58
|
+
const position = prepared.getAttribute('position')
|
|
59
|
+
const normal = prepared.getAttribute('normal')
|
|
60
|
+
const uvs = new Float32Array(position.count * 2)
|
|
61
|
+
const metricUvs = new Float32Array(position.count * 4)
|
|
62
|
+
for (let index = 0; index < position.count; index += 1) {
|
|
63
|
+
const x = position.getX(index)
|
|
64
|
+
const y = position.getY(index)
|
|
65
|
+
const z = position.getZ(index)
|
|
66
|
+
const normalX = Math.abs(normal.getX(index))
|
|
67
|
+
const normalY = Math.abs(normal.getY(index))
|
|
68
|
+
const normalZ = Math.abs(normal.getZ(index))
|
|
69
|
+
if (normalX >= normalY && normalX >= normalZ) {
|
|
70
|
+
uvs[index * 2] = z
|
|
71
|
+
uvs[index * 2 + 1] = y
|
|
72
|
+
} else if (normalY >= normalZ) {
|
|
73
|
+
uvs[index * 2] = x
|
|
74
|
+
uvs[index * 2 + 1] = z
|
|
75
|
+
} else {
|
|
76
|
+
uvs[index * 2] = x
|
|
77
|
+
uvs[index * 2 + 1] = y
|
|
78
|
+
}
|
|
79
|
+
metricUvs[index * 4 + 2] = 1
|
|
80
|
+
}
|
|
81
|
+
prepared.setAttribute('uv', new THREE.BufferAttribute(uvs, 2))
|
|
82
|
+
prepared.setAttribute('themeNetUv', new THREE.BufferAttribute(metricUvs, 4))
|
|
83
|
+
return prepared
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function transformedGeometry(
|
|
87
|
+
geometry: THREE.BufferGeometry,
|
|
88
|
+
position: THREE.Vector3,
|
|
89
|
+
rotation = new THREE.Quaternion(),
|
|
90
|
+
): THREE.BufferGeometry {
|
|
91
|
+
geometry.applyMatrix4(new THREE.Matrix4().compose(position, rotation, new THREE.Vector3(1, 1, 1)))
|
|
92
|
+
return geometry
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function roundedBox(size: THREE.Vector3, radius: number, position: THREE.Vector3): THREE.BufferGeometry {
|
|
96
|
+
return transformedGeometry(
|
|
97
|
+
metricPlanarUvGeometry(new VisualBoxGeometry(size.x, size.y, size.z, 2, radius)),
|
|
98
|
+
position,
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function addMergedBatch(group: THREE.Group, id: string, batch: GeometryBatch): THREE.Mesh | undefined {
|
|
103
|
+
if (batch.parts.length === 0) return undefined
|
|
104
|
+
const geometry = mergeGeometries(batch.parts, false)
|
|
105
|
+
batch.parts.forEach(part => part.dispose())
|
|
106
|
+
if (!geometry) throw new Error(`Unable to merge Jungle temple lift batch "${batch.name}".`)
|
|
107
|
+
geometry.name = `${id}-${batch.name}-geometry`
|
|
108
|
+
geometry.computeBoundingBox()
|
|
109
|
+
geometry.computeBoundingSphere()
|
|
110
|
+
geometry.userData.visualOnly = true
|
|
111
|
+
const mesh = new THREE.Mesh(geometry, batch.material)
|
|
112
|
+
mesh.name = `${id}-${batch.name}`
|
|
113
|
+
mesh.castShadow = true
|
|
114
|
+
mesh.receiveShadow = true
|
|
115
|
+
mesh.userData.decorative = true
|
|
116
|
+
mesh.userData.visualOnly = true
|
|
117
|
+
group.add(mesh)
|
|
118
|
+
return mesh
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function facadeUvGeometry(
|
|
122
|
+
geometry: THREE.BufferGeometry,
|
|
123
|
+
side: -1 | 1,
|
|
124
|
+
wallDepth: number,
|
|
125
|
+
wallBottom: number,
|
|
126
|
+
wallHeight: number,
|
|
127
|
+
): THREE.BufferGeometry {
|
|
128
|
+
const prepared = geometry.index ? geometry.toNonIndexed() : geometry
|
|
129
|
+
if (prepared !== geometry) geometry.dispose()
|
|
130
|
+
const position = prepared.getAttribute('position')
|
|
131
|
+
const uvs = new Float32Array(position.count * 2)
|
|
132
|
+
const metricUvs = new Float32Array(position.count * 4)
|
|
133
|
+
for (let index = 0; index < position.count; index += 1) {
|
|
134
|
+
const depthUv = THREE.MathUtils.clamp((position.getZ(index) + wallDepth / 2) / wallDepth, 0, 1)
|
|
135
|
+
uvs[index * 2] = side > 0 ? 1 - depthUv : depthUv
|
|
136
|
+
uvs[index * 2 + 1] = THREE.MathUtils.clamp((position.getY(index) - wallBottom) / wallHeight, 0, 1)
|
|
137
|
+
metricUvs[index * 4 + 2] = 1
|
|
138
|
+
}
|
|
139
|
+
prepared.setAttribute('uv', new THREE.BufferAttribute(uvs, 2))
|
|
140
|
+
prepared.setAttribute('themeNetUv', new THREE.BufferAttribute(metricUvs, 4))
|
|
141
|
+
return prepared
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function addJungleTempleLiftSkin(root: THREE.Group, options: JungleTempleLiftOptions): THREE.Group {
|
|
145
|
+
const { id, bottom, rotation, width, height, depth, releaseHeight, materials } = options
|
|
146
|
+
const group = new THREE.Group()
|
|
147
|
+
group.name = `${id}-jungle-temple-lift`
|
|
148
|
+
group.position.copy(bottom)
|
|
149
|
+
group.quaternion.copy(rotation)
|
|
150
|
+
group.userData.themeOnly = JUNGLE_TEMPLE_THEME_ID
|
|
151
|
+
group.userData.decorative = true
|
|
152
|
+
group.userData.visualOnly = true
|
|
153
|
+
group.userData.drawCallBudget = 5
|
|
154
|
+
group.userData.triangleBudget = 12_000
|
|
155
|
+
|
|
156
|
+
const stone: THREE.BufferGeometry[] = []
|
|
157
|
+
const facade: THREE.BufferGeometry[] = []
|
|
158
|
+
const timber: THREE.BufferGeometry[] = []
|
|
159
|
+
const bronze: THREE.BufferGeometry[] = []
|
|
160
|
+
const moss: THREE.BufferGeometry[] = []
|
|
161
|
+
const sideX = width / 2 + 0.18
|
|
162
|
+
const frontZ = depth / 2 + 0.2
|
|
163
|
+
const backZ = -depth / 2 - 0.2
|
|
164
|
+
const shellHeight = height + 1.02
|
|
165
|
+
const shellBottom = -0.35
|
|
166
|
+
const shellDepth = depth + 0.62
|
|
167
|
+
const shellCoreThickness = 0.34
|
|
168
|
+
const facadeBaseDepth = 0.04
|
|
169
|
+
const facadeFrameDepth = 0.15
|
|
170
|
+
const facadeBlockInset = 0.018
|
|
171
|
+
const facadeFrameRegions = [
|
|
172
|
+
[0.005, 0.205, 0.06, 0.94],
|
|
173
|
+
[0.795, 0.995, 0.06, 0.94],
|
|
174
|
+
[0.005, 0.995, 0.94, 0.995],
|
|
175
|
+
[0.005, 0.995, 0.005, 0.06],
|
|
176
|
+
] as const
|
|
177
|
+
|
|
178
|
+
for (const side of [-1, 1] as const) {
|
|
179
|
+
// The full authored facade supplies the approved serpent composition. Only its
|
|
180
|
+
// matching outer frame is raised into geometry. Internal stone seams and the
|
|
181
|
+
// serpent carving stay in the aligned normal response rather than becoming
|
|
182
|
+
// guessed geometry that can drift away from the source.
|
|
183
|
+
stone.push(
|
|
184
|
+
roundedBox(
|
|
185
|
+
new THREE.Vector3(shellCoreThickness, shellHeight, shellDepth),
|
|
186
|
+
0.055,
|
|
187
|
+
new THREE.Vector3(side * sideX, shellBottom + shellHeight / 2, 0),
|
|
188
|
+
),
|
|
189
|
+
)
|
|
190
|
+
const facadeCenterX = side * (sideX + shellCoreThickness / 2 + facadeBaseDepth / 2 - 0.008)
|
|
191
|
+
facade.push(
|
|
192
|
+
facadeUvGeometry(
|
|
193
|
+
transformedGeometry(
|
|
194
|
+
new THREE.BoxGeometry(facadeBaseDepth, shellHeight, shellDepth),
|
|
195
|
+
new THREE.Vector3(facadeCenterX, shellBottom + shellHeight / 2, 0),
|
|
196
|
+
),
|
|
197
|
+
side,
|
|
198
|
+
shellDepth,
|
|
199
|
+
shellBottom,
|
|
200
|
+
shellHeight,
|
|
201
|
+
),
|
|
202
|
+
)
|
|
203
|
+
for (const [u0, u1, v0, v1] of facadeFrameRegions) {
|
|
204
|
+
const blockDepth = (u1 - u0) * shellDepth - facadeBlockInset
|
|
205
|
+
const blockHeight = (v1 - v0) * shellHeight - facadeBlockInset
|
|
206
|
+
const blockCenterU = (u0 + u1) / 2
|
|
207
|
+
const blockCenterV = (v0 + v1) / 2
|
|
208
|
+
const blockCenterZ =
|
|
209
|
+
side > 0 ? shellDepth / 2 - blockCenterU * shellDepth : -shellDepth / 2 + blockCenterU * shellDepth
|
|
210
|
+
const blockRelief = facadeFrameDepth
|
|
211
|
+
const blockCenterX = side * (sideX + shellCoreThickness / 2 + facadeBaseDepth + blockRelief / 2 - 0.014)
|
|
212
|
+
facade.push(
|
|
213
|
+
facadeUvGeometry(
|
|
214
|
+
transformedGeometry(
|
|
215
|
+
new THREE.BoxGeometry(blockRelief, blockHeight, blockDepth),
|
|
216
|
+
new THREE.Vector3(blockCenterX, shellBottom + blockCenterV * shellHeight, blockCenterZ),
|
|
217
|
+
),
|
|
218
|
+
side,
|
|
219
|
+
shellDepth,
|
|
220
|
+
shellBottom,
|
|
221
|
+
shellHeight,
|
|
222
|
+
),
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
moss.push(
|
|
226
|
+
roundedBox(
|
|
227
|
+
new THREE.Vector3(0.12, 0.09, depth * 0.72),
|
|
228
|
+
0.035,
|
|
229
|
+
new THREE.Vector3(side * (sideX + 0.34), height + 0.66, -depth * 0.04),
|
|
230
|
+
),
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
group.userData.sideWallRelief = 'source-aligned-outer-frame'
|
|
234
|
+
group.userData.sideWallReliefBlockCount = facadeFrameRegions.length * 2
|
|
235
|
+
|
|
236
|
+
const rearPanelZ = -depth / 2 - 0.07
|
|
237
|
+
const exitOpeningBottom = THREE.MathUtils.clamp(releaseHeight - 0.34, 0.9, height - 1.5)
|
|
238
|
+
const exitOpeningTop = Math.min(height + 0.42, releaseHeight + 1.48)
|
|
239
|
+
const rearPanelBottom = -0.12
|
|
240
|
+
const rearPanelTop = height + 0.68
|
|
241
|
+
const lowerPanelHeight = exitOpeningBottom - rearPanelBottom
|
|
242
|
+
const upperPanelHeight = Math.max(0.24, rearPanelTop - exitOpeningTop)
|
|
243
|
+
group.userData.hasBackWall = true
|
|
244
|
+
group.userData.releaseOpeningBottom = exitOpeningBottom
|
|
245
|
+
group.userData.releaseOpeningTop = exitOpeningTop
|
|
246
|
+
stone.push(
|
|
247
|
+
roundedBox(
|
|
248
|
+
new THREE.Vector3(width - 0.38, lowerPanelHeight, 0.28),
|
|
249
|
+
0.07,
|
|
250
|
+
new THREE.Vector3(0, (rearPanelBottom + exitOpeningBottom) / 2, rearPanelZ),
|
|
251
|
+
),
|
|
252
|
+
roundedBox(
|
|
253
|
+
new THREE.Vector3(width - 0.38, upperPanelHeight, 0.28),
|
|
254
|
+
0.07,
|
|
255
|
+
new THREE.Vector3(0, (exitOpeningTop + rearPanelTop) / 2, rearPanelZ),
|
|
256
|
+
),
|
|
257
|
+
)
|
|
258
|
+
for (const y of [exitOpeningBottom - 0.08, exitOpeningTop + 0.08]) {
|
|
259
|
+
stone.push(
|
|
260
|
+
roundedBox(new THREE.Vector3(width - 0.14, 0.26, 0.38), 0.06, new THREE.Vector3(0, y, rearPanelZ - 0.025)),
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
for (const z of [frontZ, backZ]) {
|
|
264
|
+
stone.push(roundedBox(new THREE.Vector3(width + 0.8, 0.58, 0.52), 0.09, new THREE.Vector3(0, height + 0.3, z)))
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const postX = width / 2 - 0.57
|
|
268
|
+
for (const x of [-postX, postX]) {
|
|
269
|
+
timber.push(
|
|
270
|
+
roundedBox(new THREE.Vector3(0.42, height + 0.36, 0.4), 0.07, new THREE.Vector3(x, height / 2, frontZ + 0.035)),
|
|
271
|
+
)
|
|
272
|
+
for (const y of [height * 0.2, height * 0.5, height * 0.8]) {
|
|
273
|
+
timber.push(
|
|
274
|
+
transformedGeometry(
|
|
275
|
+
authoredUvGeometry(new VisualTorusGeometry(0.27, 0.045, 6, 16), TIMBER_UV_SPAN),
|
|
276
|
+
new THREE.Vector3(x, y, frontZ + 0.035),
|
|
277
|
+
new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), Math.PI / 2),
|
|
278
|
+
),
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const drumPosition = new THREE.Vector3(0, height + 0.55, depth / 2 + 0.2)
|
|
284
|
+
for (const x of [-width * 0.31, 0, width * 0.31]) {
|
|
285
|
+
bronze.push(
|
|
286
|
+
transformedGeometry(
|
|
287
|
+
authoredUvGeometry(new VisualTorusGeometry(0.59, 0.05, 7, 20), BRONZE_UV_SPAN),
|
|
288
|
+
drumPosition.clone().add(new THREE.Vector3(x, 0, 0)),
|
|
289
|
+
new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI / 2),
|
|
290
|
+
),
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const vinePoints = [
|
|
295
|
+
new THREE.Vector3(-width * 0.42, height + 0.54, backZ),
|
|
296
|
+
new THREE.Vector3(-width * 0.12, height + 0.78, 0),
|
|
297
|
+
new THREE.Vector3(width * 0.18, height + 0.55, frontZ),
|
|
298
|
+
new THREE.Vector3(width * 0.42, height - 0.62, frontZ + 0.04),
|
|
299
|
+
]
|
|
300
|
+
moss.push(
|
|
301
|
+
authoredUvGeometry(
|
|
302
|
+
new VisualTubeGeometry(new THREE.CatmullRomCurve3(vinePoints), 22, 0.055, 6, false),
|
|
303
|
+
STONE_UV_SPAN,
|
|
304
|
+
),
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
addMergedBatch(group, id, { name: 'temple-stone-shell', material: materials.stone, parts: stone })
|
|
308
|
+
addMergedBatch(group, id, { name: 'temple-serpent-facade', material: materials.facade, parts: facade })
|
|
309
|
+
addMergedBatch(group, id, { name: 'temple-timber-frame', material: materials.timber, parts: timber })
|
|
310
|
+
addMergedBatch(group, id, { name: 'temple-bronze-details', material: materials.accent, parts: bronze })
|
|
311
|
+
addMergedBatch(group, id, { name: 'temple-moss-details', material: materials.moss, parts: moss })
|
|
312
|
+
|
|
313
|
+
root.add(group)
|
|
314
|
+
return group
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function decorateJungleTempleShelf(
|
|
318
|
+
shelf: THREE.Mesh,
|
|
319
|
+
id: string,
|
|
320
|
+
width: number,
|
|
321
|
+
depth: number,
|
|
322
|
+
materials: JungleTempleLiftMaterials,
|
|
323
|
+
): void {
|
|
324
|
+
const decoration = new THREE.Group()
|
|
325
|
+
decoration.name = `${id}-jungle-temple-platform`
|
|
326
|
+
decoration.userData.themeOnly = JUNGLE_TEMPLE_THEME_ID
|
|
327
|
+
decoration.userData.decorative = true
|
|
328
|
+
decoration.userData.visualOnly = true
|
|
329
|
+
decoration.userData.drawCallBudget = 3
|
|
330
|
+
|
|
331
|
+
const platformGeometry = roundedBox(
|
|
332
|
+
new THREE.Vector3(width - 0.08, 0.07, depth - 0.1),
|
|
333
|
+
0.035,
|
|
334
|
+
new THREE.Vector3(0, 0.125, 0),
|
|
335
|
+
)
|
|
336
|
+
const platform = new THREE.Mesh(platformGeometry, materials.platform)
|
|
337
|
+
platform.name = `${id}-temple-bamboo-deck`
|
|
338
|
+
platform.castShadow = true
|
|
339
|
+
platform.receiveShadow = true
|
|
340
|
+
platform.userData.decorative = true
|
|
341
|
+
platform.userData.visualOnly = true
|
|
342
|
+
decoration.add(platform)
|
|
343
|
+
|
|
344
|
+
const lipGeometry = roundedBox(
|
|
345
|
+
new THREE.Vector3(width, 0.22, 0.12),
|
|
346
|
+
0.035,
|
|
347
|
+
new THREE.Vector3(0, 0.19, depth / 2 + 0.015),
|
|
348
|
+
)
|
|
349
|
+
const lip = new THREE.Mesh(lipGeometry, materials.timber)
|
|
350
|
+
lip.name = `${id}-temple-platform-lip`
|
|
351
|
+
lip.castShadow = true
|
|
352
|
+
lip.receiveShadow = true
|
|
353
|
+
lip.userData.decorative = true
|
|
354
|
+
lip.userData.visualOnly = true
|
|
355
|
+
decoration.add(lip)
|
|
356
|
+
|
|
357
|
+
const amberParts = [-width * 0.3, 0, width * 0.3].map(x =>
|
|
358
|
+
roundedBox(new THREE.Vector3(0.36, 0.1, 0.035), 0.015, new THREE.Vector3(x, 0.22, depth / 2 + 0.085)),
|
|
359
|
+
)
|
|
360
|
+
addMergedBatch(decoration, id, {
|
|
361
|
+
name: 'temple-platform-amber-inlays',
|
|
362
|
+
material: materials.accent,
|
|
363
|
+
parts: amberParts,
|
|
364
|
+
})
|
|
365
|
+
shelf.add(decoration)
|
|
366
|
+
}
|