@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,545 @@
|
|
|
1
|
+
import * as THREE from 'three'
|
|
2
|
+
|
|
3
|
+
import { TRACK_RAIL_WALL_THICKNESS } from './track-definition'
|
|
4
|
+
import { canonicalWallFrameAt, type CanonicalWallRun } from './track-surface-topology'
|
|
5
|
+
|
|
6
|
+
export const VAMPIRE_CARVED_WALL_MAXIMUM_BAY_RATIO = 0.86
|
|
7
|
+
export const VAMPIRE_CARVED_WALL_RECESS_DEPTH_RATIO = 0.02
|
|
8
|
+
export const VAMPIRE_CARVED_WALL_RECESS_TO_THICKNESS = 0.125
|
|
9
|
+
export const VAMPIRE_CARVED_WALL_PROJECTION_RATIO = 0.026
|
|
10
|
+
export const VAMPIRE_CARVED_WALL_PROJECTION_TO_THICKNESS = 0.18
|
|
11
|
+
|
|
12
|
+
const MINIMUM_WALL_HEIGHT = 0.36
|
|
13
|
+
const WALL_BASE_SINK = 0.03
|
|
14
|
+
|
|
15
|
+
interface GeometryAccumulator {
|
|
16
|
+
positions: number[]
|
|
17
|
+
uvs: number[]
|
|
18
|
+
metricUvs: number[]
|
|
19
|
+
indices: number[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface SurfacePoint {
|
|
23
|
+
distance: number
|
|
24
|
+
height: number
|
|
25
|
+
across: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Builds the complete visible wall as one solid, path-following batch. The
|
|
30
|
+
* panel field replaces the outer face and steps inward through its bevel; no
|
|
31
|
+
* backing facade or applied trim is part of this visual construction.
|
|
32
|
+
* Rapier continues to use the unchanged canonical wall collider.
|
|
33
|
+
*/
|
|
34
|
+
export function addVampireCarvedWallVisual(
|
|
35
|
+
host: THREE.Group,
|
|
36
|
+
id: string,
|
|
37
|
+
runs: readonly CanonicalWallRun[],
|
|
38
|
+
wallHeight: number,
|
|
39
|
+
material: THREE.Material,
|
|
40
|
+
): THREE.Group | undefined {
|
|
41
|
+
if (wallHeight < MINIMUM_WALL_HEIGHT || runs.length === 0) return undefined
|
|
42
|
+
|
|
43
|
+
const group = new THREE.Group()
|
|
44
|
+
group.name = `${id}-vampire-carved-wall`
|
|
45
|
+
group.userData.themeOnly = 'vampire-castle'
|
|
46
|
+
group.userData.visualOnly = true
|
|
47
|
+
group.userData.decorative = true
|
|
48
|
+
group.userData.appearance = 'vampire-castle-solid-carved-oak-wall-v2'
|
|
49
|
+
group.userData.representation = 'single-batched-solid-recessed-panel-wall'
|
|
50
|
+
group.userData.frontSideOnly = true
|
|
51
|
+
group.userData.drawCallBudget = 1
|
|
52
|
+
group.userData.runMetricLengths = runs.map(run => run.length)
|
|
53
|
+
group.userData.runTerminalOwnership = runs.map(run => ({
|
|
54
|
+
start: run.startOwnership,
|
|
55
|
+
end: run.endOwnership,
|
|
56
|
+
startPosition: run.frames[0]!.base.toArray(),
|
|
57
|
+
endPosition: run.frames.at(-1)!.base.toArray(),
|
|
58
|
+
}))
|
|
59
|
+
group.userData.wallHeight = wallHeight
|
|
60
|
+
group.userData.wallThickness = TRACK_RAIL_WALL_THICKNESS
|
|
61
|
+
|
|
62
|
+
const built = createSolidCarvedWallGeometry(runs, wallHeight)
|
|
63
|
+
const mesh = new THREE.Mesh(built.geometry, material)
|
|
64
|
+
mesh.name = `${id}-vampire-carved-wall-solid-batch`
|
|
65
|
+
mesh.castShadow = true
|
|
66
|
+
mesh.receiveShadow = true
|
|
67
|
+
mesh.userData.visualOnly = true
|
|
68
|
+
mesh.userData.decorative = true
|
|
69
|
+
mesh.userData.drawCallBudget = 1
|
|
70
|
+
mesh.userData.editorBoundsSource = 'canonical-wall-envelope'
|
|
71
|
+
group.userData.panelCount = built.panelCount
|
|
72
|
+
group.userData.recessDepth = built.recessDepth
|
|
73
|
+
group.userData.recessToThickness = built.recessDepth / TRACK_RAIL_WALL_THICKNESS
|
|
74
|
+
group.userData.maximumProjection = built.maximumProjection
|
|
75
|
+
group.userData.panelConstruction = 'outer-face-holes-plus-double-bevel-and-inset-fields'
|
|
76
|
+
group.userData.faceRule = 'both-broad-faces'
|
|
77
|
+
group.userData.terminalRule = 'canonical-connection-ownership'
|
|
78
|
+
group.userData.connectedTerminalCaps = 'open-neighbor-owned'
|
|
79
|
+
group.add(mesh)
|
|
80
|
+
host.add(group)
|
|
81
|
+
return group
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function createSolidCarvedWallGeometry(
|
|
85
|
+
runs: readonly CanonicalWallRun[],
|
|
86
|
+
wallHeight: number,
|
|
87
|
+
): {
|
|
88
|
+
geometry: THREE.BufferGeometry
|
|
89
|
+
panelCount: number
|
|
90
|
+
recessDepth: number
|
|
91
|
+
maximumProjection: number
|
|
92
|
+
} {
|
|
93
|
+
const output: GeometryAccumulator = { positions: [], uvs: [], metricUvs: [], indices: [] }
|
|
94
|
+
const maximumBaySpacing = wallHeight * VAMPIRE_CARVED_WALL_MAXIMUM_BAY_RATIO
|
|
95
|
+
const recessDepth = Math.min(
|
|
96
|
+
wallHeight * VAMPIRE_CARVED_WALL_RECESS_DEPTH_RATIO,
|
|
97
|
+
TRACK_RAIL_WALL_THICKNESS * VAMPIRE_CARVED_WALL_RECESS_TO_THICKNESS,
|
|
98
|
+
)
|
|
99
|
+
const maximumProjection = Math.min(
|
|
100
|
+
wallHeight * VAMPIRE_CARVED_WALL_PROJECTION_RATIO,
|
|
101
|
+
TRACK_RAIL_WALL_THICKNESS * VAMPIRE_CARVED_WALL_PROJECTION_TO_THICKNESS,
|
|
102
|
+
)
|
|
103
|
+
let panelCount = 0
|
|
104
|
+
|
|
105
|
+
for (const run of runs) {
|
|
106
|
+
for (const [start, end] of carvedBayIntervals(run, maximumBaySpacing)) {
|
|
107
|
+
for (const face of [-1, 1] as const) {
|
|
108
|
+
panelCount += appendCarvedBayFace(
|
|
109
|
+
output,
|
|
110
|
+
run,
|
|
111
|
+
face,
|
|
112
|
+
start,
|
|
113
|
+
end,
|
|
114
|
+
wallHeight,
|
|
115
|
+
recessDepth,
|
|
116
|
+
maximumProjection,
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
appendRunTopAndBottom(output, run, wallHeight, maximumProjection)
|
|
121
|
+
if (run.capStart) appendTerminalCap(output, run, 0, wallHeight, -1, maximumProjection)
|
|
122
|
+
if (run.capEnd) appendTerminalCap(output, run, run.length, wallHeight, 1, maximumProjection)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const geometry = new THREE.BufferGeometry()
|
|
126
|
+
geometry.name = 'vampire-castle-solid-carved-oak-wall-v2-geometry'
|
|
127
|
+
geometry.setAttribute('position', new THREE.Float32BufferAttribute(output.positions, 3))
|
|
128
|
+
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(output.uvs, 2))
|
|
129
|
+
geometry.setAttribute('uv1', new THREE.Float32BufferAttribute(output.uvs, 2))
|
|
130
|
+
geometry.setAttribute('themeNetUv', new THREE.Float32BufferAttribute(output.metricUvs, 4))
|
|
131
|
+
geometry.setIndex(output.indices)
|
|
132
|
+
geometry.computeVertexNormals()
|
|
133
|
+
geometry.computeBoundingBox()
|
|
134
|
+
geometry.computeBoundingSphere()
|
|
135
|
+
geometry.userData.wallThickness = TRACK_RAIL_WALL_THICKNESS
|
|
136
|
+
geometry.userData.wallTopologySource = 'canonical-wall-run'
|
|
137
|
+
geometry.userData.themeMaterialLongitudinalAxis = 'x'
|
|
138
|
+
geometry.userData.solidWall = true
|
|
139
|
+
geometry.userData.openings = 0
|
|
140
|
+
geometry.userData.recessDepth = recessDepth
|
|
141
|
+
geometry.userData.recessedFaces = 2
|
|
142
|
+
geometry.userData.panelFieldsPerBay = 4
|
|
143
|
+
geometry.userData.bevelSteps = 2
|
|
144
|
+
geometry.userData.maximumProjection = maximumProjection
|
|
145
|
+
geometry.userData.hybridRelief = 'raised-architecture-plus-recessed-panel-fields'
|
|
146
|
+
return { geometry, panelCount, recessDepth, maximumProjection }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function carvedBayIntervals(run: CanonicalWallRun, maximumBaySpacing: number): [number, number][] {
|
|
150
|
+
const sectionBoundaries = [0]
|
|
151
|
+
for (let index = 1; index < run.frames.length - 1; index += 1) {
|
|
152
|
+
const previous = run.frames[index - 1]!.base
|
|
153
|
+
const current = run.frames[index]!.base
|
|
154
|
+
const next = run.frames[index + 1]!.base
|
|
155
|
+
const incoming = current.clone().sub(previous)
|
|
156
|
+
const outgoing = next.clone().sub(current)
|
|
157
|
+
incoming.y = 0
|
|
158
|
+
outgoing.y = 0
|
|
159
|
+
if (incoming.lengthSq() <= 0.000_001 || outgoing.lengthSq() <= 0.000_001) continue
|
|
160
|
+
if (incoming.angleTo(outgoing) >= THREE.MathUtils.degToRad(18)) {
|
|
161
|
+
sectionBoundaries.push(run.frames[index]!.distance)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
sectionBoundaries.push(run.length)
|
|
165
|
+
|
|
166
|
+
const intervals: [number, number][] = []
|
|
167
|
+
for (let section = 0; section < sectionBoundaries.length - 1; section += 1) {
|
|
168
|
+
const start = sectionBoundaries[section]!
|
|
169
|
+
const end = sectionBoundaries[section + 1]!
|
|
170
|
+
const length = end - start
|
|
171
|
+
if (length <= 0.000_001) continue
|
|
172
|
+
const intervalCount = Math.max(1, Math.ceil(length / maximumBaySpacing))
|
|
173
|
+
for (let bay = 0; bay < intervalCount; bay += 1) {
|
|
174
|
+
intervals.push([
|
|
175
|
+
THREE.MathUtils.lerp(start, end, bay / intervalCount),
|
|
176
|
+
THREE.MathUtils.lerp(start, end, (bay + 1) / intervalCount),
|
|
177
|
+
])
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return intervals
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function appendCarvedBayFace(
|
|
184
|
+
output: GeometryAccumulator,
|
|
185
|
+
run: CanonicalWallRun,
|
|
186
|
+
face: -1 | 1,
|
|
187
|
+
start: number,
|
|
188
|
+
end: number,
|
|
189
|
+
wallHeight: number,
|
|
190
|
+
recessDepth: number,
|
|
191
|
+
maximumProjection: number,
|
|
192
|
+
): number {
|
|
193
|
+
const bayWidth = end - start
|
|
194
|
+
const sideInset = Math.min(wallHeight * 0.075, bayWidth * 0.22)
|
|
195
|
+
const left = start + sideInset
|
|
196
|
+
const right = end - sideInset
|
|
197
|
+
const upperGap = Math.min(wallHeight * 0.014, bayWidth * 0.032)
|
|
198
|
+
const upperWidth = (right - left - upperGap * 2) / 3
|
|
199
|
+
const panels: THREE.Vector2[][] = [
|
|
200
|
+
gothicLancetContour(left, right, wallHeight * 0.12, wallHeight * 0.46, wallHeight * 0.625, 7),
|
|
201
|
+
...Array.from({ length: 3 }, (_unused, index) => {
|
|
202
|
+
const panelLeft = left + index * (upperWidth + upperGap)
|
|
203
|
+
return gothicLancetContour(
|
|
204
|
+
panelLeft,
|
|
205
|
+
panelLeft + upperWidth,
|
|
206
|
+
wallHeight * 0.66,
|
|
207
|
+
wallHeight * 0.79,
|
|
208
|
+
wallHeight * 0.91,
|
|
209
|
+
5,
|
|
210
|
+
)
|
|
211
|
+
}),
|
|
212
|
+
].map(clockwiseContour)
|
|
213
|
+
const bevelWidth = Math.min(wallHeight * 0.024, bayWidth * 0.055)
|
|
214
|
+
const halfThickness = TRACK_RAIL_WALL_THICKNESS * 0.5
|
|
215
|
+
const longitudinalSegments = 26
|
|
216
|
+
const verticalSegments = 32
|
|
217
|
+
const vertexOffset = output.positions.length / 3
|
|
218
|
+
for (let vertical = 0; vertical <= verticalSegments; vertical += 1) {
|
|
219
|
+
const height = (vertical / verticalSegments) * wallHeight
|
|
220
|
+
for (let longitudinal = 0; longitudinal <= longitudinalSegments; longitudinal += 1) {
|
|
221
|
+
const distance = THREE.MathUtils.lerp(start, end, longitudinal / longitudinalSegments)
|
|
222
|
+
const depth = architecturalDepthAt(
|
|
223
|
+
new THREE.Vector2(distance, height),
|
|
224
|
+
panels,
|
|
225
|
+
start,
|
|
226
|
+
end,
|
|
227
|
+
wallHeight,
|
|
228
|
+
bevelWidth,
|
|
229
|
+
recessDepth,
|
|
230
|
+
maximumProjection,
|
|
231
|
+
)
|
|
232
|
+
appendGridVertex(output, run, {
|
|
233
|
+
distance,
|
|
234
|
+
height,
|
|
235
|
+
across: face * (halfThickness - depth),
|
|
236
|
+
})
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const stride = longitudinalSegments + 1
|
|
240
|
+
for (let vertical = 0; vertical < verticalSegments; vertical += 1) {
|
|
241
|
+
for (let longitudinal = 0; longitudinal < longitudinalSegments; longitudinal += 1) {
|
|
242
|
+
const a = vertexOffset + vertical * stride + longitudinal
|
|
243
|
+
const b = a + 1
|
|
244
|
+
const c = a + stride
|
|
245
|
+
const d = c + 1
|
|
246
|
+
const distance = THREE.MathUtils.lerp(start, end, (longitudinal + 0.5) / longitudinalSegments)
|
|
247
|
+
const expected = canonicalWallFrameAt(run, distance).outward.clone().multiplyScalar(face)
|
|
248
|
+
appendFacingIndex(output, a, b, c, expected)
|
|
249
|
+
appendFacingIndex(output, b, d, c, expected)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return panels.length
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function appendGridVertex(output: GeometryAccumulator, run: CanonicalWallRun, point: SurfacePoint): void {
|
|
256
|
+
const world = mapSurfacePoint(run, point)
|
|
257
|
+
output.positions.push(world.x, world.y, world.z)
|
|
258
|
+
output.uvs.push(point.distance, point.height)
|
|
259
|
+
output.metricUvs.push(point.height, point.distance, 1, run.length)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function appendFacingIndex(
|
|
263
|
+
output: GeometryAccumulator,
|
|
264
|
+
a: number,
|
|
265
|
+
b: number,
|
|
266
|
+
c: number,
|
|
267
|
+
expectedNormal: THREE.Vector3,
|
|
268
|
+
): void {
|
|
269
|
+
const pointAt = (index: number) =>
|
|
270
|
+
new THREE.Vector3(
|
|
271
|
+
output.positions[index * 3]!,
|
|
272
|
+
output.positions[index * 3 + 1]!,
|
|
273
|
+
output.positions[index * 3 + 2]!,
|
|
274
|
+
)
|
|
275
|
+
const origin = pointAt(a)
|
|
276
|
+
const normal = pointAt(b).sub(origin).cross(pointAt(c).sub(origin))
|
|
277
|
+
if (normal.dot(expectedNormal) >= 0) output.indices.push(a, b, c)
|
|
278
|
+
else output.indices.push(a, c, b)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function carvedDepthAt(
|
|
282
|
+
point: THREE.Vector2,
|
|
283
|
+
panels: readonly THREE.Vector2[][],
|
|
284
|
+
bevelWidth: number,
|
|
285
|
+
recessDepth: number,
|
|
286
|
+
): number {
|
|
287
|
+
for (const panel of panels) {
|
|
288
|
+
if (!pointInsideContour(point, panel)) continue
|
|
289
|
+
const edgeDistance = distanceToContour(point, panel)
|
|
290
|
+
const firstStep = bevelWidth
|
|
291
|
+
const secondStep = bevelWidth * 2.15
|
|
292
|
+
if (edgeDistance <= firstStep) {
|
|
293
|
+
return recessDepth * 0.44 * THREE.MathUtils.smoothstep(edgeDistance, 0, firstStep)
|
|
294
|
+
}
|
|
295
|
+
if (edgeDistance <= secondStep) {
|
|
296
|
+
return THREE.MathUtils.lerp(
|
|
297
|
+
recessDepth * 0.44,
|
|
298
|
+
recessDepth,
|
|
299
|
+
THREE.MathUtils.smoothstep(edgeDistance, firstStep, secondStep),
|
|
300
|
+
)
|
|
301
|
+
}
|
|
302
|
+
return recessDepth
|
|
303
|
+
}
|
|
304
|
+
return 0
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function architecturalDepthAt(
|
|
308
|
+
point: THREE.Vector2,
|
|
309
|
+
panels: readonly THREE.Vector2[][],
|
|
310
|
+
start: number,
|
|
311
|
+
end: number,
|
|
312
|
+
wallHeight: number,
|
|
313
|
+
bevelWidth: number,
|
|
314
|
+
recessDepth: number,
|
|
315
|
+
maximumProjection: number,
|
|
316
|
+
): number {
|
|
317
|
+
const bayWidth = end - start
|
|
318
|
+
const pilasterWidth = Math.min(wallHeight * 0.04, bayWidth * 0.075)
|
|
319
|
+
const edgeDistance = Math.min(Math.abs(point.x - start), Math.abs(end - point.x))
|
|
320
|
+
let projection =
|
|
321
|
+
edgeDistance <= pilasterWidth
|
|
322
|
+
? maximumProjection * (0.62 + 0.38 * (1 - THREE.MathUtils.smoothstep(edgeDistance, 0, pilasterWidth)))
|
|
323
|
+
: 0
|
|
324
|
+
|
|
325
|
+
const crownStart = wallHeight * 0.91
|
|
326
|
+
const plinthEnd = wallHeight * 0.105
|
|
327
|
+
if (point.y >= crownStart) {
|
|
328
|
+
projection = Math.max(
|
|
329
|
+
projection,
|
|
330
|
+
maximumProjection * (0.72 + 0.28 * THREE.MathUtils.smoothstep(point.y, crownStart, wallHeight)),
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
if (point.y <= plinthEnd) {
|
|
334
|
+
projection = Math.max(
|
|
335
|
+
projection,
|
|
336
|
+
maximumProjection * (0.72 + 0.28 * (1 - THREE.MathUtils.smoothstep(point.y, 0, plinthEnd))),
|
|
337
|
+
)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const moldingWidth = bevelWidth * 1.45
|
|
341
|
+
for (const panel of panels) {
|
|
342
|
+
const contourDistance = distanceToContour(point, panel)
|
|
343
|
+
if (contourDistance > moldingWidth) continue
|
|
344
|
+
const moldingProjection =
|
|
345
|
+
maximumProjection * 0.82 * (1 - THREE.MathUtils.smoothstep(contourDistance, 0, moldingWidth))
|
|
346
|
+
projection = Math.max(projection, moldingProjection)
|
|
347
|
+
}
|
|
348
|
+
return projection > 0.000_001 ? -projection : carvedDepthAt(point, panels, bevelWidth, recessDepth)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function distanceToContour(point: THREE.Vector2, contour: readonly THREE.Vector2[]): number {
|
|
352
|
+
let closestDistanceSquared = Number.POSITIVE_INFINITY
|
|
353
|
+
for (let index = 0; index < contour.length; index += 1) {
|
|
354
|
+
const start = contour[index]!
|
|
355
|
+
const end = contour[(index + 1) % contour.length]!
|
|
356
|
+
const dx = end.x - start.x
|
|
357
|
+
const dy = end.y - start.y
|
|
358
|
+
const lengthSquared = dx * dx + dy * dy
|
|
359
|
+
const progress =
|
|
360
|
+
lengthSquared <= 0.000_001
|
|
361
|
+
? 0
|
|
362
|
+
: THREE.MathUtils.clamp(((point.x - start.x) * dx + (point.y - start.y) * dy) / lengthSquared, 0, 1)
|
|
363
|
+
const offsetX = point.x - (start.x + dx * progress)
|
|
364
|
+
const offsetY = point.y - (start.y + dy * progress)
|
|
365
|
+
closestDistanceSquared = Math.min(closestDistanceSquared, offsetX * offsetX + offsetY * offsetY)
|
|
366
|
+
}
|
|
367
|
+
return Math.sqrt(closestDistanceSquared)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function pointInsideContour(point: THREE.Vector2, contour: readonly THREE.Vector2[]): boolean {
|
|
371
|
+
let inside = false
|
|
372
|
+
for (let index = 0, previous = contour.length - 1; index < contour.length; previous = index, index += 1) {
|
|
373
|
+
const currentPoint = contour[index]!
|
|
374
|
+
const previousPoint = contour[previous]!
|
|
375
|
+
const crosses =
|
|
376
|
+
currentPoint.y > point.y !== previousPoint.y > point.y &&
|
|
377
|
+
point.x <
|
|
378
|
+
((previousPoint.x - currentPoint.x) * (point.y - currentPoint.y)) /
|
|
379
|
+
(previousPoint.y - currentPoint.y) +
|
|
380
|
+
currentPoint.x
|
|
381
|
+
if (crosses) inside = !inside
|
|
382
|
+
}
|
|
383
|
+
return inside
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function gothicLancetContour(
|
|
387
|
+
left: number,
|
|
388
|
+
right: number,
|
|
389
|
+
bottom: number,
|
|
390
|
+
shoulder: number,
|
|
391
|
+
apex: number,
|
|
392
|
+
curveSegments: number,
|
|
393
|
+
): THREE.Vector2[] {
|
|
394
|
+
const center = (left + right) * 0.5
|
|
395
|
+
const width = right - left
|
|
396
|
+
const rise = apex - shoulder
|
|
397
|
+
const points = [new THREE.Vector2(left, bottom), new THREE.Vector2(left, shoulder)]
|
|
398
|
+
appendCubicArc(
|
|
399
|
+
points,
|
|
400
|
+
new THREE.Vector2(left, shoulder),
|
|
401
|
+
new THREE.Vector2(left + width * 0.05, shoulder + rise * 0.62),
|
|
402
|
+
new THREE.Vector2(center - width * 0.13, apex - rise * 0.16),
|
|
403
|
+
new THREE.Vector2(center, apex),
|
|
404
|
+
curveSegments,
|
|
405
|
+
)
|
|
406
|
+
appendCubicArc(
|
|
407
|
+
points,
|
|
408
|
+
new THREE.Vector2(center, apex),
|
|
409
|
+
new THREE.Vector2(center + width * 0.13, apex - rise * 0.16),
|
|
410
|
+
new THREE.Vector2(right - width * 0.05, shoulder + rise * 0.62),
|
|
411
|
+
new THREE.Vector2(right, shoulder),
|
|
412
|
+
curveSegments,
|
|
413
|
+
)
|
|
414
|
+
points.push(new THREE.Vector2(right, bottom))
|
|
415
|
+
return points
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function appendCubicArc(
|
|
419
|
+
target: THREE.Vector2[],
|
|
420
|
+
start: THREE.Vector2,
|
|
421
|
+
controlA: THREE.Vector2,
|
|
422
|
+
controlB: THREE.Vector2,
|
|
423
|
+
end: THREE.Vector2,
|
|
424
|
+
segments: number,
|
|
425
|
+
): void {
|
|
426
|
+
for (let segment = 1; segment <= segments; segment += 1) {
|
|
427
|
+
const progress = segment / segments
|
|
428
|
+
const inverse = 1 - progress
|
|
429
|
+
target.push(
|
|
430
|
+
new THREE.Vector2(
|
|
431
|
+
inverse ** 3 * start.x +
|
|
432
|
+
3 * inverse ** 2 * progress * controlA.x +
|
|
433
|
+
3 * inverse * progress ** 2 * controlB.x +
|
|
434
|
+
progress ** 3 * end.x,
|
|
435
|
+
inverse ** 3 * start.y +
|
|
436
|
+
3 * inverse ** 2 * progress * controlA.y +
|
|
437
|
+
3 * inverse * progress ** 2 * controlB.y +
|
|
438
|
+
progress ** 3 * end.y,
|
|
439
|
+
),
|
|
440
|
+
)
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function clockwiseContour(points: THREE.Vector2[]): THREE.Vector2[] {
|
|
445
|
+
return THREE.ShapeUtils.isClockWise(points) ? points : [...points].reverse()
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function appendRunTopAndBottom(
|
|
449
|
+
output: GeometryAccumulator,
|
|
450
|
+
run: CanonicalWallRun,
|
|
451
|
+
wallHeight: number,
|
|
452
|
+
maximumProjection: number,
|
|
453
|
+
): void {
|
|
454
|
+
const halfThickness = TRACK_RAIL_WALL_THICKNESS * 0.5 + maximumProjection
|
|
455
|
+
for (let index = 0; index < run.frames.length - 1; index += 1) {
|
|
456
|
+
const start = run.frames[index]!.distance
|
|
457
|
+
const end = run.frames[index + 1]!.distance
|
|
458
|
+
const midpoint = canonicalWallFrameAt(run, (start + end) * 0.5)
|
|
459
|
+
const bottom: [SurfacePoint, SurfacePoint, SurfacePoint, SurfacePoint] = [
|
|
460
|
+
{ distance: start, height: 0, across: -halfThickness },
|
|
461
|
+
{ distance: end, height: 0, across: -halfThickness },
|
|
462
|
+
{ distance: end, height: 0, across: halfThickness },
|
|
463
|
+
{ distance: start, height: 0, across: halfThickness },
|
|
464
|
+
]
|
|
465
|
+
const narrowSurfaceUv = (point: SurfacePoint): readonly [number, number] => [
|
|
466
|
+
point.across + halfThickness,
|
|
467
|
+
point.distance,
|
|
468
|
+
]
|
|
469
|
+
appendMappedQuad(output, run, bottom, midpoint.up.clone().negate(), narrowSurfaceUv)
|
|
470
|
+
const top = bottom.map(point => ({ ...point, height: wallHeight })) as typeof bottom
|
|
471
|
+
appendMappedQuad(output, run, top, midpoint.up, narrowSurfaceUv)
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function appendTerminalCap(
|
|
476
|
+
output: GeometryAccumulator,
|
|
477
|
+
run: CanonicalWallRun,
|
|
478
|
+
distance: number,
|
|
479
|
+
wallHeight: number,
|
|
480
|
+
direction: -1 | 1,
|
|
481
|
+
maximumProjection: number,
|
|
482
|
+
): void {
|
|
483
|
+
const halfThickness = TRACK_RAIL_WALL_THICKNESS * 0.5 + maximumProjection
|
|
484
|
+
const cap: [SurfacePoint, SurfacePoint, SurfacePoint, SurfacePoint] = [
|
|
485
|
+
{ distance, height: 0, across: -halfThickness },
|
|
486
|
+
{ distance, height: 0, across: halfThickness },
|
|
487
|
+
{ distance, height: wallHeight, across: halfThickness },
|
|
488
|
+
{ distance, height: wallHeight, across: -halfThickness },
|
|
489
|
+
]
|
|
490
|
+
const terminalPathNormal = (() => {
|
|
491
|
+
if (run.frames.length < 2) return canonicalWallFrameAt(run, distance).tangent.clone().multiplyScalar(direction)
|
|
492
|
+
if (direction < 0) {
|
|
493
|
+
return run.frames[0]!.base.clone().sub(run.frames[1]!.base).setY(0).normalize()
|
|
494
|
+
}
|
|
495
|
+
return run.frames.at(-1)!.base.clone().sub(run.frames.at(-2)!.base).setY(0).normalize()
|
|
496
|
+
})()
|
|
497
|
+
appendMappedQuad(
|
|
498
|
+
output,
|
|
499
|
+
run,
|
|
500
|
+
cap,
|
|
501
|
+
terminalPathNormal,
|
|
502
|
+
point => [point.across + halfThickness, point.height],
|
|
503
|
+
)
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function appendMappedQuad(
|
|
507
|
+
output: GeometryAccumulator,
|
|
508
|
+
run: CanonicalWallRun,
|
|
509
|
+
points: [SurfacePoint, SurfacePoint, SurfacePoint, SurfacePoint],
|
|
510
|
+
expectedNormal: THREE.Vector3,
|
|
511
|
+
uvForPoint?: (point: SurfacePoint) => readonly [number, number],
|
|
512
|
+
): void {
|
|
513
|
+
appendMappedTriangle(output, run, [points[0], points[1], points[2]], expectedNormal, uvForPoint)
|
|
514
|
+
appendMappedTriangle(output, run, [points[0], points[2], points[3]], expectedNormal, uvForPoint)
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function appendMappedTriangle(
|
|
518
|
+
output: GeometryAccumulator,
|
|
519
|
+
run: CanonicalWallRun,
|
|
520
|
+
points: [SurfacePoint, SurfacePoint, SurfacePoint],
|
|
521
|
+
expectedNormal: THREE.Vector3,
|
|
522
|
+
uvForPoint?: (point: SurfacePoint) => readonly [number, number],
|
|
523
|
+
): void {
|
|
524
|
+
const mapped = points.map(point => mapSurfacePoint(run, point)) as [THREE.Vector3, THREE.Vector3, THREE.Vector3]
|
|
525
|
+
const normal = mapped[1].clone().sub(mapped[0]).cross(mapped[2].clone().sub(mapped[0]))
|
|
526
|
+
const order = normal.dot(expectedNormal) >= 0 ? [0, 1, 2] : [0, 2, 1]
|
|
527
|
+
const baseIndex = output.positions.length / 3
|
|
528
|
+
for (const index of order) {
|
|
529
|
+
const point = points[index]!
|
|
530
|
+
const world = mapped[index]!
|
|
531
|
+
const uv = uvForPoint?.(point) ?? [point.distance, point.height]
|
|
532
|
+
output.positions.push(world.x, world.y, world.z)
|
|
533
|
+
output.uvs.push(uv[0], uv[1])
|
|
534
|
+
output.metricUvs.push(point.height, point.distance, 1, run.length)
|
|
535
|
+
}
|
|
536
|
+
output.indices.push(baseIndex, baseIndex + 1, baseIndex + 2)
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function mapSurfacePoint(run: CanonicalWallRun, point: SurfacePoint): THREE.Vector3 {
|
|
540
|
+
const frame = canonicalWallFrameAt(run, point.distance)
|
|
541
|
+
return frame.base
|
|
542
|
+
.clone()
|
|
543
|
+
.addScaledVector(frame.outward, point.across)
|
|
544
|
+
.addScaledVector(frame.up, point.height - WALL_BASE_SINK)
|
|
545
|
+
}
|