@call-me-sensei/toonlab 0.1.1

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.
Files changed (146) hide show
  1. package/ATTRIBUTION.md +40 -0
  2. package/LICENSE +21 -0
  3. package/README.md +125 -0
  4. package/package.json +97 -0
  5. package/src/character/characterRig.js +353 -0
  6. package/src/character/freestyleSwimClip.js +541 -0
  7. package/src/character/index.js +4 -0
  8. package/src/character/modelLoader.js +433 -0
  9. package/src/core/materialRoles.js +438 -0
  10. package/src/core/presetDocuments.js +110 -0
  11. package/src/core/shaderBackend.js +24 -0
  12. package/src/debrisgen/debrisFields.js +544 -0
  13. package/src/debrisgen/debrisGenerator.js +1929 -0
  14. package/src/debrisgen/debrisPalettes.js +71 -0
  15. package/src/debrisgen/debrisPhysics.js +198 -0
  16. package/src/debrisgen/debrisPresets.js +155 -0
  17. package/src/debrisgen/debrisSettings.js +333 -0
  18. package/src/debrisgen/debrisTextures.js +380 -0
  19. package/src/debrisgen/index.js +5 -0
  20. package/src/debug/fieldValues.js +95 -0
  21. package/src/debug/index.js +7 -0
  22. package/src/debug/settingsPanel.js +170 -0
  23. package/src/environment/environmentAmbientProbe.js +160 -0
  24. package/src/environment/environmentMaterialAdapter.js +336 -0
  25. package/src/environment/environmentMaterialClassifier.js +178 -0
  26. package/src/environment/environmentPlanarReflection.js +201 -0
  27. package/src/environment/environmentPresets.js +443 -0
  28. package/src/environment/environmentRigs.js +684 -0
  29. package/src/environment/environmentSettings.js +504 -0
  30. package/src/environment/environmentShaderMaterials.js +318 -0
  31. package/src/environment/environmentSunShadowPass.js +268 -0
  32. package/src/environment/environmentTextureResolver.js +161 -0
  33. package/src/environment/environmentTimeOfDay.js +167 -0
  34. package/src/environment/environmentVertexAo.js +188 -0
  35. package/src/environment/index.js +8 -0
  36. package/src/environment/scanAssetStylize.js +103 -0
  37. package/src/index.js +21 -0
  38. package/src/loaders/index.js +1 -0
  39. package/src/post/index.js +2 -0
  40. package/src/post/postProcessing.js +1087 -0
  41. package/src/rockgen/export/glbExport.js +177 -0
  42. package/src/rockgen/heightfield/heightfieldErosion.js +7 -0
  43. package/src/rockgen/heightfield/heightfieldPatch.js +183 -0
  44. package/src/rockgen/heightfield/stylizedErosionSim.js +356 -0
  45. package/src/rockgen/index.js +14 -0
  46. package/src/rockgen/mesh/meshAttributes.js +374 -0
  47. package/src/rockgen/mesh/meshDocument.js +158 -0
  48. package/src/rockgen/mesh/surfaceNets.js +329 -0
  49. package/src/rockgen/noise/cellularNoise3.js +92 -0
  50. package/src/rockgen/noise/prng.js +62 -0
  51. package/src/rockgen/noise/simplexNoise3.js +89 -0
  52. package/src/rockgen/noise/valueNoise3.js +87 -0
  53. package/src/rockgen/rockDocument.js +255 -0
  54. package/src/rockgen/rockHelpers.js +8 -0
  55. package/src/rockgen/rockgenPresets.js +559 -0
  56. package/src/rockgen/rockgenSettings.js +756 -0
  57. package/src/rockgen/sdf/fieldCompiler.js +544 -0
  58. package/src/rockgen/sdf/sculptEdits.js +66 -0
  59. package/src/rockgen/sdf/sdfModifiers.js +45 -0
  60. package/src/rockgen/sdf/sdfOps.js +37 -0
  61. package/src/rockgen/sdf/sdfPrimitives.js +88 -0
  62. package/src/shaders-tsl/anime.js +963 -0
  63. package/src/shaders-tsl/chunks/character-color.js +64 -0
  64. package/src/shaders-tsl/chunks/character-highlights.js +174 -0
  65. package/src/shaders-tsl/chunks/character-lighting.js +309 -0
  66. package/src/shaders-tsl/chunks/character-material-maps.js +146 -0
  67. package/src/shaders-tsl/chunks/character-roles.js +52 -0
  68. package/src/shaders-tsl/chunks/character-scene-lights.js +270 -0
  69. package/src/shaders-tsl/chunks/character-shadow-color.js +61 -0
  70. package/src/shaders-tsl/chunks/character-skinning.js +140 -0
  71. package/src/shaders-tsl/chunks/environment-color.js +47 -0
  72. package/src/shaders-tsl/chunks/environment-debug.js +79 -0
  73. package/src/shaders-tsl/chunks/environment-lighting.js +260 -0
  74. package/src/shaders-tsl/chunks/environment-sun-shadow.js +87 -0
  75. package/src/shaders-tsl/chunks/foliage-fog.js +60 -0
  76. package/src/shaders-tsl/chunks/pass-depth-color.js +89 -0
  77. package/src/shaders-tsl/chunks/stylized-cloud-shadow.js +70 -0
  78. package/src/shaders-tsl/chunks/water-color.js +190 -0
  79. package/src/shaders-tsl/chunks/water-common.js +124 -0
  80. package/src/shaders-tsl/chunks/water-foam.js +93 -0
  81. package/src/shaders-tsl/chunks/water-lighting.js +116 -0
  82. package/src/shaders-tsl/chunks/water-ripple.js +64 -0
  83. package/src/shaders-tsl/chunks/water-waves.js +100 -0
  84. package/src/shaders-tsl/environment-ao-overlay.js +60 -0
  85. package/src/shaders-tsl/environment.js +680 -0
  86. package/src/shaders-tsl/flower.js +267 -0
  87. package/src/shaders-tsl/grass.js +193 -0
  88. package/src/shaders-tsl/post-composite.js +461 -0
  89. package/src/shaders-tsl/sky.js +175 -0
  90. package/src/shaders-tsl/tree-leaf.js +264 -0
  91. package/src/shaders-tsl/water-breaker.js +423 -0
  92. package/src/shaders-tsl/water-kelp.js +171 -0
  93. package/src/shaders-tsl/water-rain.js +107 -0
  94. package/src/shaders-tsl/water-simulation.js +140 -0
  95. package/src/shaders-tsl/water-splash.js +298 -0
  96. package/src/shaders-tsl/water.js +509 -0
  97. package/src/sky/index.js +2 -0
  98. package/src/sky/stylizedSky.js +341 -0
  99. package/src/toon/characterRenderPasses.js +646 -0
  100. package/src/toon/index.js +4 -0
  101. package/src/toon/settings/alphaSettings.js +170 -0
  102. package/src/toon/settings/averageShadowSettings.js +181 -0
  103. package/src/toon/settings/baseTextureSettings.js +133 -0
  104. package/src/toon/settings/celShadeSettings.js +96 -0
  105. package/src/toon/settings/contactShadowSettings.js +114 -0
  106. package/src/toon/settings/eyeHighlightSettings.js +187 -0
  107. package/src/toon/settings/faceLightingSettings.js +135 -0
  108. package/src/toon/settings/furSettings.js +112 -0
  109. package/src/toon/settings/glitterSettings.js +102 -0
  110. package/src/toon/settings/hairHighlightSettings.js +325 -0
  111. package/src/toon/settings/indirectLightSettings.js +244 -0
  112. package/src/toon/settings/localLightSettings.js +176 -0
  113. package/src/toon/settings/materialMapSettings.js +299 -0
  114. package/src/toon/settings/outlineSettings.js +263 -0
  115. package/src/toon/settings/perspectiveRemovalSettings.js +60 -0
  116. package/src/toon/settings/rimLightSettings.js +268 -0
  117. package/src/toon/settings/sceneShadowSettings.js +147 -0
  118. package/src/toon/settings/selfShadowSettings.js +221 -0
  119. package/src/toon/settings/shadowColorSettings.js +238 -0
  120. package/src/toon/settings/skinToneSettings.js +150 -0
  121. package/src/toon/settings/specularSettings.js +314 -0
  122. package/src/toon/settings/stickerSettings.js +102 -0
  123. package/src/toon/toonMaterialAdapter.js +2235 -0
  124. package/src/toon/toonSettings.js +860 -0
  125. package/src/vegetation/flowerSpecies.js +260 -0
  126. package/src/vegetation/index.js +10 -0
  127. package/src/vegetation/stylizedBush.js +136 -0
  128. package/src/vegetation/stylizedFlower.js +213 -0
  129. package/src/vegetation/stylizedFlowers.js +297 -0
  130. package/src/vegetation/stylizedGrass.js +491 -0
  131. package/src/vegetation/stylizedTree.js +2449 -0
  132. package/src/vegetation/stylizedTreeFoliage.js +691 -0
  133. package/src/vegetation/treeExport.js +300 -0
  134. package/src/vegetation/treeRecipe.js +647 -0
  135. package/src/water/index.js +2 -0
  136. package/src/water/water.js +20 -0
  137. package/src/water/waterBreakerSystem.js +591 -0
  138. package/src/water/waterInteraction.js +193 -0
  139. package/src/water/waterMaterial.js +263 -0
  140. package/src/water/waterRain.js +136 -0
  141. package/src/water/waterRippleSimulation.js +227 -0
  142. package/src/water/waterScenePasses.js +415 -0
  143. package/src/water/waterSettings.js +1138 -0
  144. package/src/water/waterSplashSystem.js +251 -0
  145. package/src/water/waterSurface.js +475 -0
  146. package/src/water/waterVegetation.js +83 -0
