@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.
- package/ATTRIBUTION.md +40 -0
- package/LICENSE +21 -0
- package/README.md +125 -0
- package/package.json +97 -0
- package/src/character/characterRig.js +353 -0
- package/src/character/freestyleSwimClip.js +541 -0
- package/src/character/index.js +4 -0
- package/src/character/modelLoader.js +433 -0
- package/src/core/materialRoles.js +438 -0
- package/src/core/presetDocuments.js +110 -0
- package/src/core/shaderBackend.js +24 -0
- package/src/debrisgen/debrisFields.js +544 -0
- package/src/debrisgen/debrisGenerator.js +1929 -0
- package/src/debrisgen/debrisPalettes.js +71 -0
- package/src/debrisgen/debrisPhysics.js +198 -0
- package/src/debrisgen/debrisPresets.js +155 -0
- package/src/debrisgen/debrisSettings.js +333 -0
- package/src/debrisgen/debrisTextures.js +380 -0
- package/src/debrisgen/index.js +5 -0
- package/src/debug/fieldValues.js +95 -0
- package/src/debug/index.js +7 -0
- package/src/debug/settingsPanel.js +170 -0
- package/src/environment/environmentAmbientProbe.js +160 -0
- package/src/environment/environmentMaterialAdapter.js +336 -0
- package/src/environment/environmentMaterialClassifier.js +178 -0
- package/src/environment/environmentPlanarReflection.js +201 -0
- package/src/environment/environmentPresets.js +443 -0
- package/src/environment/environmentRigs.js +684 -0
- package/src/environment/environmentSettings.js +504 -0
- package/src/environment/environmentShaderMaterials.js +318 -0
- package/src/environment/environmentSunShadowPass.js +268 -0
- package/src/environment/environmentTextureResolver.js +161 -0
- package/src/environment/environmentTimeOfDay.js +167 -0
- package/src/environment/environmentVertexAo.js +188 -0
- package/src/environment/index.js +8 -0
- package/src/environment/scanAssetStylize.js +103 -0
- package/src/index.js +21 -0
- package/src/loaders/index.js +1 -0
- package/src/post/index.js +2 -0
- package/src/post/postProcessing.js +1087 -0
- package/src/rockgen/export/glbExport.js +177 -0
- package/src/rockgen/heightfield/heightfieldErosion.js +7 -0
- package/src/rockgen/heightfield/heightfieldPatch.js +183 -0
- package/src/rockgen/heightfield/stylizedErosionSim.js +356 -0
- package/src/rockgen/index.js +14 -0
- package/src/rockgen/mesh/meshAttributes.js +374 -0
- package/src/rockgen/mesh/meshDocument.js +158 -0
- package/src/rockgen/mesh/surfaceNets.js +329 -0
- package/src/rockgen/noise/cellularNoise3.js +92 -0
- package/src/rockgen/noise/prng.js +62 -0
- package/src/rockgen/noise/simplexNoise3.js +89 -0
- package/src/rockgen/noise/valueNoise3.js +87 -0
- package/src/rockgen/rockDocument.js +255 -0
- package/src/rockgen/rockHelpers.js +8 -0
- package/src/rockgen/rockgenPresets.js +559 -0
- package/src/rockgen/rockgenSettings.js +756 -0
- package/src/rockgen/sdf/fieldCompiler.js +544 -0
- package/src/rockgen/sdf/sculptEdits.js +66 -0
- package/src/rockgen/sdf/sdfModifiers.js +45 -0
- package/src/rockgen/sdf/sdfOps.js +37 -0
- package/src/rockgen/sdf/sdfPrimitives.js +88 -0
- package/src/shaders-tsl/anime.js +963 -0
- package/src/shaders-tsl/chunks/character-color.js +64 -0
- package/src/shaders-tsl/chunks/character-highlights.js +174 -0
- package/src/shaders-tsl/chunks/character-lighting.js +309 -0
- package/src/shaders-tsl/chunks/character-material-maps.js +146 -0
- package/src/shaders-tsl/chunks/character-roles.js +52 -0
- package/src/shaders-tsl/chunks/character-scene-lights.js +270 -0
- package/src/shaders-tsl/chunks/character-shadow-color.js +61 -0
- package/src/shaders-tsl/chunks/character-skinning.js +140 -0
- package/src/shaders-tsl/chunks/environment-color.js +47 -0
- package/src/shaders-tsl/chunks/environment-debug.js +79 -0
- package/src/shaders-tsl/chunks/environment-lighting.js +260 -0
- package/src/shaders-tsl/chunks/environment-sun-shadow.js +87 -0
- package/src/shaders-tsl/chunks/foliage-fog.js +60 -0
- package/src/shaders-tsl/chunks/pass-depth-color.js +89 -0
- package/src/shaders-tsl/chunks/stylized-cloud-shadow.js +70 -0
- package/src/shaders-tsl/chunks/water-color.js +190 -0
- package/src/shaders-tsl/chunks/water-common.js +124 -0
- package/src/shaders-tsl/chunks/water-foam.js +93 -0
- package/src/shaders-tsl/chunks/water-lighting.js +116 -0
- package/src/shaders-tsl/chunks/water-ripple.js +64 -0
- package/src/shaders-tsl/chunks/water-waves.js +100 -0
- package/src/shaders-tsl/environment-ao-overlay.js +60 -0
- package/src/shaders-tsl/environment.js +680 -0
- package/src/shaders-tsl/flower.js +267 -0
- package/src/shaders-tsl/grass.js +193 -0
- package/src/shaders-tsl/post-composite.js +461 -0
- package/src/shaders-tsl/sky.js +175 -0
- package/src/shaders-tsl/tree-leaf.js +264 -0
- package/src/shaders-tsl/water-breaker.js +423 -0
- package/src/shaders-tsl/water-kelp.js +171 -0
- package/src/shaders-tsl/water-rain.js +107 -0
- package/src/shaders-tsl/water-simulation.js +140 -0
- package/src/shaders-tsl/water-splash.js +298 -0
- package/src/shaders-tsl/water.js +509 -0
- package/src/sky/index.js +2 -0
- package/src/sky/stylizedSky.js +341 -0
- package/src/toon/characterRenderPasses.js +646 -0
- package/src/toon/index.js +4 -0
- package/src/toon/settings/alphaSettings.js +170 -0
- package/src/toon/settings/averageShadowSettings.js +181 -0
- package/src/toon/settings/baseTextureSettings.js +133 -0
- package/src/toon/settings/celShadeSettings.js +96 -0
- package/src/toon/settings/contactShadowSettings.js +114 -0
- package/src/toon/settings/eyeHighlightSettings.js +187 -0
- package/src/toon/settings/faceLightingSettings.js +135 -0
- package/src/toon/settings/furSettings.js +112 -0
- package/src/toon/settings/glitterSettings.js +102 -0
- package/src/toon/settings/hairHighlightSettings.js +325 -0
- package/src/toon/settings/indirectLightSettings.js +244 -0
- package/src/toon/settings/localLightSettings.js +176 -0
- package/src/toon/settings/materialMapSettings.js +299 -0
- package/src/toon/settings/outlineSettings.js +263 -0
- package/src/toon/settings/perspectiveRemovalSettings.js +60 -0
- package/src/toon/settings/rimLightSettings.js +268 -0
- package/src/toon/settings/sceneShadowSettings.js +147 -0
- package/src/toon/settings/selfShadowSettings.js +221 -0
- package/src/toon/settings/shadowColorSettings.js +238 -0
- package/src/toon/settings/skinToneSettings.js +150 -0
- package/src/toon/settings/specularSettings.js +314 -0
- package/src/toon/settings/stickerSettings.js +102 -0
- package/src/toon/toonMaterialAdapter.js +2235 -0
- package/src/toon/toonSettings.js +860 -0
- package/src/vegetation/flowerSpecies.js +260 -0
- package/src/vegetation/index.js +10 -0
- package/src/vegetation/stylizedBush.js +136 -0
- package/src/vegetation/stylizedFlower.js +213 -0
- package/src/vegetation/stylizedFlowers.js +297 -0
- package/src/vegetation/stylizedGrass.js +491 -0
- package/src/vegetation/stylizedTree.js +2449 -0
- package/src/vegetation/stylizedTreeFoliage.js +691 -0
- package/src/vegetation/treeExport.js +300 -0
- package/src/vegetation/treeRecipe.js +647 -0
- package/src/water/index.js +2 -0
- package/src/water/water.js +20 -0
- package/src/water/waterBreakerSystem.js +591 -0
- package/src/water/waterInteraction.js +193 -0
- package/src/water/waterMaterial.js +263 -0
- package/src/water/waterRain.js +136 -0
- package/src/water/waterRippleSimulation.js +227 -0
- package/src/water/waterScenePasses.js +415 -0
- package/src/water/waterSettings.js +1138 -0
- package/src/water/waterSplashSystem.js +251 -0
- package/src/water/waterSurface.js +475 -0
- 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
|
+
}
|