@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,684 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import {
|
|
3
|
+
cameraProjectionMatrix,
|
|
4
|
+
cameraViewMatrix,
|
|
5
|
+
Discard,
|
|
6
|
+
float,
|
|
7
|
+
Fn,
|
|
8
|
+
If,
|
|
9
|
+
instancedBufferAttribute,
|
|
10
|
+
length,
|
|
11
|
+
max,
|
|
12
|
+
mix,
|
|
13
|
+
modelWorldMatrix,
|
|
14
|
+
positionLocal,
|
|
15
|
+
sin,
|
|
16
|
+
smoothstep,
|
|
17
|
+
uniform,
|
|
18
|
+
uv,
|
|
19
|
+
varying,
|
|
20
|
+
vec2,
|
|
21
|
+
vec3,
|
|
22
|
+
vec4,
|
|
23
|
+
viewportSize,
|
|
24
|
+
} from 'three/tsl';
|
|
25
|
+
import { NodeMaterial } from 'three/webgpu';
|
|
26
|
+
|
|
27
|
+
// Scene-level environment rigs as reusable library modules: the sun rig
|
|
28
|
+
// (directional light + shadow + painterly disk/spill/beam/shaft accents),
|
|
29
|
+
// the lamp rig (warm point+spot pairs with optional spot shadows), the
|
|
30
|
+
// window backdrop billboard, and drifting dust motes. Together with
|
|
31
|
+
// applyEnvironmentShader these are the out-of-the-box recipe:
|
|
32
|
+
//
|
|
33
|
+
// await applyEnvironmentShader(root, { environmentBox, hasSun: true });
|
|
34
|
+
// const sun = createEnvironmentSunRig({ scene, environmentBox });
|
|
35
|
+
// const lamps = createEnvironmentLampRig({ scene, environmentBox, root });
|
|
36
|
+
// const backdrop = createEnvironmentBackdrop({ scene, environmentBox, textures });
|
|
37
|
+
|
|
38
|
+
const backdropTextureLoader = new THREE.TextureLoader();
|
|
39
|
+
const backdropTextureCache = new Map();
|
|
40
|
+
|
|
41
|
+
export function environmentRelativePoint(environmentBox, ratios, target = new THREE.Vector3()) {
|
|
42
|
+
const center = environmentBox.getCenter(new THREE.Vector3());
|
|
43
|
+
const size = environmentBox.getSize(new THREE.Vector3());
|
|
44
|
+
return target.set(
|
|
45
|
+
center.x + (ratios.x ?? 0) * size.x * 0.5,
|
|
46
|
+
center.y + (ratios.y ?? 0) * size.y * 0.5,
|
|
47
|
+
center.z + (ratios.z ?? 0) * size.z * 0.5,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function additiveQuadMaterial({ color, opacity, tslAlpha, tslDiscardBelow = 0 }) {
|
|
52
|
+
const uniforms = {
|
|
53
|
+
color: uniform(new THREE.Color(color)),
|
|
54
|
+
opacity: uniform(opacity),
|
|
55
|
+
};
|
|
56
|
+
const material = new NodeMaterial();
|
|
57
|
+
material.transparent = true;
|
|
58
|
+
material.depthWrite = false;
|
|
59
|
+
material.blending = THREE.AdditiveBlending;
|
|
60
|
+
material.fog = false;
|
|
61
|
+
material.fragmentNode = Fn(() => {
|
|
62
|
+
const alpha = tslAlpha(uv(), uniforms).toVar();
|
|
63
|
+
if (tslDiscardBelow > 0) Discard(alpha.lessThan(tslDiscardBelow));
|
|
64
|
+
return vec4(uniforms.color, alpha);
|
|
65
|
+
})();
|
|
66
|
+
material.uniforms = uniforms;
|
|
67
|
+
return material;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function createSunDiskMaterial({ color = new THREE.Color(1.0, 0.78, 0.38), opacity = 0.62 } = {}) {
|
|
71
|
+
return additiveQuadMaterial({
|
|
72
|
+
color,
|
|
73
|
+
opacity,
|
|
74
|
+
tslAlpha: (uvNode, uniforms) => {
|
|
75
|
+
const radius = length(uvNode.mul(2.0).sub(1.0));
|
|
76
|
+
const core = smoothstep(0.0, 0.34, radius).oneMinus();
|
|
77
|
+
const halo = smoothstep(0.26, 1.0, radius).oneMinus();
|
|
78
|
+
return core.mul(0.72).add(halo.mul(0.42)).mul(uniforms.opacity);
|
|
79
|
+
},
|
|
80
|
+
tslDiscardBelow: 0.01,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function createSunSpillMaterial({ color = new THREE.Color(1.0, 0.7, 0.34), opacity = 0.3 } = {}) {
|
|
85
|
+
const material = additiveQuadMaterial({
|
|
86
|
+
color,
|
|
87
|
+
opacity,
|
|
88
|
+
tslAlpha: (uvNode, uniforms) => {
|
|
89
|
+
const sideFade = smoothstep(0.0, 0.18, uvNode.x).mul(smoothstep(0.0, 0.18, uvNode.x.oneMinus()));
|
|
90
|
+
const startFade = smoothstep(0.0, 0.12, uvNode.y);
|
|
91
|
+
const endFade = smoothstep(0.72, 1.0, uvNode.y).oneMinus();
|
|
92
|
+
const streak = sin(uvNode.x.mul(7.0).add(uvNode.y.mul(2.5)).mul(3.14159)).mul(0.22).add(0.78);
|
|
93
|
+
return sideFade.mul(startFade).mul(endFade).mul(streak).mul(uniforms.opacity);
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
return material;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function createSunBeamMaterial({ color = new THREE.Color(1.0, 0.72, 0.34), opacity = 0.28 } = {}) {
|
|
100
|
+
const material = additiveQuadMaterial({
|
|
101
|
+
color,
|
|
102
|
+
opacity,
|
|
103
|
+
tslAlpha: (uvNode, uniforms) => {
|
|
104
|
+
const sideFade = smoothstep(0.0, 0.2, uvNode.x).mul(smoothstep(0.0, 0.2, uvNode.x.oneMinus()));
|
|
105
|
+
const lengthFade = smoothstep(0.0, 0.16, uvNode.y).mul(smoothstep(0.72, 1.0, uvNode.y).oneMinus());
|
|
106
|
+
const rib = sin(uvNode.x.mul(5.0).add(uvNode.y.mul(1.5)).mul(3.14159)).mul(0.14).add(0.86);
|
|
107
|
+
return sideFade.mul(lengthFade).mul(rib).mul(uniforms.opacity);
|
|
108
|
+
},
|
|
109
|
+
tslDiscardBelow: 0.01,
|
|
110
|
+
});
|
|
111
|
+
material.side = THREE.DoubleSide;
|
|
112
|
+
return material;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function createSunShaftMaterial({ color = new THREE.Color(1.0, 0.82, 0.52), opacity = 0.1 } = {}) {
|
|
116
|
+
return additiveQuadMaterial({
|
|
117
|
+
color,
|
|
118
|
+
opacity,
|
|
119
|
+
tslAlpha: (uvNode, uniforms) => {
|
|
120
|
+
const sideFade = smoothstep(0.0, 0.22, uvNode.x).mul(smoothstep(0.0, 0.22, uvNode.x.oneMinus()));
|
|
121
|
+
const verticalFade = smoothstep(0.0, 0.16, uvNode.y).mul(smoothstep(0.0, 0.18, uvNode.y.oneMinus()));
|
|
122
|
+
const diagonalCoord = uvNode.x.add(uvNode.y.oneMinus().mul(0.42));
|
|
123
|
+
const diagonal = smoothstep(0.08, 0.62, diagonalCoord)
|
|
124
|
+
.mul(smoothstep(0.72, 1.22, diagonalCoord).oneMinus());
|
|
125
|
+
return sideFade.mul(verticalFade).mul(diagonal).mul(uniforms.opacity);
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Directional sun with a tuned shadow map plus optional painterly accents:
|
|
131
|
+
// visible sun disk, floor light pool, angled beam, and volumetric-looking
|
|
132
|
+
// shaft quad. Every accent has an enabled flag and an opacity.
|
|
133
|
+
export function createEnvironmentSunRig({
|
|
134
|
+
scene,
|
|
135
|
+
environmentBox,
|
|
136
|
+
color = new THREE.Color(1.0, 0.94, 0.85),
|
|
137
|
+
intensity = 1,
|
|
138
|
+
sourceRatios = { x: -0.46, y: 0.98, z: -0.42 },
|
|
139
|
+
targetRatios = { x: 0.08, y: 0.12, z: 0.42 },
|
|
140
|
+
shadow = {},
|
|
141
|
+
accents = {},
|
|
142
|
+
} = {}) {
|
|
143
|
+
if (!scene || !environmentBox) return null;
|
|
144
|
+
|
|
145
|
+
const center = environmentBox.getCenter(new THREE.Vector3());
|
|
146
|
+
const size = environmentBox.getSize(new THREE.Vector3());
|
|
147
|
+
const {
|
|
148
|
+
bias = -0.00004,
|
|
149
|
+
extentRatio = 0.78,
|
|
150
|
+
mapSize = 4096,
|
|
151
|
+
normalBias = 0.004,
|
|
152
|
+
radius = 2.0,
|
|
153
|
+
} = shadow;
|
|
154
|
+
|
|
155
|
+
const group = new THREE.Group();
|
|
156
|
+
group.name = 'Environment sun rig';
|
|
157
|
+
|
|
158
|
+
const light = new THREE.DirectionalLight(color, intensity);
|
|
159
|
+
light.name = 'Environment sun key';
|
|
160
|
+
light.castShadow = shadow.enabled !== false;
|
|
161
|
+
light.shadow.mapSize.set(mapSize, mapSize);
|
|
162
|
+
light.shadow.bias = bias;
|
|
163
|
+
light.shadow.normalBias = normalBias;
|
|
164
|
+
light.shadow.radius = radius;
|
|
165
|
+
const shadowExtent = Math.max(size.x, size.z) * extentRatio;
|
|
166
|
+
light.shadow.camera.near = 0.1;
|
|
167
|
+
light.shadow.camera.far = size.length() * 3.2;
|
|
168
|
+
light.shadow.camera.left = -shadowExtent;
|
|
169
|
+
light.shadow.camera.right = shadowExtent;
|
|
170
|
+
light.shadow.camera.top = shadowExtent;
|
|
171
|
+
light.shadow.camera.bottom = -shadowExtent;
|
|
172
|
+
light.position.copy(environmentRelativePoint(environmentBox, sourceRatios));
|
|
173
|
+
light.target.position.copy(environmentRelativePoint(environmentBox, targetRatios));
|
|
174
|
+
light.shadow.camera.updateProjectionMatrix();
|
|
175
|
+
group.add(light);
|
|
176
|
+
group.add(light.target);
|
|
177
|
+
|
|
178
|
+
const diskOptions = { enabled: true, sizeRatio: 0.22, opacity: 0.62, ...accents.disk };
|
|
179
|
+
const spillOptions = { enabled: true, widthRatio: 0.5, depthRatio: 0.48, opacity: 0.3, ...accents.spill };
|
|
180
|
+
const beamOptions = { enabled: true, opacity: 0.28, ...accents.beam };
|
|
181
|
+
const shaftOptions = { enabled: true, opacity: 0.1, ...accents.shaft };
|
|
182
|
+
|
|
183
|
+
let disk = null;
|
|
184
|
+
if (diskOptions.enabled) {
|
|
185
|
+
const diskSize = size.y * diskOptions.sizeRatio;
|
|
186
|
+
disk = new THREE.Mesh(
|
|
187
|
+
new THREE.PlaneGeometry(diskSize, diskSize),
|
|
188
|
+
createSunDiskMaterial({ opacity: diskOptions.opacity }),
|
|
189
|
+
);
|
|
190
|
+
disk.name = 'Environment sun disk';
|
|
191
|
+
disk.renderOrder = -8;
|
|
192
|
+
disk.frustumCulled = false;
|
|
193
|
+
disk.position.copy(light.position).multiplyScalar(1.15);
|
|
194
|
+
group.add(disk);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let spill = null;
|
|
198
|
+
if (spillOptions.enabled) {
|
|
199
|
+
spill = new THREE.Mesh(
|
|
200
|
+
new THREE.PlaneGeometry(size.x * spillOptions.widthRatio, size.z * spillOptions.depthRatio),
|
|
201
|
+
createSunSpillMaterial({ opacity: spillOptions.opacity }),
|
|
202
|
+
);
|
|
203
|
+
spill.name = 'Environment sun floor spill';
|
|
204
|
+
spill.rotation.x = -Math.PI / 2;
|
|
205
|
+
spill.rotation.z = -0.08;
|
|
206
|
+
spill.position.set(center.x, environmentBox.min.y + 0.018, environmentBox.min.z + size.z * 0.38);
|
|
207
|
+
spill.renderOrder = 12;
|
|
208
|
+
spill.frustumCulled = false;
|
|
209
|
+
group.add(spill);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
let beam = null;
|
|
213
|
+
if (beamOptions.enabled) {
|
|
214
|
+
beam = new THREE.Mesh(
|
|
215
|
+
new THREE.PlaneGeometry(size.x * 0.38, size.z * 0.72),
|
|
216
|
+
createSunBeamMaterial({ opacity: beamOptions.opacity }),
|
|
217
|
+
);
|
|
218
|
+
beam.name = 'Environment sun beam';
|
|
219
|
+
beam.rotation.x = -Math.PI / 2;
|
|
220
|
+
beam.rotation.z = -0.34;
|
|
221
|
+
beam.position.set(center.x - size.x * 0.08, environmentBox.min.y + 0.028, environmentBox.min.z + size.z * 0.35);
|
|
222
|
+
beam.renderOrder = 11;
|
|
223
|
+
beam.frustumCulled = false;
|
|
224
|
+
group.add(beam);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let shaft = null;
|
|
228
|
+
if (shaftOptions.enabled) {
|
|
229
|
+
shaft = new THREE.Mesh(
|
|
230
|
+
new THREE.PlaneGeometry(size.x * 0.92, size.y * 0.7),
|
|
231
|
+
createSunShaftMaterial({ opacity: shaftOptions.opacity }),
|
|
232
|
+
);
|
|
233
|
+
shaft.name = 'Environment sun shaft';
|
|
234
|
+
shaft.position.set(center.x, environmentBox.min.y + size.y * 0.5, environmentBox.min.z + size.z * 0.22);
|
|
235
|
+
shaft.renderOrder = 9;
|
|
236
|
+
shaft.frustumCulled = false;
|
|
237
|
+
group.add(shaft);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
scene.add(group);
|
|
241
|
+
|
|
242
|
+
// Applies a time-of-day style state in one call: light color/intensity,
|
|
243
|
+
// sun position ratios, and accent opacities.
|
|
244
|
+
function setState({
|
|
245
|
+
color: nextColor,
|
|
246
|
+
intensity: nextIntensity,
|
|
247
|
+
sourceRatios: nextSource,
|
|
248
|
+
targetRatios: nextTarget,
|
|
249
|
+
diskOpacity,
|
|
250
|
+
spillOpacity,
|
|
251
|
+
beamOpacity,
|
|
252
|
+
shaftOpacity,
|
|
253
|
+
} = {}) {
|
|
254
|
+
if (nextColor) light.color.set(nextColor);
|
|
255
|
+
if (Number.isFinite(nextIntensity)) light.intensity = nextIntensity;
|
|
256
|
+
if (nextSource) {
|
|
257
|
+
light.position.copy(environmentRelativePoint(environmentBox, nextSource));
|
|
258
|
+
disk?.position.copy(light.position).multiplyScalar(1.15);
|
|
259
|
+
}
|
|
260
|
+
if (nextTarget) light.target.position.copy(environmentRelativePoint(environmentBox, nextTarget));
|
|
261
|
+
if (disk && Number.isFinite(diskOpacity)) disk.material.uniforms.opacity.value = diskOpacity;
|
|
262
|
+
if (spill && Number.isFinite(spillOpacity)) spill.material.uniforms.opacity.value = spillOpacity;
|
|
263
|
+
if (beam && Number.isFinite(beamOpacity)) beam.material.uniforms.opacity.value = beamOpacity;
|
|
264
|
+
if (shaft && Number.isFinite(shaftOpacity)) shaft.material.uniforms.opacity.value = shaftOpacity;
|
|
265
|
+
if (nextColor && disk) disk.material.uniforms.color.value.set(nextColor);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function setEnabled(value) {
|
|
269
|
+
group.visible = Boolean(value);
|
|
270
|
+
light.visible = Boolean(value);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function dispose() {
|
|
274
|
+
scene.remove(group);
|
|
275
|
+
[disk, spill, beam, shaft].forEach((mesh) => {
|
|
276
|
+
mesh?.geometry.dispose();
|
|
277
|
+
mesh?.material.dispose();
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return { beam, disk, dispose, group, light, setEnabled, setState, shaft, spill };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function detectLampPositions(root, environmentBox, pattern) {
|
|
285
|
+
if (!root || !environmentBox) return [];
|
|
286
|
+
const fixtureBox = new THREE.Box3();
|
|
287
|
+
const fixtureSize = new THREE.Vector3();
|
|
288
|
+
const fixtureCenter = new THREE.Vector3();
|
|
289
|
+
const found = [];
|
|
290
|
+
root.updateMatrixWorld(true);
|
|
291
|
+
root.traverse((obj) => {
|
|
292
|
+
if (!obj.isMesh || !obj.geometry) return;
|
|
293
|
+
if (!pattern.test(obj.name || '')) return;
|
|
294
|
+
fixtureBox.setFromObject(obj);
|
|
295
|
+
if (fixtureBox.isEmpty()) return;
|
|
296
|
+
fixtureBox.getSize(fixtureSize);
|
|
297
|
+
fixtureBox.getCenter(fixtureCenter);
|
|
298
|
+
found.push(new THREE.Vector3(
|
|
299
|
+
fixtureCenter.x,
|
|
300
|
+
fixtureBox.min.y + fixtureSize.y * 0.22,
|
|
301
|
+
fixtureCenter.z,
|
|
302
|
+
));
|
|
303
|
+
});
|
|
304
|
+
return found.sort((a, b) => a.x - b.x);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function fallbackLampPositions(environmentBox, { xOffsetRatio = 0.28, zOffsetRatio = 0.03, heightRatio = 0.14 } = {}) {
|
|
308
|
+
const center = environmentBox.getCenter(new THREE.Vector3());
|
|
309
|
+
const size = environmentBox.getSize(new THREE.Vector3());
|
|
310
|
+
const lampY = environmentBox.max.y - size.y * heightRatio;
|
|
311
|
+
return [
|
|
312
|
+
new THREE.Vector3(center.x - size.x * xOffsetRatio, lampY, center.z + size.z * zOffsetRatio),
|
|
313
|
+
new THREE.Vector3(center.x + size.x * xOffsetRatio, lampY, center.z + size.z * zOffsetRatio),
|
|
314
|
+
];
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Warm interior lamps: one fill point light + one shadowed downlight spot per
|
|
318
|
+
// fixture, plus an additive glow sphere. Positions are detected from fixture
|
|
319
|
+
// mesh names when possible and fall back to a symmetric ceiling pair.
|
|
320
|
+
export function createEnvironmentLampRig({
|
|
321
|
+
scene,
|
|
322
|
+
environmentBox,
|
|
323
|
+
root = null,
|
|
324
|
+
detectPattern = /lamp|light/i,
|
|
325
|
+
positions = null,
|
|
326
|
+
maxLamps = 4,
|
|
327
|
+
color = 0xffc27a,
|
|
328
|
+
intensity = 2.2,
|
|
329
|
+
distance = null,
|
|
330
|
+
decay = 1.8,
|
|
331
|
+
spot = {},
|
|
332
|
+
glow = {},
|
|
333
|
+
fallback = {},
|
|
334
|
+
} = {}) {
|
|
335
|
+
if (!scene || !environmentBox) return null;
|
|
336
|
+
|
|
337
|
+
const center = environmentBox.getCenter(new THREE.Vector3());
|
|
338
|
+
const size = environmentBox.getSize(new THREE.Vector3());
|
|
339
|
+
const {
|
|
340
|
+
enabled: spotEnabled = true,
|
|
341
|
+
intensityScale: spotIntensityScale = 1.45,
|
|
342
|
+
angle: spotAngle = 0.74,
|
|
343
|
+
penumbra: spotPenumbra = 0.82,
|
|
344
|
+
decay: spotDecay = 1.35,
|
|
345
|
+
castShadow: spotCastShadow = false,
|
|
346
|
+
shadowMapSize = 1024,
|
|
347
|
+
shadowBias = -0.0002,
|
|
348
|
+
shadowNormalBias = 0.01,
|
|
349
|
+
shadowCasterLimit = 2,
|
|
350
|
+
} = spot;
|
|
351
|
+
const {
|
|
352
|
+
enabled: glowEnabled = true,
|
|
353
|
+
sizeRatio: glowSizeRatio = 0.025,
|
|
354
|
+
opacity: glowOpacity = 0.12,
|
|
355
|
+
color: glowColor = 0xffc56f,
|
|
356
|
+
} = glow;
|
|
357
|
+
|
|
358
|
+
const lightDistance = distance ?? Math.max(2.2, size.y * 1.02);
|
|
359
|
+
const spotDistance = spot.distance ?? Math.max(lightDistance, size.y * 1.38);
|
|
360
|
+
const detected = positions ?? detectLampPositions(root, environmentBox, detectPattern);
|
|
361
|
+
const lampPositions = (detected.length >= 2 ? detected : fallbackLampPositions(environmentBox, fallback))
|
|
362
|
+
.slice(0, maxLamps);
|
|
363
|
+
|
|
364
|
+
const group = new THREE.Group();
|
|
365
|
+
group.name = 'Environment lamp rig';
|
|
366
|
+
const lamps = [];
|
|
367
|
+
|
|
368
|
+
lampPositions.forEach((position, index) => {
|
|
369
|
+
const point = new THREE.PointLight(color, intensity, lightDistance, decay);
|
|
370
|
+
point.name = `Environment lamp ${index + 1} fill`;
|
|
371
|
+
point.castShadow = false;
|
|
372
|
+
point.userData.ceilingLightBaseIntensity = intensity;
|
|
373
|
+
point.userData.ceilingLightIntensity = intensity;
|
|
374
|
+
point.position.copy(position);
|
|
375
|
+
group.add(point);
|
|
376
|
+
|
|
377
|
+
let spotLight = null;
|
|
378
|
+
if (spotEnabled) {
|
|
379
|
+
const spotIntensity = intensity * spotIntensityScale;
|
|
380
|
+
spotLight = new THREE.SpotLight(color, spotIntensity, spotDistance, spotAngle, spotPenumbra, spotDecay);
|
|
381
|
+
spotLight.name = `Environment lamp ${index + 1} downlight`;
|
|
382
|
+
// Shadowed downlights ground furniture under each lamp; budgeted to the
|
|
383
|
+
// first few fixtures because every casting spot adds a shadow pass.
|
|
384
|
+
spotLight.castShadow = Boolean(spotCastShadow) && index < shadowCasterLimit;
|
|
385
|
+
if (spotLight.castShadow) {
|
|
386
|
+
spotLight.shadow.mapSize.set(shadowMapSize, shadowMapSize);
|
|
387
|
+
spotLight.shadow.bias = shadowBias;
|
|
388
|
+
spotLight.shadow.normalBias = shadowNormalBias;
|
|
389
|
+
spotLight.shadow.camera.near = 0.1;
|
|
390
|
+
spotLight.shadow.camera.far = spotDistance;
|
|
391
|
+
}
|
|
392
|
+
spotLight.userData.ceilingLightBaseIntensity = spotIntensity;
|
|
393
|
+
spotLight.userData.ceilingLightIntensity = spotIntensity;
|
|
394
|
+
spotLight.position.copy(position);
|
|
395
|
+
spotLight.target.position.set(
|
|
396
|
+
THREE.MathUtils.lerp(position.x, center.x, 0.62),
|
|
397
|
+
environmentBox.min.y + size.y * 0.12,
|
|
398
|
+
center.z - size.z * 0.08,
|
|
399
|
+
);
|
|
400
|
+
group.add(spotLight);
|
|
401
|
+
group.add(spotLight.target);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
let glowMesh = null;
|
|
405
|
+
if (glowEnabled) {
|
|
406
|
+
glowMesh = new THREE.Mesh(
|
|
407
|
+
new THREE.SphereGeometry(size.y * glowSizeRatio, 16, 8),
|
|
408
|
+
new THREE.MeshBasicMaterial({
|
|
409
|
+
color: glowColor,
|
|
410
|
+
transparent: true,
|
|
411
|
+
opacity: glowOpacity,
|
|
412
|
+
depthWrite: false,
|
|
413
|
+
blending: THREE.AdditiveBlending,
|
|
414
|
+
}),
|
|
415
|
+
);
|
|
416
|
+
glowMesh.name = `Environment lamp ${index + 1} glow`;
|
|
417
|
+
glowMesh.userData.ceilingLightBaseOpacity = glowOpacity;
|
|
418
|
+
glowMesh.userData.ceilingLightOpacity = glowOpacity;
|
|
419
|
+
glowMesh.position.copy(position);
|
|
420
|
+
glowMesh.renderOrder = 18;
|
|
421
|
+
glowMesh.frustumCulled = false;
|
|
422
|
+
group.add(glowMesh);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
lamps.push({ glow: glowMesh, point, position: position.clone(), spot: spotLight });
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
scene.add(group);
|
|
429
|
+
|
|
430
|
+
function setIntensity(multiplier) {
|
|
431
|
+
const scale = Number.isFinite(multiplier) ? Math.max(multiplier, 0) : 1;
|
|
432
|
+
for (const lamp of lamps) {
|
|
433
|
+
lamp.point.intensity = lamp.point.userData.ceilingLightBaseIntensity * scale;
|
|
434
|
+
lamp.point.userData.ceilingLightIntensity = lamp.point.intensity;
|
|
435
|
+
if (lamp.spot) {
|
|
436
|
+
lamp.spot.intensity = lamp.spot.userData.ceilingLightBaseIntensity * scale;
|
|
437
|
+
lamp.spot.userData.ceilingLightIntensity = lamp.spot.intensity;
|
|
438
|
+
}
|
|
439
|
+
if (lamp.glow) {
|
|
440
|
+
lamp.glow.material.opacity = Math.min(lamp.glow.userData.ceilingLightBaseOpacity * scale, 0.6);
|
|
441
|
+
lamp.glow.userData.ceilingLightOpacity = lamp.glow.material.opacity;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function setEnabled(value) {
|
|
447
|
+
group.visible = Boolean(value);
|
|
448
|
+
for (const lamp of lamps) {
|
|
449
|
+
lamp.point.visible = Boolean(value);
|
|
450
|
+
if (lamp.spot) lamp.spot.visible = Boolean(value);
|
|
451
|
+
if (lamp.glow) lamp.glow.visible = Boolean(value);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function dispose() {
|
|
456
|
+
scene.remove(group);
|
|
457
|
+
for (const lamp of lamps) {
|
|
458
|
+
lamp.glow?.geometry.dispose();
|
|
459
|
+
lamp.glow?.material.dispose();
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return { dispose, group, lamps, setEnabled, setIntensity };
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Keeps fixture emissive materials in step with lamp intensity so a dimmed
|
|
467
|
+
// or extinguished lamp does not leave its shade glowing. Base strengths come
|
|
468
|
+
// from the per-material settings snapshot taken at conversion.
|
|
469
|
+
export function applyEnvironmentLampEmissive(root, multiplier) {
|
|
470
|
+
const scale = Number.isFinite(multiplier) ? Math.max(multiplier, 0) : 1;
|
|
471
|
+
root?.traverse?.((obj) => {
|
|
472
|
+
if (!obj.isMesh || !obj.material) return;
|
|
473
|
+
const materials = Array.isArray(obj.material) ? obj.material : [obj.material];
|
|
474
|
+
for (const mat of materials) {
|
|
475
|
+
if (mat?.userData?.environmentRole !== 'emissive') continue;
|
|
476
|
+
const uniform = mat.uniforms?.emissiveStrength;
|
|
477
|
+
if (!uniform) continue;
|
|
478
|
+
const base = mat.userData.environmentBaseUniformValues?.emissiveStrength ?? uniform.value;
|
|
479
|
+
uniform.value = base * scale;
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
export function environmentBackdropPeriodForHour(hour) {
|
|
485
|
+
const normalized = ((hour % 24) + 24) % 24;
|
|
486
|
+
if (normalized >= 5 && normalized < 9) return 'morning';
|
|
487
|
+
if (normalized >= 9 && normalized < 17) return 'day';
|
|
488
|
+
if (normalized >= 17 && normalized < 20) return 'evening';
|
|
489
|
+
return 'night';
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Window backdrop billboard with time-of-day texture switching. textures can
|
|
493
|
+
// be a single url/Texture or a { day, morning, evening, night } map.
|
|
494
|
+
export function createEnvironmentBackdrop({
|
|
495
|
+
scene,
|
|
496
|
+
environmentBox,
|
|
497
|
+
textures,
|
|
498
|
+
distance = null,
|
|
499
|
+
scale = null,
|
|
500
|
+
yOffset = null,
|
|
501
|
+
} = {}) {
|
|
502
|
+
if (!scene || !environmentBox || !textures) return null;
|
|
503
|
+
|
|
504
|
+
const center = environmentBox.getCenter(new THREE.Vector3());
|
|
505
|
+
const size = environmentBox.getSize(new THREE.Vector3());
|
|
506
|
+
const textureMap = typeof textures === 'string' || textures?.isTexture
|
|
507
|
+
? { day: textures }
|
|
508
|
+
: { ...textures };
|
|
509
|
+
const backdropDistance = THREE.MathUtils.clamp(
|
|
510
|
+
distance ?? size.z * 0.65,
|
|
511
|
+
Math.max(2, size.z * 0.65),
|
|
512
|
+
Math.max(30, size.z * 7.5),
|
|
513
|
+
);
|
|
514
|
+
const backdropScale = scale ?? (size.z / Math.max(backdropDistance, 0.001)) * 0.14 * backdropDistance;
|
|
515
|
+
|
|
516
|
+
const mesh = new THREE.Mesh(
|
|
517
|
+
new THREE.PlaneGeometry(1, 1),
|
|
518
|
+
new THREE.MeshBasicMaterial({ toneMapped: false }),
|
|
519
|
+
);
|
|
520
|
+
mesh.name = 'Environment backdrop';
|
|
521
|
+
mesh.position.set(
|
|
522
|
+
center.x,
|
|
523
|
+
yOffset ?? environmentBox.min.y + size.y * 0.57,
|
|
524
|
+
environmentBox.min.z - backdropDistance,
|
|
525
|
+
);
|
|
526
|
+
mesh.renderOrder = -10;
|
|
527
|
+
mesh.frustumCulled = false;
|
|
528
|
+
scene.add(mesh);
|
|
529
|
+
|
|
530
|
+
function applyTexture(texture) {
|
|
531
|
+
texture.colorSpace = THREE.SRGBColorSpace;
|
|
532
|
+
mesh.material.map = texture;
|
|
533
|
+
mesh.material.needsUpdate = true;
|
|
534
|
+
const image = texture.image;
|
|
535
|
+
const aspect = image?.width && image?.height ? image.width / image.height : 16 / 9;
|
|
536
|
+
mesh.scale.set(backdropScale * aspect, backdropScale, 1);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function setTexture(source) {
|
|
540
|
+
if (source?.isTexture) {
|
|
541
|
+
applyTexture(source);
|
|
542
|
+
return Promise.resolve(source);
|
|
543
|
+
}
|
|
544
|
+
const url = String(source);
|
|
545
|
+
if (!backdropTextureCache.has(url)) {
|
|
546
|
+
backdropTextureCache.set(url, new Promise((resolve, reject) => {
|
|
547
|
+
backdropTextureLoader.load(url, resolve, undefined, reject);
|
|
548
|
+
}));
|
|
549
|
+
}
|
|
550
|
+
return backdropTextureCache.get(url).then((texture) => {
|
|
551
|
+
applyTexture(texture);
|
|
552
|
+
return texture;
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function setPeriod(period) {
|
|
557
|
+
const source = textureMap[period] ?? textureMap.day ?? Object.values(textureMap)[0];
|
|
558
|
+
return source ? setTexture(source) : Promise.resolve(null);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function dispose() {
|
|
562
|
+
scene.remove(mesh);
|
|
563
|
+
mesh.geometry.dispose();
|
|
564
|
+
mesh.material.dispose();
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
setPeriod('day');
|
|
568
|
+
return { dispose, mesh, setPeriod, setTexture };
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Slow-drifting dust motes for sun shafts — the classic anime-interior touch.
|
|
572
|
+
// Deterministic layout (index-hashed, no Math.random) so captures repeat.
|
|
573
|
+
export function createEnvironmentDustMotes({
|
|
574
|
+
scene,
|
|
575
|
+
bounds,
|
|
576
|
+
count = 140,
|
|
577
|
+
color = 0xffe9c4,
|
|
578
|
+
size = 0.02,
|
|
579
|
+
opacity = 0.4,
|
|
580
|
+
driftSpeed = 0.05,
|
|
581
|
+
} = {}) {
|
|
582
|
+
if (!scene || !bounds) return null;
|
|
583
|
+
|
|
584
|
+
const min = bounds.min;
|
|
585
|
+
const boundsSize = bounds.getSize(new THREE.Vector3());
|
|
586
|
+
const positions = new Float32Array(count * 3);
|
|
587
|
+
const phases = new Float32Array(count);
|
|
588
|
+
const hash = (i, salt) => {
|
|
589
|
+
const value = Math.sin(i * 127.1 + salt * 311.7) * 43758.5453;
|
|
590
|
+
return value - Math.floor(value);
|
|
591
|
+
};
|
|
592
|
+
for (let i = 0; i < count; i += 1) {
|
|
593
|
+
positions[i * 3] = min.x + hash(i, 1) * boundsSize.x;
|
|
594
|
+
positions[i * 3 + 1] = min.y + hash(i, 2) * boundsSize.y;
|
|
595
|
+
positions[i * 3 + 2] = min.z + hash(i, 3) * boundsSize.z;
|
|
596
|
+
phases[i] = hash(i, 4) * Math.PI * 2;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// WGSL has no point size, so the motes become one InstancedMesh of
|
|
600
|
+
// billboarded quads whose clip-space offset reproduces the GLSL
|
|
601
|
+
// gl_PointSize pixel math exactly (docs/tsl-conventions.md).
|
|
602
|
+
const uniforms = {
|
|
603
|
+
boundsHeight: uniform(boundsSize.y),
|
|
604
|
+
boundsMinY: uniform(min.y),
|
|
605
|
+
color: uniform(new THREE.Color(color)),
|
|
606
|
+
driftSpeed: uniform(driftSpeed),
|
|
607
|
+
opacity: uniform(opacity),
|
|
608
|
+
pointSize: uniform(size),
|
|
609
|
+
time: uniform(0),
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
const quad = new THREE.PlaneGeometry(1, 1);
|
|
613
|
+
const positionAttribute = new THREE.InstancedBufferAttribute(positions, 3);
|
|
614
|
+
const phaseAttribute = new THREE.InstancedBufferAttribute(phases, 1);
|
|
615
|
+
|
|
616
|
+
const material = new NodeMaterial();
|
|
617
|
+
material.transparent = true;
|
|
618
|
+
material.depthWrite = false;
|
|
619
|
+
material.blending = THREE.AdditiveBlending;
|
|
620
|
+
material.fog = false;
|
|
621
|
+
|
|
622
|
+
const vFade = varying(float(), 'vMoteFade');
|
|
623
|
+
material.vertexNode = Fn(() => {
|
|
624
|
+
const motePosition = instancedBufferAttribute(positionAttribute, 'vec3');
|
|
625
|
+
const motePhase = instancedBufferAttribute(phaseAttribute, 'float');
|
|
626
|
+
|
|
627
|
+
const driftedY = uniforms.boundsMinY.add(
|
|
628
|
+
motePosition.y.sub(uniforms.boundsMinY)
|
|
629
|
+
.add(uniforms.time.mul(uniforms.driftSpeed))
|
|
630
|
+
.mod(uniforms.boundsHeight),
|
|
631
|
+
);
|
|
632
|
+
const drifted = vec3(
|
|
633
|
+
motePosition.x.add(sin(uniforms.time.mul(0.31).add(motePhase)).mul(0.06)),
|
|
634
|
+
driftedY,
|
|
635
|
+
motePosition.z.add(uniforms.time.mul(0.23).add(motePhase.mul(1.7)).cos().mul(0.06)),
|
|
636
|
+
);
|
|
637
|
+
vFade.assign(sin(uniforms.time.mul(0.8).add(motePhase.mul(3.1))).mul(0.45).add(0.55));
|
|
638
|
+
|
|
639
|
+
const mvPosition = cameraViewMatrix.mul(modelWorldMatrix).mul(vec4(drifted, 1.0)).toVar();
|
|
640
|
+
const pixels = uniforms.pointSize.mul(320.0).div(max(mvPosition.z.negate(), 0.1));
|
|
641
|
+
const clipPosition = cameraProjectionMatrix.mul(mvPosition).toVar();
|
|
642
|
+
// plane(1,1) local xy spans [-0.5, 0.5] → full quad = `pixels` px, the
|
|
643
|
+
// same square gl_PointSize would rasterize.
|
|
644
|
+
clipPosition.xy.addAssign(
|
|
645
|
+
positionLocal.xy.mul(pixels).mul(float(2.0).div(viewportSize)).mul(clipPosition.w),
|
|
646
|
+
);
|
|
647
|
+
return clipPosition;
|
|
648
|
+
})();
|
|
649
|
+
|
|
650
|
+
material.fragmentNode = Fn(() => {
|
|
651
|
+
const p = uv().mul(2.0).sub(1.0);
|
|
652
|
+
const alpha = smoothstep(0.3, 1.0, length(p)).oneMinus()
|
|
653
|
+
.mul(uniforms.opacity)
|
|
654
|
+
.mul(vFade)
|
|
655
|
+
.toVar();
|
|
656
|
+
Discard(alpha.lessThan(0.01));
|
|
657
|
+
return vec4(uniforms.color, alpha);
|
|
658
|
+
})();
|
|
659
|
+
material.uniforms = uniforms;
|
|
660
|
+
|
|
661
|
+
const instances = new THREE.InstancedMesh(quad, material, count);
|
|
662
|
+
const identity = new THREE.Matrix4();
|
|
663
|
+
for (let i = 0; i < count; i += 1) instances.setMatrixAt(i, identity);
|
|
664
|
+
instances.instanceMatrix.needsUpdate = true;
|
|
665
|
+
instances.name = 'Environment dust motes';
|
|
666
|
+
instances.frustumCulled = false;
|
|
667
|
+
instances.renderOrder = 16;
|
|
668
|
+
scene.add(instances);
|
|
669
|
+
|
|
670
|
+
return {
|
|
671
|
+
dispose() {
|
|
672
|
+
scene.remove(instances);
|
|
673
|
+
quad.dispose();
|
|
674
|
+
material.dispose();
|
|
675
|
+
},
|
|
676
|
+
points: instances,
|
|
677
|
+
setOpacity(value) {
|
|
678
|
+
if (Number.isFinite(value)) uniforms.opacity.value = value;
|
|
679
|
+
},
|
|
680
|
+
update(delta) {
|
|
681
|
+
uniforms.time.value += Math.min(Math.max(delta ?? 0, 0), 0.1);
|
|
682
|
+
},
|
|
683
|
+
};
|
|
684
|
+
}
|