@@ -0,0 +1,160 @@
1
+ import * as THREE from 'three';
2
+
3
+ import { setEnvironmentAmbientProbeColors } from './environmentShaderMaterials.js';
4
+
5
+ // Six-direction ambient probe: renders the scene along ±X/±Y/±Z from one
6
+ // point (typically the room center at head height), averages each view to a
7
+ // single color, and publishes the result to every environment material via
8
+ // the shared ambientProbe uniform. One capture, no per-frame cost — refresh
9
+ // explicitly when lighting changes (time of day, lamps toggled).
10
+
11
+ const PROBE_DIRECTIONS = [
12
+ { look: new THREE.Vector3(1, 0, 0), up: new THREE.Vector3(0, 1, 0) },
13
+ { look: new THREE.Vector3(-1, 0, 0), up: new THREE.Vector3(0, 1, 0) },
14
+ { look: new THREE.Vector3(0, 1, 0), up: new THREE.Vector3(0, 0, -1) },
15
+ { look: new THREE.Vector3(0, -1, 0), up: new THREE.Vector3(0, 0, 1) },
16
+ { look: new THREE.Vector3(0, 0, 1), up: new THREE.Vector3(0, 1, 0) },
17
+ { look: new THREE.Vector3(0, 0, -1), up: new THREE.Vector3(0, 1, 0) },
18
+ ];
19
+
20
+ function averageProbePixels(pixels, resolution, intensity) {
21
+ let r = 0;
22
+ let g = 0;
23
+ let b = 0;
24
+ const pixelCount = resolution * resolution;
25
+ for (let i = 0; i < pixelCount; i += 1) {
26
+ r += pixels[i * 4];
27
+ g += pixels[i * 4 + 1];
28
+ b += pixels[i * 4 + 2];
29
+ }
30
+ const scale = intensity / (pixelCount * 255);
31
+ return new THREE.Color(r * scale, g * scale, b * scale);
32
+ }
33
+
34
+ /**
35
+ * On the classic renderer this is fully synchronous and returns the colors.
36
+ * On the node backends (WebGPU / forced WebGL2) pixel readback is async, so
37
+ * it returns a Promise<colors>; `apply` publishes to the environment
38
+ * materials either way, so fire-and-forget callers work on both.
39
+ */
40
+ // Node backends keep one cached probe target per renderer: disposing the
41
+ // target immediately after the async readback races WebGPU's deferred
42
+ // command submission (validation: "buffer used in submit while destroyed").
43
+ const nodeBackendProbeTargets = new WeakMap();
44
+
45
+ export function captureEnvironmentAmbientProbe({
46
+ renderer,
47
+ scene,
48
+ position = new THREE.Vector3(),
49
+ near = 0.05,
50
+ far = 200,
51
+ resolution = 16,
52
+ // Scales the averaged colors before publishing; 1 keeps the scene's own
53
+ // brightness.
54
+ intensity = 1,
55
+ apply = true,
56
+ } = {}) {
57
+ if (!renderer || !scene) return null;
58
+
59
+ const isNodeBackend = Boolean(renderer.isWebGPURenderer);
60
+ let renderTarget = null;
61
+ if (isNodeBackend) {
62
+ const atlasWidth = resolution * PROBE_DIRECTIONS.length;
63
+ const cached = nodeBackendProbeTargets.get(renderer);
64
+ if (cached?.width === atlasWidth) {
65
+ renderTarget = cached;
66
+ } else {
67
+ cached?.dispose();
68
+ renderTarget = new THREE.WebGLRenderTarget(atlasWidth, resolution, {
69
+ minFilter: THREE.LinearFilter,
70
+ magFilter: THREE.LinearFilter,
71
+ });
72
+ nodeBackendProbeTargets.set(renderer, renderTarget);
73
+ }
74
+ } else {
75
+ renderTarget = new THREE.WebGLRenderTarget(resolution, resolution, {
76
+ minFilter: THREE.LinearFilter,
77
+ magFilter: THREE.LinearFilter,
78
+ });
79
+ }
80
+ const probeCamera = new THREE.PerspectiveCamera(90, 1, near, far);
81
+ probeCamera.position.copy(position);
82
+
83
+ const colors = [];
84
+
85
+ const currentRenderTarget = renderer.getRenderTarget();
86
+ const currentXrEnabled = renderer.xr.enabled;
87
+ renderer.xr.enabled = false;
88
+
89
+ const finish = () => {
90
+ renderer.setRenderTarget(currentRenderTarget);
91
+ renderer.xr.enabled = currentXrEnabled;
92
+ if (!isNodeBackend) renderTarget.dispose();
93
+ // The camera looking along +X sees the -X half of the room; a surface
94
+ // normal pointing +X is lit by what lies in that view. So the probe slot
95
+ // for +X normals is the +X-looking capture directly.
96
+ if (apply) setEnvironmentAmbientProbeColors(colors);
97
+ return colors;
98
+ };
99
+
100
+ const aimCamera = (direction) => {
101
+ probeCamera.up.copy(direction.up);
102
+ probeCamera.lookAt(position.clone().add(direction.look));
103
+ probeCamera.updateMatrixWorld();
104
+ };
105
+
106
+ if (renderer.isWebGPURenderer) {
107
+ // All six faces render back-to-back into one 6-tile atlas within a single
108
+ // JS tick, then ONE async readback: awaiting between renders lets main
109
+ // frames interleave, and the render-object revalidation that follows
110
+ // destroys probe-context buffers while earlier probe submits are still
111
+ // queued (WebGPU validation: "used in submit while destroyed").
112
+ return (async () => {
113
+ const previousAutoClear = renderer.autoClear;
114
+ renderer.setRenderTarget(renderTarget);
115
+ renderer.setScissorTest?.(false);
116
+ renderer.clear();
117
+ // One manual clear for the whole atlas; per-render autoClear would wipe
118
+ // the previously rendered tiles.
119
+ renderer.autoClear = false;
120
+ PROBE_DIRECTIONS.forEach((direction, index) => {
121
+ aimCamera(direction);
122
+ renderer.setViewport(index * resolution, 0, resolution, resolution);
123
+ renderer.render(scene, probeCamera);
124
+ });
125
+ renderer.autoClear = previousAutoClear;
126
+ const pixels = await renderer.readRenderTargetPixelsAsync(
127
+ renderTarget, 0, 0, resolution * PROBE_DIRECTIONS.length, resolution,
128
+ );
129
+ const atlasWidth = resolution * PROBE_DIRECTIONS.length;
130
+ for (let index = 0; index < PROBE_DIRECTIONS.length; index += 1) {
131
+ let r = 0;
132
+ let g = 0;
133
+ let b = 0;
134
+ for (let y = 0; y < resolution; y += 1) {
135
+ for (let x = 0; x < resolution; x += 1) {
136
+ const p = (y * atlasWidth + index * resolution + x) * 4;
137
+ r += pixels[p];
138
+ g += pixels[p + 1];
139
+ b += pixels[p + 2];
140
+ }
141
+ }
142
+ const scale = intensity / (resolution * resolution * 255);
143
+ colors.push(new THREE.Color(r * scale, g * scale, b * scale));
144
+ }
145
+ renderer.setViewport(0, 0, renderer.domElement.width, renderer.domElement.height);
146
+ return finish();
147
+ })();
148
+ }
149
+
150
+ const pixels = new Uint8Array(resolution * resolution * 4);
151
+ for (const direction of PROBE_DIRECTIONS) {
152
+ aimCamera(direction);
153
+ renderer.setRenderTarget(renderTarget);
154
+ renderer.clear();
155
+ renderer.render(scene, probeCamera);
156
+ renderer.readRenderTargetPixels(renderTarget, 0, 0, resolution, resolution, pixels);
157
+ colors.push(averageProbePixels(pixels, resolution, intensity));
158
+ }
159
+ return finish();
160
+ }
@@ -0,0 +1,336 @@
1
+ import { syncToonSceneLights } from '../shaders-tsl/chunks/character-scene-lights.js';
2
+ import {
3
+ classifyEnvironmentMaterialRole,
4
+ toMaterialArray,
5
+ } from './environmentMaterialClassifier.js';
6
+
7
+ // Environment node materials read scene lights from the shared toon light
8
+ // uniforms (shaders-tsl/chunks/character-scene-lights.js) instead of three's
9
+ // classic light blocks; converted meshes mirror the light state right before
10
+ // they render, exactly like the toon adapter's toonNodeLightSync. Both
11
+ // renderers invoke Object3D.onBeforeRender.
12
+ function environmentNodeLightSync(renderer, scene, camera) {
13
+ syncToonSceneLights(scene, camera);
14
+ }
15
+ import {
16
+ applyEnvironmentSettingsToMaterial,
17
+ createEnvironmentSettings,
18
+ normalizeEnvironmentDebugMode,
19
+ } from './environmentSettings.js';
20
+ import {
21
+ createDebugEnvironmentMaterial,
22
+ createEnvironmentAoMaterial,
23
+ createEnvironmentMaterial,
24
+ createEnvironmentShadowMaterial,
25
+ setEnvironmentDebugOutput,
26
+ } from './environmentShaderMaterials.js';
27
+ import {
28
+ resolveEnvironmentTextureSet,
29
+ } from './environmentTextureResolver.js';
30
+ import {
31
+ bakeEnvironmentVertexAo,
32
+ } from './environmentVertexAo.js';
33
+ import {
34
+ DEFAULT_SCAN_STYLIZE_PARAMS,
35
+ applyScanStylizeToMaterial,
36
+ isScanAssetMaterial,
37
+ stylizeScanBaseMap,
38
+ } from './scanAssetStylize.js';
39
+
40
+ export {
41
+ DEFAULT_SCAN_STYLIZE_PARAMS,
42
+ applyScanStylizeToMaterial,
43
+ isScanAssetMaterial,
44
+ stylizeScanBaseMap,
45
+ } from './scanAssetStylize.js';
46
+
47
+ export {
48
+ ENVIRONMENT_MATERIAL_ROLES,
49
+ alphaCutoffForMaterial,
50
+ classifyEnvironmentMaterialRole,
51
+ isAoOverlayMaterial,
52
+ isEmissiveEnvironmentMaterial,
53
+ isEnvironmentAoOverlay,
54
+ isEnvironmentShadowMesh,
55
+ isFoliageMaterial,
56
+ isUtilityTextureLabel,
57
+ isWindowCutoutMaterial,
58
+ materialBaseColor,
59
+ materialText,
60
+ objectMaterialText,
61
+ sourceOpacity,
62
+ textureLabel,
63
+ textureSourceUrl,
64
+ toMaterialArray,
65
+ usesAlphaCutout,
66
+ } from './environmentMaterialClassifier.js';
67
+
68
+ export {
69
+ DEFAULT_ENVIRONMENT_FEATURES,
70
+ DEFAULT_ENVIRONMENT_PARAMETERS,
71
+ ENVIRONMENT_DEBUG_MODES,
72
+ ENVIRONMENT_SETTING_FIELD_SCHEMA,
73
+ ENVIRONMENT_SETTING_GROUPS,
74
+ applyEnvironmentSettingsToMaterial,
75
+ createEnvironmentSettings,
76
+ normalizeEnvironmentDebugMode,
77
+ updateEnvironmentBoundsUniforms,
78
+ } from './environmentSettings.js';
79
+
80
+ export {
81
+ advanceEnvironmentShaderTime,
82
+ createDebugEnvironmentMaterial,
83
+ createEnvironmentAoMaterial,
84
+ createEnvironmentMaterial,
85
+ createEnvironmentShadowMaterial,
86
+ createEnvironmentWindowOpeningMaterial,
87
+ resetEnvironmentShaderTime,
88
+ setEnvironmentAmbientProbeColors,
89
+ setEnvironmentCloudShadow,
90
+ setEnvironmentDebugOutput,
91
+ setEnvironmentOpenings,
92
+ setEnvironmentPlanarReflection,
93
+ } from './environmentShaderMaterials.js';
94
+
95
+ export {
96
+ copyTextureTransform,
97
+ fallbackEnvironmentBlackTexture,
98
+ fallbackEnvironmentNormalTexture,
99
+ fallbackEnvironmentWhiteTexture,
100
+ loadEnvironmentTexture,
101
+ resolveEnvironmentTextureSet,
102
+ textureKindCandidateUrl,
103
+ textureUrlExists,
104
+ } from './environmentTextureResolver.js';
105
+
106
+ export { bakeEnvironmentVertexAo } from './environmentVertexAo.js';
107
+
108
+ function resolveEnvironmentSettings({ features = {}, parameters = {}, settings = {} } = {}) {
109
+ const mergedSettings = createEnvironmentSettings(settings);
110
+ return createEnvironmentSettings({
111
+ features: {
112
+ ...mergedSettings.features,
113
+ ...features,
114
+ },
115
+ parameters: {
116
+ ...mergedSettings.parameters,
117
+ ...parameters,
118
+ },
119
+ });
120
+ }
121
+
122
+ // The FBX/legacy loaders name the second uv set uv2; the shader reads uv1
123
+ // (the modern three.js convention). Alias so both work.
124
+ function ensureUv2Attribute(geometry) {
125
+ if (geometry.attributes.uv1) return true;
126
+ if (geometry.attributes.uv2) {
127
+ geometry.setAttribute('uv1', geometry.attributes.uv2);
128
+ return true;
129
+ }
130
+ return false;
131
+ }
132
+
133
+ function createConvertedEnvironmentMaterial(mat, options) {
134
+ return options.normalizedShaderMode === 'anime'
135
+ ? createEnvironmentMaterial(mat, options.textureSet, options)
136
+ : createDebugEnvironmentMaterial(mat, options.normalizedShaderMode, options.textureSet, {
137
+ hasVertexColors: options.hasVertexColors,
138
+ });
139
+ }
140
+
141
+ // Converts every mesh under root to the environment shader.
142
+ //
143
+ // Options beyond the original set:
144
+ // - roleOverrides: [{ match: string|RegExp, role }] — corrects heuristic
145
+ // classification without renaming assets (userData.envRole wins over both).
146
+ // - bakeVertexAo: 'auto' | true | false — bake per-vertex ambient occlusion
147
+ // at conversion for grounding. 'auto' bakes untextured meshes only (the
148
+ // input class with no baked AO of its own) within the vertex budget.
149
+ // - vertexAoOptions: forwarded to bakeEnvironmentVertexAo.
150
+ // - debugOutputMode: compile debug views in from the start ('off' keeps
151
+ // normal rendering; setEnvironmentDebugOutput can enable them later).
152
+ // - scanStylize: 'auto' | true | false — photoscan stylization pass
153
+ // (albedo simplify + detail-map compression, see scanAssetStylize.js).
154
+ // 'auto' applies it only to materials that detect as Megascans/Fab scans;
155
+ // material.userData.envScanStylize overrides detection per material.
156
+ // - scanStylizeParams: overrides merged over DEFAULT_SCAN_STYLIZE_PARAMS.
157
+ //
158
+ // Returns conversion counts plus a classification report:
159
+ // [{ object, material, role, source }] for every converted material.
160
+ export async function applyEnvironmentShader(root, {
161
+ bakeVertexAo = 'auto',
162
+ debugOutputMode = 'off',
163
+ environmentBox = null,
164
+ features = {},
165
+ hasSun = false,
166
+ parameters = {},
167
+ roleOverrides = null,
168
+ scanStylize = 'auto',
169
+ scanStylizeParams = {},
170
+ settings = {},
171
+ shaderMode = 'anime',
172
+ openWindows = false,
173
+ vertexAoOptions = {},
174
+ } = {}) {
175
+ const environmentSettings = resolveEnvironmentSettings({ features, parameters, settings });
176
+ const normalizedShaderMode = ['anime', 'basic', 'standard'].includes(shaderMode)
177
+ ? shaderMode
178
+ : 'anime';
179
+ const resolvedDebugMode = normalizeEnvironmentDebugMode(debugOutputMode);
180
+ let convertedMeshCount = 0;
181
+ let aoOverlayMeshCount = 0;
182
+ let windowCutoutMaterialCount = 0;
183
+ let shadowMeshCount = 0;
184
+ let vertexAoMeshCount = 0;
185
+ let scanStylizedMaterialCount = 0;
186
+ const classification = [];
187
+ const materialTextureSets = new WeakMap();
188
+ const materialPromises = [];
189
+ const scanMaterials = new Set();
190
+ const scanParams = { ...DEFAULT_SCAN_STYLIZE_PARAMS, ...scanStylizeParams };
191
+
192
+ root.traverse((obj) => {
193
+ if (!obj.isMesh || !obj.geometry || !obj.material) return;
194
+ if (obj.userData?.environmentShaderExclude) return;
195
+
196
+ const originalMaterials = toMaterialArray(obj.material);
197
+ if (!obj.geometry.attributes.normal) obj.geometry.computeVertexNormals();
198
+
199
+ for (const mat of originalMaterials) {
200
+ if (!mat) continue;
201
+ if (scanStylize === true || (scanStylize === 'auto' && isScanAssetMaterial(obj, mat))) {
202
+ scanMaterials.add(mat);
203
+ }
204
+ if (materialTextureSets.has(mat)) continue;
205
+ const promise = resolveEnvironmentTextureSet(mat)
206
+ .then((textureSet) => {
207
+ materialTextureSets.set(mat, textureSet);
208
+ });
209
+ materialPromises.push(promise);
210
+ }
211
+ });
212
+
213
+ await Promise.all(materialPromises);
214
+
215
+ // Photoscan albedo simplify runs on the resolved texture sets so both the
216
+ // converted material and the vertex-AO bake below see the stylized map.
217
+ for (const mat of scanMaterials) {
218
+ const textureSet = materialTextureSets.get(mat);
219
+ if (!textureSet || textureSet.untextured) continue;
220
+ textureSet.baseMap = stylizeScanBaseMap(textureSet.baseMap, scanParams);
221
+ }
222
+
223
+ // Vertex AO bakes before material creation so baked meshes compile with
224
+ // USE_ENV_VERTEX_AO. 'auto' targets untextured meshes with no ao/lightmap
225
+ // of their own — the flat-color input class that needs grounding most.
226
+ const vertexAoTargets = [];
227
+ if (environmentSettings.features.vertexAo && bakeVertexAo !== false && normalizedShaderMode === 'anime') {
228
+ root.traverse((obj) => {
229
+ if (!obj.isMesh || !obj.geometry || !obj.material) return;
230
+ if (obj.userData?.environmentShaderExclude) return;
231
+ if (obj.geometry.attributes.envVertexAo) return;
232
+ const materials = toMaterialArray(obj.material);
233
+ const textureSet = materialTextureSets.get(materials[0]);
234
+ if (!textureSet) return;
235
+ if (bakeVertexAo === 'auto'
236
+ && (!textureSet.untextured || textureSet.aoMap || textureSet.lightMap)) return;
237
+ vertexAoTargets.push(obj);
238
+ });
239
+ }
240
+ if (vertexAoTargets.length > 0) {
241
+ const baked = await bakeEnvironmentVertexAo(vertexAoTargets, {
242
+ occluderRoot: root,
243
+ environmentBox,
244
+ ...vertexAoOptions,
245
+ });
246
+ vertexAoMeshCount = baked.bakedMeshCount;
247
+ }
248
+
249
+ root.traverse((obj) => {
250
+ if (!obj.isMesh || !obj.geometry || !obj.material) return;
251
+ // Meshes with bespoke shaders (e.g. leaf-card tree canopies) opt out of
252
+ // conversion; the adapter would otherwise replace their material.
253
+ if (obj.userData?.environmentShaderExclude) return;
254
+
255
+ const originalMaterials = toMaterialArray(obj.material);
256
+ const roleInfos = originalMaterials.map((mat) => {
257
+ const info = classifyEnvironmentMaterialRole(obj, mat, { roleOverrides });
258
+ classification.push({
259
+ material: mat?.name ?? '',
260
+ object: obj.name ?? '',
261
+ role: info.role,
262
+ source: info.source,
263
+ });
264
+ return info;
265
+ });
266
+ const shadowMesh = environmentSettings.features.shadowMesh
267
+ && roleInfos.some((info) => info.role === 'shadowMesh');
268
+ const aoOverlay = !shadowMesh && environmentSettings.features.aoOverlay
269
+ && roleInfos.some((info) => info.role === 'aoOverlay');
270
+ const hasVertexColors = Boolean(obj.geometry.attributes.color);
271
+ const hasVertexAo = Boolean(obj.geometry.attributes.envVertexAo);
272
+ const hasUv2 = ensureUv2Attribute(obj.geometry);
273
+
274
+ const convertMaterial = (mat, index) => {
275
+ const converted = createConvertedEnvironmentMaterial(mat, {
276
+ debugOutputMode: resolvedDebugMode,
277
+ environmentBox,
278
+ environmentSettings,
279
+ hasSun,
280
+ hasUv2,
281
+ hasVertexAo,
282
+ hasVertexColors,
283
+ normalizedShaderMode,
284
+ openWindows,
285
+ role: roleInfos[index]?.role ?? null,
286
+ textureSet: materialTextureSets.get(mat),
287
+ });
288
+ if (scanMaterials.has(mat)) {
289
+ applyScanStylizeToMaterial(converted, scanParams);
290
+ scanStylizedMaterialCount += 1;
291
+ }
292
+ return converted;
293
+ };
294
+
295
+ obj.material = shadowMesh
296
+ ? (Array.isArray(obj.material)
297
+ ? originalMaterials.map((mat) => createEnvironmentShadowMaterial(mat, { hasVertexColors }))
298
+ : createEnvironmentShadowMaterial(originalMaterials[0], { hasVertexColors }))
299
+ : aoOverlay
300
+ ? (Array.isArray(obj.material)
301
+ ? originalMaterials.map((mat) => createEnvironmentAoMaterial(mat, materialTextureSets.get(mat)))
302
+ : createEnvironmentAoMaterial(originalMaterials[0], materialTextureSets.get(originalMaterials[0])))
303
+ : Array.isArray(obj.material)
304
+ ? originalMaterials.map((mat, index) => convertMaterial(mat, index))
305
+ : convertMaterial(originalMaterials[0], 0);
306
+
307
+ for (const mat of toMaterialArray(obj.material)) {
308
+ if (mat?.userData?.windowCutout) windowCutoutMaterialCount += 1;
309
+ }
310
+ if (toMaterialArray(obj.material).some((mat) => mat?.userData?.environmentMaterial)) {
311
+ obj.onBeforeRender = environmentNodeLightSync;
312
+ }
313
+ if (shadowMesh) shadowMeshCount += 1;
314
+ if (aoOverlay) aoOverlayMeshCount += 1;
315
+
316
+ obj.frustumCulled = false;
317
+ obj.castShadow = hasSun && environmentSettings.features.shadowMask;
318
+ obj.receiveShadow = hasSun && environmentSettings.features.shadowMask;
319
+ convertedMeshCount += 1;
320
+ });
321
+
322
+ // Materials compiled with the debug define still need the shared debug-mode
323
+ // uniform pointed at the requested view.
324
+ if (resolvedDebugMode > 0) setEnvironmentDebugOutput(root, resolvedDebugMode);
325
+
326
+ return {
327
+ aoOverlayMeshCount,
328
+ classification,
329
+ convertedMeshCount,
330
+ scanStylizedMaterialCount,
331
+ shaderMode: normalizedShaderMode,
332
+ shadowMeshCount,
333
+ vertexAoMeshCount,
334
+ windowCutoutMaterialCount,
335
+ };
336
+ }
@@ -0,0 +1,178 @@
1
+ import * as THREE from 'three';
2
+
3
+ export function toMaterialArray(material) {
4
+ return (Array.isArray(material) ? material : [material]).filter(Boolean);
5
+ }
6
+
7
+ export function textureLabel(texture) {
8
+ const image = texture?.image;
9
+ return [
10
+ texture?.name,
11
+ image?.currentSrc,
12
+ image?.src,
13
+ image?.name,
14
+ ].filter(Boolean).join(' ');
15
+ }
16
+
17
+ export function textureSourceUrl(texture) {
18
+ const image = texture?.image;
19
+ return image?.currentSrc || image?.src || '';
20
+ }
21
+
22
+ export function materialText(mat) {
23
+ return [
24
+ mat?.name,
25
+ textureLabel(mat?.map),
26
+ textureLabel(mat?.emissiveMap),
27
+ textureLabel(mat?.alphaMap),
28
+ ].filter(Boolean).join(' ').toLowerCase();
29
+ }
30
+
31
+ export function isUtilityTextureLabel(text) {
32
+ return /(?:^|[_\-/])(lsab|smbe|normal|nrm|height|ao|mask|esa)(?:[_.?#\-/]|$)/i.test(text);
33
+ }
34
+
35
+ export function isFoliageMaterial(mat) {
36
+ const text = materialText(mat);
37
+ return (
38
+ text.includes('leaf') ||
39
+ text.includes('grass') ||
40
+ text.includes('plant') ||
41
+ text.includes('potted') ||
42
+ text.includes('tree') ||
43
+ text.includes('banana')
44
+ );
45
+ }
46
+
47
+ export function sourceOpacity(mat) {
48
+ return Number.isFinite(mat?.opacity) ? mat.opacity : 1;
49
+ }
50
+
51
+ export function usesAlphaCutout(mat) {
52
+ const text = materialText(mat);
53
+ return (
54
+ (Number.isFinite(mat?.alphaTest) && mat.alphaTest > 0) ||
55
+ Boolean(mat?.alphaMap) ||
56
+ mat?.map?.transparent === true ||
57
+ text.includes('leaf') ||
58
+ text.includes('grass') ||
59
+ text.includes('plant') ||
60
+ text.includes('potted') ||
61
+ text.includes('mask')
62
+ );
63
+ }
64
+
65
+ export function alphaCutoffForMaterial(mat) {
66
+ if (!usesAlphaCutout(mat)) return -1.0;
67
+ if (isFoliageMaterial(mat)) return Math.max(mat?.alphaTest ?? 0.12, 0.08);
68
+ return Math.max(mat?.alphaTest ?? 0.35, 0.35);
69
+ }
70
+
71
+ export function isEmissiveEnvironmentMaterial(mat) {
72
+ const text = materialText(mat);
73
+ return (
74
+ Boolean(mat?.emissiveMap) ||
75
+ text.includes('light') ||
76
+ text.includes('lamp') ||
77
+ text.includes('fire') ||
78
+ text.includes('glow')
79
+ );
80
+ }
81
+
82
+ export function materialBaseColor(mat, { baseMapWasUtility = false, resolvedDiffuseMap = false } = {}) {
83
+ const color = mat?.color?.isColor ? mat.color.clone() : new THREE.Color(1, 1, 1);
84
+ const strongestChannel = Math.max(color.r, color.g, color.b);
85
+ if (isFoliageMaterial(mat) && strongestChannel < 0.14) {
86
+ return resolvedDiffuseMap || !baseMapWasUtility
87
+ ? new THREE.Color(1, 1, 1)
88
+ : new THREE.Color(0.26, 0.36, 0.17);
89
+ }
90
+
91
+ return color;
92
+ }
93
+
94
+ export function isWindowCutoutMaterial(mat) {
95
+ const text = materialText(mat);
96
+ if (/(screen|curtain|blind|painting|banner|poster|floor|ceiling|pillar|chair|table|cup|plate|book|vase|plant|leaf|grass|tree)/i.test(text)) {
97
+ return false;
98
+ }
99
+
100
+ return /(window|glass|pane|shutter)/i.test(text);
101
+ }
102
+
103
+ export function objectMaterialText(obj, materials) {
104
+ return [
105
+ obj?.name,
106
+ obj?.parent?.name,
107
+ ...materials.map((mat) => materialText(mat)),
108
+ ].filter(Boolean).join(' ').toLowerCase();
109
+ }
110
+
111
+ export function isEnvironmentShadowMesh(obj, materials) {
112
+ return /(?:^|[_\-\s])shadow\s*mesh(?:$|[_\-\s])|(?:^|[_\-\s])shadow(?:$|[_\-\s])/i
113
+ .test(objectMaterialText(obj, materials));
114
+ }
115
+
116
+ export function isAoOverlayMaterial(mat) {
117
+ return /(?:^|[_\-/])ao(?:[_.?#\-/]|$)|ambient.?occlusion/i.test(materialText(mat));
118
+ }
119
+
120
+ export function isEnvironmentAoOverlay(obj, materials) {
121
+ const text = objectMaterialText(obj, materials);
122
+ return /(?:^|[_\-\s])ao\d*(?:$|[_\-\s(])|mdbuild_ao/i.test(text) ||
123
+ materials.some((mat) => isAoOverlayMaterial(mat));
124
+ }
125
+
126
+ // Explicit material roles. Heuristics above are the fallback for unlabeled
127
+ // packs; authors and integrators override them without renaming assets via:
128
+ // 1. userData.envRole on the material or mesh ('standard' opts out of all
129
+ // heuristic special-casing),
130
+ // 2. conversion-time roleOverrides: [{ match: string|RegExp, role }] tested
131
+ // against the material/object naming text.
132
+ export const ENVIRONMENT_MATERIAL_ROLES = Object.freeze([
133
+ 'standard',
134
+ 'foliage',
135
+ 'window',
136
+ 'emissive',
137
+ 'shadowMesh',
138
+ 'aoOverlay',
139
+ 'glossFloor',
140
+ ]);
141
+
142
+ function normalizeEnvironmentRole(role) {
143
+ return ENVIRONMENT_MATERIAL_ROLES.includes(role) ? role : null;
144
+ }
145
+
146
+ function roleFromOverrides(text, roleOverrides) {
147
+ if (!Array.isArray(roleOverrides)) return null;
148
+ for (const override of roleOverrides) {
149
+ const match = override?.match;
150
+ const role = normalizeEnvironmentRole(override?.role);
151
+ if (!role || !match) continue;
152
+ if (match instanceof RegExp ? match.test(text) : text.includes(String(match).toLowerCase())) {
153
+ return role;
154
+ }
155
+ }
156
+ return null;
157
+ }
158
+
159
+ // Resolves the effective role for one material on one object, and reports
160
+ // where the decision came from so misclassification is diagnosable:
161
+ // { role, source: 'userData' | 'override' | 'heuristic' | 'default' }.
162
+ export function classifyEnvironmentMaterialRole(obj, mat, { roleOverrides = null } = {}) {
163
+ const explicit = normalizeEnvironmentRole(mat?.userData?.envRole)
164
+ ?? normalizeEnvironmentRole(obj?.userData?.envRole);
165
+ if (explicit) return { role: explicit, source: 'userData' };
166
+
167
+ const text = objectMaterialText(obj, [mat]);
168
+ const overridden = roleFromOverrides(text, roleOverrides);
169
+ if (overridden) return { role: overridden, source: 'override' };
170
+
171
+ const materials = [mat];
172
+ if (isEnvironmentShadowMesh(obj, materials)) return { role: 'shadowMesh', source: 'heuristic' };
173
+ if (isEnvironmentAoOverlay(obj, materials)) return { role: 'aoOverlay', source: 'heuristic' };
174
+ if (isWindowCutoutMaterial(mat)) return { role: 'window', source: 'heuristic' };
175
+ if (isFoliageMaterial(mat)) return { role: 'foliage', source: 'heuristic' };
176
+ if (isEmissiveEnvironmentMaterial(mat)) return { role: 'emissive', source: 'heuristic' };
177
+ return { role: 'standard', source: 'default' };
178
+ }