@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,274 @@
|
|
|
1
|
+
import * as THREE from 'three'
|
|
2
|
+
|
|
3
|
+
import type { TrackDefinition } from './track-definition'
|
|
4
|
+
|
|
5
|
+
const COMPACT_MIN_DURATION_SECONDS = 2.75
|
|
6
|
+
const SHOWCASE_MIN_DURATION_SECONDS = 5
|
|
7
|
+
const MAX_DURATION_SECONDS = 9
|
|
8
|
+
const TOUR_SAMPLE_COUNT = 241
|
|
9
|
+
const MAX_EXTRA_CORKSCREW_TURNS = 1.15
|
|
10
|
+
const COMPACT_TRACK_SPAN = 16
|
|
11
|
+
const FULL_CORKSCREW_TRACK_SPAN = 64
|
|
12
|
+
|
|
13
|
+
export type CameraTourRouteDirection = 'corkscrew'
|
|
14
|
+
|
|
15
|
+
export interface CameraTourPose {
|
|
16
|
+
readonly position: readonly [number, number, number]
|
|
17
|
+
readonly target: readonly [number, number, number]
|
|
18
|
+
readonly fov: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface CameraTourDiagnostics {
|
|
22
|
+
readonly footprintWidth: number
|
|
23
|
+
readonly footprintDepth: number
|
|
24
|
+
readonly occupiedCells: number
|
|
25
|
+
readonly routeDirection: CameraTourRouteDirection
|
|
26
|
+
readonly routeLength: number
|
|
27
|
+
readonly minimumClearance: number
|
|
28
|
+
readonly validationAttempts: number
|
|
29
|
+
readonly cruiseAltitude: number
|
|
30
|
+
readonly pitchChanges: number
|
|
31
|
+
readonly fallbackReason?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface CameraTourPlan {
|
|
35
|
+
readonly durationSeconds: number
|
|
36
|
+
readonly sampleCount: number
|
|
37
|
+
readonly positions: Float64Array
|
|
38
|
+
readonly targets: Float64Array
|
|
39
|
+
readonly fovs: Float32Array
|
|
40
|
+
readonly startPose: CameraTourPose
|
|
41
|
+
readonly diagnostics: CameraTourDiagnostics
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface CameraTourPlannerInput {
|
|
45
|
+
readonly track: TrackDefinition
|
|
46
|
+
readonly finishPose: CameraTourPose
|
|
47
|
+
readonly startPose: CameraTourPose
|
|
48
|
+
readonly startForward: THREE.Vector3
|
|
49
|
+
readonly durationSeconds?: number
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface CameraTourSampleOutput {
|
|
53
|
+
readonly position: THREE.Vector3
|
|
54
|
+
readonly target: THREE.Vector3
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface Point2 {
|
|
58
|
+
x: number
|
|
59
|
+
y: number
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function clamp01(value: number): number {
|
|
63
|
+
return Math.max(0, Math.min(1, value))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function smootherstep(value: number): number {
|
|
67
|
+
const t = clamp01(value)
|
|
68
|
+
return t * t * t * (t * (t * 6 - 15) + 10)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function cross(origin: Point2, left: Point2, right: Point2): number {
|
|
72
|
+
return (left.x - origin.x) * (right.y - origin.y) - (left.y - origin.y) * (right.x - origin.x)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function convexHull2D(points: readonly Point2[]): Point2[] {
|
|
76
|
+
const finitePoints = points.filter(point => Number.isFinite(point.x) && Number.isFinite(point.y))
|
|
77
|
+
if (finitePoints.length <= 1) return finitePoints.map(point => ({ ...point }))
|
|
78
|
+
const sorted = [...finitePoints].sort((left, right) => left.x - right.x || left.y - right.y)
|
|
79
|
+
const unique = sorted.filter(
|
|
80
|
+
(point, index) => index === 0 || point.x !== sorted[index - 1]!.x || point.y !== sorted[index - 1]!.y,
|
|
81
|
+
)
|
|
82
|
+
if (unique.length <= 2) return unique.map(point => ({ ...point }))
|
|
83
|
+
const lower: Point2[] = []
|
|
84
|
+
for (const point of unique) {
|
|
85
|
+
while (lower.length >= 2 && cross(lower.at(-2)!, lower.at(-1)!, point) <= 0) lower.pop()
|
|
86
|
+
lower.push(point)
|
|
87
|
+
}
|
|
88
|
+
const upper: Point2[] = []
|
|
89
|
+
for (let index = unique.length - 1; index >= 0; index -= 1) {
|
|
90
|
+
const point = unique[index]!
|
|
91
|
+
while (upper.length >= 2 && cross(upper.at(-2)!, upper.at(-1)!, point) <= 0) upper.pop()
|
|
92
|
+
upper.push(point)
|
|
93
|
+
}
|
|
94
|
+
lower.pop()
|
|
95
|
+
upper.pop()
|
|
96
|
+
return [...lower, ...upper]
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function trackBounds(track: TrackDefinition): { bounds: THREE.Box3; pointCount: number } {
|
|
100
|
+
const bounds = new THREE.Box3()
|
|
101
|
+
let pointCount = 0
|
|
102
|
+
for (const path of [track.cameraPath, ...track.projectionPaths]) {
|
|
103
|
+
for (const point of path.points) {
|
|
104
|
+
const position = new THREE.Vector3().fromArray(point.position)
|
|
105
|
+
if (![position.x, position.y, position.z].every(Number.isFinite)) continue
|
|
106
|
+
bounds.expandByPoint(position)
|
|
107
|
+
pointCount += 1
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (bounds.isEmpty()) bounds.setFromCenterAndSize(new THREE.Vector3(), new THREE.Vector3(1, 1, 1))
|
|
111
|
+
return { bounds, pointCount }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function shortestSignedAngle(from: number, to: number): number {
|
|
115
|
+
return Math.atan2(Math.sin(to - from), Math.cos(to - from))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function countPitchChanges(positions: Float64Array, targets: Float64Array): number {
|
|
119
|
+
let changes = 0
|
|
120
|
+
let previousPitch: number | undefined
|
|
121
|
+
let previousDirection = 0
|
|
122
|
+
for (let sample = 0; sample < TOUR_SAMPLE_COUNT; sample += 1) {
|
|
123
|
+
const offset = sample * 3
|
|
124
|
+
const horizontal = Math.hypot(targets[offset]! - positions[offset]!, targets[offset + 2]! - positions[offset + 2]!)
|
|
125
|
+
const pitch = Math.atan2(targets[offset + 1]! - positions[offset + 1]!, Math.max(horizontal, 0.000_001))
|
|
126
|
+
if (previousPitch !== undefined) {
|
|
127
|
+
const direction = Math.sign(pitch - previousPitch)
|
|
128
|
+
if (direction !== 0 && previousDirection !== 0 && direction !== previousDirection) changes += 1
|
|
129
|
+
if (direction !== 0) previousDirection = direction
|
|
130
|
+
}
|
|
131
|
+
previousPitch = pitch
|
|
132
|
+
}
|
|
133
|
+
return changes
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Creates the authored finish-to-start corkscrew with geometry alone.
|
|
138
|
+
*
|
|
139
|
+
* Runtime camera motion is presentation, not physics. It must never block a
|
|
140
|
+
* race while exhaustively querying every Rapier collider. The conservative
|
|
141
|
+
* ellipse is derived from the track guides, rises above their highest point,
|
|
142
|
+
* and hands off exactly to the existing start-line composition.
|
|
143
|
+
*/
|
|
144
|
+
export function createCameraTourPlan(input: CameraTourPlannerInput): CameraTourPlan {
|
|
145
|
+
const { bounds, pointCount } = trackBounds(input.track)
|
|
146
|
+
const center = bounds.getCenter(new THREE.Vector3())
|
|
147
|
+
const size = bounds.getSize(new THREE.Vector3())
|
|
148
|
+
const finishPosition = new THREE.Vector3().fromArray(input.finishPose.position)
|
|
149
|
+
const finishTarget = new THREE.Vector3().fromArray(input.finishPose.target)
|
|
150
|
+
const startPosition = new THREE.Vector3().fromArray(input.startPose.position)
|
|
151
|
+
const authoredStartTarget = new THREE.Vector3().fromArray(input.startPose.target)
|
|
152
|
+
const startLookOffset = input.startForward.clone()
|
|
153
|
+
if (startLookOffset.lengthSq() < 0.000_001) startLookOffset.subVectors(authoredStartTarget, startPosition)
|
|
154
|
+
const startLookDistance = Math.max(1, authoredStartTarget.distanceTo(startPosition))
|
|
155
|
+
startLookOffset.normalize().multiplyScalar(startLookDistance)
|
|
156
|
+
const startTarget = startPosition.clone().add(startLookOffset)
|
|
157
|
+
const resolvedStartPose: CameraTourPose = {
|
|
158
|
+
position: input.startPose.position,
|
|
159
|
+
target: [startTarget.x, startTarget.y, startTarget.z],
|
|
160
|
+
fov: input.startPose.fov,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const radiusX = THREE.MathUtils.clamp(size.x * 0.58 + 7, 11, 52)
|
|
164
|
+
const radiusZ = THREE.MathUtils.clamp(size.z * 0.58 + 7, 11, 52)
|
|
165
|
+
const finishAngle = Math.atan2(finishPosition.z - center.z, finishPosition.x - center.x)
|
|
166
|
+
const startAngle = Math.atan2(startPosition.z - center.z, startPosition.x - center.x)
|
|
167
|
+
const footprintSpan = Math.hypot(size.x, size.z)
|
|
168
|
+
const showcaseWeight = smootherstep(
|
|
169
|
+
(footprintSpan - COMPACT_TRACK_SPAN) / (FULL_CORKSCREW_TRACK_SPAN - COMPACT_TRACK_SPAN),
|
|
170
|
+
)
|
|
171
|
+
const shortestArc = shortestSignedAngle(finishAngle, startAngle)
|
|
172
|
+
const orbitDirection = Math.sign(shortestArc) || 1
|
|
173
|
+
const angularTravel = shortestArc + orbitDirection * Math.PI * 2 * MAX_EXTRA_CORKSCREW_TURNS * showcaseWeight
|
|
174
|
+
const cruiseAltitude = Math.max(
|
|
175
|
+
finishPosition.y + 4,
|
|
176
|
+
startPosition.y + 4,
|
|
177
|
+
bounds.max.y + THREE.MathUtils.clamp(Math.max(size.x, size.z) * 0.12, 8, 18),
|
|
178
|
+
)
|
|
179
|
+
const positions = new Float64Array(TOUR_SAMPLE_COUNT * 3)
|
|
180
|
+
const targets = new Float64Array(TOUR_SAMPLE_COUNT * 3)
|
|
181
|
+
const fovs = new Float32Array(TOUR_SAMPLE_COUNT)
|
|
182
|
+
const position = new THREE.Vector3()
|
|
183
|
+
const target = new THREE.Vector3()
|
|
184
|
+
const orbit = new THREE.Vector3()
|
|
185
|
+
const orbitTarget = new THREE.Vector3()
|
|
186
|
+
const finalLookTarget = new THREE.Vector3()
|
|
187
|
+
const previousPosition = new THREE.Vector3()
|
|
188
|
+
let routeLength = 0
|
|
189
|
+
|
|
190
|
+
for (let sample = 0; sample < TOUR_SAMPLE_COUNT; sample += 1) {
|
|
191
|
+
const fraction = sample / (TOUR_SAMPLE_COUNT - 1)
|
|
192
|
+
const eased = smootherstep(fraction)
|
|
193
|
+
const angle = finishAngle + angularTravel * fraction
|
|
194
|
+
const rise = Math.sin(Math.PI * fraction)
|
|
195
|
+
orbit.set(
|
|
196
|
+
center.x + Math.cos(angle) * radiusX,
|
|
197
|
+
THREE.MathUtils.lerp(finishPosition.y, startPosition.y, eased) +
|
|
198
|
+
rise * (cruiseAltitude - THREE.MathUtils.lerp(finishPosition.y, startPosition.y, eased)),
|
|
199
|
+
center.z + Math.sin(angle) * radiusZ,
|
|
200
|
+
)
|
|
201
|
+
orbitTarget.copy(center)
|
|
202
|
+
|
|
203
|
+
const finishBlend = smootherstep(fraction / 0.12)
|
|
204
|
+
const startBlend = smootherstep((fraction - 0.8) / 0.2)
|
|
205
|
+
const finalPitchBlend = smootherstep((fraction - 0.55) / 0.3)
|
|
206
|
+
position.copy(finishPosition).lerp(orbit, finishBlend).lerp(startPosition, startBlend)
|
|
207
|
+
finalLookTarget.copy(position).add(startLookOffset)
|
|
208
|
+
target.copy(finishTarget).lerp(orbitTarget, finishBlend).lerp(finalLookTarget, finalPitchBlend)
|
|
209
|
+
|
|
210
|
+
if (sample === 0) {
|
|
211
|
+
position.copy(finishPosition)
|
|
212
|
+
target.copy(finishTarget)
|
|
213
|
+
} else {
|
|
214
|
+
routeLength += position.distanceTo(previousPosition)
|
|
215
|
+
}
|
|
216
|
+
if (sample === TOUR_SAMPLE_COUNT - 1) {
|
|
217
|
+
routeLength += position.distanceTo(startPosition)
|
|
218
|
+
position.copy(startPosition)
|
|
219
|
+
target.copy(startTarget)
|
|
220
|
+
}
|
|
221
|
+
previousPosition.copy(position)
|
|
222
|
+
const offset = sample * 3
|
|
223
|
+
position.toArray(positions, offset)
|
|
224
|
+
target.toArray(targets, offset)
|
|
225
|
+
fovs[sample] = THREE.MathUtils.lerp(input.finishPose.fov, input.startPose.fov, eased)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return Object.freeze({
|
|
229
|
+
durationSeconds:
|
|
230
|
+
input.durationSeconds !== undefined && Number.isFinite(input.durationSeconds) && input.durationSeconds > 0
|
|
231
|
+
? input.durationSeconds
|
|
232
|
+
: THREE.MathUtils.clamp(
|
|
233
|
+
routeLength / 32,
|
|
234
|
+
THREE.MathUtils.lerp(COMPACT_MIN_DURATION_SECONDS, SHOWCASE_MIN_DURATION_SECONDS, showcaseWeight),
|
|
235
|
+
MAX_DURATION_SECONDS,
|
|
236
|
+
),
|
|
237
|
+
sampleCount: TOUR_SAMPLE_COUNT,
|
|
238
|
+
positions,
|
|
239
|
+
targets,
|
|
240
|
+
fovs,
|
|
241
|
+
startPose: resolvedStartPose,
|
|
242
|
+
diagnostics: Object.freeze({
|
|
243
|
+
footprintWidth: size.x,
|
|
244
|
+
footprintDepth: size.z,
|
|
245
|
+
occupiedCells: pointCount,
|
|
246
|
+
routeDirection: 'corkscrew' as const,
|
|
247
|
+
routeLength,
|
|
248
|
+
minimumClearance: 0,
|
|
249
|
+
validationAttempts: 0,
|
|
250
|
+
cruiseAltitude,
|
|
251
|
+
pitchChanges: countPitchChanges(positions, targets),
|
|
252
|
+
}),
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function sampleCameraTourPlan(plan: CameraTourPlan, progress: number, output: CameraTourSampleOutput): number {
|
|
257
|
+
const scaled = clamp01(progress) * (plan.sampleCount - 1)
|
|
258
|
+
const left = Math.floor(scaled)
|
|
259
|
+
const right = Math.min(plan.sampleCount - 1, left + 1)
|
|
260
|
+
const alpha = scaled - left
|
|
261
|
+
const leftOffset = left * 3
|
|
262
|
+
const rightOffset = right * 3
|
|
263
|
+
output.position.set(
|
|
264
|
+
THREE.MathUtils.lerp(plan.positions[leftOffset]!, plan.positions[rightOffset]!, alpha),
|
|
265
|
+
THREE.MathUtils.lerp(plan.positions[leftOffset + 1]!, plan.positions[rightOffset + 1]!, alpha),
|
|
266
|
+
THREE.MathUtils.lerp(plan.positions[leftOffset + 2]!, plan.positions[rightOffset + 2]!, alpha),
|
|
267
|
+
)
|
|
268
|
+
output.target.set(
|
|
269
|
+
THREE.MathUtils.lerp(plan.targets[leftOffset]!, plan.targets[rightOffset]!, alpha),
|
|
270
|
+
THREE.MathUtils.lerp(plan.targets[leftOffset + 1]!, plan.targets[rightOffset + 1]!, alpha),
|
|
271
|
+
THREE.MathUtils.lerp(plan.targets[leftOffset + 2]!, plan.targets[rightOffset + 2]!, alpha),
|
|
272
|
+
)
|
|
273
|
+
return THREE.MathUtils.lerp(plan.fovs[left]!, plan.fovs[right]!, alpha)
|
|
274
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import * as THREE from 'three'
|
|
2
|
+
|
|
3
|
+
/** The race camera has exactly two authored views. */
|
|
4
|
+
export const RACE_CAMERA_VIEWS = ['race', 'focus'] as const
|
|
5
|
+
|
|
6
|
+
export type RaceCameraView = (typeof RACE_CAMERA_VIEWS)[number]
|
|
7
|
+
|
|
8
|
+
/** Zooming out releases a view; Race orbit also releases while Focus orbit retains its subject lock. */
|
|
9
|
+
export const RACE_CAMERA_CONTROLS = ['locked', 'free'] as const
|
|
10
|
+
|
|
11
|
+
export type RaceCameraControl = (typeof RACE_CAMERA_CONTROLS)[number]
|
|
12
|
+
|
|
13
|
+
export const RACE_CAMERA_STATES = ['race-locked', 'race-free', 'focus-locked', 'focus-free'] as const
|
|
14
|
+
|
|
15
|
+
export type RaceCameraState = (typeof RACE_CAMERA_STATES)[number]
|
|
16
|
+
|
|
17
|
+
export interface RaceCameraSnapshot {
|
|
18
|
+
view: RaceCameraView
|
|
19
|
+
control: RaceCameraControl
|
|
20
|
+
targetSlotId?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function raceCameraState(view: RaceCameraView, control: RaceCameraControl): RaceCameraState {
|
|
24
|
+
return `${view}-${control}`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const CAMERA_ZOOM_DIRECTION_EPSILON = 0.000_1
|
|
28
|
+
const CAMERA_RETURN_PROGRESS_PER_SECOND = 2.4
|
|
29
|
+
const CAMERA_ORBIT_RETURN_INPUT_RATIO = 0.3
|
|
30
|
+
|
|
31
|
+
/** Close-enough boundary where the remaining curve is consumed into the authored pose. */
|
|
32
|
+
export const CAMERA_ZOOM_LOCK_DISTANCE_RATIO = 1.03
|
|
33
|
+
|
|
34
|
+
export function cameraZoomIsInward(previousDistance: number, requestedDistance: number): boolean {
|
|
35
|
+
return (
|
|
36
|
+
Number.isFinite(previousDistance) &&
|
|
37
|
+
Number.isFinite(requestedDistance) &&
|
|
38
|
+
requestedDistance < previousDistance - CAMERA_ZOOM_DIRECTION_EPSILON
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function cameraZoomIsOutward(previousDistance: number, requestedDistance: number): boolean {
|
|
43
|
+
return (
|
|
44
|
+
Number.isFinite(previousDistance) &&
|
|
45
|
+
Number.isFinite(requestedDistance) &&
|
|
46
|
+
requestedDistance > previousDistance + CAMERA_ZOOM_DIRECTION_EPSILON
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function cameraReturnProgress(startDistance: number, requestedDistance: number, lockedDistance: number): number {
|
|
51
|
+
if (
|
|
52
|
+
!Number.isFinite(startDistance) ||
|
|
53
|
+
!Number.isFinite(requestedDistance) ||
|
|
54
|
+
!Number.isFinite(lockedDistance) ||
|
|
55
|
+
lockedDistance <= 0
|
|
56
|
+
) {
|
|
57
|
+
return 0
|
|
58
|
+
}
|
|
59
|
+
if (requestedDistance <= lockedDistance * CAMERA_ZOOM_LOCK_DISTANCE_RATIO) return 1
|
|
60
|
+
if (startDistance <= lockedDistance + CAMERA_ZOOM_DIRECTION_EPSILON) return 0
|
|
61
|
+
return THREE.MathUtils.clamp((startDistance - requestedDistance) / (startDistance - lockedDistance), 0, 1)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Moves a free orbit with its subject without changing the user-authored orbit vector. */
|
|
65
|
+
export function translateFreeCameraRig(
|
|
66
|
+
position: THREE.Vector3,
|
|
67
|
+
target: THREE.Vector3,
|
|
68
|
+
anchor: THREE.Vector3,
|
|
69
|
+
nextAnchor: THREE.Vector3,
|
|
70
|
+
translation: THREE.Vector3,
|
|
71
|
+
): void {
|
|
72
|
+
translation.subVectors(nextAnchor, anchor)
|
|
73
|
+
anchor.copy(nextAnchor)
|
|
74
|
+
position.add(translation)
|
|
75
|
+
target.add(translation)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Allocation-free curve from a user-authored orbit back to the live authored
|
|
80
|
+
* race/focus pose. Target, radius, direction, and FOV bend together, while
|
|
81
|
+
* `translate` carries the captured free pose with the moving race subject.
|
|
82
|
+
*/
|
|
83
|
+
export class RaceCameraReturnCurve {
|
|
84
|
+
private readonly startPosition = new THREE.Vector3()
|
|
85
|
+
private readonly startTarget = new THREE.Vector3()
|
|
86
|
+
private readonly startOffset = new THREE.Vector3()
|
|
87
|
+
private readonly lockedOffset = new THREE.Vector3()
|
|
88
|
+
private readonly startDirection = new THREE.Vector3()
|
|
89
|
+
private readonly lockedDirection = new THREE.Vector3()
|
|
90
|
+
private readonly interpolatedDirection = new THREE.Vector3()
|
|
91
|
+
private readonly shortestRotation = new THREE.Quaternion()
|
|
92
|
+
private readonly interpolatedRotation = new THREE.Quaternion()
|
|
93
|
+
private startFov = 52
|
|
94
|
+
private startDistance = 0
|
|
95
|
+
private requestedDistance = 0
|
|
96
|
+
private requestedInputProgress = 0
|
|
97
|
+
private progress = 0
|
|
98
|
+
private active = false
|
|
99
|
+
private inputDriven = false
|
|
100
|
+
|
|
101
|
+
begin(position: THREE.Vector3, target: THREE.Vector3, fov: number, distance: number): void {
|
|
102
|
+
this.startPosition.copy(position)
|
|
103
|
+
this.startTarget.copy(target)
|
|
104
|
+
this.startFov = fov
|
|
105
|
+
this.startDistance = Math.max(0, distance)
|
|
106
|
+
this.requestedDistance = this.startDistance
|
|
107
|
+
this.requestedInputProgress = 0
|
|
108
|
+
this.progress = 0
|
|
109
|
+
this.active = true
|
|
110
|
+
this.inputDriven = false
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
cancel(): void {
|
|
114
|
+
this.active = false
|
|
115
|
+
this.progress = 0
|
|
116
|
+
this.requestedInputProgress = 0
|
|
117
|
+
this.inputDriven = false
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
isActive(): boolean {
|
|
121
|
+
return this.active
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
isComplete(): boolean {
|
|
125
|
+
return this.active && this.progress >= 1
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
setRequestedDistance(distance: number, inputDriven = false): void {
|
|
129
|
+
if (!Number.isFinite(distance)) return
|
|
130
|
+
this.requestedDistance = Math.max(0, distance)
|
|
131
|
+
this.inputDriven = inputDriven
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
addInwardInput(previousDistance: number, requestedDistance: number, lockedDistance: number): void {
|
|
135
|
+
if (
|
|
136
|
+
!this.active ||
|
|
137
|
+
!Number.isFinite(previousDistance) ||
|
|
138
|
+
!Number.isFinite(requestedDistance) ||
|
|
139
|
+
!Number.isFinite(lockedDistance) ||
|
|
140
|
+
lockedDistance <= 0
|
|
141
|
+
) {
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
const inwardDistance = Math.max(0, previousDistance - requestedDistance)
|
|
145
|
+
const returnInputDistance = Math.max(
|
|
146
|
+
this.startDistance - lockedDistance,
|
|
147
|
+
lockedDistance * CAMERA_ORBIT_RETURN_INPUT_RATIO,
|
|
148
|
+
CAMERA_ZOOM_DIRECTION_EPSILON,
|
|
149
|
+
)
|
|
150
|
+
this.requestedInputProgress = THREE.MathUtils.clamp(
|
|
151
|
+
this.requestedInputProgress + inwardDistance / returnInputDistance,
|
|
152
|
+
0,
|
|
153
|
+
1,
|
|
154
|
+
)
|
|
155
|
+
this.inputDriven = true
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
translate(delta: THREE.Vector3): void {
|
|
159
|
+
if (!this.active) return
|
|
160
|
+
this.startPosition.add(delta)
|
|
161
|
+
this.startTarget.add(delta)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
writePose(
|
|
165
|
+
lockedPosition: THREE.Vector3,
|
|
166
|
+
lockedTarget: THREE.Vector3,
|
|
167
|
+
lockedFov: number,
|
|
168
|
+
outputPosition: THREE.Vector3,
|
|
169
|
+
outputTarget: THREE.Vector3,
|
|
170
|
+
deltaSeconds: number,
|
|
171
|
+
): number {
|
|
172
|
+
if (!this.active) return lockedFov
|
|
173
|
+
|
|
174
|
+
this.startOffset.subVectors(this.startPosition, this.startTarget)
|
|
175
|
+
this.lockedOffset.subVectors(lockedPosition, lockedTarget)
|
|
176
|
+
const lockedDistance = this.lockedOffset.length()
|
|
177
|
+
const requestedProgress = Math.max(
|
|
178
|
+
this.progress,
|
|
179
|
+
this.inputDriven
|
|
180
|
+
? this.requestedInputProgress
|
|
181
|
+
: cameraReturnProgress(this.startDistance, this.requestedDistance, lockedDistance),
|
|
182
|
+
)
|
|
183
|
+
this.progress = this.inputDriven
|
|
184
|
+
? requestedProgress
|
|
185
|
+
: Math.min(
|
|
186
|
+
requestedProgress,
|
|
187
|
+
this.progress + CAMERA_RETURN_PROGRESS_PER_SECOND * THREE.MathUtils.clamp(deltaSeconds, 0, 0.1),
|
|
188
|
+
)
|
|
189
|
+
const curvedProgress = THREE.MathUtils.smootherstep(this.progress, 0, 1)
|
|
190
|
+
|
|
191
|
+
outputTarget.lerpVectors(this.startTarget, lockedTarget, curvedProgress)
|
|
192
|
+
if (this.startOffset.lengthSq() < 0.000_001 || this.lockedOffset.lengthSq() < 0.000_001) {
|
|
193
|
+
outputPosition.lerpVectors(this.startPosition, lockedPosition, curvedProgress)
|
|
194
|
+
return THREE.MathUtils.lerp(this.startFov, lockedFov, curvedProgress)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
this.startDirection.copy(this.startOffset).normalize()
|
|
198
|
+
this.lockedDirection.copy(this.lockedOffset).normalize()
|
|
199
|
+
this.shortestRotation.setFromUnitVectors(this.startDirection, this.lockedDirection)
|
|
200
|
+
this.interpolatedRotation.identity().slerp(this.shortestRotation, curvedProgress)
|
|
201
|
+
this.interpolatedDirection.copy(this.startDirection).applyQuaternion(this.interpolatedRotation).normalize()
|
|
202
|
+
const radius = THREE.MathUtils.lerp(this.startDistance, lockedDistance, this.progress)
|
|
203
|
+
outputPosition.copy(outputTarget).addScaledVector(this.interpolatedDirection, radius)
|
|
204
|
+
return THREE.MathUtils.lerp(this.startFov, lockedFov, curvedProgress)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createRaceConfiguration as createCoreRaceConfiguration,
|
|
3
|
+
createRaceTuning as createCoreRaceTuning,
|
|
4
|
+
raceConfigurationKey,
|
|
5
|
+
RACE_MARBLE_RADIUS,
|
|
6
|
+
} from '@marble.fun/physics-engine'
|
|
7
|
+
|
|
8
|
+
import { MAX_MARBLES, MIN_MARBLES, type MarbleAssignment, type RaceConfiguration, type RaceTuning } from './race-types'
|
|
9
|
+
import { STOCK_MARBLES } from '../../lobby/lobby-types'
|
|
10
|
+
import { normalizeSeed } from './seeded-random'
|
|
11
|
+
import type { TrackDefinition } from './track-definition'
|
|
12
|
+
|
|
13
|
+
export const STOCK_PRESETS = [
|
|
14
|
+
...STOCK_MARBLES.map(stock => ({ label: stock.id, color: stock.color })),
|
|
15
|
+
{ label: 'DEMO 5', color: '#fd9d55' },
|
|
16
|
+
{ label: 'DEMO 6', color: '#8d7cff' },
|
|
17
|
+
] as const
|
|
18
|
+
|
|
19
|
+
export const MARBLE_RADIUS = RACE_MARBLE_RADIUS
|
|
20
|
+
|
|
21
|
+
export function createAssignments(marbleCount: number): MarbleAssignment[] {
|
|
22
|
+
return STOCK_PRESETS.slice(0, marbleCount).map((preset, index) => ({
|
|
23
|
+
slotId: `M${index + 1}`,
|
|
24
|
+
label: preset.label,
|
|
25
|
+
color: preset.color,
|
|
26
|
+
}))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createRaceConfiguration(
|
|
30
|
+
seed: string,
|
|
31
|
+
marbleCount: number,
|
|
32
|
+
track: Pick<TrackDefinition, 'id' | 'version'>,
|
|
33
|
+
): RaceConfiguration {
|
|
34
|
+
const safeCount = Math.min(MAX_MARBLES, Math.max(MIN_MARBLES, Math.round(marbleCount)))
|
|
35
|
+
return createCoreRaceConfiguration(normalizeSeed(seed), safeCount, track, createAssignments(safeCount))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function createLabTestRunConfiguration(
|
|
39
|
+
seed: string,
|
|
40
|
+
marbleCount: number,
|
|
41
|
+
track: Pick<TrackDefinition, 'id' | 'version'>,
|
|
42
|
+
): RaceConfiguration {
|
|
43
|
+
const configuration = createRaceConfiguration(seed, marbleCount, track)
|
|
44
|
+
return {
|
|
45
|
+
...configuration,
|
|
46
|
+
assignments: configuration.assignments.map(assignment => ({
|
|
47
|
+
...assignment,
|
|
48
|
+
label: assignment.slotId,
|
|
49
|
+
})),
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function createRaceTuning(configuration: RaceConfiguration, track: TrackDefinition): RaceTuning {
|
|
54
|
+
return createCoreRaceTuning(configuration, track)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export { raceConfigurationKey }
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import * as THREE from 'three'
|
|
2
|
+
import { FullScreenQuad } from 'three/addons/postprocessing/Pass.js'
|
|
3
|
+
|
|
4
|
+
const vertexShader = /* glsl */ `
|
|
5
|
+
varying vec2 vUv;
|
|
6
|
+
|
|
7
|
+
void main() {
|
|
8
|
+
vUv = uv;
|
|
9
|
+
gl_Position = vec4(position.xy, 0.0, 1.0);
|
|
10
|
+
}
|
|
11
|
+
`
|
|
12
|
+
|
|
13
|
+
const fragmentShader = /* glsl */ `
|
|
14
|
+
uniform sampler2D tColor;
|
|
15
|
+
uniform vec2 texelSize;
|
|
16
|
+
uniform vec2 focusUv;
|
|
17
|
+
uniform float aspect;
|
|
18
|
+
uniform float focusRadius;
|
|
19
|
+
uniform float focusFeather;
|
|
20
|
+
uniform float maxBlurPixels;
|
|
21
|
+
|
|
22
|
+
varying vec2 vUv;
|
|
23
|
+
|
|
24
|
+
void main() {
|
|
25
|
+
vec4 centerColor = texture2D(tColor, vUv);
|
|
26
|
+
vec2 focusDelta = (vUv - focusUv) * vec2(aspect, 1.0);
|
|
27
|
+
float blur = smoothstep(focusRadius, focusRadius + focusFeather, length(focusDelta));
|
|
28
|
+
|
|
29
|
+
if (blur <= 0.001) {
|
|
30
|
+
gl_FragColor = centerColor;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
vec2 radius = texelSize * maxBlurPixels * blur;
|
|
35
|
+
vec4 blurredColor = centerColor * 0.2;
|
|
36
|
+
for (int index = 0; index < 8; index++) {
|
|
37
|
+
float angle = float(index) * 0.7853981634;
|
|
38
|
+
vec2 tap = vec2(cos(angle), sin(angle));
|
|
39
|
+
vec2 sampleUv = clamp(vUv + tap * radius, vec2(0.0), vec2(1.0));
|
|
40
|
+
blurredColor += texture2D(tColor, sampleUv) * 0.1;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// The framebuffer copy already contains final display pixels. Interpolate
|
|
44
|
+
// those raw values only; never tone-map, expose, or encode them again.
|
|
45
|
+
gl_FragColor = mix(centerColor, blurredColor, blur);
|
|
46
|
+
}
|
|
47
|
+
`
|
|
48
|
+
|
|
49
|
+
/** Keeps the ball and nearby track sharp, then gradually softens the surrounding frame. */
|
|
50
|
+
export class RaceMarbleFocusRenderer {
|
|
51
|
+
private colorTexture = this.createColorTexture(1, 1)
|
|
52
|
+
private readonly material: THREE.ShaderMaterial
|
|
53
|
+
private readonly quad: FullScreenQuad
|
|
54
|
+
private readonly drawingBufferSize = new THREE.Vector2()
|
|
55
|
+
private readonly projectedFocus = new THREE.Vector3()
|
|
56
|
+
private width = 0
|
|
57
|
+
private height = 0
|
|
58
|
+
|
|
59
|
+
constructor() {
|
|
60
|
+
this.material = new THREE.ShaderMaterial({
|
|
61
|
+
depthTest: false,
|
|
62
|
+
depthWrite: false,
|
|
63
|
+
toneMapped: false,
|
|
64
|
+
transparent: false,
|
|
65
|
+
uniforms: {
|
|
66
|
+
tColor: { value: this.colorTexture },
|
|
67
|
+
texelSize: { value: new THREE.Vector2(1, 1) },
|
|
68
|
+
focusUv: { value: new THREE.Vector2(0.5, 0.5) },
|
|
69
|
+
aspect: { value: 1 },
|
|
70
|
+
// Screen-height-relative radii after aspect correction: a broad sharp
|
|
71
|
+
// neighborhood around the marble followed by a long, gentle falloff.
|
|
72
|
+
focusRadius: { value: 0.14 },
|
|
73
|
+
focusFeather: { value: 0.22 },
|
|
74
|
+
maxBlurPixels: { value: 4 },
|
|
75
|
+
},
|
|
76
|
+
vertexShader,
|
|
77
|
+
fragmentShader,
|
|
78
|
+
})
|
|
79
|
+
this.quad = new FullScreenQuad(this.material)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
render(
|
|
83
|
+
renderer: THREE.WebGLRenderer,
|
|
84
|
+
scene: THREE.Scene,
|
|
85
|
+
camera: THREE.PerspectiveCamera,
|
|
86
|
+
focusWorldPosition: THREE.Vector3,
|
|
87
|
+
): void {
|
|
88
|
+
this.syncSize(renderer)
|
|
89
|
+
|
|
90
|
+
// Establish the authoritative color first through the ordinary renderer.
|
|
91
|
+
// Copying it avoids a second scene render and guarantees identical grading.
|
|
92
|
+
renderer.render(scene, camera)
|
|
93
|
+
renderer.copyFramebufferToTexture(this.colorTexture)
|
|
94
|
+
|
|
95
|
+
this.projectedFocus.copy(focusWorldPosition).project(camera)
|
|
96
|
+
;(this.material.uniforms.focusUv!.value as THREE.Vector2).set(
|
|
97
|
+
this.projectedFocus.x * 0.5 + 0.5,
|
|
98
|
+
this.projectedFocus.y * 0.5 + 0.5,
|
|
99
|
+
)
|
|
100
|
+
this.quad.render(renderer)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
dispose(): void {
|
|
104
|
+
this.quad.dispose()
|
|
105
|
+
this.material.dispose()
|
|
106
|
+
this.colorTexture.dispose()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private createColorTexture(width: number, height: number): THREE.FramebufferTexture {
|
|
110
|
+
const texture = new THREE.FramebufferTexture(width, height)
|
|
111
|
+
texture.name = 'race-marble-focus-screen-copy'
|
|
112
|
+
// The copy contains already encoded display values. Treat them as raw data
|
|
113
|
+
// so the focus shader cannot alter exposure or color-space transforms.
|
|
114
|
+
texture.colorSpace = THREE.NoColorSpace
|
|
115
|
+
texture.magFilter = THREE.LinearFilter
|
|
116
|
+
texture.minFilter = THREE.LinearFilter
|
|
117
|
+
return texture
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private syncSize(renderer: THREE.WebGLRenderer): void {
|
|
121
|
+
renderer.getDrawingBufferSize(this.drawingBufferSize)
|
|
122
|
+
const width = Math.max(1, Math.round(this.drawingBufferSize.x))
|
|
123
|
+
const height = Math.max(1, Math.round(this.drawingBufferSize.y))
|
|
124
|
+
if (width === this.width && height === this.height) return
|
|
125
|
+
|
|
126
|
+
this.width = width
|
|
127
|
+
this.height = height
|
|
128
|
+
this.colorTexture.dispose()
|
|
129
|
+
this.colorTexture = this.createColorTexture(width, height)
|
|
130
|
+
this.material.uniforms.tColor!.value = this.colorTexture
|
|
131
|
+
;(this.material.uniforms.texelSize!.value as THREE.Vector2).set(1 / width, 1 / height)
|
|
132
|
+
this.material.uniforms.aspect!.value = width / height
|
|
133
|
+
}
|
|
134
|
+
}
|