sparcoon 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/emitter/FXEmitter.Internal.d.ts +1 -0
- package/dist/emitter/FXEmitter.Internal.js +2 -1
- package/dist/emitter/FXEmitter.d.ts +7 -0
- package/dist/emitter/FXEmitter.js +15 -2
- package/dist/instancedParticle/FXInstancedParticle.d.ts +5 -1
- package/dist/instancedParticle/FXInstancedParticle.js +82 -5
- package/dist/materials/FXDiffuseMaterial/FXDiffuseMaterial.Internal.d.ts +18 -2
- package/dist/materials/FXDiffuseMaterial/FXDiffuseMaterial.Internal.js +166 -26
- package/dist/materials/FXDiffuseMaterial/FXDiffuseMaterial.d.ts +14 -3
- package/dist/materials/FXDiffuseMaterial/FXDiffuseMaterial.js +17 -1
- package/dist/materials/FXMaterial/FXDepthMaterial.Internal.d.ts +4 -0
- package/dist/materials/FXMaterial/FXDepthMaterial.Internal.js +139 -0
- package/dist/materials/FXMaterial/FXDistanceMaterial.Internal.d.ts +4 -0
- package/dist/materials/FXMaterial/FXDistanceMaterial.Internal.js +171 -0
- package/dist/materials/FXMaterial/FXMaterial.d.ts +2 -1
- package/dist/materials/FXMaterial/FXMaterial.js +6 -2
- package/dist/materials/FXUnlitMaterial/FXUnlitMaterial.Internal.js +13 -1
- package/dist/nodes/color/FXColorOverLifeNode.js +10 -4
- package/dist/nodes/texture/FXAnimatedTextureNode.d.ts +1 -0
- package/dist/nodes/texture/FXAnimatedTextureNode.js +19 -11
- package/dist/nodes/texture/FXStaticTextureNode.d.ts +1 -0
- package/dist/nodes/texture/FXStaticTextureNode.js +12 -4
- package/package.json +1 -1
- package/dist/materials/FXMaterial/FXMaterial.Internal.d.ts +0 -5
- package/dist/materials/FXMaterial/FXMaterial.Internal.js +0 -106
|
@@ -7,6 +7,7 @@ export declare const EMITTER_DEFAULT_CAPACITY_STEP = 32;
|
|
|
7
7
|
export declare const EMITTER_DEFAULT_AUTOMATICALLY_DESTROY_MODULES = true;
|
|
8
8
|
export declare const EMITTER_DEFAULT_CAST_SHADOW = false;
|
|
9
9
|
export declare const EMITTER_DEFAULT_RECEIVE_SHADOW = false;
|
|
10
|
+
export declare const EMITTER_DEFAULT_SORT_FRACTION: number;
|
|
10
11
|
export declare function collectProperties(keeper: Record<string, GLTypeInfo>, modules: readonly {
|
|
11
12
|
requiredProperties?: Record<string, FXPropertyName>;
|
|
12
13
|
}[], debugContext: string): void;
|
|
@@ -5,6 +5,7 @@ const EMITTER_DEFAULT_CAPACITY_STEP = 32;
|
|
|
5
5
|
const EMITTER_DEFAULT_AUTOMATICALLY_DESTROY_MODULES = true;
|
|
6
6
|
const EMITTER_DEFAULT_CAST_SHADOW = false;
|
|
7
7
|
const EMITTER_DEFAULT_RECEIVE_SHADOW = false;
|
|
8
|
+
const EMITTER_DEFAULT_SORT_FRACTION = 1 / 10;
|
|
8
9
|
function collectProperties(keeper, modules, debugContext) {
|
|
9
10
|
for (const module of modules) {
|
|
10
11
|
if (module.requiredProperties === undefined) {
|
|
@@ -23,4 +24,4 @@ function collectProperties(keeper, modules, debugContext) {
|
|
|
23
24
|
}
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
export { EMITTER_DEFAULT_AUTOMATICALLY_DESTROY_MODULES, EMITTER_DEFAULT_CAPACITY_STEP, EMITTER_DEFAULT_CAST_SHADOW, EMITTER_DEFAULT_EXPECTED_CAPACITY, EMITTER_DEFAULT_RECEIVE_SHADOW, collectProperties };
|
|
27
|
+
export { EMITTER_DEFAULT_AUTOMATICALLY_DESTROY_MODULES, EMITTER_DEFAULT_CAPACITY_STEP, EMITTER_DEFAULT_CAST_SHADOW, EMITTER_DEFAULT_EXPECTED_CAPACITY, EMITTER_DEFAULT_RECEIVE_SHADOW, EMITTER_DEFAULT_SORT_FRACTION, collectProperties };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Camera } from "three";
|
|
1
2
|
import { Object3D } from "three";
|
|
2
3
|
import type { FXMaterial } from "../materials/FXMaterial/FXMaterial";
|
|
3
4
|
import type { FXBehavior } from "./behavior/FXBehavior";
|
|
@@ -9,17 +10,23 @@ export interface FXEmitterOptions {
|
|
|
9
10
|
automaticallyDestroyModules: boolean;
|
|
10
11
|
castShadow: boolean;
|
|
11
12
|
receiveShadow: boolean;
|
|
13
|
+
sortCamera: Camera;
|
|
14
|
+
sortFraction: number;
|
|
12
15
|
}
|
|
13
16
|
export declare class FXEmitter extends Object3D {
|
|
14
17
|
private readonly spawnSequence;
|
|
15
18
|
private readonly behaviorSequence;
|
|
16
19
|
automaticallyDestroyModules: boolean;
|
|
20
|
+
sortCamera: Camera | undefined;
|
|
21
|
+
sortFraction: number;
|
|
17
22
|
private readonly mesh;
|
|
18
23
|
private readonly material;
|
|
19
24
|
private emissionRate;
|
|
20
25
|
private emissionAccumulator;
|
|
21
26
|
private emissionDuration;
|
|
22
27
|
private emissionElapsed;
|
|
28
|
+
private sortingAccumulator;
|
|
29
|
+
private readonly sortingCameraWorldPosition;
|
|
23
30
|
constructor(spawnSequence: FXSpawn[], behaviorSequence: readonly FXBehavior[], material: FXMaterial, options?: Partial<FXEmitterOptions>);
|
|
24
31
|
static onWillRender: (deltaTime: number) => void;
|
|
25
32
|
destroy(): void;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { Object3D } from 'three';
|
|
1
|
+
import { Object3D, Vector3 } from 'three';
|
|
2
2
|
import { FXInstancedParticle } from '../instancedParticle/FXInstancedParticle.js';
|
|
3
3
|
import { resolveGLSLTypeInfo } from '../instancedParticle/shared.js';
|
|
4
4
|
import { BUILTIN_OFFSET_AGE, BUILTIN_OFFSET_POSITION_X, BUILTIN_OFFSET_VELOCITY_X, BUILTIN_OFFSET_POSITION_Y, BUILTIN_OFFSET_VELOCITY_Y, BUILTIN_OFFSET_POSITION_Z, BUILTIN_OFFSET_VELOCITY_Z, BUILTIN_OFFSET_ROTATION, BUILTIN_OFFSET_TORQUE, BUILTIN_OFFSET_RANDOM_A, BUILTIN_OFFSET_RANDOM_B, BUILTIN_OFFSET_RANDOM_C } from '../miscellaneous/miscellaneous.js';
|
|
5
|
-
import { collectProperties, EMITTER_DEFAULT_EXPECTED_CAPACITY, EMITTER_DEFAULT_CAPACITY_STEP, EMITTER_DEFAULT_CAST_SHADOW, EMITTER_DEFAULT_RECEIVE_SHADOW, EMITTER_DEFAULT_AUTOMATICALLY_DESTROY_MODULES } from './FXEmitter.Internal.js';
|
|
5
|
+
import { collectProperties, EMITTER_DEFAULT_EXPECTED_CAPACITY, EMITTER_DEFAULT_CAPACITY_STEP, EMITTER_DEFAULT_CAST_SHADOW, EMITTER_DEFAULT_RECEIVE_SHADOW, EMITTER_DEFAULT_AUTOMATICALLY_DESTROY_MODULES, EMITTER_DEFAULT_SORT_FRACTION } from './FXEmitter.Internal.js';
|
|
6
6
|
|
|
7
7
|
const EMITTERS = new Array();
|
|
8
8
|
class FXEmitter extends Object3D {
|
|
@@ -14,6 +14,8 @@ class FXEmitter extends Object3D {
|
|
|
14
14
|
this._private_emissionAccumulator = 0;
|
|
15
15
|
this._private_emissionDuration = Infinity;
|
|
16
16
|
this._private_emissionElapsed = 0;
|
|
17
|
+
this._private_sortingAccumulator = 0;
|
|
18
|
+
this._private_sortingCameraWorldPosition = new Vector3();
|
|
17
19
|
this._private_onRendering = (deltaTime) => {
|
|
18
20
|
{
|
|
19
21
|
const { builtin } = this._private_mesh.propertyBuffers;
|
|
@@ -69,6 +71,14 @@ class FXEmitter extends Object3D {
|
|
|
69
71
|
}
|
|
70
72
|
builtin.needsUpdate = true;
|
|
71
73
|
}
|
|
74
|
+
if (this.sortCamera !== undefined) {
|
|
75
|
+
this._private_sortingAccumulator += this.sortFraction;
|
|
76
|
+
if (this._private_sortingAccumulator >= 1) {
|
|
77
|
+
this._private_sortingAccumulator -= 1;
|
|
78
|
+
this.sortCamera.getWorldPosition(this._private_sortingCameraWorldPosition);
|
|
79
|
+
this._private_mesh.sortByDistance(this._private_sortingCameraWorldPosition);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
72
82
|
};
|
|
73
83
|
const collectedProperties = {
|
|
74
84
|
builtin: resolveGLSLTypeInfo("Matrix4"),
|
|
@@ -80,12 +90,15 @@ class FXEmitter extends Object3D {
|
|
|
80
90
|
if (options.castShadow ?? EMITTER_DEFAULT_CAST_SHADOW) {
|
|
81
91
|
this._private_mesh.castShadow = true;
|
|
82
92
|
this._private_mesh.customDepthMaterial = material.buildDepthMaterial();
|
|
93
|
+
this._private_mesh.customDistanceMaterial = material.buildDistanceMaterial();
|
|
83
94
|
}
|
|
84
95
|
if (options.receiveShadow ?? EMITTER_DEFAULT_RECEIVE_SHADOW) {
|
|
85
96
|
this._private_mesh.receiveShadow = true;
|
|
86
97
|
}
|
|
87
98
|
this.automaticallyDestroyModules =
|
|
88
99
|
options.automaticallyDestroyModules ?? EMITTER_DEFAULT_AUTOMATICALLY_DESTROY_MODULES;
|
|
100
|
+
this.sortCamera = options.sortCamera;
|
|
101
|
+
this.sortFraction = options.sortFraction ?? EMITTER_DEFAULT_SORT_FRACTION;
|
|
89
102
|
this._private_material = material;
|
|
90
103
|
this.add(this._private_mesh);
|
|
91
104
|
EMITTERS.push(this);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Material } from "three";
|
|
1
|
+
import type { Material, Vector3 } from "three";
|
|
2
2
|
import { InstancedBufferAttribute, Mesh } from "three";
|
|
3
3
|
import type { GLTypeInfo } from "./shared";
|
|
4
4
|
export declare class FXInstancedParticle extends Mesh {
|
|
@@ -7,10 +7,14 @@ export declare class FXInstancedParticle extends Mesh {
|
|
|
7
7
|
private readonly instancedGeometry;
|
|
8
8
|
private readonly particleMaterial;
|
|
9
9
|
private capacity;
|
|
10
|
+
private sortingIndices;
|
|
11
|
+
private sortingSquaredDistances;
|
|
12
|
+
private sortingTemporaryBuffer;
|
|
10
13
|
constructor(varyings: Record<string, GLTypeInfo>, expectedCapacity: number, capacityStep: number, material: Material);
|
|
11
14
|
get instanceCount(): number;
|
|
12
15
|
createInstances(count: number): void;
|
|
13
16
|
removeDeadParticles(): void;
|
|
17
|
+
sortByDistance(cameraWorldPosition: Vector3): void;
|
|
14
18
|
drop(): void;
|
|
15
19
|
destroy(): void;
|
|
16
20
|
private ensureCapacity;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Mesh, InstancedBufferAttribute, StreamDrawUsage } from 'three';
|
|
2
|
-
import { BUILTIN_OFFSET_AGE, BUILTIN_OFFSET_LIFETIME } from '../miscellaneous/miscellaneous.js';
|
|
2
|
+
import { BUILTIN_OFFSET_AGE, BUILTIN_OFFSET_LIFETIME, BUILTIN_OFFSET_POSITION_X, BUILTIN_OFFSET_POSITION_Y, BUILTIN_OFFSET_POSITION_Z } from '../miscellaneous/miscellaneous.js';
|
|
3
3
|
import { INSTANCED_PARTICLE_GEOMETRY } from './FXInstancedParticle.Internal.js';
|
|
4
4
|
|
|
5
5
|
class FXInstancedParticle extends Mesh {
|
|
@@ -8,6 +8,9 @@ class FXInstancedParticle extends Mesh {
|
|
|
8
8
|
super(instancedGeometry, material);
|
|
9
9
|
this._private_capacityStep = _private_capacityStep;
|
|
10
10
|
this.propertyBuffers = {};
|
|
11
|
+
this._private_sortingIndices = new Int32Array(0);
|
|
12
|
+
this._private_sortingSquaredDistances = new Float64Array(0);
|
|
13
|
+
this._private_sortingTemporaryBuffer = new Float32Array(0);
|
|
11
14
|
this.frustumCulled = false;
|
|
12
15
|
this._private_instancedGeometry = instancedGeometry;
|
|
13
16
|
this._private_instancedGeometry.instanceCount = 0;
|
|
@@ -37,6 +40,7 @@ class FXInstancedParticle extends Mesh {
|
|
|
37
40
|
const { array: builtinArray, itemSize: builtinItemSize } = builtinBuffer;
|
|
38
41
|
const { instanceCount } = this._private_instancedGeometry;
|
|
39
42
|
let writeIndex = 0;
|
|
43
|
+
let didCompact = false;
|
|
40
44
|
for (let readIndex = 0; readIndex < instanceCount; readIndex++) {
|
|
41
45
|
const offset = readIndex * builtinItemSize;
|
|
42
46
|
if (builtinArray[offset + BUILTIN_OFFSET_AGE] < builtinArray[offset + BUILTIN_OFFSET_LIFETIME]) {
|
|
@@ -47,14 +51,72 @@ class FXInstancedParticle extends Mesh {
|
|
|
47
51
|
const srcOffset = readIndex * dataItemSize;
|
|
48
52
|
const dstOffset = writeIndex * dataItemSize;
|
|
49
53
|
dataArray.copyWithin(dstOffset, srcOffset, srcOffset + dataItemSize);
|
|
50
|
-
attribute.needsUpdate = true;
|
|
51
54
|
}
|
|
55
|
+
didCompact = true;
|
|
52
56
|
}
|
|
53
57
|
writeIndex++;
|
|
54
58
|
}
|
|
55
59
|
}
|
|
60
|
+
if (didCompact) {
|
|
61
|
+
for (const name in this.propertyBuffers) {
|
|
62
|
+
this.propertyBuffers[name].needsUpdate = true;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
56
65
|
this._private_instancedGeometry.instanceCount = writeIndex;
|
|
57
66
|
}
|
|
67
|
+
sortByDistance(cameraWorldPosition) {
|
|
68
|
+
const { instanceCount } = this._private_instancedGeometry;
|
|
69
|
+
if (instanceCount < 2) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const builtinBuffer = this.propertyBuffers.builtin;
|
|
73
|
+
if (builtinBuffer === undefined) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (this._private_sortingIndices.length < instanceCount) {
|
|
77
|
+
this._private_sortingIndices = new Int32Array(this._private_capacity);
|
|
78
|
+
this._private_sortingSquaredDistances = new Float64Array(this._private_capacity);
|
|
79
|
+
}
|
|
80
|
+
const { array: builtinArray, itemSize: builtinItemSize } = builtinBuffer;
|
|
81
|
+
for (let particleIndex = 0; particleIndex < instanceCount; particleIndex++) {
|
|
82
|
+
this._private_sortingIndices[particleIndex] = particleIndex;
|
|
83
|
+
const itemOffset = particleIndex * builtinItemSize;
|
|
84
|
+
const dx = builtinArray[itemOffset + BUILTIN_OFFSET_POSITION_X] - cameraWorldPosition.x;
|
|
85
|
+
const dy = builtinArray[itemOffset + BUILTIN_OFFSET_POSITION_Y] - cameraWorldPosition.y;
|
|
86
|
+
const dz = builtinArray[itemOffset + BUILTIN_OFFSET_POSITION_Z] - cameraWorldPosition.z;
|
|
87
|
+
this._private_sortingSquaredDistances[particleIndex] = dx * dx + dy * dy + dz * dz;
|
|
88
|
+
}
|
|
89
|
+
const sortingSquaredDistances = this._private_sortingSquaredDistances;
|
|
90
|
+
const sortingIndices = this._private_sortingIndices.subarray(0, instanceCount);
|
|
91
|
+
sortingIndices.sort((indexA, indexB) => sortingSquaredDistances[indexB] - sortingSquaredDistances[indexA]);
|
|
92
|
+
let maximumItemSize = 0;
|
|
93
|
+
for (const name in this.propertyBuffers) {
|
|
94
|
+
if (this.propertyBuffers[name].itemSize > maximumItemSize) {
|
|
95
|
+
maximumItemSize = this.propertyBuffers[name].itemSize;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const requiredTemporaryBufferSize = instanceCount * maximumItemSize;
|
|
99
|
+
if (this._private_sortingTemporaryBuffer.length < requiredTemporaryBufferSize) {
|
|
100
|
+
this._private_sortingTemporaryBuffer = new Float32Array(this._private_capacity * maximumItemSize);
|
|
101
|
+
}
|
|
102
|
+
for (const name in this.propertyBuffers) {
|
|
103
|
+
const attribute = this.propertyBuffers[name];
|
|
104
|
+
const { array, itemSize } = attribute;
|
|
105
|
+
for (let newIndex = 0; newIndex < instanceCount; newIndex++) {
|
|
106
|
+
const originalIndex = sortingIndices[newIndex];
|
|
107
|
+
const sourceOffset = originalIndex * itemSize;
|
|
108
|
+
const destinationOffset = newIndex * itemSize;
|
|
109
|
+
for (let componentIndex = 0; componentIndex < itemSize; componentIndex++) {
|
|
110
|
+
this._private_sortingTemporaryBuffer[destinationOffset + componentIndex] =
|
|
111
|
+
array[sourceOffset + componentIndex];
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
for (let i = 0; i < instanceCount * itemSize; i++) {
|
|
115
|
+
array[i] = this._private_sortingTemporaryBuffer[i];
|
|
116
|
+
}
|
|
117
|
+
attribute.needsUpdate = true;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
58
120
|
drop() {
|
|
59
121
|
this._private_instancedGeometry.instanceCount = 0;
|
|
60
122
|
}
|
|
@@ -68,17 +130,32 @@ class FXInstancedParticle extends Mesh {
|
|
|
68
130
|
}
|
|
69
131
|
const newCapacity = Math.ceil(requiredCapacity / this._private_capacityStep) * this._private_capacityStep;
|
|
70
132
|
for (const name in this.propertyBuffers) {
|
|
71
|
-
const
|
|
133
|
+
const oldAttribute = this.propertyBuffers[name];
|
|
134
|
+
const { itemSize, array, usage } = oldAttribute;
|
|
72
135
|
const newArray = new Float32Array(newCapacity * itemSize);
|
|
73
136
|
newArray.set(array);
|
|
74
137
|
const newAttribute = new InstancedBufferAttribute(newArray, itemSize);
|
|
75
138
|
newAttribute.setUsage(usage);
|
|
76
139
|
this._private_instancedGeometry.setAttribute(`a_${name}`, newAttribute);
|
|
77
140
|
this.propertyBuffers[name] = newAttribute;
|
|
141
|
+
// Detach old typed array to help GC release GPU-side memory sooner.
|
|
142
|
+
// Three.js does not expose dispose() on BufferAttribute directly,
|
|
143
|
+
// but clearing the array reference prevents the old data from lingering.
|
|
144
|
+
oldAttribute.array = null;
|
|
78
145
|
}
|
|
79
146
|
this._private_capacity = newCapacity;
|
|
80
|
-
//
|
|
81
|
-
|
|
147
|
+
// Force the renderer to recalculate _maxInstanceCount on the next
|
|
148
|
+
// setupVertexAttributes pass. The new InstancedBufferAttributes will
|
|
149
|
+
// trigger a VAO rebind, at which point _maxInstanceCount is recomputed
|
|
150
|
+
// from the current attribute sizes.
|
|
151
|
+
//
|
|
152
|
+
// _maxInstanceCount is an internal renderer cache field set by
|
|
153
|
+
// WebGLBindingStates.js. It was never promoted to a public API in the
|
|
154
|
+
// r157–r180 range. Deleting it is the standard workaround used across
|
|
155
|
+
// the Three.js ecosystem (see #19706, #26363, #27205).
|
|
156
|
+
//
|
|
157
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
158
|
+
delete this._private_instancedGeometry._maxInstanceCount;
|
|
82
159
|
}
|
|
83
160
|
}
|
|
84
161
|
|
|
@@ -1,7 +1,23 @@
|
|
|
1
1
|
import type { Blending } from "three";
|
|
2
|
-
import { MeshLambertMaterial } from "three";
|
|
2
|
+
import { Color, MeshLambertMaterial } from "three";
|
|
3
3
|
import type { GLTypeInfo } from "../../instancedParticle/shared";
|
|
4
4
|
import type { FXColorNode } from "../../nodes/color/FXColorNode";
|
|
5
5
|
import type { FXNormalNode } from "../../nodes/normal/FXNormalNode";
|
|
6
6
|
import { FXTextureNode } from "../../nodes/texture/FXTextureNode";
|
|
7
|
-
export
|
|
7
|
+
export interface FXScatterUniforms {
|
|
8
|
+
u_ScatterTint: {
|
|
9
|
+
value: Color;
|
|
10
|
+
};
|
|
11
|
+
u_ScatterPower: {
|
|
12
|
+
value: number;
|
|
13
|
+
};
|
|
14
|
+
u_ForwardScatterStrength: {
|
|
15
|
+
value: number;
|
|
16
|
+
};
|
|
17
|
+
u_BackScatterStrength: {
|
|
18
|
+
value: number;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export declare function buildFXDiffuseMaterial(varyings: Record<string, GLTypeInfo>, blending: Blending, useAlphaHashing: boolean, alphaTest: number, albedoNodes: readonly (FXColorNode | FXTextureNode)[], normalNodes: readonly (FXTextureNode | FXNormalNode)[], emissionNodes: readonly (FXColorNode | FXTextureNode)[], scatterUniforms?: FXScatterUniforms, shadowSensitivity?: {
|
|
22
|
+
value: number;
|
|
23
|
+
}, premultipliedAlpha?: boolean): MeshLambertMaterial;
|
|
@@ -2,7 +2,44 @@ import { MeshLambertMaterial, Color, DoubleSide } from 'three';
|
|
|
2
2
|
import { PARTICLE_DEFINES } from '../../miscellaneous/miscellaneous.js';
|
|
3
3
|
import { FXTextureNode } from '../../nodes/texture/FXTextureNode.js';
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Wraps every `getShadow(...)` call in the shader source with
|
|
7
|
+
* `mix(1.0, getShadow(...), <uniformName>)` so that a uniform
|
|
8
|
+
* can scale shadow influence from 0 (no shadows) to 1 (full shadows).
|
|
9
|
+
*/
|
|
10
|
+
function wrapShadowCalls(source, uniformName) {
|
|
11
|
+
const target = "getShadow(";
|
|
12
|
+
let result = "";
|
|
13
|
+
let position = 0;
|
|
14
|
+
while (position < source.length) {
|
|
15
|
+
const callStart = source.indexOf(target, position);
|
|
16
|
+
if (callStart === -1) {
|
|
17
|
+
result += source.slice(position);
|
|
18
|
+
break;
|
|
19
|
+
}
|
|
20
|
+
result += source.slice(position, callStart);
|
|
21
|
+
let depth = 0;
|
|
22
|
+
let cursor = callStart + target.length - 1;
|
|
23
|
+
for (; cursor < source.length; cursor++) {
|
|
24
|
+
if (source[cursor] === "(") {
|
|
25
|
+
depth++;
|
|
26
|
+
}
|
|
27
|
+
if (source[cursor] === ")") {
|
|
28
|
+
depth--;
|
|
29
|
+
}
|
|
30
|
+
if (depth === 0) {
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const originalCall = source.slice(callStart, cursor + 1);
|
|
35
|
+
result += `mix(1.0, ${originalCall}, ${uniformName})`;
|
|
36
|
+
position = cursor + 1;
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
function buildFXDiffuseMaterial(varyings, blending, useAlphaHashing, alphaTest, albedoNodes, normalNodes, emissionNodes, scatterUniforms, shadowSensitivity, premultipliedAlpha) {
|
|
41
|
+
const useScatter = scatterUniforms !== undefined;
|
|
42
|
+
const useShadowSensitivity = shadowSensitivity !== undefined;
|
|
6
43
|
const attributeDeclarations = [];
|
|
7
44
|
const varyingDeclarations = [];
|
|
8
45
|
const vertexAssignments = [];
|
|
@@ -13,6 +50,7 @@ function buildFXDiffuseMaterial(varyings, blending, useAlphaHashing, alphaTest,
|
|
|
13
50
|
vertexAssignments.push(`p_${name} = a_${name};`);
|
|
14
51
|
}
|
|
15
52
|
const material = new MeshLambertMaterial({
|
|
53
|
+
name: "FXDiffuseMaterial",
|
|
16
54
|
transparent: !useAlphaHashing,
|
|
17
55
|
depthWrite: useAlphaHashing,
|
|
18
56
|
depthTest: true,
|
|
@@ -22,12 +60,14 @@ function buildFXDiffuseMaterial(varyings, blending, useAlphaHashing, alphaTest,
|
|
|
22
60
|
forceSinglePass: true,
|
|
23
61
|
alphaTest,
|
|
24
62
|
emissive: emissionNodes.length > 0 ? new Color(1, 1, 1) : new Color(0, 0, 0),
|
|
63
|
+
premultipliedAlpha,
|
|
25
64
|
});
|
|
26
65
|
material.onBeforeCompile = (shader) => {
|
|
27
66
|
const vertexPreamble = [
|
|
28
67
|
PARTICLE_DEFINES,
|
|
29
68
|
attributeDeclarations.join("\n"),
|
|
30
69
|
"varying vec2 p_uv;",
|
|
70
|
+
useScatter ? "varying vec3 v_viewPosition;" : "",
|
|
31
71
|
varyingDeclarations.join("\n"),
|
|
32
72
|
].join("\n");
|
|
33
73
|
shader.vertexShader =
|
|
@@ -44,29 +84,26 @@ function buildFXDiffuseMaterial(varyings, blending, useAlphaHashing, alphaTest,
|
|
|
44
84
|
" vPosition = vec3(PARTICLE_POSITION_X, PARTICLE_POSITION_Y, PARTICLE_POSITION_Z) + vec3(position.xy, 0.0);",
|
|
45
85
|
"#endif",
|
|
46
86
|
].join("\n"))
|
|
47
|
-
.replace("#include <project_vertex>",
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
`)
|
|
87
|
+
.replace("#include <project_vertex>", [
|
|
88
|
+
"vec2 billboardOffset = position.xy;",
|
|
89
|
+
"billboardOffset.x *= PARTICLE_SCALE_X;",
|
|
90
|
+
"billboardOffset.y *= PARTICLE_SCALE_Y;",
|
|
91
|
+
"",
|
|
92
|
+
"float cosRotation = cos(PARTICLE_ROTATION);",
|
|
93
|
+
"float sinRotation = sin(PARTICLE_ROTATION);",
|
|
94
|
+
"billboardOffset = vec2(",
|
|
95
|
+
" billboardOffset.x * cosRotation - billboardOffset.y * sinRotation,",
|
|
96
|
+
" billboardOffset.x * sinRotation + billboardOffset.y * cosRotation",
|
|
97
|
+
");",
|
|
98
|
+
"",
|
|
99
|
+
"vec3 particleCenter = vec3(PARTICLE_POSITION_X, PARTICLE_POSITION_Y, PARTICLE_POSITION_Z);",
|
|
100
|
+
"vec4 mvPosition = modelViewMatrix * vec4(particleCenter, 1.0);",
|
|
101
|
+
"mvPosition.xy += billboardOffset;",
|
|
102
|
+
useScatter ? "v_viewPosition = mvPosition.xyz;" : "",
|
|
103
|
+
"gl_Position = projectionMatrix * mvPosition;",
|
|
104
|
+
].join("\n"))
|
|
66
105
|
.replace("#include <worldpos_vertex>", `
|
|
67
106
|
#if defined(USE_SHADOWMAP) || defined(USE_ENVMAP) || defined(DISTANCE) || (NUM_SPOT_LIGHT_COORDS > 0)
|
|
68
|
-
// Reconstruct world-space position for shadow/environment maps.
|
|
69
|
-
// Camera right and up are the first two rows of the view matrix rotation (transposed).
|
|
70
107
|
vec3 cameraRight = vec3(viewMatrix[0][0], viewMatrix[1][0], viewMatrix[2][0]);
|
|
71
108
|
vec3 cameraUp = vec3(viewMatrix[0][1], viewMatrix[1][1], viewMatrix[2][1]);
|
|
72
109
|
vec4 worldPosition = vec4(
|
|
@@ -84,18 +121,58 @@ function buildFXDiffuseMaterial(varyings, blending, useAlphaHashing, alphaTest,
|
|
|
84
121
|
}
|
|
85
122
|
`
|
|
86
123
|
: "";
|
|
124
|
+
const seenCacheKeys = new Set();
|
|
125
|
+
const uniqueHelpers = (nodes) => nodes
|
|
126
|
+
.filter((n) => {
|
|
127
|
+
if (seenCacheKeys.has(n.cacheKey)) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
seenCacheKeys.add(n.cacheKey);
|
|
131
|
+
return true;
|
|
132
|
+
})
|
|
133
|
+
.map((n) => n.helperFunctions)
|
|
134
|
+
.join("\n");
|
|
135
|
+
const scatterDeclarations = useScatter
|
|
136
|
+
? [
|
|
137
|
+
"varying vec3 v_viewPosition;",
|
|
138
|
+
"uniform vec3 u_ScatterTint;",
|
|
139
|
+
"uniform float u_ScatterPower;",
|
|
140
|
+
"uniform float u_ForwardScatterStrength;",
|
|
141
|
+
"uniform float u_BackScatterStrength;",
|
|
142
|
+
`vec3 calculateScatter(vec3 viewDirection, vec3 lightDirection, vec3 lightColor, float alpha) {
|
|
143
|
+
float viewDotLight = dot(viewDirection, lightDirection);
|
|
144
|
+
float scatterForward = pow(max(-viewDotLight, 0.0), u_ScatterPower) * u_ForwardScatterStrength;
|
|
145
|
+
float scatterBackward = pow(max( viewDotLight, 0.0), 2.0) * u_BackScatterStrength;
|
|
146
|
+
return lightColor * u_ScatterTint * (scatterForward + scatterBackward) * alpha;
|
|
147
|
+
}`,
|
|
148
|
+
]
|
|
149
|
+
: [];
|
|
150
|
+
const shadowSensitivityDeclarations = useShadowSensitivity
|
|
151
|
+
? ["uniform float u_shadowSensitivity;"]
|
|
152
|
+
: [];
|
|
87
153
|
const fragmentPreamble = [
|
|
88
154
|
PARTICLE_DEFINES,
|
|
89
155
|
"varying vec2 p_uv;",
|
|
90
156
|
varyingDeclarations.join("\n"),
|
|
91
157
|
albedoNodes.flatMap((n) => n.uniformDeclarations).join("\n"),
|
|
92
|
-
albedoNodes
|
|
158
|
+
uniqueHelpers(albedoNodes),
|
|
93
159
|
normalNodes.flatMap((n) => n.uniformDeclarations).join("\n"),
|
|
94
|
-
normalNodes
|
|
160
|
+
uniqueHelpers(normalNodes),
|
|
95
161
|
blendReorientedNormalsHelper,
|
|
96
162
|
emissionNodes.flatMap((n) => n.uniformDeclarations).join("\n"),
|
|
97
|
-
emissionNodes
|
|
163
|
+
uniqueHelpers(emissionNodes),
|
|
164
|
+
...scatterDeclarations,
|
|
165
|
+
...shadowSensitivityDeclarations,
|
|
98
166
|
].join("\n");
|
|
167
|
+
if (useScatter) {
|
|
168
|
+
shader.uniforms["u_ScatterTint"] = scatterUniforms.u_ScatterTint;
|
|
169
|
+
shader.uniforms["u_ScatterPower"] = scatterUniforms.u_ScatterPower;
|
|
170
|
+
shader.uniforms["u_ForwardScatterStrength"] = scatterUniforms.u_ForwardScatterStrength;
|
|
171
|
+
shader.uniforms["u_BackScatterStrength"] = scatterUniforms.u_BackScatterStrength;
|
|
172
|
+
}
|
|
173
|
+
if (useShadowSensitivity) {
|
|
174
|
+
shader.uniforms["u_shadowSensitivity"] = shadowSensitivity;
|
|
175
|
+
}
|
|
99
176
|
for (const node of albedoNodes) {
|
|
100
177
|
Object.assign(shader.uniforms, node.uniforms);
|
|
101
178
|
}
|
|
@@ -120,6 +197,7 @@ function buildFXDiffuseMaterial(varyings, blending, useAlphaHashing, alphaTest,
|
|
|
120
197
|
"float faceDirection = gl_FrontFacing ? 1.0 : -1.0;",
|
|
121
198
|
`vec3 normal = ${normalExpressions[0]};`,
|
|
122
199
|
...blendLines,
|
|
200
|
+
"normal *= faceDirection;",
|
|
123
201
|
].join("\n");
|
|
124
202
|
fragmentShader = fragmentShader.replace("#include <normal_fragment_begin>", normalCode);
|
|
125
203
|
}
|
|
@@ -132,9 +210,71 @@ function buildFXDiffuseMaterial(varyings, blending, useAlphaHashing, alphaTest,
|
|
|
132
210
|
const combinedExpression = emissionNodes.map((n) => n.colorExpression).join(" * ");
|
|
133
211
|
fragmentShader = fragmentShader.replace("#include <emissivemap_fragment>", `totalEmissiveRadiance = (${combinedExpression}).rgb;`);
|
|
134
212
|
}
|
|
213
|
+
if (useScatter) {
|
|
214
|
+
// Per-fragment Mie scatter for all light types.
|
|
215
|
+
// viewDirection and all light directions/positions are in view space.
|
|
216
|
+
fragmentShader = fragmentShader.replace("#include <opaque_fragment>", `
|
|
217
|
+
#ifdef OPAQUE
|
|
218
|
+
diffuseColor.a = 1.0;
|
|
219
|
+
#endif
|
|
220
|
+
|
|
221
|
+
vec3 viewDirection = normalize(-v_viewPosition);
|
|
222
|
+
vec3 totalScatter = vec3(0.0);
|
|
223
|
+
|
|
224
|
+
#if NUM_DIR_LIGHTS > 0
|
|
225
|
+
for (int i = 0; i < NUM_DIR_LIGHTS; i++) {
|
|
226
|
+
totalScatter += calculateScatter(viewDirection, directionalLights[i].direction, directionalLights[i].color, diffuseColor.a);
|
|
227
|
+
}
|
|
228
|
+
#endif
|
|
229
|
+
|
|
230
|
+
#if NUM_POINT_LIGHTS > 0
|
|
231
|
+
for (int i = 0; i < NUM_POINT_LIGHTS; i++) {
|
|
232
|
+
vec3 toLightDirection = normalize(pointLights[i].position - v_viewPosition);
|
|
233
|
+
float lightDistance = length(pointLights[i].position - v_viewPosition);
|
|
234
|
+
float attenuation = 1.0 / (1.0 + 0.09 * lightDistance + 0.032 * lightDistance * lightDistance);
|
|
235
|
+
totalScatter += calculateScatter(viewDirection, toLightDirection, pointLights[i].color, diffuseColor.a) * attenuation;
|
|
236
|
+
}
|
|
237
|
+
#endif
|
|
238
|
+
|
|
239
|
+
#if NUM_SPOT_LIGHTS > 0
|
|
240
|
+
for (int i = 0; i < NUM_SPOT_LIGHTS; i++) {
|
|
241
|
+
vec3 toLightDirection = normalize(spotLights[i].position - v_viewPosition);
|
|
242
|
+
float lightDistance = length(spotLights[i].position - v_viewPosition);
|
|
243
|
+
float attenuation = 1.0 / (1.0 + 0.09 * lightDistance + 0.032 * lightDistance * lightDistance);
|
|
244
|
+
float spotCosine = dot(normalize(spotLights[i].direction), -toLightDirection);
|
|
245
|
+
float spotFalloff = smoothstep(spotLights[i].coneCos, spotLights[i].penumbraCos, spotCosine);
|
|
246
|
+
totalScatter += calculateScatter(viewDirection, toLightDirection, spotLights[i].color, diffuseColor.a) * attenuation * spotFalloff;
|
|
247
|
+
}
|
|
248
|
+
#endif
|
|
249
|
+
|
|
250
|
+
// calculateScatter() includes a "* alpha" density factor. Compositing
|
|
251
|
+
// will apply another "* alpha" (via blend equation in straight mode,
|
|
252
|
+
// or via premultiplied_alpha_fragment + One/OneMinusSrcAlpha blend).
|
|
253
|
+
// Without correction, scatter would get alpha² while diffuse lighting
|
|
254
|
+
// only gets alpha¹. Dividing diffuseColor.rgb by alpha cancels the
|
|
255
|
+
// extra factor, so scatter composites at the same alpha power as
|
|
256
|
+
// the rest of outgoingLight.
|
|
257
|
+
vec3 albedo = diffuseColor.a > 0.001
|
|
258
|
+
? diffuseColor.rgb / diffuseColor.a
|
|
259
|
+
: vec3(0.0);
|
|
260
|
+
gl_FragColor = vec4(outgoingLight + totalScatter * albedo, diffuseColor.a);
|
|
261
|
+
`);
|
|
262
|
+
}
|
|
263
|
+
if (useShadowSensitivity) {
|
|
264
|
+
fragmentShader = wrapShadowCalls(fragmentShader, "u_shadowSensitivity");
|
|
265
|
+
}
|
|
135
266
|
shader.fragmentShader = fragmentPreamble + "\n" + fragmentShader;
|
|
136
267
|
};
|
|
137
|
-
|
|
268
|
+
const scatterKey = useScatter ? `scatter(${scatterUniforms.u_ScatterPower.value})` : "no-scatter";
|
|
269
|
+
const shadowKey = useShadowSensitivity ? "shadow-sens" : "no-shadow-sens";
|
|
270
|
+
material.customProgramCacheKey = () => [
|
|
271
|
+
"fx",
|
|
272
|
+
albedoNodes.map((n) => n.cacheKey).join("-") || "none",
|
|
273
|
+
normalNodes.map((n) => n.cacheKey).join("-") || "none",
|
|
274
|
+
emissionNodes.map((n) => n.cacheKey).join("-") || "none",
|
|
275
|
+
scatterKey,
|
|
276
|
+
shadowKey,
|
|
277
|
+
].join("_");
|
|
138
278
|
return material;
|
|
139
279
|
}
|
|
140
280
|
|
|
@@ -1,15 +1,26 @@
|
|
|
1
|
+
import type { ColorRepresentation } from "three";
|
|
1
2
|
import type { FXColorNode } from "../../nodes/color/FXColorNode";
|
|
2
3
|
import type { FXNormalNode } from "../../nodes/normal/FXNormalNode";
|
|
3
4
|
import type { FXTextureNode } from "../../nodes/texture/FXTextureNode";
|
|
4
5
|
import type { FXMaterialOptions } from "../FXMaterial/FXMaterial";
|
|
5
6
|
import { FXMaterial } from "../FXMaterial/FXMaterial";
|
|
6
7
|
export interface FXDiffuseMaterialOptions extends FXMaterialOptions {
|
|
7
|
-
normalNodes
|
|
8
|
-
emissionNodes
|
|
8
|
+
normalNodes: (FXTextureNode | FXNormalNode)[];
|
|
9
|
+
emissionNodes: (FXColorNode | FXTextureNode)[];
|
|
10
|
+
enableScatter: boolean;
|
|
11
|
+
scatterTint: ColorRepresentation;
|
|
12
|
+
scatterPower: number;
|
|
13
|
+
forwardScatterStrength: number;
|
|
14
|
+
backScatterStrength: number;
|
|
15
|
+
shadowSensitivity: number;
|
|
16
|
+
premultipliedAlpha: boolean;
|
|
9
17
|
}
|
|
10
18
|
export declare class FXDiffuseMaterial extends FXMaterial {
|
|
11
19
|
private readonly normalNodes;
|
|
12
20
|
private readonly emissionNodes;
|
|
13
|
-
|
|
21
|
+
private readonly scatterUniforms;
|
|
22
|
+
private readonly shadowSensitivity;
|
|
23
|
+
private readonly premultipliedAlpha;
|
|
24
|
+
constructor(options?: Partial<FXDiffuseMaterialOptions>);
|
|
14
25
|
destroy(): void;
|
|
15
26
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { FXColor } from '../../miscellaneous/color/FXColor.js';
|
|
1
2
|
import { FXSphericalNormalNode } from '../../nodes/normal/FXSphericalNormalNode.js';
|
|
2
3
|
import { FXMaterial } from '../FXMaterial/FXMaterial.js';
|
|
3
4
|
import { buildFXDiffuseMaterial } from './FXDiffuseMaterial.Internal.js';
|
|
@@ -7,6 +8,21 @@ class FXDiffuseMaterial extends FXMaterial {
|
|
|
7
8
|
super(options);
|
|
8
9
|
this._private_normalNodes = options.normalNodes ?? [new FXSphericalNormalNode()];
|
|
9
10
|
this._private_emissionNodes = options.emissionNodes ?? [];
|
|
11
|
+
this._private_scatterUniforms =
|
|
12
|
+
options.enableScatter === true
|
|
13
|
+
? {
|
|
14
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
15
|
+
u_ScatterTint: { value: new FXColor(options.scatterTint ?? 0xffd999).toThreeColor() },
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
17
|
+
u_ScatterPower: { value: options.scatterPower ?? 10 },
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
19
|
+
u_ForwardScatterStrength: { value: options.forwardScatterStrength ?? 1 },
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
21
|
+
u_BackScatterStrength: { value: options.backScatterStrength ?? 1 },
|
|
22
|
+
}
|
|
23
|
+
: undefined;
|
|
24
|
+
this._private_shadowSensitivity = { value: options.shadowSensitivity ?? 1 };
|
|
25
|
+
this._private_premultipliedAlpha = options.premultipliedAlpha ?? false;
|
|
10
26
|
}
|
|
11
27
|
destroy() {
|
|
12
28
|
super.destroy();
|
|
@@ -19,7 +35,7 @@ class FXDiffuseMaterial extends FXMaterial {
|
|
|
19
35
|
}
|
|
20
36
|
/** @internal */
|
|
21
37
|
buildThreeMaterial(varyings) {
|
|
22
|
-
return buildFXDiffuseMaterial(varyings, this.blending, this.useAlphaHashing, this.alphaTest, this.albedoNodes, this._private_normalNodes, this._private_emissionNodes);
|
|
38
|
+
return buildFXDiffuseMaterial(varyings, this.blending, this.useAlphaHashing, this.alphaTest, this.albedoNodes, this._private_normalNodes, this._private_emissionNodes, this._private_scatterUniforms, this._private_shadowSensitivity, this._private_premultipliedAlpha);
|
|
23
39
|
}
|
|
24
40
|
}
|
|
25
41
|
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { MeshDepthMaterial } from "three";
|
|
2
|
+
import type { FXColorNode } from "../../nodes/color/FXColorNode";
|
|
3
|
+
import type { FXTextureNode } from "../../nodes/texture/FXTextureNode";
|
|
4
|
+
export declare function buildDepthMaterial(albedoNodes: readonly (FXColorNode | FXTextureNode)[], depthAlphaTest: number, useDepthAlphaHash: boolean, useSphericalDepth: boolean): MeshDepthMaterial;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { MeshDepthMaterial, DoubleSide, RGBADepthPacking } from 'three';
|
|
2
|
+
import { PARTICLE_DEFINES } from '../../miscellaneous/miscellaneous.js';
|
|
3
|
+
|
|
4
|
+
const DEPTH_VERTEX_SHADER = `
|
|
5
|
+
#define PARTICLE_POSITION_X a_builtin[0][0]
|
|
6
|
+
#define PARTICLE_POSITION_Y a_builtin[0][1]
|
|
7
|
+
#define PARTICLE_POSITION_Z a_builtin[0][2]
|
|
8
|
+
|
|
9
|
+
#define PARTICLE_SCALE_X a_builtin[1][2]
|
|
10
|
+
#define PARTICLE_SCALE_Y a_builtin[1][3]
|
|
11
|
+
|
|
12
|
+
#define PARTICLE_ROTATION a_builtin[2][1]
|
|
13
|
+
|
|
14
|
+
attribute mat4 a_builtin;
|
|
15
|
+
varying mat4 p_builtin;
|
|
16
|
+
varying vec2 p_uv;
|
|
17
|
+
varying vec2 vHighPrecisionZW;
|
|
18
|
+
varying float vCenterViewZ;
|
|
19
|
+
|
|
20
|
+
#ifdef USE_ALPHAHASH
|
|
21
|
+
varying vec3 vPosition;
|
|
22
|
+
#endif
|
|
23
|
+
|
|
24
|
+
void main() {
|
|
25
|
+
p_builtin = a_builtin;
|
|
26
|
+
p_uv = uv;
|
|
27
|
+
|
|
28
|
+
#ifdef USE_ALPHAHASH
|
|
29
|
+
vPosition = vec3(PARTICLE_POSITION_X, PARTICLE_POSITION_Y, PARTICLE_POSITION_Z) + vec3(position.xy, 0.0);
|
|
30
|
+
#endif
|
|
31
|
+
|
|
32
|
+
vec2 billboardOffset = position.xy;
|
|
33
|
+
billboardOffset.x *= PARTICLE_SCALE_X;
|
|
34
|
+
billboardOffset.y *= PARTICLE_SCALE_Y;
|
|
35
|
+
|
|
36
|
+
float cosRotation = cos(PARTICLE_ROTATION);
|
|
37
|
+
float sinRotation = sin(PARTICLE_ROTATION);
|
|
38
|
+
billboardOffset = vec2(
|
|
39
|
+
billboardOffset.x * cosRotation - billboardOffset.y * sinRotation,
|
|
40
|
+
billboardOffset.x * sinRotation + billboardOffset.y * cosRotation
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
vec3 particleCenter = vec3(PARTICLE_POSITION_X, PARTICLE_POSITION_Y, PARTICLE_POSITION_Z);
|
|
44
|
+
vec4 modelViewPosition = modelViewMatrix * vec4(particleCenter, 1.0);
|
|
45
|
+
vCenterViewZ = modelViewPosition.z;
|
|
46
|
+
modelViewPosition.xy += billboardOffset;
|
|
47
|
+
gl_Position = projectionMatrix * modelViewPosition;
|
|
48
|
+
vHighPrecisionZW = gl_Position.zw;
|
|
49
|
+
}
|
|
50
|
+
`;
|
|
51
|
+
/**
|
|
52
|
+
* GLSL snippet that replaces the flat `fragCoordZ` with a spherical depth
|
|
53
|
+
* approximation. Treats each billboard as the front hemisphere of a sphere
|
|
54
|
+
* whose radius equals half the smaller particle scale dimension. Fragments
|
|
55
|
+
* outside that radius are discarded, producing a circular silhouette.
|
|
56
|
+
*
|
|
57
|
+
* Expects `vCenterViewZ` (view-space Z of the particle center), `p_uv`,
|
|
58
|
+
* `PARTICLE_SCALE_X / Y`, and `projectionMatrix` to be available.
|
|
59
|
+
*/
|
|
60
|
+
const SPHERICAL_DEPTH_FRAG_COORD_Z = `
|
|
61
|
+
float sphereOffsetX = (p_uv.x - 0.5) * PARTICLE_SCALE_X;
|
|
62
|
+
float sphereOffsetY = (p_uv.y - 0.5) * PARTICLE_SCALE_Y;
|
|
63
|
+
float sphereRadius = min(PARTICLE_SCALE_X, PARTICLE_SCALE_Y) * 0.5;
|
|
64
|
+
float squaredDistanceFromCenter = sphereOffsetX * sphereOffsetX + sphereOffsetY * sphereOffsetY;
|
|
65
|
+
if (squaredDistanceFromCenter > sphereRadius * sphereRadius) discard;
|
|
66
|
+
|
|
67
|
+
float sphereViewZ = vCenterViewZ + sqrt(sphereRadius * sphereRadius - squaredDistanceFromCenter);
|
|
68
|
+
float projectedZ = projectionMatrix[2][2] * sphereViewZ + projectionMatrix[3][2];
|
|
69
|
+
float projectedW = projectionMatrix[2][3] * sphereViewZ + projectionMatrix[3][3];
|
|
70
|
+
float normalizedDeviceZ = projectedZ / projectedW;
|
|
71
|
+
|
|
72
|
+
#ifdef USE_REVERSEDEPTHBUF
|
|
73
|
+
float fragCoordZ = normalizedDeviceZ;
|
|
74
|
+
#else
|
|
75
|
+
float fragCoordZ = 0.5 * normalizedDeviceZ + 0.5;
|
|
76
|
+
#endif
|
|
77
|
+
`;
|
|
78
|
+
const FRAG_COORD_Z_PATTERN = /float fragCoordZ\b[^;]*;/;
|
|
79
|
+
function buildDepthMaterial(albedoNodes, depthAlphaTest, useDepthAlphaHash, useSphericalDepth) {
|
|
80
|
+
const depthNodes = albedoNodes.filter((node) => node.affectsDepth);
|
|
81
|
+
const needsFragmentPatch = depthNodes.length > 0 || useSphericalDepth;
|
|
82
|
+
const material = new MeshDepthMaterial({
|
|
83
|
+
name: "FXDepthMaterial",
|
|
84
|
+
depthPacking: RGBADepthPacking,
|
|
85
|
+
side: DoubleSide,
|
|
86
|
+
forceSinglePass: true,
|
|
87
|
+
alphaHash: useDepthAlphaHash,
|
|
88
|
+
});
|
|
89
|
+
material.onBeforeCompile = (shader) => {
|
|
90
|
+
shader.vertexShader = DEPTH_VERTEX_SHADER;
|
|
91
|
+
if (!needsFragmentPatch) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const seenCacheKeys = new Set();
|
|
95
|
+
const uniqueHelpers = depthNodes
|
|
96
|
+
.filter((node) => {
|
|
97
|
+
if (seenCacheKeys.has(node.cacheKey)) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
seenCacheKeys.add(node.cacheKey);
|
|
101
|
+
return true;
|
|
102
|
+
})
|
|
103
|
+
.map((node) => node.helperFunctions)
|
|
104
|
+
.join("\n");
|
|
105
|
+
const fragmentPreamble = [
|
|
106
|
+
PARTICLE_DEFINES,
|
|
107
|
+
"varying mat4 p_builtin;",
|
|
108
|
+
"varying vec2 p_uv;",
|
|
109
|
+
"varying float vCenterViewZ;",
|
|
110
|
+
useSphericalDepth ? "uniform mat4 projectionMatrix;" : "",
|
|
111
|
+
depthNodes.flatMap((node) => node.uniformDeclarations).join("\n"),
|
|
112
|
+
uniqueHelpers,
|
|
113
|
+
].join("\n");
|
|
114
|
+
for (const node of depthNodes) {
|
|
115
|
+
Object.assign(shader.uniforms, node.uniforms);
|
|
116
|
+
}
|
|
117
|
+
let fragmentShader = shader.fragmentShader;
|
|
118
|
+
if (depthNodes.length > 0) {
|
|
119
|
+
const combinedAlphaExpression = depthNodes.map((node) => node.colorExpression).join(" * ");
|
|
120
|
+
fragmentShader = fragmentShader.replace("#include <map_fragment>", `diffuseColor.a = (${combinedAlphaExpression}).a;
|
|
121
|
+
if (diffuseColor.a < ${depthAlphaTest.toFixed(4)}) discard;`);
|
|
122
|
+
}
|
|
123
|
+
if (useSphericalDepth) {
|
|
124
|
+
if (!FRAG_COORD_Z_PATTERN.test(fragmentShader)) {
|
|
125
|
+
console.warn("FXDepthMaterial: could not find 'float fragCoordZ' in the depth fragment shader. " +
|
|
126
|
+
"Spherical depth will not be applied. " +
|
|
127
|
+
"This may indicate a Three.js version mismatch.");
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
fragmentShader = fragmentShader.replace(FRAG_COORD_Z_PATTERN, SPHERICAL_DEPTH_FRAG_COORD_Z);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
shader.fragmentShader = fragmentPreamble + "\n" + fragmentShader;
|
|
134
|
+
};
|
|
135
|
+
material.customProgramCacheKey = () => `fx-depth-${useDepthAlphaHash ? "alphaHash" : "alphaTest"}-${useSphericalDepth ? "spherical" : "flat"}-${depthNodes.map((node) => node.cacheKey).join("-") || "none"}`;
|
|
136
|
+
return material;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export { buildDepthMaterial };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { MeshDistanceMaterial } from "three";
|
|
2
|
+
import type { FXColorNode } from "../../nodes/color/FXColorNode";
|
|
3
|
+
import type { FXTextureNode } from "../../nodes/texture/FXTextureNode";
|
|
4
|
+
export declare function buildDistanceMaterial(albedoNodes: readonly (FXColorNode | FXTextureNode)[], distanceAlphaTest: number, useDistanceAlphaHash: boolean, useSphericalDepth: boolean): MeshDistanceMaterial;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { MeshDistanceMaterial, DoubleSide } from 'three';
|
|
2
|
+
import { PARTICLE_DEFINES } from '../../miscellaneous/miscellaneous.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Custom vertex shader for point/spot light shadow maps.
|
|
6
|
+
*
|
|
7
|
+
* Unlike the depth material (which writes clip-space Z for directional light
|
|
8
|
+
* shadows), the distance material writes the world-space distance from each
|
|
9
|
+
* fragment to the light position. This means we need to reconstruct world
|
|
10
|
+
* position for each billboarded vertex.
|
|
11
|
+
*
|
|
12
|
+
* `vWorldPosition` is a Three.js convention consumed by the distance fragment
|
|
13
|
+
* shader. `viewMatrix` here is the shadow camera's view matrix, so the
|
|
14
|
+
* billboard faces the light — which is correct for shadow shape.
|
|
15
|
+
*/
|
|
16
|
+
const DISTANCE_VERTEX_SHADER = `
|
|
17
|
+
#define DISTANCE
|
|
18
|
+
|
|
19
|
+
#define PARTICLE_POSITION_X a_builtin[0][0]
|
|
20
|
+
#define PARTICLE_POSITION_Y a_builtin[0][1]
|
|
21
|
+
#define PARTICLE_POSITION_Z a_builtin[0][2]
|
|
22
|
+
|
|
23
|
+
#define PARTICLE_SCALE_X a_builtin[1][2]
|
|
24
|
+
#define PARTICLE_SCALE_Y a_builtin[1][3]
|
|
25
|
+
|
|
26
|
+
#define PARTICLE_ROTATION a_builtin[2][1]
|
|
27
|
+
|
|
28
|
+
attribute mat4 a_builtin;
|
|
29
|
+
varying mat4 p_builtin;
|
|
30
|
+
varying vec2 p_uv;
|
|
31
|
+
varying vec3 vWorldPosition;
|
|
32
|
+
|
|
33
|
+
#ifdef USE_ALPHAHASH
|
|
34
|
+
varying vec3 vPosition;
|
|
35
|
+
#endif
|
|
36
|
+
|
|
37
|
+
void main() {
|
|
38
|
+
p_builtin = a_builtin;
|
|
39
|
+
p_uv = uv;
|
|
40
|
+
|
|
41
|
+
#ifdef USE_ALPHAHASH
|
|
42
|
+
vPosition = vec3(PARTICLE_POSITION_X, PARTICLE_POSITION_Y, PARTICLE_POSITION_Z) + vec3(position.xy, 0.0);
|
|
43
|
+
#endif
|
|
44
|
+
|
|
45
|
+
vec2 billboardOffset = position.xy;
|
|
46
|
+
billboardOffset.x *= PARTICLE_SCALE_X;
|
|
47
|
+
billboardOffset.y *= PARTICLE_SCALE_Y;
|
|
48
|
+
|
|
49
|
+
float cosRotation = cos(PARTICLE_ROTATION);
|
|
50
|
+
float sinRotation = sin(PARTICLE_ROTATION);
|
|
51
|
+
billboardOffset = vec2(
|
|
52
|
+
billboardOffset.x * cosRotation - billboardOffset.y * sinRotation,
|
|
53
|
+
billboardOffset.x * sinRotation + billboardOffset.y * cosRotation
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
vec3 particleCenter = vec3(PARTICLE_POSITION_X, PARTICLE_POSITION_Y, PARTICLE_POSITION_Z);
|
|
57
|
+
|
|
58
|
+
// Reconstruct world-space position for the billboarded vertex.
|
|
59
|
+
// Camera axes are extracted from the shadow camera's view matrix (transposed
|
|
60
|
+
// rotation part), then scaled by the billboard offset.
|
|
61
|
+
vec3 cameraRight = vec3(viewMatrix[0][0], viewMatrix[1][0], viewMatrix[2][0]);
|
|
62
|
+
vec3 cameraUp = vec3(viewMatrix[0][1], viewMatrix[1][1], viewMatrix[2][1]);
|
|
63
|
+
vec3 worldCenter = (modelMatrix * vec4(particleCenter, 1.0)).xyz;
|
|
64
|
+
vWorldPosition = worldCenter + cameraRight * billboardOffset.x + cameraUp * billboardOffset.y;
|
|
65
|
+
|
|
66
|
+
// Standard clip-space projection via the shadow camera.
|
|
67
|
+
vec4 modelViewPosition = modelViewMatrix * vec4(particleCenter, 1.0);
|
|
68
|
+
modelViewPosition.xy += billboardOffset;
|
|
69
|
+
gl_Position = projectionMatrix * modelViewPosition;
|
|
70
|
+
}
|
|
71
|
+
`;
|
|
72
|
+
/**
|
|
73
|
+
* GLSL snippet for spherical distance adjustment.
|
|
74
|
+
*
|
|
75
|
+
* Instead of measuring distance from the flat billboard surface, this
|
|
76
|
+
* approximates the front hemisphere of a sphere. The sphere radius is half
|
|
77
|
+
* the smaller scale dimension. Fragments outside the sphere are discarded.
|
|
78
|
+
*
|
|
79
|
+
* The depth offset is subtracted from the flat distance, bringing the
|
|
80
|
+
* perceived surface closer to the light. This is an approximation — the
|
|
81
|
+
* true offset direction depends on the light-to-fragment vector, not the
|
|
82
|
+
* camera-to-fragment vector — but for smoke particles the error is
|
|
83
|
+
* negligible.
|
|
84
|
+
*/
|
|
85
|
+
const SPHERICAL_DISTANCE_SNIPPET = `
|
|
86
|
+
{
|
|
87
|
+
float sphereOffsetX = (p_uv.x - 0.5) * PARTICLE_SCALE_X;
|
|
88
|
+
float sphereOffsetY = (p_uv.y - 0.5) * PARTICLE_SCALE_Y;
|
|
89
|
+
float sphereRadius = min(PARTICLE_SCALE_X, PARTICLE_SCALE_Y) * 0.5;
|
|
90
|
+
float squaredDistanceFromCenter = sphereOffsetX * sphereOffsetX + sphereOffsetY * sphereOffsetY;
|
|
91
|
+
if (squaredDistanceFromCenter > sphereRadius * sphereRadius) discard;
|
|
92
|
+
float sphereDepthOffset = sqrt(sphereRadius * sphereRadius - squaredDistanceFromCenter);
|
|
93
|
+
dist = max(0.0, dist - sphereDepthOffset);
|
|
94
|
+
}
|
|
95
|
+
`;
|
|
96
|
+
const DISTANCE_CALCULATION_PATTERN = /float\s+dist\s*=\s*length\s*\(\s*vWorldPosition\s*-\s*referencePosition\s*\)\s*;/;
|
|
97
|
+
function buildDistanceMaterial(albedoNodes, distanceAlphaTest, useDistanceAlphaHash, useSphericalDepth) {
|
|
98
|
+
const distanceNodes = albedoNodes.filter((node) => node.affectsDepth);
|
|
99
|
+
const needsFragmentPatch = distanceNodes.length > 0 || useSphericalDepth;
|
|
100
|
+
const material = new MeshDistanceMaterial({
|
|
101
|
+
name: "FXDistanceMaterial",
|
|
102
|
+
side: DoubleSide,
|
|
103
|
+
forceSinglePass: true,
|
|
104
|
+
alphaHash: useDistanceAlphaHash,
|
|
105
|
+
});
|
|
106
|
+
material.onBeforeCompile = (shader) => {
|
|
107
|
+
shader.vertexShader = DISTANCE_VERTEX_SHADER;
|
|
108
|
+
if (!needsFragmentPatch) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const seenCacheKeys = new Set();
|
|
112
|
+
const uniqueHelpers = distanceNodes
|
|
113
|
+
.filter((node) => {
|
|
114
|
+
if (seenCacheKeys.has(node.cacheKey)) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
seenCacheKeys.add(node.cacheKey);
|
|
118
|
+
return true;
|
|
119
|
+
})
|
|
120
|
+
.map((node) => node.helperFunctions)
|
|
121
|
+
.join("\n");
|
|
122
|
+
const fragmentPreamble = [
|
|
123
|
+
PARTICLE_DEFINES,
|
|
124
|
+
"varying mat4 p_builtin;",
|
|
125
|
+
"varying vec2 p_uv;",
|
|
126
|
+
distanceNodes.flatMap((node) => node.uniformDeclarations).join("\n"),
|
|
127
|
+
uniqueHelpers,
|
|
128
|
+
].join("\n");
|
|
129
|
+
for (const node of distanceNodes) {
|
|
130
|
+
Object.assign(shader.uniforms, node.uniforms);
|
|
131
|
+
}
|
|
132
|
+
let fragmentShader = shader.fragmentShader;
|
|
133
|
+
// Alpha from nodes — MeshDistanceMaterial may or may not include
|
|
134
|
+
// <map_fragment> depending on the Three.js version. If the chunk is
|
|
135
|
+
// absent, insert the alpha discard before the distance calculation.
|
|
136
|
+
if (distanceNodes.length > 0) {
|
|
137
|
+
const combinedAlphaExpression = distanceNodes.map((node) => node.colorExpression).join(" * ");
|
|
138
|
+
const alphaDiscardCode = `float nodeAlpha = (${combinedAlphaExpression}).a;\n` +
|
|
139
|
+
`if (nodeAlpha < ${distanceAlphaTest.toFixed(4)}) discard;`;
|
|
140
|
+
if (fragmentShader.includes("#include <map_fragment>")) {
|
|
141
|
+
fragmentShader = fragmentShader.replace("#include <map_fragment>", alphaDiscardCode);
|
|
142
|
+
}
|
|
143
|
+
else if (DISTANCE_CALCULATION_PATTERN.test(fragmentShader)) {
|
|
144
|
+
// Fallback: inject alpha discard just before the distance calculation.
|
|
145
|
+
fragmentShader = fragmentShader.replace(DISTANCE_CALCULATION_PATTERN, (match) => alphaDiscardCode + "\n" + match);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
console.warn("FXDistanceMaterial: could not find a suitable injection point for alpha discard. " +
|
|
149
|
+
"This may indicate a Three.js version mismatch.");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// Spherical distance adjustment — offset the flat-billboard distance by
|
|
153
|
+
// the sphere's front surface depth.
|
|
154
|
+
if (useSphericalDepth) {
|
|
155
|
+
if (!DISTANCE_CALCULATION_PATTERN.test(fragmentShader)) {
|
|
156
|
+
console.warn("FXDistanceMaterial: could not find the distance calculation line. " +
|
|
157
|
+
"Spherical depth will not be applied. " +
|
|
158
|
+
"This may indicate a Three.js version mismatch.");
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
// Inject the spherical adjustment right after the distance calculation.
|
|
162
|
+
fragmentShader = fragmentShader.replace(DISTANCE_CALCULATION_PATTERN, (match) => match + "\n" + SPHERICAL_DISTANCE_SNIPPET);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
shader.fragmentShader = fragmentPreamble + "\n" + fragmentShader;
|
|
166
|
+
};
|
|
167
|
+
material.customProgramCacheKey = () => `fx-distance-${useDistanceAlphaHash ? "alphaHash" : "alphaTest"}-${useSphericalDepth ? "spherical" : "flat"}-${distanceNodes.map((node) => node.cacheKey).join("-") || "none"}`;
|
|
168
|
+
return material;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export { buildDistanceMaterial };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Blending } from "three";
|
|
1
|
+
import type { Blending, MeshDistanceMaterial } from "three";
|
|
2
2
|
import type { FXColorNode } from "../../nodes/color/FXColorNode";
|
|
3
3
|
import type { FXTextureNode } from "../../nodes/texture/FXTextureNode";
|
|
4
4
|
export interface FXMaterialOptions {
|
|
@@ -16,4 +16,5 @@ export declare abstract class FXMaterial {
|
|
|
16
16
|
private readonly useSphericalDepth;
|
|
17
17
|
constructor(options?: FXMaterialOptions);
|
|
18
18
|
destroy(): void;
|
|
19
|
+
buildDistanceMaterial(): MeshDistanceMaterial;
|
|
19
20
|
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { NormalBlending } from 'three';
|
|
2
|
-
import { buildDepthMaterial } from './
|
|
2
|
+
import { buildDepthMaterial } from './FXDepthMaterial.Internal.js';
|
|
3
|
+
import { buildDistanceMaterial } from './FXDistanceMaterial.Internal.js';
|
|
3
4
|
|
|
4
5
|
class FXMaterial {
|
|
5
6
|
constructor(options = {}) {
|
|
6
7
|
this.albedoNodes = options.albedoNodes ?? [];
|
|
7
8
|
this.blending = options.blending ?? NormalBlending;
|
|
8
9
|
this.useAlphaHashing = options.useAlphaHashing ?? false;
|
|
9
|
-
this.alphaTest = options.alphaTest ??
|
|
10
|
+
this.alphaTest = options.alphaTest ?? 0.0075;
|
|
10
11
|
this._private_depthAlphaTest = options.depthAlphaTest ?? 0.5;
|
|
11
12
|
this._private_useDepthAlphaHash = options.useDepthAlphaHash ?? false;
|
|
12
13
|
this._private_useSphericalDepth = options.useSphericalDepth ?? true;
|
|
@@ -20,6 +21,9 @@ class FXMaterial {
|
|
|
20
21
|
buildDepthMaterial() {
|
|
21
22
|
return buildDepthMaterial(this.albedoNodes, this._private_depthAlphaTest, this._private_useDepthAlphaHash, this._private_useSphericalDepth);
|
|
22
23
|
}
|
|
24
|
+
buildDistanceMaterial() {
|
|
25
|
+
return buildDistanceMaterial(this.albedoNodes, this._private_depthAlphaTest, this._private_useDepthAlphaHash, this._private_useSphericalDepth);
|
|
26
|
+
}
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
export { FXMaterial };
|
|
@@ -12,6 +12,7 @@ function buildFXUnlitMaterial(varyings, blending, useAlphaHashing, alphaTest, al
|
|
|
12
12
|
vertexAssignments.push(`p_${name} = a_${name};`);
|
|
13
13
|
}
|
|
14
14
|
const material = new MeshBasicMaterial({
|
|
15
|
+
name: "FXUnlitMaterial",
|
|
15
16
|
transparent: !useAlphaHashing,
|
|
16
17
|
depthWrite: useAlphaHashing,
|
|
17
18
|
depthTest: true,
|
|
@@ -59,12 +60,23 @@ function buildFXUnlitMaterial(varyings, blending, useAlphaHashing, alphaTest, al
|
|
|
59
60
|
mvPosition.xy += billboardOffset;
|
|
60
61
|
gl_Position = projectionMatrix * mvPosition;
|
|
61
62
|
`);
|
|
63
|
+
const seenCacheKeys = new Set();
|
|
64
|
+
const uniqueHelpers = (nodes) => nodes
|
|
65
|
+
.filter((n) => {
|
|
66
|
+
if (seenCacheKeys.has(n.cacheKey)) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
seenCacheKeys.add(n.cacheKey);
|
|
70
|
+
return true;
|
|
71
|
+
})
|
|
72
|
+
.map((n) => n.helperFunctions)
|
|
73
|
+
.join("\n");
|
|
62
74
|
const fragmentPreamble = [
|
|
63
75
|
PARTICLE_DEFINES,
|
|
64
76
|
"varying vec2 p_uv;",
|
|
65
77
|
varyingDeclarations.join("\n"),
|
|
66
78
|
albedoNodes.flatMap((n) => n.uniformDeclarations).join("\n"),
|
|
67
|
-
albedoNodes
|
|
79
|
+
uniqueHelpers(albedoNodes),
|
|
68
80
|
].join("\n");
|
|
69
81
|
for (const node of albedoNodes) {
|
|
70
82
|
Object.assign(shader.uniforms, node.uniforms);
|
|
@@ -2,19 +2,21 @@ import { buildGradientTexture } from '../../miscellaneous/miscellaneous.js';
|
|
|
2
2
|
import { checkSRGBSupport } from '../../miscellaneous/webglCapabilities.js';
|
|
3
3
|
import { FXColorNode } from './FXColorNode.js';
|
|
4
4
|
|
|
5
|
+
const INSTANCES = [];
|
|
5
6
|
class FXColorOverLifeNode extends FXColorNode {
|
|
6
7
|
constructor(colors) {
|
|
7
8
|
if (colors.length === 0) {
|
|
8
9
|
throw new Error("FXColorOverLifeNode: colors array cannot be empty");
|
|
9
10
|
}
|
|
10
11
|
super();
|
|
12
|
+
INSTANCES.push(this);
|
|
13
|
+
const uniformName = `u_ColorOverLife_${INSTANCES.length - 1}`;
|
|
11
14
|
this._private_gradientTexture = buildGradientTexture(colors);
|
|
12
15
|
this.affectsDepth = true;
|
|
13
16
|
this.cacheKey = "color-over-life";
|
|
14
|
-
this.uniformDeclarations = [
|
|
17
|
+
this.uniformDeclarations = [`uniform sampler2D ${uniformName};`];
|
|
15
18
|
this.uniforms = {
|
|
16
|
-
|
|
17
|
-
u_ColorOverLife: { value: this._private_gradientTexture },
|
|
19
|
+
[uniformName]: { value: this._private_gradientTexture },
|
|
18
20
|
};
|
|
19
21
|
this.helperFunctions = `
|
|
20
22
|
vec4 sampleColorOverLife(sampler2D gradientTexture) {
|
|
@@ -28,10 +30,14 @@ class FXColorOverLifeNode extends FXColorNode {
|
|
|
28
30
|
return color;
|
|
29
31
|
}
|
|
30
32
|
`;
|
|
31
|
-
this.colorExpression =
|
|
33
|
+
this.colorExpression = `sampleColorOverLife(${uniformName})`;
|
|
32
34
|
}
|
|
33
35
|
destroy() {
|
|
34
36
|
this._private_gradientTexture.dispose();
|
|
37
|
+
const index = INSTANCES.indexOf(this);
|
|
38
|
+
if (index !== -1) {
|
|
39
|
+
INSTANCES.splice(index, 1);
|
|
40
|
+
}
|
|
35
41
|
}
|
|
36
42
|
}
|
|
37
43
|
|
|
@@ -1,26 +1,29 @@
|
|
|
1
1
|
import { checkSRGBSupport } from '../../miscellaneous/webglCapabilities.js';
|
|
2
2
|
import { FXTextureNode } from './FXTextureNode.js';
|
|
3
3
|
|
|
4
|
+
const INSTANCES = [];
|
|
4
5
|
class FXAnimatedTextureNode extends FXTextureNode {
|
|
5
6
|
constructor({ texture, columns, rows, interpolate = false }) {
|
|
6
7
|
super();
|
|
8
|
+
INSTANCES.push(this);
|
|
9
|
+
const idx = INSTANCES.length - 1;
|
|
10
|
+
const uniformTexture = `u_AnimatedTexture_${idx}`;
|
|
11
|
+
const uniformColumns = `u_AnimatedTextureColumns_${idx}`;
|
|
12
|
+
const uniformRows = `u_AnimatedTextureRows_${idx}`;
|
|
7
13
|
const gammaCorrection = checkSRGBSupport()
|
|
8
14
|
? ""
|
|
9
15
|
: "color = vec4(pow(color.rgb, vec3(2.2)), color.a);";
|
|
10
16
|
this.affectsDepth = true;
|
|
11
17
|
this.cacheKey = interpolate ? "animated-interpolated" : "animated";
|
|
12
18
|
this.uniformDeclarations = [
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
19
|
+
`uniform sampler2D ${uniformTexture};`,
|
|
20
|
+
`uniform float ${uniformColumns};`,
|
|
21
|
+
`uniform float ${uniformRows};`,
|
|
16
22
|
];
|
|
17
23
|
this.uniforms = {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
u_AnimatedTextureColumns: { value: columns },
|
|
22
|
-
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
23
|
-
u_AnimatedTextureRows: { value: rows },
|
|
24
|
+
[uniformTexture]: { value: texture },
|
|
25
|
+
[uniformColumns]: { value: columns },
|
|
26
|
+
[uniformRows]: { value: rows },
|
|
24
27
|
};
|
|
25
28
|
this.helperFunctions = interpolate
|
|
26
29
|
? `
|
|
@@ -72,8 +75,13 @@ class FXAnimatedTextureNode extends FXTextureNode {
|
|
|
72
75
|
return color;
|
|
73
76
|
}
|
|
74
77
|
`;
|
|
75
|
-
this.colorExpression =
|
|
76
|
-
|
|
78
|
+
this.colorExpression = `sampleAnimatedTexture(${uniformTexture}, p_uv, ${uniformColumns}, ${uniformRows})`;
|
|
79
|
+
}
|
|
80
|
+
destroy() {
|
|
81
|
+
const index = INSTANCES.indexOf(this);
|
|
82
|
+
if (index !== -1) {
|
|
83
|
+
INSTANCES.splice(index, 1);
|
|
84
|
+
}
|
|
77
85
|
}
|
|
78
86
|
}
|
|
79
87
|
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { checkSRGBSupport } from '../../miscellaneous/webglCapabilities.js';
|
|
2
2
|
import { FXTextureNode } from './FXTextureNode.js';
|
|
3
3
|
|
|
4
|
+
const INSTANCES = [];
|
|
4
5
|
class FXStaticTextureNode extends FXTextureNode {
|
|
5
6
|
constructor(texture) {
|
|
6
7
|
super();
|
|
8
|
+
INSTANCES.push(this);
|
|
9
|
+
const uniformName = `u_StaticTexture_${INSTANCES.length - 1}`;
|
|
7
10
|
this.affectsDepth = true;
|
|
8
11
|
this.cacheKey = "static";
|
|
9
|
-
this.uniformDeclarations = [
|
|
12
|
+
this.uniformDeclarations = [`uniform sampler2D ${uniformName};`];
|
|
10
13
|
this.uniforms = {
|
|
11
|
-
|
|
12
|
-
u_StaticTexture: { value: texture },
|
|
14
|
+
[uniformName]: { value: texture },
|
|
13
15
|
};
|
|
14
16
|
this.helperFunctions = `
|
|
15
17
|
vec4 sampleStaticTexture(sampler2D colorTexture, vec2 uv) {
|
|
@@ -19,7 +21,13 @@ class FXStaticTextureNode extends FXTextureNode {
|
|
|
19
21
|
return color;
|
|
20
22
|
}
|
|
21
23
|
`;
|
|
22
|
-
this.colorExpression =
|
|
24
|
+
this.colorExpression = `sampleStaticTexture(${uniformName}, p_uv)`;
|
|
25
|
+
}
|
|
26
|
+
destroy() {
|
|
27
|
+
const index = INSTANCES.indexOf(this);
|
|
28
|
+
if (index !== -1) {
|
|
29
|
+
INSTANCES.splice(index, 1);
|
|
30
|
+
}
|
|
23
31
|
}
|
|
24
32
|
}
|
|
25
33
|
|
package/package.json
CHANGED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import { MeshDepthMaterial } from "three";
|
|
2
|
-
import type { FXColorNode } from "../../nodes/color/FXColorNode";
|
|
3
|
-
import type { FXTextureNode } from "../../nodes/texture/FXTextureNode";
|
|
4
|
-
export declare const DEPTH_VERTEX_SHADER = "\n #define PARTICLE_POSITION_X a_builtin[0][0]\n #define PARTICLE_POSITION_Y a_builtin[0][1]\n #define PARTICLE_POSITION_Z a_builtin[0][2]\n\n #define PARTICLE_SCALE_X a_builtin[1][2]\n #define PARTICLE_SCALE_Y a_builtin[1][3]\n\n #define PARTICLE_ROTATION a_builtin[2][1]\n\n attribute mat4 a_builtin;\n varying mat4 p_builtin;\n varying vec2 p_uv;\n varying vec2 vHighPrecisionZW;\n varying float v_viewZ;\n #ifdef USE_ALPHAHASH\n varying vec3 vPosition;\n #endif\n\n void main() {\n p_builtin = a_builtin;\n p_uv = uv;\n #ifdef USE_ALPHAHASH\n vPosition = vec3(PARTICLE_POSITION_X, PARTICLE_POSITION_Y, PARTICLE_POSITION_Z) + vec3(position.xy, 0.0);\n #endif\n\n vec2 transformedPosition = position.xy;\n transformedPosition.x *= PARTICLE_SCALE_X;\n transformedPosition.y *= PARTICLE_SCALE_Y;\n\n float cosR = cos(PARTICLE_ROTATION);\n float sinR = sin(PARTICLE_ROTATION);\n transformedPosition = vec2(\n transformedPosition.x * cosR - transformedPosition.y * sinR,\n transformedPosition.x * sinR + transformedPosition.y * cosR\n );\n\n vec3 center = vec3(PARTICLE_POSITION_X, PARTICLE_POSITION_Y, PARTICLE_POSITION_Z);\n vec4 modelViewCenter = modelViewMatrix * vec4(center, 1.0);\n v_viewZ = modelViewCenter.z;\n modelViewCenter.xy += transformedPosition;\n gl_Position = projectionMatrix * modelViewCenter;\n vHighPrecisionZW = gl_Position.zw;\n }\n";
|
|
5
|
-
export declare function buildDepthMaterial(albedoNodes: readonly (FXColorNode | FXTextureNode)[], depthAlphaTest: number, useDepthAlphaHash: boolean, useSphericalDepth: boolean): MeshDepthMaterial;
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import { MeshDepthMaterial, DoubleSide, RGBADepthPacking } from 'three';
|
|
2
|
-
import { PARTICLE_DEFINES } from '../../miscellaneous/miscellaneous.js';
|
|
3
|
-
|
|
4
|
-
const DEPTH_VERTEX_SHADER = `
|
|
5
|
-
#define PARTICLE_POSITION_X a_builtin[0][0]
|
|
6
|
-
#define PARTICLE_POSITION_Y a_builtin[0][1]
|
|
7
|
-
#define PARTICLE_POSITION_Z a_builtin[0][2]
|
|
8
|
-
|
|
9
|
-
#define PARTICLE_SCALE_X a_builtin[1][2]
|
|
10
|
-
#define PARTICLE_SCALE_Y a_builtin[1][3]
|
|
11
|
-
|
|
12
|
-
#define PARTICLE_ROTATION a_builtin[2][1]
|
|
13
|
-
|
|
14
|
-
attribute mat4 a_builtin;
|
|
15
|
-
varying mat4 p_builtin;
|
|
16
|
-
varying vec2 p_uv;
|
|
17
|
-
varying vec2 vHighPrecisionZW;
|
|
18
|
-
varying float v_viewZ;
|
|
19
|
-
#ifdef USE_ALPHAHASH
|
|
20
|
-
varying vec3 vPosition;
|
|
21
|
-
#endif
|
|
22
|
-
|
|
23
|
-
void main() {
|
|
24
|
-
p_builtin = a_builtin;
|
|
25
|
-
p_uv = uv;
|
|
26
|
-
#ifdef USE_ALPHAHASH
|
|
27
|
-
vPosition = vec3(PARTICLE_POSITION_X, PARTICLE_POSITION_Y, PARTICLE_POSITION_Z) + vec3(position.xy, 0.0);
|
|
28
|
-
#endif
|
|
29
|
-
|
|
30
|
-
vec2 transformedPosition = position.xy;
|
|
31
|
-
transformedPosition.x *= PARTICLE_SCALE_X;
|
|
32
|
-
transformedPosition.y *= PARTICLE_SCALE_Y;
|
|
33
|
-
|
|
34
|
-
float cosR = cos(PARTICLE_ROTATION);
|
|
35
|
-
float sinR = sin(PARTICLE_ROTATION);
|
|
36
|
-
transformedPosition = vec2(
|
|
37
|
-
transformedPosition.x * cosR - transformedPosition.y * sinR,
|
|
38
|
-
transformedPosition.x * sinR + transformedPosition.y * cosR
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
vec3 center = vec3(PARTICLE_POSITION_X, PARTICLE_POSITION_Y, PARTICLE_POSITION_Z);
|
|
42
|
-
vec4 modelViewCenter = modelViewMatrix * vec4(center, 1.0);
|
|
43
|
-
v_viewZ = modelViewCenter.z;
|
|
44
|
-
modelViewCenter.xy += transformedPosition;
|
|
45
|
-
gl_Position = projectionMatrix * modelViewCenter;
|
|
46
|
-
vHighPrecisionZW = gl_Position.zw;
|
|
47
|
-
}
|
|
48
|
-
`;
|
|
49
|
-
const DEPTH_SPHERICAL_FRAG_COORD_Z = `
|
|
50
|
-
float _dx = (p_uv.x - 0.5) * PARTICLE_SCALE_X;
|
|
51
|
-
float _dy = (p_uv.y - 0.5) * PARTICLE_SCALE_Y;
|
|
52
|
-
float _r = min(PARTICLE_SCALE_X, PARTICLE_SCALE_Y) * 0.5;
|
|
53
|
-
float _d2 = _dx * _dx + _dy * _dy;
|
|
54
|
-
if (_d2 > _r * _r) discard;
|
|
55
|
-
float _sphereZ = v_viewZ + sqrt(_r * _r - _d2);
|
|
56
|
-
float _pz = projectionMatrix[2][2] * _sphereZ + projectionMatrix[3][2];
|
|
57
|
-
float _pw = projectionMatrix[2][3] * _sphereZ + projectionMatrix[3][3];
|
|
58
|
-
float _ndc_z = _pz / _pw;
|
|
59
|
-
#ifdef USE_REVERSEDEPTHBUF
|
|
60
|
-
float fragCoordZ = _ndc_z;
|
|
61
|
-
#else
|
|
62
|
-
float fragCoordZ = 0.5 * _ndc_z + 0.5;
|
|
63
|
-
#endif
|
|
64
|
-
`;
|
|
65
|
-
function buildDepthMaterial(albedoNodes, depthAlphaTest, useDepthAlphaHash, useSphericalDepth) {
|
|
66
|
-
const depthNodes = albedoNodes.filter((n) => n.affectsDepth);
|
|
67
|
-
const needsFragmentMod = depthNodes.length > 0 || useSphericalDepth;
|
|
68
|
-
const material = new MeshDepthMaterial({
|
|
69
|
-
depthPacking: RGBADepthPacking,
|
|
70
|
-
side: DoubleSide,
|
|
71
|
-
forceSinglePass: true,
|
|
72
|
-
alphaHash: useDepthAlphaHash,
|
|
73
|
-
});
|
|
74
|
-
material.onBeforeCompile = (shader) => {
|
|
75
|
-
shader.vertexShader = DEPTH_VERTEX_SHADER;
|
|
76
|
-
if (!needsFragmentMod) {
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
const fragmentPreamble = [
|
|
80
|
-
PARTICLE_DEFINES,
|
|
81
|
-
"varying mat4 p_builtin;",
|
|
82
|
-
"varying vec2 p_uv;",
|
|
83
|
-
"varying float v_viewZ;",
|
|
84
|
-
useSphericalDepth ? "uniform mat4 projectionMatrix;" : "",
|
|
85
|
-
depthNodes.flatMap((n) => n.uniformDeclarations).join("\n"),
|
|
86
|
-
depthNodes.map((n) => n.helperFunctions).join("\n"),
|
|
87
|
-
].join("\n");
|
|
88
|
-
for (const node of depthNodes) {
|
|
89
|
-
Object.assign(shader.uniforms, node.uniforms);
|
|
90
|
-
}
|
|
91
|
-
let fs = shader.fragmentShader;
|
|
92
|
-
if (depthNodes.length > 0) {
|
|
93
|
-
const combinedAlpha = depthNodes.map((n) => n.colorExpression).join(" * ");
|
|
94
|
-
fs = fs.replace("#include <map_fragment>", `diffuseColor.a = (${combinedAlpha}).a;
|
|
95
|
-
if (diffuseColor.a < ${depthAlphaTest.toFixed(4)}) discard;`);
|
|
96
|
-
}
|
|
97
|
-
if (useSphericalDepth) {
|
|
98
|
-
fs = fs.replace(/float fragCoordZ\b[^;]*;/, DEPTH_SPHERICAL_FRAG_COORD_Z);
|
|
99
|
-
}
|
|
100
|
-
shader.fragmentShader = fragmentPreamble + "\n" + fs;
|
|
101
|
-
};
|
|
102
|
-
material.customProgramCacheKey = () => `fx-depth-${useDepthAlphaHash ? "ah" : "at"}-${useSphericalDepth ? "sd" : "flat"}-${depthNodes.map((n) => n.cacheKey).join("-") || "none"}`;
|
|
103
|
-
return material;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export { DEPTH_VERTEX_SHADER, buildDepthMaterial };
|