@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,24 @@
|
|
|
1
|
+
import type * as THREE from 'three'
|
|
2
|
+
|
|
3
|
+
/** Preserve intentional attribute aliases when Three copies/merges buffers. */
|
|
4
|
+
export function preserveGeometryAttributeAliases(
|
|
5
|
+
target: THREE.BufferGeometry,
|
|
6
|
+
sources: readonly THREE.BufferGeometry[],
|
|
7
|
+
): void {
|
|
8
|
+
if (sources.length === 0) return
|
|
9
|
+
const names = Object.keys(target.attributes)
|
|
10
|
+
for (let index = 1; index < names.length; index += 1) {
|
|
11
|
+
const name = names[index]!
|
|
12
|
+
for (let previous = 0; previous < index; previous += 1) {
|
|
13
|
+
const other = names[previous]!
|
|
14
|
+
if (
|
|
15
|
+
sources.every(
|
|
16
|
+
source => source.getAttribute(name) !== undefined && source.getAttribute(name) === source.getAttribute(other),
|
|
17
|
+
)
|
|
18
|
+
) {
|
|
19
|
+
target.setAttribute(name, target.getAttribute(other))
|
|
20
|
+
break
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Development experiment: geometry only; renderer policy stays at High. */
|
|
2
|
+
export type GeometryDetail = 'max' | 'mid' | 'lo'
|
|
3
|
+
export const GEOMETRY_DETAIL_STORAGE_KEY = 'marble.geometry-detail'
|
|
4
|
+
export const GEOMETRY_DETAIL_PROFILES = {
|
|
5
|
+
max: { triangleRatio: 0.2, minimumScale: 1, surfaceSpacing: 1, ribbonTolerance: 1 },
|
|
6
|
+
mid: { triangleRatio: 0.08, minimumScale: 0.75, surfaceSpacing: 2, ribbonTolerance: 1.5 },
|
|
7
|
+
lo: { triangleRatio: 0.025, minimumScale: 0.5, surfaceSpacing: 4, ribbonTolerance: 2.5 },
|
|
8
|
+
} as const
|
|
9
|
+
|
|
10
|
+
export function parseGeometryDetail(value: string | null | undefined): GeometryDetail | undefined {
|
|
11
|
+
return value === 'max' || value === 'mid' || value === 'lo' ? value : undefined
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function initialGeometryDetail(): GeometryDetail {
|
|
15
|
+
const query = parseGeometryDetail(new URLSearchParams(globalThis.location?.search ?? '').get('geometryDetail'))
|
|
16
|
+
if (query) return query
|
|
17
|
+
try {
|
|
18
|
+
return parseGeometryDetail(globalThis.localStorage?.getItem(GEOMETRY_DETAIL_STORAGE_KEY)) ?? 'max'
|
|
19
|
+
} catch {
|
|
20
|
+
return 'max'
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Deliberately immutable for the page lifetime: caches and async model loads
|
|
25
|
+
// must use the same profile. Changing the dial requires a full page reload.
|
|
26
|
+
export const geometryDetail = initialGeometryDetail()
|
|
27
|
+
export const geometryDetailProfile = GEOMETRY_DETAIL_PROFILES[geometryDetail]
|
|
28
|
+
|
|
29
|
+
export function geometryDetailModelUrl(url: string): string {
|
|
30
|
+
return geometryDetail === 'max' ? url : url.replace('/models/', `/models/geometry/${geometryDetail}/`)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Counts for custom render-only grids. Never use for collision geometry. */
|
|
34
|
+
export function geometryDetailSegments(current: number, minimum = 1): number {
|
|
35
|
+
return Math.max(minimum, Math.floor(current / geometryDetailProfile.surfaceSpacing))
|
|
36
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { SphereGeometry } from 'three'
|
|
2
|
+
|
|
3
|
+
const geometries = new Map<string, SphereGeometry>()
|
|
4
|
+
|
|
5
|
+
/** Original authored homepage tessellation, independent of course detail. */
|
|
6
|
+
export function homepageMarbleGeometry(radius: number, width: number, height: number): SphereGeometry {
|
|
7
|
+
const key = `${radius}:${width}:${height}`
|
|
8
|
+
let geometry = geometries.get(key)
|
|
9
|
+
if (!geometry) {
|
|
10
|
+
geometry = new SphereGeometry(radius, width, height)
|
|
11
|
+
geometry.userData.shared = true
|
|
12
|
+
geometries.set(key, geometry)
|
|
13
|
+
}
|
|
14
|
+
return geometry
|
|
15
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import type { MaterialRecipe } from './track-theme'
|
|
2
|
+
|
|
3
|
+
export const JAPANESE_SAKURA_CASTLE_WALL_ID = 'japanese-sakura.castle-wall'
|
|
4
|
+
export const JAPANESE_SAKURA_CASTLE_TIMBER_ID = 'japanese-sakura.castle-wall-timber'
|
|
5
|
+
export const JAPANESE_SAKURA_CASTLE_MASONRY_ID = 'japanese-sakura.castle-wall-masonry'
|
|
6
|
+
export const JAPANESE_SAKURA_CASTLE_KAWARA_ID = 'japanese-sakura.castle-wall-kawara'
|
|
7
|
+
export const JAPANESE_SAKURA_BRIDGE_FENCE_ID = 'japanese-sakura.vermilion-bridge-fence'
|
|
8
|
+
export const JAPANESE_SAKURA_BRIDGE_TIMBER_ID = 'japanese-sakura.vermilion-bridge-timber'
|
|
9
|
+
export const JAPANESE_SAKURA_BRIDGE_HARDWARE_ID = 'japanese-sakura.vermilion-bridge-hardware'
|
|
10
|
+
export const JAPANESE_SAKURA_CASTLE_PAVERS_ID = 'japanese-sakura.castle-pavers'
|
|
11
|
+
export const JAPANESE_SAKURA_REDWOOD_PLANKS_ID = 'japanese-sakura.redwood-planks'
|
|
12
|
+
export const JAPANESE_SAKURA_PORTAL_FOUNDATION_STONE_ID = 'japanese-sakura.portal-foundation-stone'
|
|
13
|
+
export const JAPANESE_SAKURA_VERMILION_UNDERBRIDGE_SUPPORT_ID = 'japanese-sakura.vermilion-underbridge-support'
|
|
14
|
+
export const JAPANESE_SAKURA_DRAGON_STONE_SUPPORT_ID = 'japanese-sakura.dragon-stone-support'
|
|
15
|
+
|
|
16
|
+
const textureRoot = '/themes/textures/japanese-sakura-castle'
|
|
17
|
+
|
|
18
|
+
/** Weathered bridge-plank grain recolored as deep Japanese vermilion redwood. */
|
|
19
|
+
export const JAPANESE_SAKURA_REDWOOD_PLANKS_MATERIAL: MaterialRecipe = {
|
|
20
|
+
kind: 'physical',
|
|
21
|
+
color: '#ffffff',
|
|
22
|
+
roughness: 0.48,
|
|
23
|
+
metalness: 0,
|
|
24
|
+
envMapIntensity: 0.86,
|
|
25
|
+
clearcoat: 0.34,
|
|
26
|
+
clearcoatRoughness: 0.42,
|
|
27
|
+
pattern: {
|
|
28
|
+
kind: 'bridge-planks',
|
|
29
|
+
primary: '#8f2f25',
|
|
30
|
+
secondary: '#2c0d0a',
|
|
31
|
+
worldSize: [24, 14.4],
|
|
32
|
+
},
|
|
33
|
+
textureUrl: '/themes/textures/japanese-sakura-redwood-planks-v1-albedo.webp',
|
|
34
|
+
normalTextureUrl: '/themes/textures/japanese-sakura-redwood-planks-v1-normal.png',
|
|
35
|
+
heightTextureUrl: '/themes/textures/japanese-sakura-redwood-planks-v1-height.png',
|
|
36
|
+
roughnessTextureUrl: '/themes/textures/japanese-sakura-redwood-planks-v1-roughness.png',
|
|
37
|
+
aoTextureUrl: '/themes/textures/japanese-sakura-redwood-planks-v1-ao.png',
|
|
38
|
+
textureWorldSize: [5.4, 2.16],
|
|
39
|
+
normalScale: [0.62, 0.62],
|
|
40
|
+
bumpScale: 0.025,
|
|
41
|
+
aoMapIntensity: 0.38,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const JAPANESE_SAKURA_CASTLE_PLASTER_MATERIAL: MaterialRecipe = {
|
|
45
|
+
kind: 'standard',
|
|
46
|
+
color: '#ffffff',
|
|
47
|
+
roughness: 0.88,
|
|
48
|
+
metalness: 0,
|
|
49
|
+
envMapIntensity: 0.68,
|
|
50
|
+
pattern: {
|
|
51
|
+
kind: 'painted-stone',
|
|
52
|
+
primary: '#f3e4c4',
|
|
53
|
+
secondary: '#d8c29b',
|
|
54
|
+
worldSize: [1.6, 1.6],
|
|
55
|
+
},
|
|
56
|
+
textureUrl: `${textureRoot}-plaster-v1-albedo.webp`,
|
|
57
|
+
normalTextureUrl: `${textureRoot}-plaster-v1-normal.png`,
|
|
58
|
+
heightTextureUrl: `${textureRoot}-plaster-v1-height.png`,
|
|
59
|
+
roughnessTextureUrl: `${textureRoot}-plaster-v1-roughness.png`,
|
|
60
|
+
aoTextureUrl: `${textureRoot}-plaster-v1-ao.png`,
|
|
61
|
+
textureWorldSize: [1.6, 1.6],
|
|
62
|
+
normalScale: [0.32, 0.32],
|
|
63
|
+
bumpScale: 0.024,
|
|
64
|
+
aoMapIntensity: 0.42,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const JAPANESE_SAKURA_CASTLE_TIMBER_MATERIAL: MaterialRecipe = {
|
|
68
|
+
kind: 'standard',
|
|
69
|
+
color: '#ffffff',
|
|
70
|
+
roughness: 0.74,
|
|
71
|
+
metalness: 0,
|
|
72
|
+
envMapIntensity: 0.72,
|
|
73
|
+
pattern: {
|
|
74
|
+
kind: 'wood',
|
|
75
|
+
primary: '#57311f',
|
|
76
|
+
secondary: '#21130e',
|
|
77
|
+
worldSize: [0.55, 1.8],
|
|
78
|
+
},
|
|
79
|
+
textureUrl: `${textureRoot}-timber-v1-albedo.webp`,
|
|
80
|
+
normalTextureUrl: `${textureRoot}-timber-v1-normal.png`,
|
|
81
|
+
heightTextureUrl: `${textureRoot}-timber-v1-height.png`,
|
|
82
|
+
roughnessTextureUrl: `${textureRoot}-timber-v1-roughness.png`,
|
|
83
|
+
aoTextureUrl: `${textureRoot}-timber-v1-ao.png`,
|
|
84
|
+
textureWorldSize: [0.55, 1.8],
|
|
85
|
+
normalScale: [0.4, 0.4],
|
|
86
|
+
bumpScale: 0.035,
|
|
87
|
+
aoMapIntensity: 0.5,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const JAPANESE_SAKURA_CASTLE_MASONRY_MATERIAL: MaterialRecipe = {
|
|
91
|
+
kind: 'standard',
|
|
92
|
+
color: '#ffffff',
|
|
93
|
+
roughness: 0.8,
|
|
94
|
+
metalness: 0,
|
|
95
|
+
envMapIntensity: 0.6,
|
|
96
|
+
pattern: {
|
|
97
|
+
kind: 'ancient-bricks',
|
|
98
|
+
primary: '#75402b',
|
|
99
|
+
secondary: '#301d18',
|
|
100
|
+
worldSize: [1.4, 0.7],
|
|
101
|
+
},
|
|
102
|
+
textureUrl: `${textureRoot}-masonry-sill-v1-albedo.webp`,
|
|
103
|
+
normalTextureUrl: `${textureRoot}-masonry-sill-v1-normal.png`,
|
|
104
|
+
heightTextureUrl: `${textureRoot}-masonry-sill-v1-height.png`,
|
|
105
|
+
roughnessTextureUrl: `${textureRoot}-masonry-sill-v1-roughness.png`,
|
|
106
|
+
aoTextureUrl: `${textureRoot}-masonry-sill-v1-ao.png`,
|
|
107
|
+
textureWorldSize: [1.4, 0.7],
|
|
108
|
+
normalScale: [0.58, 0.58],
|
|
109
|
+
bumpScale: 0.055,
|
|
110
|
+
aoMapIntensity: 0.62,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export const JAPANESE_SAKURA_PORTAL_FOUNDATION_STONE_MATERIAL: MaterialRecipe = {
|
|
114
|
+
kind: 'standard',
|
|
115
|
+
color: '#ffffff',
|
|
116
|
+
roughness: 0.84,
|
|
117
|
+
metalness: 0,
|
|
118
|
+
envMapIntensity: 0.66,
|
|
119
|
+
pattern: {
|
|
120
|
+
kind: 'painted-stone',
|
|
121
|
+
primary: '#34363a',
|
|
122
|
+
secondary: '#1f2125',
|
|
123
|
+
worldSize: [1.6, 1.6],
|
|
124
|
+
},
|
|
125
|
+
textureUrl: '/themes/textures/japanese-sakura-portal-foundation-stone-v1-albedo.webp',
|
|
126
|
+
normalTextureUrl: '/themes/textures/japanese-sakura-portal-foundation-stone-v1-normal.png',
|
|
127
|
+
heightTextureUrl: '/themes/textures/japanese-sakura-portal-foundation-stone-v1-height.png',
|
|
128
|
+
roughnessTextureUrl: '/themes/textures/japanese-sakura-portal-foundation-stone-v1-roughness.png',
|
|
129
|
+
aoTextureUrl: '/themes/textures/japanese-sakura-portal-foundation-stone-v1-ao.png',
|
|
130
|
+
textureWorldSize: [1.6, 1.6],
|
|
131
|
+
normalScale: [0.42, 0.42],
|
|
132
|
+
bumpScale: 0.028,
|
|
133
|
+
aoMapIntensity: 0.5,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export const JAPANESE_SAKURA_CASTLE_KAWARA_MATERIAL: MaterialRecipe = {
|
|
137
|
+
kind: 'physical',
|
|
138
|
+
color: '#ffffff',
|
|
139
|
+
roughness: 0.5,
|
|
140
|
+
metalness: 0,
|
|
141
|
+
envMapIntensity: 0.9,
|
|
142
|
+
clearcoat: 0.22,
|
|
143
|
+
clearcoatRoughness: 0.46,
|
|
144
|
+
pattern: {
|
|
145
|
+
kind: 'panels',
|
|
146
|
+
primary: '#4d5054',
|
|
147
|
+
secondary: '#202328',
|
|
148
|
+
worldSize: [2.7, 1.8],
|
|
149
|
+
},
|
|
150
|
+
textureUrl: `${textureRoot}-kawara-v1-albedo.webp`,
|
|
151
|
+
normalTextureUrl: `${textureRoot}-kawara-v1-normal.png`,
|
|
152
|
+
heightTextureUrl: `${textureRoot}-kawara-v1-height.png`,
|
|
153
|
+
roughnessTextureUrl: `${textureRoot}-kawara-v1-roughness.png`,
|
|
154
|
+
aoTextureUrl: `${textureRoot}-kawara-v1-ao.png`,
|
|
155
|
+
textureWorldSize: [2.7, 1.8],
|
|
156
|
+
normalScale: [0.7, 0.7],
|
|
157
|
+
bumpScale: 0.07,
|
|
158
|
+
aoMapIntensity: 0.58,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Dedicated low-contrast vermilion lacquer; deliberately independent of the castle timber. */
|
|
162
|
+
export const JAPANESE_SAKURA_BRIDGE_TIMBER_MATERIAL: MaterialRecipe = {
|
|
163
|
+
kind: 'physical',
|
|
164
|
+
color: '#ffffff',
|
|
165
|
+
roughness: 0.3,
|
|
166
|
+
metalness: 0,
|
|
167
|
+
envMapIntensity: 1.3,
|
|
168
|
+
clearcoat: 0.84,
|
|
169
|
+
clearcoatRoughness: 0.14,
|
|
170
|
+
pattern: {
|
|
171
|
+
kind: 'wood',
|
|
172
|
+
primary: '#8a2119',
|
|
173
|
+
secondary: '#651611',
|
|
174
|
+
worldSize: [3.2, 3.2],
|
|
175
|
+
},
|
|
176
|
+
textureUrl: '/themes/textures/japanese-sakura-vermilion-lacquer-v2-albedo.webp',
|
|
177
|
+
normalTextureUrl: '/themes/textures/japanese-sakura-vermilion-lacquer-v2-normal.png',
|
|
178
|
+
heightTextureUrl: '/themes/textures/japanese-sakura-vermilion-lacquer-v2-height.png',
|
|
179
|
+
roughnessTextureUrl: '/themes/textures/japanese-sakura-vermilion-lacquer-v2-roughness.png',
|
|
180
|
+
aoTextureUrl: '/themes/textures/japanese-sakura-vermilion-lacquer-v2-ao.png',
|
|
181
|
+
textureWorldSize: [3.2, 3.2],
|
|
182
|
+
normalScale: [0.18, 0.18],
|
|
183
|
+
bumpScale: 0.004,
|
|
184
|
+
aoMapIntensity: 0.08,
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export const JAPANESE_SAKURA_BRIDGE_HARDWARE_MATERIAL: MaterialRecipe = {
|
|
188
|
+
kind: 'physical',
|
|
189
|
+
color: '#6d7476',
|
|
190
|
+
roughness: 0.3,
|
|
191
|
+
metalness: 0.92,
|
|
192
|
+
envMapIntensity: 1.3,
|
|
193
|
+
clearcoat: 0.12,
|
|
194
|
+
clearcoatRoughness: 0.24,
|
|
195
|
+
pattern: {
|
|
196
|
+
kind: 'gunmetal-silver',
|
|
197
|
+
primary: '#8e9799',
|
|
198
|
+
secondary: '#252a2c',
|
|
199
|
+
worldSize: [0.42, 0.42],
|
|
200
|
+
},
|
|
201
|
+
bumpScale: 0.008,
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Pale aligned ashlar used by the approved castle under-track support. */
|
|
205
|
+
export const JAPANESE_SAKURA_DRAGON_STONE_MATERIAL: MaterialRecipe = {
|
|
206
|
+
kind: 'standard',
|
|
207
|
+
color: '#ffffff',
|
|
208
|
+
roughness: 0.78,
|
|
209
|
+
metalness: 0,
|
|
210
|
+
envMapIntensity: 0.58,
|
|
211
|
+
textureUrl: '/themes/textures/japanese-sakura-castle-ashlar-albedo-v1.png',
|
|
212
|
+
normalTextureUrl: '/themes/textures/japanese-sakura-castle-ashlar-normal-v1.png',
|
|
213
|
+
heightTextureUrl: '/themes/textures/japanese-sakura-castle-ashlar-height-v1.png',
|
|
214
|
+
roughnessTextureUrl: '/themes/textures/japanese-sakura-castle-ashlar-roughness-v1.png',
|
|
215
|
+
aoTextureUrl: '/themes/textures/japanese-sakura-castle-ashlar-ao-v1.png',
|
|
216
|
+
textureWorldSize: [4, 4],
|
|
217
|
+
normalScale: [0.38, 0.38],
|
|
218
|
+
bumpScale: 0.03,
|
|
219
|
+
aoMapIntensity: 0.42,
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export const JAPANESE_SAKURA_CASTLE_PAVERS_MATERIAL: MaterialRecipe = {
|
|
223
|
+
kind: 'standard',
|
|
224
|
+
color: '#ffffff',
|
|
225
|
+
roughness: 0.78,
|
|
226
|
+
metalness: 0,
|
|
227
|
+
envMapIntensity: 0.72,
|
|
228
|
+
pattern: {
|
|
229
|
+
kind: 'ancient-bricks',
|
|
230
|
+
primary: '#ead6aa',
|
|
231
|
+
secondary: '#c7ad7f',
|
|
232
|
+
worldSize: [3.8, 3.8],
|
|
233
|
+
},
|
|
234
|
+
textureUrl: '/themes/textures/japanese-sakura-castle-pavers-v1-albedo.webp',
|
|
235
|
+
normalTextureUrl: '/themes/textures/japanese-sakura-castle-pavers-v1-normal.png',
|
|
236
|
+
heightTextureUrl: '/themes/textures/japanese-sakura-castle-pavers-v1-height.png',
|
|
237
|
+
roughnessTextureUrl: '/themes/textures/japanese-sakura-castle-pavers-v1-roughness.png',
|
|
238
|
+
aoTextureUrl: '/themes/textures/japanese-sakura-castle-pavers-v1-ao.png',
|
|
239
|
+
textureWorldSize: [3.8, 3.8],
|
|
240
|
+
normalScale: [0.48, 0.48],
|
|
241
|
+
bumpScale: 0.035,
|
|
242
|
+
aoMapIntensity: 0.48,
|
|
243
|
+
visualRelief: { scale: 0.12, bias: -0.11, targetEdgeLength: 0.09 },
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function isJapaneseSakuraCastleWallAppearance(appearanceId: string | undefined): boolean {
|
|
247
|
+
return appearanceId === JAPANESE_SAKURA_CASTLE_WALL_ID
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function isJapaneseSakuraBridgeFenceAppearance(appearanceId: string | undefined): boolean {
|
|
251
|
+
return appearanceId === JAPANESE_SAKURA_BRIDGE_FENCE_ID
|
|
252
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export {
|
|
2
|
+
PAINTED_VALLEY_TROPICAL_FLOWER_HEDGE_CORE_MATERIAL as JUNGLE_FLOWER_HEDGE_CORE_MATERIAL,
|
|
3
|
+
PAINTED_VALLEY_TROPICAL_FLOWER_HEDGE_WALL_ID as JUNGLE_FLOWER_HEDGE_WALL_ID,
|
|
4
|
+
PAINTED_VALLEY_GRASS_ORANGE_DIRT_PATH_FLOOR_ID as JUNGLE_GRASS_DIRT_PATH_FLOOR_ID,
|
|
5
|
+
PAINTED_VALLEY_GRASS_ORANGE_DIRT_PATH_MATERIAL as JUNGLE_GRASS_DIRT_PATH_MATERIAL,
|
|
6
|
+
isTropicalFlowerHedgeAppearance as isJungleFlowerHedgeAppearance,
|
|
7
|
+
} from './painted-valley-appearance'
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export const JUNGLE_TEMPLE_STONE_MATERIAL_ID = 'jungle.temple-stone'
|
|
2
|
+
export const JUNGLE_TEMPLE_FACADE_MATERIAL_ID = 'jungle.temple-serpent-facade'
|
|
3
|
+
export const JUNGLE_TEMPLE_TIMBER_MATERIAL_ID = 'jungle.temple-timber'
|
|
4
|
+
export const JUNGLE_TEMPLE_PLATFORM_MATERIAL_ID = 'jungle.temple-platform'
|
|
5
|
+
export const JUNGLE_TEMPLE_BRONZE_MATERIAL_ID = 'jungle.temple-bronze'
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import * as THREE from 'three'
|
|
2
|
+
|
|
3
|
+
import type { VisualQualityPolicy } from './visual-quality'
|
|
4
|
+
|
|
5
|
+
type LitMaterial =
|
|
6
|
+
THREE.MeshStandardMaterial | THREE.MeshPhongMaterial | THREE.MeshLambertMaterial | THREE.MeshToonMaterial
|
|
7
|
+
|
|
8
|
+
/** Render-only experiment. Source materials, geometry and simulation are never edited. */
|
|
9
|
+
export class LowQualitySceneRenderer {
|
|
10
|
+
private readonly materials = new Map<
|
|
11
|
+
LitMaterial,
|
|
12
|
+
{ material: THREE.MeshBasicMaterial; release: () => void; frame: number }
|
|
13
|
+
>()
|
|
14
|
+
private readonly arrays = new WeakMap<THREE.Material[], THREE.Material[]>()
|
|
15
|
+
private readonly meshes: THREE.Mesh[] = []
|
|
16
|
+
private readonly originals: Array<THREE.Material | THREE.Material[]> = []
|
|
17
|
+
private readonly particles: THREE.Points[] = []
|
|
18
|
+
private frame = 0
|
|
19
|
+
private cameraLayers = 1
|
|
20
|
+
|
|
21
|
+
render(renderer: THREE.WebGLRenderer, scene: THREE.Scene, camera: THREE.Camera, policy: VisualQualityPolicy): void {
|
|
22
|
+
if (policy.tier !== 'low') {
|
|
23
|
+
renderer.render(scene, camera)
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
this.frame += 1
|
|
27
|
+
this.cameraLayers = camera.layers.mask
|
|
28
|
+
const shadowsEnabled = renderer.shadowMap.enabled
|
|
29
|
+
renderer.shadowMap.enabled = false
|
|
30
|
+
try {
|
|
31
|
+
scene.traverseVisible(this.prepareObject)
|
|
32
|
+
renderer.render(scene, camera)
|
|
33
|
+
} finally {
|
|
34
|
+
for (let index = 0; index < this.meshes.length; index += 1) {
|
|
35
|
+
this.meshes[index]!.material = this.originals[index]!
|
|
36
|
+
}
|
|
37
|
+
for (const particle of this.particles) particle.visible = true
|
|
38
|
+
this.meshes.length = 0
|
|
39
|
+
this.originals.length = 0
|
|
40
|
+
this.particles.length = 0
|
|
41
|
+
renderer.shadowMap.enabled = shadowsEnabled
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
dispose(): void {
|
|
46
|
+
for (const { release } of this.materials.values()) release()
|
|
47
|
+
this.materials.clear()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private readonly prepareObject = (object: THREE.Object3D): void => {
|
|
51
|
+
if (object instanceof THREE.Points) {
|
|
52
|
+
this.particles.push(object)
|
|
53
|
+
object.visible = false
|
|
54
|
+
} else if (object instanceof THREE.Mesh && (object.layers.mask & this.cameraLayers) !== 0) {
|
|
55
|
+
const original = object.material
|
|
56
|
+
let replacement: THREE.Material | THREE.Material[]
|
|
57
|
+
if (Array.isArray(original)) {
|
|
58
|
+
let array = this.arrays.get(original)
|
|
59
|
+
if (!array) {
|
|
60
|
+
array = []
|
|
61
|
+
this.arrays.set(original, array)
|
|
62
|
+
}
|
|
63
|
+
array.length = original.length
|
|
64
|
+
for (let index = 0; index < original.length; index += 1) array[index] = this.simplify(original[index]!)
|
|
65
|
+
replacement = array
|
|
66
|
+
} else {
|
|
67
|
+
replacement = this.simplify(original)
|
|
68
|
+
}
|
|
69
|
+
if (replacement === original) return
|
|
70
|
+
this.meshes.push(object)
|
|
71
|
+
this.originals.push(original)
|
|
72
|
+
object.material = replacement
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private simplify(source: THREE.Material): THREE.Material {
|
|
77
|
+
if (!(
|
|
78
|
+
source instanceof THREE.MeshStandardMaterial ||
|
|
79
|
+
source instanceof THREE.MeshPhongMaterial ||
|
|
80
|
+
source instanceof THREE.MeshLambertMaterial ||
|
|
81
|
+
source instanceof THREE.MeshToonMaterial
|
|
82
|
+
))
|
|
83
|
+
return source
|
|
84
|
+
let entry = this.materials.get(source)
|
|
85
|
+
if (!entry) {
|
|
86
|
+
const material = new THREE.MeshBasicMaterial()
|
|
87
|
+
material.name = `low:${source.name}`
|
|
88
|
+
const release = (): void => {
|
|
89
|
+
source.removeEventListener('dispose', release)
|
|
90
|
+
material.dispose()
|
|
91
|
+
this.materials.delete(source)
|
|
92
|
+
}
|
|
93
|
+
source.addEventListener('dispose', release)
|
|
94
|
+
entry = { material, release, frame: -1 }
|
|
95
|
+
this.materials.set(source, entry)
|
|
96
|
+
}
|
|
97
|
+
// Synchronize shared materials once per render, not once per mesh.
|
|
98
|
+
if (entry.frame === this.frame) return entry.material
|
|
99
|
+
entry.frame = this.frame
|
|
100
|
+
const target = entry.material
|
|
101
|
+
// Keep labels, foliage cutouts, vertex colors and presentation fades. Normal,
|
|
102
|
+
// roughness, metalness, environment, transmission and lighting are omitted.
|
|
103
|
+
if (
|
|
104
|
+
target.map !== source.map ||
|
|
105
|
+
target.alphaMap !== source.alphaMap ||
|
|
106
|
+
target.vertexColors !== source.vertexColors ||
|
|
107
|
+
target.side !== source.side ||
|
|
108
|
+
target.transparent !== source.transparent ||
|
|
109
|
+
target.alphaTest !== source.alphaTest
|
|
110
|
+
)
|
|
111
|
+
target.needsUpdate = true
|
|
112
|
+
target.color.copy(source.color)
|
|
113
|
+
target.map = source.map
|
|
114
|
+
target.alphaMap = source.alphaMap
|
|
115
|
+
target.vertexColors = source.vertexColors
|
|
116
|
+
target.side = source.side
|
|
117
|
+
target.transparent = source.transparent
|
|
118
|
+
target.opacity = source.opacity
|
|
119
|
+
target.alphaTest = source.alphaTest
|
|
120
|
+
target.depthTest = source.depthTest
|
|
121
|
+
target.depthWrite = source.depthWrite
|
|
122
|
+
target.colorWrite = source.colorWrite
|
|
123
|
+
target.blending = source.blending
|
|
124
|
+
target.polygonOffset = source.polygonOffset
|
|
125
|
+
target.polygonOffsetFactor = source.polygonOffsetFactor
|
|
126
|
+
target.polygonOffsetUnits = source.polygonOffsetUnits
|
|
127
|
+
target.visible = source.visible
|
|
128
|
+
target.toneMapped = source.toneMapped
|
|
129
|
+
target.fog = source.fog
|
|
130
|
+
return target
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { SphereGeometry } from 'three'
|
|
2
|
+
|
|
3
|
+
/** Preserve the pre-experiment High marble silhouette at every course detail. */
|
|
4
|
+
export class MarbleSphereGeometry extends SphereGeometry {
|
|
5
|
+
constructor(radius: number, width: number, height: number) {
|
|
6
|
+
const highAxisRatio = Math.sqrt(0.2)
|
|
7
|
+
super(
|
|
8
|
+
radius,
|
|
9
|
+
Math.min(width, Math.max(8, Math.floor(width * highAxisRatio))),
|
|
10
|
+
Math.min(height, Math.max(4, Math.floor(height * highAxisRatio))),
|
|
11
|
+
)
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const MOBILE_STATIC_BATCH_ALLOCATION_BUDGET_BYTES = 32 * 1024 * 1024
|
|
2
|
+
|
|
3
|
+
/** Phone/tablet rendering budgets must survive landscape and browser rotation. */
|
|
4
|
+
export function usesMobileRenderingBudget(
|
|
5
|
+
viewportWidth: number = typeof window === 'undefined' ? Number.POSITIVE_INFINITY : window.innerWidth,
|
|
6
|
+
coarsePrimaryPointer: boolean = typeof matchMedia !== 'undefined' && matchMedia('(pointer: coarse)').matches,
|
|
7
|
+
): boolean {
|
|
8
|
+
// Narrow desktop windows retain the existing compact budget. Device input
|
|
9
|
+
// capability is independent of layout width, unlike a CSS breakpoint.
|
|
10
|
+
return viewportWidth < 768 || coarsePrimaryPointer
|
|
11
|
+
}
|