@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,201 @@
|
|
|
1
|
+
import * as THREE from 'three'
|
|
2
|
+
|
|
3
|
+
import { VisualCylinderGeometry, VisualBoxGeometry } from '../../../three/visual-geometry'
|
|
4
|
+
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
|
5
|
+
|
|
6
|
+
const LOCAL_UP = new THREE.Vector3(0, 1, 0)
|
|
7
|
+
|
|
8
|
+
export interface JapaneseSakuraKawaraRoofOptions {
|
|
9
|
+
name: string
|
|
10
|
+
ridgeY: number
|
|
11
|
+
widthScale?: number
|
|
12
|
+
halfDepth?: number
|
|
13
|
+
capCourses?: number
|
|
14
|
+
themeRolePrefix?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function metricPlanarUvs(geometry: THREE.BufferGeometry): THREE.BufferGeometry {
|
|
18
|
+
const prepared = geometry.index ? geometry.toNonIndexed() : geometry
|
|
19
|
+
if (prepared !== geometry) geometry.dispose()
|
|
20
|
+
const position = prepared.getAttribute('position')
|
|
21
|
+
const normal = prepared.getAttribute('normal')
|
|
22
|
+
const uv = new Float32Array(position.count * 2)
|
|
23
|
+
const themeNetUv = new Float32Array(position.count * 4)
|
|
24
|
+
for (let index = 0; index < position.count; index += 1) {
|
|
25
|
+
const x = position.getX(index)
|
|
26
|
+
const y = position.getY(index)
|
|
27
|
+
const z = position.getZ(index)
|
|
28
|
+
const nx = Math.abs(normal.getX(index))
|
|
29
|
+
const ny = Math.abs(normal.getY(index))
|
|
30
|
+
const nz = Math.abs(normal.getZ(index))
|
|
31
|
+
if (ny >= nx && ny >= nz) {
|
|
32
|
+
uv[index * 2] = x
|
|
33
|
+
uv[index * 2 + 1] = z
|
|
34
|
+
} else if (nx >= nz) {
|
|
35
|
+
uv[index * 2] = z
|
|
36
|
+
uv[index * 2 + 1] = y
|
|
37
|
+
} else {
|
|
38
|
+
uv[index * 2] = x
|
|
39
|
+
uv[index * 2 + 1] = y
|
|
40
|
+
}
|
|
41
|
+
themeNetUv[index * 4 + 2] = 1
|
|
42
|
+
}
|
|
43
|
+
prepared.setAttribute('uv', new THREE.BufferAttribute(uv, 2))
|
|
44
|
+
prepared.setAttribute('themeNetUv', new THREE.BufferAttribute(themeNetUv, 4))
|
|
45
|
+
return prepared
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function transformed(geometry: THREE.BufferGeometry, position: THREE.Vector3, quaternion = new THREE.Quaternion()) {
|
|
49
|
+
geometry.applyMatrix4(new THREE.Matrix4().compose(position, quaternion, new THREE.Vector3(1, 1, 1)))
|
|
50
|
+
return metricPlanarUvs(geometry)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function mark(object: THREE.Object3D, role: string): void {
|
|
54
|
+
object.userData.themeOnly = 'japanese-sakura-lake'
|
|
55
|
+
object.userData.themeRole = role
|
|
56
|
+
object.userData.decorative = true
|
|
57
|
+
object.userData.visualOnly = true
|
|
58
|
+
object.userData.roofConstruction = 'shared-japanese-sakura-portal-kawara-v1'
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function mergedRoofPart(
|
|
62
|
+
root: THREE.Group,
|
|
63
|
+
name: string,
|
|
64
|
+
material: THREE.Material,
|
|
65
|
+
parts: THREE.BufferGeometry[],
|
|
66
|
+
role: string,
|
|
67
|
+
): THREE.Mesh {
|
|
68
|
+
const geometry = mergeGeometries(parts, false)
|
|
69
|
+
parts.forEach(part => part.dispose())
|
|
70
|
+
if (!geometry) throw new Error(`Unable to merge Japanese Sakura roof batch "${name}".`)
|
|
71
|
+
geometry.computeBoundingBox()
|
|
72
|
+
geometry.computeBoundingSphere()
|
|
73
|
+
const mesh = new THREE.Mesh(geometry, material)
|
|
74
|
+
mesh.name = name
|
|
75
|
+
mesh.castShadow = true
|
|
76
|
+
mesh.receiveShadow = true
|
|
77
|
+
mark(mesh, role)
|
|
78
|
+
root.add(mesh)
|
|
79
|
+
return mesh
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The solved portal roof, shared verbatim by portals and vertical mechanisms.
|
|
84
|
+
* Width and course count may expand to clear a larger mechanism while the
|
|
85
|
+
* portal pitch, tile scale, lifted eaves, pan/cap rhythm, and ridge stay intact.
|
|
86
|
+
*/
|
|
87
|
+
export function addJapaneseSakuraKawaraRoof(
|
|
88
|
+
root: THREE.Group,
|
|
89
|
+
material: THREE.Material,
|
|
90
|
+
options: JapaneseSakuraKawaraRoofOptions,
|
|
91
|
+
): THREE.Group {
|
|
92
|
+
const roof = new THREE.Group()
|
|
93
|
+
roof.name = `${options.name}-roof-assembly`
|
|
94
|
+
roof.scale.x = options.widthScale ?? 1
|
|
95
|
+
roof.userData.construction = 'shared-japanese-sakura-portal-kawara-v1'
|
|
96
|
+
const rolePrefix = options.themeRolePrefix ?? 'sakura'
|
|
97
|
+
const roofHalfDepth = options.halfDepth ?? 1.32
|
|
98
|
+
const capCourses = options.capCourses ?? 5
|
|
99
|
+
const panCourses = Math.max(1, capCourses - 1)
|
|
100
|
+
const isPortalDepth = options.halfDepth === undefined
|
|
101
|
+
const slopeDrop = 0.82 * (roofHalfDepth / 1.32)
|
|
102
|
+
const roofPitch = 0.5
|
|
103
|
+
const frontRoofRotation = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), roofPitch)
|
|
104
|
+
const backRoofRotation = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), -roofPitch)
|
|
105
|
+
const backingLength = isPortalDepth ? 1.38 : Math.hypot(roofHalfDepth, slopeDrop)
|
|
106
|
+
const backingZ = isPortalDepth ? 0.58 : roofHalfDepth / 2
|
|
107
|
+
const backing = mergedRoofPart(
|
|
108
|
+
roof,
|
|
109
|
+
`${options.name}-kawara-backing`,
|
|
110
|
+
material,
|
|
111
|
+
[
|
|
112
|
+
transformed(new VisualBoxGeometry(5.48, 0.12, backingLength, 2, 0.035), new THREE.Vector3(0, options.ridgeY - slopeDrop / 2, backingZ), frontRoofRotation),
|
|
113
|
+
transformed(new VisualBoxGeometry(5.48, 0.12, backingLength, 2, 0.035), new THREE.Vector3(0, options.ridgeY - slopeDrop / 2, -backingZ), backRoofRotation),
|
|
114
|
+
],
|
|
115
|
+
`${rolePrefix}-kawara-roof-backing`,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
const columns = 19
|
|
119
|
+
// The roof backing supplies the solid surface; tiles only need a curved outer shell.
|
|
120
|
+
const tileGeometry = new VisualCylinderGeometry(0.5, 0.5, 1, 4, 1, true, 0, Math.PI)
|
|
121
|
+
const tiles = new THREE.InstancedMesh(tileGeometry, material, columns * capCourses * 2)
|
|
122
|
+
tiles.name = `${options.name}-kawara-tiles`
|
|
123
|
+
let tileIndex = 0
|
|
124
|
+
for (const roofSide of [-1, 1] as const) {
|
|
125
|
+
for (let column = 0; column < columns; column += 1) {
|
|
126
|
+
const x = -2.52 + column * 0.28
|
|
127
|
+
const eaveLift = Math.max(0, Math.abs(x) - 2.08) * 0.3
|
|
128
|
+
for (let segment = 0; segment < capCourses; segment += 1) {
|
|
129
|
+
const inner = segment / capCourses
|
|
130
|
+
const outer = Math.min(1, (segment + 1) / capCourses + 0.055)
|
|
131
|
+
const start = new THREE.Vector3(x, options.ridgeY - inner * slopeDrop + eaveLift, roofSide * inner * roofHalfDepth)
|
|
132
|
+
const end = new THREE.Vector3(x, options.ridgeY - outer * slopeDrop + eaveLift, roofSide * outer * roofHalfDepth)
|
|
133
|
+
const direction = end.clone().sub(start)
|
|
134
|
+
const quaternion = new THREE.Quaternion().setFromUnitVectors(LOCAL_UP, direction.clone().normalize())
|
|
135
|
+
quaternion.multiply(new THREE.Quaternion().setFromAxisAngle(LOCAL_UP, roofSide > 0 ? 0 : Math.PI))
|
|
136
|
+
tiles.setMatrixAt(
|
|
137
|
+
tileIndex++,
|
|
138
|
+
new THREE.Matrix4().compose(
|
|
139
|
+
start.clone().lerp(end, 0.5).add(new THREE.Vector3(0, 0.065, 0)),
|
|
140
|
+
quaternion,
|
|
141
|
+
new THREE.Vector3(0.2, direction.length(), 0.2),
|
|
142
|
+
),
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
tiles.instanceMatrix.needsUpdate = true
|
|
148
|
+
tiles.userData.kawaraTileCount = tiles.count
|
|
149
|
+
tiles.userData.kawaraCourseCount = capCourses
|
|
150
|
+
tiles.userData.mirroredRoofSides = true
|
|
151
|
+
mark(tiles, `${rolePrefix}-kawara-tile-shell`)
|
|
152
|
+
roof.add(tiles)
|
|
153
|
+
|
|
154
|
+
// A one-segment rounded box preserves the tiny pan tile's chamfered
|
|
155
|
+
// silhouette while avoiding 192 interior bevel triangles per instance.
|
|
156
|
+
const panGeometry = new VisualBoxGeometry(0.22, 0.055, 0.42, 1, 0.025)
|
|
157
|
+
const panTiles = new THREE.InstancedMesh(panGeometry, material, 18 * panCourses * 2)
|
|
158
|
+
panTiles.name = `${options.name}-kawara-pan-tiles`
|
|
159
|
+
let panIndex = 0
|
|
160
|
+
for (const roofSide of [-1, 1] as const) {
|
|
161
|
+
const rotation = roofSide > 0 ? frontRoofRotation : backRoofRotation
|
|
162
|
+
for (let course = 0; course < panCourses; course += 1) {
|
|
163
|
+
const progress = (course + 0.72) / (panCourses + 0.25)
|
|
164
|
+
for (let column = 0; column < 18; column += 1) {
|
|
165
|
+
const x = -2.38 + column * 0.28 + (course % 2) * 0.04
|
|
166
|
+
const eaveLift = Math.max(0, Math.abs(x) - 2.08) * 0.3
|
|
167
|
+
panTiles.setMatrixAt(
|
|
168
|
+
panIndex++,
|
|
169
|
+
new THREE.Matrix4().compose(
|
|
170
|
+
new THREE.Vector3(x, options.ridgeY - progress * slopeDrop + 0.065 + eaveLift, roofSide * progress * roofHalfDepth),
|
|
171
|
+
rotation,
|
|
172
|
+
new THREE.Vector3(1, 1, 1),
|
|
173
|
+
),
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
panTiles.instanceMatrix.needsUpdate = true
|
|
179
|
+
panTiles.userData.kawaraPanTileCount = panTiles.count
|
|
180
|
+
panTiles.userData.kawaraCourseCount = panCourses
|
|
181
|
+
panTiles.userData.mirroredRoofSides = true
|
|
182
|
+
mark(panTiles, `${rolePrefix}-kawara-pan-shell`)
|
|
183
|
+
roof.add(panTiles)
|
|
184
|
+
|
|
185
|
+
const ridgeParts: THREE.BufferGeometry[] = []
|
|
186
|
+
for (const [y, z, radius] of [[options.ridgeY + 0.04, 0, 0.14], [options.ridgeY - 0.05, 0.13, 0.09], [options.ridgeY - 0.05, -0.13, 0.09]] as const) {
|
|
187
|
+
ridgeParts.push(
|
|
188
|
+
transformed(
|
|
189
|
+
new VisualCylinderGeometry(radius, radius, 5.58, 16),
|
|
190
|
+
new THREE.Vector3(0, y, z),
|
|
191
|
+
new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), Math.PI / 2),
|
|
192
|
+
),
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
mergedRoofPart(roof, `${options.name}-kawara-ridge`, material, ridgeParts, `${rolePrefix}-kawara-ridge`)
|
|
196
|
+
backing.userData.roofWidthScale = options.widthScale ?? 1
|
|
197
|
+
backing.userData.roofHalfDepth = roofHalfDepth
|
|
198
|
+
mark(roof, `${rolePrefix}-kawara-roof-assembly`)
|
|
199
|
+
root.add(roof)
|
|
200
|
+
return roof
|
|
201
|
+
}
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import * as THREE from 'three'
|
|
2
|
+
|
|
3
|
+
import { VisualSphereGeometry, VisualCylinderGeometry, VisualTorusGeometry, VisualTubeGeometry, VisualBoxGeometry } from '../../../three/visual-geometry'
|
|
4
|
+
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
|
5
|
+
|
|
6
|
+
import { PORTAL_APERTURE_HALF_WIDTH } from './portal-proportions'
|
|
7
|
+
import { addJapaneseSakuraKawaraRoof } from './japanese-sakura-kawara-roof'
|
|
8
|
+
|
|
9
|
+
export const JAPANESE_SAKURA_PORTAL_FRAME_THEME_ID = 'japanese-sakura-lake'
|
|
10
|
+
|
|
11
|
+
export interface JapaneseSakuraPortalFrameMaterials {
|
|
12
|
+
vermilion: THREE.Material
|
|
13
|
+
plaster: THREE.Material
|
|
14
|
+
foundationStone: THREE.Material
|
|
15
|
+
kawara: THREE.Material
|
|
16
|
+
hardware: THREE.Material
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const LOCAL_UP = new THREE.Vector3(0, 1, 0)
|
|
20
|
+
|
|
21
|
+
function markVisualOnly(object: THREE.Object3D, role: string): void {
|
|
22
|
+
object.userData.themeOnly = JAPANESE_SAKURA_PORTAL_FRAME_THEME_ID
|
|
23
|
+
object.userData.themeRole = role
|
|
24
|
+
object.userData.decorative = true
|
|
25
|
+
object.userData.visualOnly = true
|
|
26
|
+
object.userData.portalFrameStyle = 'japanese-sakura-roofed-gate'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function metricPlanarUvs(geometry: THREE.BufferGeometry): THREE.BufferGeometry {
|
|
30
|
+
const prepared = geometry.index ? geometry.toNonIndexed() : geometry
|
|
31
|
+
if (prepared !== geometry) geometry.dispose()
|
|
32
|
+
const position = prepared.getAttribute('position')
|
|
33
|
+
const normal = prepared.getAttribute('normal')
|
|
34
|
+
const uv = new Float32Array(position.count * 2)
|
|
35
|
+
const themeNetUv = new Float32Array(position.count * 4)
|
|
36
|
+
for (let index = 0; index < position.count; index += 1) {
|
|
37
|
+
const x = position.getX(index)
|
|
38
|
+
const y = position.getY(index)
|
|
39
|
+
const z = position.getZ(index)
|
|
40
|
+
const nx = Math.abs(normal.getX(index))
|
|
41
|
+
const ny = Math.abs(normal.getY(index))
|
|
42
|
+
const nz = Math.abs(normal.getZ(index))
|
|
43
|
+
if (ny >= nx && ny >= nz) {
|
|
44
|
+
uv[index * 2] = x
|
|
45
|
+
uv[index * 2 + 1] = z
|
|
46
|
+
} else if (nx >= nz) {
|
|
47
|
+
uv[index * 2] = z
|
|
48
|
+
uv[index * 2 + 1] = y
|
|
49
|
+
} else {
|
|
50
|
+
uv[index * 2] = x
|
|
51
|
+
uv[index * 2 + 1] = y
|
|
52
|
+
}
|
|
53
|
+
themeNetUv[index * 4 + 2] = 1
|
|
54
|
+
}
|
|
55
|
+
prepared.setAttribute('uv', new THREE.BufferAttribute(uv, 2))
|
|
56
|
+
prepared.setAttribute('themeNetUv', new THREE.BufferAttribute(themeNetUv, 4))
|
|
57
|
+
return prepared
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function transformed(geometry: THREE.BufferGeometry, position: THREE.Vector3, quaternion = new THREE.Quaternion()) {
|
|
61
|
+
geometry.applyMatrix4(new THREE.Matrix4().compose(position, quaternion, new THREE.Vector3(1, 1, 1)))
|
|
62
|
+
return metricPlanarUvs(geometry)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function roundedBox(size: THREE.Vector3, position: THREE.Vector3, radius = 0.045): THREE.BufferGeometry {
|
|
66
|
+
return transformed(new VisualBoxGeometry(size.x, size.y, size.z, 2, radius), position)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function boxBeam(start: THREE.Vector3, end: THREE.Vector3, width: number, depth: number): THREE.BufferGeometry {
|
|
70
|
+
const direction = end.clone().sub(start)
|
|
71
|
+
const quaternion = new THREE.Quaternion().setFromUnitVectors(LOCAL_UP, direction.clone().normalize())
|
|
72
|
+
return transformed(
|
|
73
|
+
new VisualBoxGeometry(width, direction.length(), depth, 2, Math.min(width, depth) * 0.16),
|
|
74
|
+
start.clone().lerp(end, 0.5),
|
|
75
|
+
quaternion,
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function mergeBatch(
|
|
80
|
+
root: THREE.Group,
|
|
81
|
+
name: string,
|
|
82
|
+
material: THREE.Material,
|
|
83
|
+
parts: THREE.BufferGeometry[],
|
|
84
|
+
role: string,
|
|
85
|
+
): THREE.Mesh {
|
|
86
|
+
const geometry = mergeGeometries(parts, false)
|
|
87
|
+
parts.forEach(part => part.dispose())
|
|
88
|
+
if (!geometry) throw new Error(`Unable to merge Japanese Sakura portal batch "${name}".`)
|
|
89
|
+
geometry.computeBoundingBox()
|
|
90
|
+
geometry.computeBoundingSphere()
|
|
91
|
+
const mesh = new THREE.Mesh(geometry, material)
|
|
92
|
+
mesh.name = name
|
|
93
|
+
mesh.castShadow = true
|
|
94
|
+
mesh.receiveShadow = true
|
|
95
|
+
markVisualOnly(mesh, role)
|
|
96
|
+
root.add(mesh)
|
|
97
|
+
return mesh
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function addLantern(frame: THREE.Group, name: string, color: string, hardware: THREE.Material): void {
|
|
101
|
+
const lantern = new THREE.Group()
|
|
102
|
+
lantern.name = `${name}-sakura-pair-lantern`
|
|
103
|
+
lantern.position.set(0, 0.83, 0.58)
|
|
104
|
+
lantern.userData.portalPairColor = color
|
|
105
|
+
lantern.userData.portalLightRole = 'pair-color-paper-lantern'
|
|
106
|
+
markVisualOnly(lantern, 'sakura-pair-lantern')
|
|
107
|
+
|
|
108
|
+
const pairColor = new THREE.Color(color)
|
|
109
|
+
const paperMaterial = new THREE.MeshStandardMaterial({
|
|
110
|
+
color: '#fff1df',
|
|
111
|
+
emissive: pairColor,
|
|
112
|
+
emissiveIntensity: 0.88,
|
|
113
|
+
roughness: 0.72,
|
|
114
|
+
transparent: true,
|
|
115
|
+
opacity: 0.96,
|
|
116
|
+
})
|
|
117
|
+
const paper = new THREE.Mesh(new VisualSphereGeometry(0.33, 24, 16), paperMaterial)
|
|
118
|
+
paper.scale.y = 1.18
|
|
119
|
+
paper.name = `${name}-sakura-lantern-paper`
|
|
120
|
+
paper.userData.ownsMaterial = true
|
|
121
|
+
markVisualOnly(paper, 'sakura-pair-lantern-paper')
|
|
122
|
+
lantern.add(paper)
|
|
123
|
+
|
|
124
|
+
const rings = new THREE.InstancedMesh(new VisualTorusGeometry(0.35, 0.024, 7, 24), hardware, 5)
|
|
125
|
+
rings.name = `${name}-sakura-lantern-rings`
|
|
126
|
+
;[-0.3, -0.15, 0, 0.15, 0.3].forEach((y, index) => {
|
|
127
|
+
const radiusScale = Math.sqrt(Math.max(0.18, 1 - (y / 0.39) ** 2))
|
|
128
|
+
rings.setMatrixAt(
|
|
129
|
+
index,
|
|
130
|
+
new THREE.Matrix4().compose(
|
|
131
|
+
new THREE.Vector3(0, y, 0),
|
|
132
|
+
new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), Math.PI / 2),
|
|
133
|
+
new THREE.Vector3(radiusScale, radiusScale, radiusScale),
|
|
134
|
+
),
|
|
135
|
+
)
|
|
136
|
+
})
|
|
137
|
+
rings.instanceMatrix.needsUpdate = true
|
|
138
|
+
markVisualOnly(rings, 'sakura-lantern-hardware')
|
|
139
|
+
lantern.add(rings)
|
|
140
|
+
|
|
141
|
+
const cageParts: THREE.BufferGeometry[] = []
|
|
142
|
+
for (let longitude = 0; longitude < 6; longitude += 1) {
|
|
143
|
+
const angle = (longitude / 6) * Math.PI * 2
|
|
144
|
+
const points = Array.from({ length: 9 }, (_, index) => {
|
|
145
|
+
const latitude = -Math.PI / 2 + (index / 8) * Math.PI
|
|
146
|
+
return new THREE.Vector3(
|
|
147
|
+
Math.cos(angle) * Math.cos(latitude) * 0.335,
|
|
148
|
+
Math.sin(latitude) * 0.39,
|
|
149
|
+
Math.sin(angle) * Math.cos(latitude) * 0.335,
|
|
150
|
+
)
|
|
151
|
+
})
|
|
152
|
+
cageParts.push(new VisualTubeGeometry(new THREE.CatmullRomCurve3(points), 24, 0.018, 5, false))
|
|
153
|
+
}
|
|
154
|
+
mergeBatch(lantern, `${name}-sakura-lantern-cage`, hardware, cageParts, 'sakura-lantern-hardware')
|
|
155
|
+
|
|
156
|
+
const top = new THREE.Mesh(new VisualCylinderGeometry(0.25, 0.34, 0.14, 18), hardware)
|
|
157
|
+
top.position.y = 0.45
|
|
158
|
+
const bottom = new THREE.Mesh(new VisualCylinderGeometry(0.34, 0.24, 0.13, 18), hardware)
|
|
159
|
+
bottom.position.y = -0.45
|
|
160
|
+
const chain = new THREE.Mesh(new VisualCylinderGeometry(0.014, 0.014, 0.56, 8), hardware)
|
|
161
|
+
chain.position.y = 0.79
|
|
162
|
+
for (const object of [top, bottom, chain]) {
|
|
163
|
+
markVisualOnly(object, 'sakura-lantern-hardware')
|
|
164
|
+
lantern.add(object)
|
|
165
|
+
}
|
|
166
|
+
const light = new THREE.PointLight(color, 0.58, 3.4, 2)
|
|
167
|
+
light.position.z = 0.15
|
|
168
|
+
light.userData.decorative = true
|
|
169
|
+
light.userData.visualOnly = true
|
|
170
|
+
lantern.add(light)
|
|
171
|
+
frame.add(lantern)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function addBlossoms(frame: THREE.Group, name: string): void {
|
|
175
|
+
const branchMaterial = new THREE.MeshStandardMaterial({ color: '#4b2d25', roughness: 0.92 })
|
|
176
|
+
const blossomMaterial = new THREE.MeshStandardMaterial({ color: '#f4a9ca', roughness: 0.78 })
|
|
177
|
+
const branches: THREE.BufferGeometry[] = []
|
|
178
|
+
const blossomPoints: THREE.Vector3[] = []
|
|
179
|
+
for (const side of [-1, 1] as const) {
|
|
180
|
+
const start = new THREE.Vector3(side * 2.08, 2.65, 0.24)
|
|
181
|
+
const end = new THREE.Vector3(side * 3.16, 3.18, 0.04)
|
|
182
|
+
const direction = end.clone().sub(start)
|
|
183
|
+
const quaternion = new THREE.Quaternion().setFromUnitVectors(LOCAL_UP, direction.clone().normalize())
|
|
184
|
+
branches.push(
|
|
185
|
+
transformed(
|
|
186
|
+
new VisualCylinderGeometry(0.024, 0.038, direction.length(), 7),
|
|
187
|
+
start.clone().lerp(end, 0.5),
|
|
188
|
+
quaternion,
|
|
189
|
+
),
|
|
190
|
+
)
|
|
191
|
+
const fork = start.clone().lerp(end, 0.54)
|
|
192
|
+
const forkEnd = new THREE.Vector3(side * 3.3, 2.82, 0.12)
|
|
193
|
+
const forkDirection = forkEnd.clone().sub(fork)
|
|
194
|
+
branches.push(
|
|
195
|
+
transformed(
|
|
196
|
+
new VisualCylinderGeometry(0.014, 0.024, forkDirection.length(), 7),
|
|
197
|
+
fork.clone().lerp(forkEnd, 0.5),
|
|
198
|
+
new THREE.Quaternion().setFromUnitVectors(LOCAL_UP, forkDirection.clone().normalize()),
|
|
199
|
+
),
|
|
200
|
+
)
|
|
201
|
+
for (let index = 0; index < 10; index += 1) {
|
|
202
|
+
const point = start.clone().lerp(end, index / 9)
|
|
203
|
+
point.y += Math.sin(index * 1.7) * 0.1
|
|
204
|
+
point.z += 0.12
|
|
205
|
+
blossomPoints.push(point)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
mergeBatch(
|
|
209
|
+
frame,
|
|
210
|
+
`${name}-sakura-branches`,
|
|
211
|
+
branchMaterial,
|
|
212
|
+
branches,
|
|
213
|
+
'sakura-blossom-branches',
|
|
214
|
+
).userData.ownsMaterial = true
|
|
215
|
+
const blossoms = new THREE.InstancedMesh(
|
|
216
|
+
new VisualSphereGeometry(0.045, 8, 6),
|
|
217
|
+
blossomMaterial,
|
|
218
|
+
blossomPoints.length * 5,
|
|
219
|
+
)
|
|
220
|
+
blossoms.name = `${name}-sakura-blossoms`
|
|
221
|
+
let blossomIndex = 0
|
|
222
|
+
blossomPoints.forEach((point, flowerIndex) => {
|
|
223
|
+
for (let petal = 0; petal < 5; petal += 1) {
|
|
224
|
+
const angle = (petal / 5) * Math.PI * 2
|
|
225
|
+
const center = point.clone().add(new THREE.Vector3(Math.cos(angle) * 0.052, Math.sin(angle) * 0.052, 0))
|
|
226
|
+
const scale = 0.78 + (flowerIndex % 4) * 0.07
|
|
227
|
+
blossoms.setMatrixAt(
|
|
228
|
+
blossomIndex++,
|
|
229
|
+
new THREE.Matrix4().compose(
|
|
230
|
+
center,
|
|
231
|
+
new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), angle),
|
|
232
|
+
new THREE.Vector3(scale * 1.5, scale * 0.68, scale * 0.48),
|
|
233
|
+
),
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
})
|
|
237
|
+
blossoms.instanceMatrix.needsUpdate = true
|
|
238
|
+
blossoms.userData.ownsMaterial = true
|
|
239
|
+
markVisualOnly(blossoms, 'sakura-blossoms')
|
|
240
|
+
frame.add(blossoms)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Adds only the theme-local frame; the shared aperture, camera rift, glow and particles stay global. */
|
|
244
|
+
export function addJapaneseSakuraPortalFrame(
|
|
245
|
+
root: THREE.Group,
|
|
246
|
+
name: string,
|
|
247
|
+
color: string,
|
|
248
|
+
materials: JapaneseSakuraPortalFrameMaterials,
|
|
249
|
+
): THREE.Group {
|
|
250
|
+
const frame = new THREE.Group()
|
|
251
|
+
frame.name = `${name}-japanese-sakura-frame`
|
|
252
|
+
// This frame is authored directly against the four-unit portal deck. Unlike
|
|
253
|
+
// the wider source geometry used by Jungle and Egypt it must not receive the
|
|
254
|
+
// shared 0.7 compensation a second time.
|
|
255
|
+
frame.scale.x = 1
|
|
256
|
+
frame.userData.portalApertureClearHalfWidth = PORTAL_APERTURE_HALF_WIDTH
|
|
257
|
+
frame.userData.drawCallBudget = 14
|
|
258
|
+
frame.userData.triangleBudget = 28_000
|
|
259
|
+
markVisualOnly(frame, 'japanese-sakura-roofed-gate')
|
|
260
|
+
|
|
261
|
+
const vermilion: THREE.BufferGeometry[] = []
|
|
262
|
+
const masonry: THREE.BufferGeometry[] = []
|
|
263
|
+
for (const side of [-1, 1] as const) {
|
|
264
|
+
const x = side * 1.68
|
|
265
|
+
vermilion.push(
|
|
266
|
+
roundedBox(new THREE.Vector3(0.52, 3.34, 0.62), new THREE.Vector3(x, 0.08, 0.02), 0.055),
|
|
267
|
+
roundedBox(new THREE.Vector3(0.7, 0.18, 0.7), new THREE.Vector3(x, 1.42, 0.02), 0.04),
|
|
268
|
+
roundedBox(new THREE.Vector3(0.96, 0.2, 0.7), new THREE.Vector3(x, 1.6, 0.02), 0.04),
|
|
269
|
+
boxBeam(new THREE.Vector3(side * 1.42, 0.82, 0.12), new THREE.Vector3(side * 0.98, 1.34, 0.12), 0.2, 0.34),
|
|
270
|
+
)
|
|
271
|
+
masonry.push(
|
|
272
|
+
roundedBox(new THREE.Vector3(0.62, 0.28, 0.86), new THREE.Vector3(x, -1.59, 0), 0.045),
|
|
273
|
+
roundedBox(new THREE.Vector3(0.56, 0.17, 0.74), new THREE.Vector3(x, -1.4, 0), 0.035),
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
vermilion.push(
|
|
277
|
+
roundedBox(new THREE.Vector3(4.92, 0.34, 0.68), new THREE.Vector3(0, 1.72, 0), 0.055),
|
|
278
|
+
roundedBox(new THREE.Vector3(3.78, 0.22, 0.54), new THREE.Vector3(0, 1.39, 0.03), 0.045),
|
|
279
|
+
)
|
|
280
|
+
for (let index = -5; index <= 5; index += 1) {
|
|
281
|
+
vermilion.push(roundedBox(new THREE.Vector3(0.18, 0.22, 0.82), new THREE.Vector3(index * 0.4, 1.94, 0), 0.025))
|
|
282
|
+
}
|
|
283
|
+
mergeBatch(frame, `${name}-sakura-vermilion-frame`, materials.vermilion, vermilion, 'sakura-vermilion-frame')
|
|
284
|
+
mergeBatch(
|
|
285
|
+
frame,
|
|
286
|
+
`${name}-sakura-foundation-stone-feet`,
|
|
287
|
+
materials.foundationStone,
|
|
288
|
+
masonry,
|
|
289
|
+
'sakura-foundation-stone-feet',
|
|
290
|
+
)
|
|
291
|
+
addJapaneseSakuraKawaraRoof(frame, materials.kawara, {
|
|
292
|
+
name: `${name}-sakura`,
|
|
293
|
+
ridgeY: 2.8,
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
const footStraps = [-1, 1].flatMap(side => [
|
|
297
|
+
roundedBox(new THREE.Vector3(0.14, 0.34, 0.05), new THREE.Vector3(side * 1.78, -1.06, 0.335), 0.018),
|
|
298
|
+
roundedBox(new THREE.Vector3(0.14, 0.34, 0.05), new THREE.Vector3(side * 1.58, -1.06, 0.335), 0.018),
|
|
299
|
+
])
|
|
300
|
+
mergeBatch(frame, `${name}-sakura-foot-hardware`, materials.hardware, footStraps, 'sakura-frame-hardware')
|
|
301
|
+
|
|
302
|
+
const bolts = new THREE.InstancedMesh(new VisualCylinderGeometry(0.045, 0.045, 0.08, 12), materials.hardware, 4)
|
|
303
|
+
bolts.name = `${name}-sakura-frame-bolts`
|
|
304
|
+
let boltIndex = 0
|
|
305
|
+
for (const x of [-1.68, 1.68]) {
|
|
306
|
+
for (const y of [-1.16, -0.96]) {
|
|
307
|
+
bolts.setMatrixAt(
|
|
308
|
+
boltIndex++,
|
|
309
|
+
new THREE.Matrix4().compose(
|
|
310
|
+
new THREE.Vector3(x, y, 0.33),
|
|
311
|
+
new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), Math.PI / 2),
|
|
312
|
+
new THREE.Vector3(1, 1, 1),
|
|
313
|
+
),
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
bolts.instanceMatrix.needsUpdate = true
|
|
318
|
+
markVisualOnly(bolts, 'sakura-frame-hardware')
|
|
319
|
+
frame.add(bolts)
|
|
320
|
+
|
|
321
|
+
addLantern(frame, name, color, materials.hardware)
|
|
322
|
+
addBlossoms(frame, name)
|
|
323
|
+
root.add(frame)
|
|
324
|
+
return frame
|
|
325
|
+
}
|