topazcube 0.1.33 → 0.1.35
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/Renderer.cjs +2925 -281
- package/dist/Renderer.cjs.map +1 -1
- package/dist/Renderer.js +2925 -281
- package/dist/Renderer.js.map +1 -1
- package/package.json +1 -1
- package/src/renderer/DebugUI.js +176 -45
- package/src/renderer/Material.js +3 -0
- package/src/renderer/Pipeline.js +3 -1
- package/src/renderer/Renderer.js +185 -13
- package/src/renderer/core/AssetManager.js +40 -5
- package/src/renderer/core/CullingSystem.js +1 -0
- package/src/renderer/gltf.js +17 -0
- package/src/renderer/rendering/RenderGraph.js +224 -30
- package/src/renderer/rendering/passes/BloomPass.js +5 -2
- package/src/renderer/rendering/passes/CRTPass.js +724 -0
- package/src/renderer/rendering/passes/FogPass.js +26 -0
- package/src/renderer/rendering/passes/GBufferPass.js +31 -7
- package/src/renderer/rendering/passes/HiZPass.js +30 -0
- package/src/renderer/rendering/passes/LightingPass.js +14 -0
- package/src/renderer/rendering/passes/ParticlePass.js +10 -4
- package/src/renderer/rendering/passes/PostProcessPass.js +127 -4
- package/src/renderer/rendering/passes/SSGIPass.js +3 -2
- package/src/renderer/rendering/passes/SSGITilePass.js +14 -5
- package/src/renderer/rendering/passes/ShadowPass.js +265 -15
- package/src/renderer/rendering/passes/VolumetricFogPass.js +715 -0
- package/src/renderer/rendering/shaders/crt.wgsl +455 -0
- package/src/renderer/rendering/shaders/geometry.wgsl +36 -6
- package/src/renderer/rendering/shaders/particle_render.wgsl +153 -6
- package/src/renderer/rendering/shaders/postproc.wgsl +23 -2
- package/src/renderer/rendering/shaders/shadow.wgsl +42 -1
- package/src/renderer/rendering/shaders/volumetric_blur.wgsl +80 -0
- package/src/renderer/rendering/shaders/volumetric_composite.wgsl +80 -0
- package/src/renderer/rendering/shaders/volumetric_raymarch.wgsl +634 -0
- package/src/renderer/utils/Raycaster.js +761 -0
package/dist/Renderer.js
CHANGED
|
@@ -751,6 +751,13 @@ function sphereInCascade(bsphere, cascadeMatrix) {
|
|
|
751
751
|
if (clipZ + clipRadius < 0 || clipZ - clipRadius > 1) return false;
|
|
752
752
|
return true;
|
|
753
753
|
}
|
|
754
|
+
const BoundingSphere = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
755
|
+
__proto__: null,
|
|
756
|
+
calculateBoundingSphere,
|
|
757
|
+
calculateShadowBoundingSphere,
|
|
758
|
+
sphereInCascade,
|
|
759
|
+
transformBoundingSphere
|
|
760
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
754
761
|
var _UID$2 = 30001;
|
|
755
762
|
class Geometry {
|
|
756
763
|
constructor(engine, attributes) {
|
|
@@ -1913,6 +1920,7 @@ class Material {
|
|
|
1913
1920
|
this.luminanceToAlpha = false;
|
|
1914
1921
|
this.forceEmissive = false;
|
|
1915
1922
|
this.specularBoost = 0;
|
|
1923
|
+
this.doubleSided = false;
|
|
1916
1924
|
}
|
|
1917
1925
|
/**
|
|
1918
1926
|
* Get textures array, substituting albedo for emission if forceEmissive is true
|
|
@@ -2648,6 +2656,13 @@ class ShadowPass extends BasePass {
|
|
|
2648
2656
|
this.noiseAnimated = true;
|
|
2649
2657
|
this.hizPass = null;
|
|
2650
2658
|
this._meshBindGroups = /* @__PURE__ */ new WeakMap();
|
|
2659
|
+
this._cameraShadowBuffer = null;
|
|
2660
|
+
this._cameraShadowReadBuffer = null;
|
|
2661
|
+
this._cameraShadowPipeline = null;
|
|
2662
|
+
this._cameraShadowBindGroup = null;
|
|
2663
|
+
this._cameraShadowUniformBuffer = null;
|
|
2664
|
+
this._cameraInShadow = false;
|
|
2665
|
+
this._cameraShadowPending = false;
|
|
2651
2666
|
}
|
|
2652
2667
|
/**
|
|
2653
2668
|
* Set the HiZ pass for occlusion culling of static meshes
|
|
@@ -2764,6 +2779,7 @@ class ShadowPass extends BasePass {
|
|
|
2764
2779
|
});
|
|
2765
2780
|
this._createPlaceholderTextures();
|
|
2766
2781
|
await this._createPipeline();
|
|
2782
|
+
await this._createCameraShadowDetection();
|
|
2767
2783
|
}
|
|
2768
2784
|
async _createPipeline() {
|
|
2769
2785
|
const { device } = this.engine;
|
|
@@ -3207,12 +3223,13 @@ class ShadowPass extends BasePass {
|
|
|
3207
3223
|
* @param {mat4} cascadeMatrix - Cascade's view-projection matrix
|
|
3208
3224
|
* @param {Array} lightDir - Normalized light direction (pointing to light)
|
|
3209
3225
|
* @param {number} groundLevel - Ground plane Y coordinate
|
|
3226
|
+
* @param {Object|null} combinedBsphere - Combined bsphere for skinned models (optional)
|
|
3210
3227
|
* @returns {{ data: Float32Array, count: number }}
|
|
3211
3228
|
*/
|
|
3212
|
-
_buildCascadeFilteredInstances(geometry, cascadeMatrix, lightDir, groundLevel) {
|
|
3229
|
+
_buildCascadeFilteredInstances(geometry, cascadeMatrix, lightDir, groundLevel, combinedBsphere = null) {
|
|
3213
3230
|
const instanceStride = 28;
|
|
3214
3231
|
const visibleIndices = [];
|
|
3215
|
-
const localBsphere = geometry.getBoundingSphere?.();
|
|
3232
|
+
const localBsphere = combinedBsphere || geometry.getBoundingSphere?.();
|
|
3216
3233
|
for (let i = 0; i < geometry.instanceCount; i++) {
|
|
3217
3234
|
const offset = i * instanceStride;
|
|
3218
3235
|
let bsphere = {
|
|
@@ -3260,12 +3277,13 @@ class ShadowPass extends BasePass {
|
|
|
3260
3277
|
* @param {Array} lightDir - Normalized light direction
|
|
3261
3278
|
* @param {number} maxDistance - Max shadow distance (min of light radius and spotMaxDistance)
|
|
3262
3279
|
* @param {number} coneAngle - Half-angle of spotlight cone in radians
|
|
3280
|
+
* @param {Object|null} combinedBsphere - Combined bsphere for skinned models (optional)
|
|
3263
3281
|
* @returns {{ data: Float32Array, count: number }}
|
|
3264
3282
|
*/
|
|
3265
|
-
_buildFilteredInstances(geometry, lightPos, lightDir, maxDistance, coneAngle) {
|
|
3283
|
+
_buildFilteredInstances(geometry, lightPos, lightDir, maxDistance, coneAngle, combinedBsphere = null) {
|
|
3266
3284
|
const instanceStride = 28;
|
|
3267
3285
|
const visibleIndices = [];
|
|
3268
|
-
const localBsphere = geometry.getBoundingSphere?.();
|
|
3286
|
+
const localBsphere = combinedBsphere || geometry.getBoundingSphere?.();
|
|
3269
3287
|
for (let i = 0; i < geometry.instanceCount; i++) {
|
|
3270
3288
|
const offset = i * instanceStride;
|
|
3271
3289
|
let bsphere = {
|
|
@@ -3485,6 +3503,10 @@ class ShadowPass extends BasePass {
|
|
|
3485
3503
|
console.warn("ShadowPass: No pipeline or meshes", { pipeline: !!this.pipeline, meshes: !!meshes });
|
|
3486
3504
|
return;
|
|
3487
3505
|
}
|
|
3506
|
+
this._meshBindGroups = /* @__PURE__ */ new WeakMap();
|
|
3507
|
+
if (this._skinBindGroups) {
|
|
3508
|
+
this._skinBindGroups = /* @__PURE__ */ new WeakMap();
|
|
3509
|
+
}
|
|
3488
3510
|
let shadowDrawCalls = 0;
|
|
3489
3511
|
let shadowTriangles = 0;
|
|
3490
3512
|
let shadowCulledInstances = 0;
|
|
@@ -3523,7 +3545,7 @@ class ShadowPass extends BasePass {
|
|
|
3523
3545
|
visibleMeshes[name] = mesh;
|
|
3524
3546
|
continue;
|
|
3525
3547
|
}
|
|
3526
|
-
const localBsphere = geometry.getBoundingSphere?.();
|
|
3548
|
+
const localBsphere = mesh.combinedBsphere || geometry.getBoundingSphere?.();
|
|
3527
3549
|
if (!localBsphere || localBsphere.radius <= 0) {
|
|
3528
3550
|
meshNoBsphere++;
|
|
3529
3551
|
visibleMeshes[name] = mesh;
|
|
@@ -3532,23 +3554,28 @@ class ShadowPass extends BasePass {
|
|
|
3532
3554
|
const matrix = geometry.instanceData?.subarray(0, 16);
|
|
3533
3555
|
const worldBsphere = matrix ? transformBoundingSphere(localBsphere, matrix) : localBsphere;
|
|
3534
3556
|
const shadowBsphere = mainLightEnabled ? calculateShadowBoundingSphere(worldBsphere, lightDir, groundLevel) : worldBsphere;
|
|
3535
|
-
const
|
|
3536
|
-
const
|
|
3537
|
-
|
|
3538
|
-
|
|
3557
|
+
const skinnedExpansion = this.engine?.settings?.shadow?.skinnedBsphereExpansion ?? 2;
|
|
3558
|
+
const cullBsphere = mesh.hasSkin ? {
|
|
3559
|
+
center: shadowBsphere.center,
|
|
3560
|
+
radius: shadowBsphere.radius * skinnedExpansion
|
|
3561
|
+
} : shadowBsphere;
|
|
3562
|
+
const dx = cullBsphere.center[0] - camera.position[0];
|
|
3563
|
+
const dy = cullBsphere.center[1] - camera.position[1];
|
|
3564
|
+
const dz = cullBsphere.center[2] - camera.position[2];
|
|
3565
|
+
const distance = Math.sqrt(dx * dx + dy * dy + dz * dz) - cullBsphere.radius;
|
|
3539
3566
|
if (distance > shadowMaxDistance) {
|
|
3540
3567
|
meshDistanceCulled++;
|
|
3541
3568
|
continue;
|
|
3542
3569
|
}
|
|
3543
3570
|
if (shadowFrustumCullingEnabled && cameraFrustum) {
|
|
3544
|
-
if (!cameraFrustum.testSpherePlanes(
|
|
3571
|
+
if (!cameraFrustum.testSpherePlanes(cullBsphere)) {
|
|
3545
3572
|
meshFrustumCulled++;
|
|
3546
3573
|
continue;
|
|
3547
3574
|
}
|
|
3548
3575
|
}
|
|
3549
3576
|
if (shadowHiZEnabled && this.hizPass) {
|
|
3550
3577
|
const occluded = this.hizPass.testSphereOcclusion(
|
|
3551
|
-
|
|
3578
|
+
cullBsphere,
|
|
3552
3579
|
camera.viewProj,
|
|
3553
3580
|
camera.near,
|
|
3554
3581
|
camera.far,
|
|
@@ -3610,7 +3637,9 @@ class ShadowPass extends BasePass {
|
|
|
3610
3637
|
geometry,
|
|
3611
3638
|
this.cascadeMatrices[cascade],
|
|
3612
3639
|
lightDir,
|
|
3613
|
-
groundLevel
|
|
3640
|
+
groundLevel,
|
|
3641
|
+
mesh.combinedBsphere
|
|
3642
|
+
// Use combined bsphere for skinned models
|
|
3614
3643
|
);
|
|
3615
3644
|
if (filtered.count === 0) {
|
|
3616
3645
|
cascadeCulledInstances += geometry.instanceCount;
|
|
@@ -3729,6 +3758,9 @@ class ShadowPass extends BasePass {
|
|
|
3729
3758
|
this.cascadeMatricesData.set(this.cascadeMatrices[i], i * 16);
|
|
3730
3759
|
}
|
|
3731
3760
|
device.queue.writeBuffer(this.cascadeMatricesBuffer, 0, this.cascadeMatricesData);
|
|
3761
|
+
if (mainLightEnabled) {
|
|
3762
|
+
this._updateCameraShadowDetection(camera);
|
|
3763
|
+
}
|
|
3732
3764
|
if (!mainLightEnabled) {
|
|
3733
3765
|
for (const name in meshes) {
|
|
3734
3766
|
const mesh = meshes[name];
|
|
@@ -3813,7 +3845,9 @@ class ShadowPass extends BasePass {
|
|
|
3813
3845
|
lightPos,
|
|
3814
3846
|
spotLightDir,
|
|
3815
3847
|
spotShadowMaxDist,
|
|
3816
|
-
coneAngle
|
|
3848
|
+
coneAngle,
|
|
3849
|
+
mesh.combinedBsphere
|
|
3850
|
+
// Use combined bsphere for skinned models
|
|
3817
3851
|
);
|
|
3818
3852
|
if (filtered.count === 0) {
|
|
3819
3853
|
spotCulledInstances += geometry.instanceCount;
|
|
@@ -4042,6 +4076,184 @@ class ShadowPass extends BasePass {
|
|
|
4042
4076
|
getLastSlotInfo() {
|
|
4043
4077
|
return this.lastSlotInfo;
|
|
4044
4078
|
}
|
|
4079
|
+
/**
|
|
4080
|
+
* Get cascade matrices as JavaScript arrays (for CPU-side calculations)
|
|
4081
|
+
*/
|
|
4082
|
+
getCascadeMatrices() {
|
|
4083
|
+
return this.cascadeMatrices;
|
|
4084
|
+
}
|
|
4085
|
+
/**
|
|
4086
|
+
* Create resources for camera shadow detection
|
|
4087
|
+
* Uses a compute shader to sample shadow at camera position
|
|
4088
|
+
*/
|
|
4089
|
+
async _createCameraShadowDetection() {
|
|
4090
|
+
const { device } = this.engine;
|
|
4091
|
+
this._cameraShadowUniformBuffer = device.createBuffer({
|
|
4092
|
+
label: "Camera Shadow Detection Uniforms",
|
|
4093
|
+
size: 256,
|
|
4094
|
+
// Aligned
|
|
4095
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
4096
|
+
});
|
|
4097
|
+
this._cameraShadowBuffer = device.createBuffer({
|
|
4098
|
+
label: "Camera Shadow Result",
|
|
4099
|
+
size: 4,
|
|
4100
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
|
|
4101
|
+
});
|
|
4102
|
+
this._cameraShadowReadBuffer = device.createBuffer({
|
|
4103
|
+
label: "Camera Shadow Readback",
|
|
4104
|
+
size: 4,
|
|
4105
|
+
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
|
|
4106
|
+
});
|
|
4107
|
+
const shaderModule = device.createShaderModule({
|
|
4108
|
+
label: "Camera Shadow Detection Shader",
|
|
4109
|
+
code: `
|
|
4110
|
+
struct Uniforms {
|
|
4111
|
+
cameraPosition: vec3f,
|
|
4112
|
+
_pad0: f32,
|
|
4113
|
+
cascadeMatrix0: mat4x4f,
|
|
4114
|
+
cascadeMatrix1: mat4x4f,
|
|
4115
|
+
cascadeMatrix2: mat4x4f,
|
|
4116
|
+
}
|
|
4117
|
+
|
|
4118
|
+
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
|
|
4119
|
+
@group(0) @binding(1) var shadowMap: texture_depth_2d_array;
|
|
4120
|
+
@group(0) @binding(2) var shadowSampler: sampler_comparison;
|
|
4121
|
+
@group(0) @binding(3) var<storage, read_write> result: f32;
|
|
4122
|
+
|
|
4123
|
+
fn sampleShadowCascade(worldPos: vec3f, cascadeMatrix: mat4x4f, cascadeIndex: i32) -> f32 {
|
|
4124
|
+
let lightSpacePos = cascadeMatrix * vec4f(worldPos, 1.0);
|
|
4125
|
+
let projCoords = lightSpacePos.xyz / lightSpacePos.w;
|
|
4126
|
+
|
|
4127
|
+
// Convert to UV space
|
|
4128
|
+
let uv = vec2f(projCoords.x * 0.5 + 0.5, 0.5 - projCoords.y * 0.5);
|
|
4129
|
+
|
|
4130
|
+
// Check bounds
|
|
4131
|
+
if (uv.x < 0.01 || uv.x > 0.99 || uv.y < 0.01 || uv.y > 0.99 ||
|
|
4132
|
+
projCoords.z < 0.0 || projCoords.z > 1.0) {
|
|
4133
|
+
return -1.0; // Out of bounds, try next cascade
|
|
4134
|
+
}
|
|
4135
|
+
|
|
4136
|
+
let bias = 0.005;
|
|
4137
|
+
let depth = projCoords.z - bias;
|
|
4138
|
+
return textureSampleCompareLevel(shadowMap, shadowSampler, uv, cascadeIndex, depth);
|
|
4139
|
+
}
|
|
4140
|
+
|
|
4141
|
+
@compute @workgroup_size(1)
|
|
4142
|
+
fn main() {
|
|
4143
|
+
let pos = uniforms.cameraPosition;
|
|
4144
|
+
|
|
4145
|
+
// Sample multiple points around camera (5m sphere)
|
|
4146
|
+
var totalShadow = 0.0;
|
|
4147
|
+
var sampleCount = 0.0;
|
|
4148
|
+
|
|
4149
|
+
let offsets = array<vec3f, 7>(
|
|
4150
|
+
vec3f(0.0, 0.0, 0.0), // Center
|
|
4151
|
+
vec3f(0.0, 3.0, 0.0), // Above
|
|
4152
|
+
vec3f(0.0, -2.0, 0.0), // Below
|
|
4153
|
+
vec3f(4.0, 0.0, 0.0), // Right
|
|
4154
|
+
vec3f(-4.0, 0.0, 0.0), // Left
|
|
4155
|
+
vec3f(0.0, 0.0, 4.0), // Front
|
|
4156
|
+
vec3f(0.0, 0.0, -4.0), // Back
|
|
4157
|
+
);
|
|
4158
|
+
|
|
4159
|
+
for (var i = 0; i < 7; i++) {
|
|
4160
|
+
let samplePos = pos + offsets[i];
|
|
4161
|
+
|
|
4162
|
+
// Try cascade 0 first (closest)
|
|
4163
|
+
var shadow = sampleShadowCascade(samplePos, uniforms.cascadeMatrix0, 0);
|
|
4164
|
+
if (shadow < 0.0) {
|
|
4165
|
+
// Try cascade 1
|
|
4166
|
+
shadow = sampleShadowCascade(samplePos, uniforms.cascadeMatrix1, 1);
|
|
4167
|
+
}
|
|
4168
|
+
if (shadow < 0.0) {
|
|
4169
|
+
// Try cascade 2
|
|
4170
|
+
shadow = sampleShadowCascade(samplePos, uniforms.cascadeMatrix2, 2);
|
|
4171
|
+
}
|
|
4172
|
+
|
|
4173
|
+
if (shadow >= 0.0) {
|
|
4174
|
+
totalShadow += shadow;
|
|
4175
|
+
sampleCount += 1.0;
|
|
4176
|
+
}
|
|
4177
|
+
}
|
|
4178
|
+
|
|
4179
|
+
// Average shadow (0 = all in shadow, 1 = all lit)
|
|
4180
|
+
// If no valid samples, assume lit
|
|
4181
|
+
if (sampleCount > 0.0) {
|
|
4182
|
+
result = totalShadow / sampleCount;
|
|
4183
|
+
} else {
|
|
4184
|
+
result = 1.0;
|
|
4185
|
+
}
|
|
4186
|
+
}
|
|
4187
|
+
`
|
|
4188
|
+
});
|
|
4189
|
+
this._cameraShadowBGL = device.createBindGroupLayout({
|
|
4190
|
+
label: "Camera Shadow Detection BGL",
|
|
4191
|
+
entries: [
|
|
4192
|
+
{ binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "uniform" } },
|
|
4193
|
+
{ binding: 1, visibility: GPUShaderStage.COMPUTE, texture: { sampleType: "depth", viewDimension: "2d-array" } },
|
|
4194
|
+
{ binding: 2, visibility: GPUShaderStage.COMPUTE, sampler: { type: "comparison" } },
|
|
4195
|
+
{ binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } }
|
|
4196
|
+
]
|
|
4197
|
+
});
|
|
4198
|
+
this._cameraShadowPipeline = await device.createComputePipelineAsync({
|
|
4199
|
+
label: "Camera Shadow Detection Pipeline",
|
|
4200
|
+
layout: device.createPipelineLayout({ bindGroupLayouts: [this._cameraShadowBGL] }),
|
|
4201
|
+
compute: { module: shaderModule, entryPoint: "main" }
|
|
4202
|
+
});
|
|
4203
|
+
}
|
|
4204
|
+
/**
|
|
4205
|
+
* Update camera shadow detection (called during execute)
|
|
4206
|
+
* Dispatches compute shader and starts async readback
|
|
4207
|
+
*/
|
|
4208
|
+
_updateCameraShadowDetection(camera) {
|
|
4209
|
+
if (!this._cameraShadowPipeline || !this.directionalShadowMap) return;
|
|
4210
|
+
if (this._cameraShadowPending) return;
|
|
4211
|
+
const { device } = this.engine;
|
|
4212
|
+
const cameraPos = camera.position || [0, 0, 0];
|
|
4213
|
+
const data = new Float32Array(64);
|
|
4214
|
+
data[0] = cameraPos[0];
|
|
4215
|
+
data[1] = cameraPos[1];
|
|
4216
|
+
data[2] = cameraPos[2];
|
|
4217
|
+
data[3] = 0;
|
|
4218
|
+
if (this.cascadeMatrices[0]) data.set(this.cascadeMatrices[0], 4);
|
|
4219
|
+
if (this.cascadeMatrices[1]) data.set(this.cascadeMatrices[1], 20);
|
|
4220
|
+
if (this.cascadeMatrices[2]) data.set(this.cascadeMatrices[2], 36);
|
|
4221
|
+
device.queue.writeBuffer(this._cameraShadowUniformBuffer, 0, data);
|
|
4222
|
+
const bindGroup = device.createBindGroup({
|
|
4223
|
+
layout: this._cameraShadowBGL,
|
|
4224
|
+
entries: [
|
|
4225
|
+
{ binding: 0, resource: { buffer: this._cameraShadowUniformBuffer } },
|
|
4226
|
+
{ binding: 1, resource: this.directionalShadowMapView },
|
|
4227
|
+
{ binding: 2, resource: this.shadowSampler },
|
|
4228
|
+
{ binding: 3, resource: { buffer: this._cameraShadowBuffer } }
|
|
4229
|
+
]
|
|
4230
|
+
});
|
|
4231
|
+
const encoder = device.createCommandEncoder({ label: "Camera Shadow Detection" });
|
|
4232
|
+
const pass = encoder.beginComputePass();
|
|
4233
|
+
pass.setPipeline(this._cameraShadowPipeline);
|
|
4234
|
+
pass.setBindGroup(0, bindGroup);
|
|
4235
|
+
pass.dispatchWorkgroups(1);
|
|
4236
|
+
pass.end();
|
|
4237
|
+
encoder.copyBufferToBuffer(this._cameraShadowBuffer, 0, this._cameraShadowReadBuffer, 0, 4);
|
|
4238
|
+
device.queue.submit([encoder.finish()]);
|
|
4239
|
+
this._cameraShadowPending = true;
|
|
4240
|
+
this._cameraShadowReadBuffer.mapAsync(GPUMapMode.READ).then(() => {
|
|
4241
|
+
const data2 = new Float32Array(this._cameraShadowReadBuffer.getMappedRange());
|
|
4242
|
+
const shadowValue = data2[0];
|
|
4243
|
+
this._cameraShadowReadBuffer.unmap();
|
|
4244
|
+
this._cameraShadowPending = false;
|
|
4245
|
+
this._cameraInShadow = shadowValue < 0.3;
|
|
4246
|
+
}).catch(() => {
|
|
4247
|
+
this._cameraShadowPending = false;
|
|
4248
|
+
});
|
|
4249
|
+
}
|
|
4250
|
+
/**
|
|
4251
|
+
* Check if camera is in shadow (uses async readback result from previous frames)
|
|
4252
|
+
* @returns {boolean} True if camera is mostly in shadow
|
|
4253
|
+
*/
|
|
4254
|
+
isCameraInShadow() {
|
|
4255
|
+
return this._cameraInShadow;
|
|
4256
|
+
}
|
|
4045
4257
|
}
|
|
4046
4258
|
class ProbeCapture {
|
|
4047
4259
|
constructor(engine) {
|
|
@@ -5699,8 +5911,10 @@ class Pipeline {
|
|
|
5699
5911
|
// Optional tile light indices buffer for tiled lighting
|
|
5700
5912
|
lightBuffer = null,
|
|
5701
5913
|
// Optional light storage buffer for tiled lighting
|
|
5702
|
-
noiseTexture = null
|
|
5914
|
+
noiseTexture = null,
|
|
5703
5915
|
// Optional noise texture for alpha hashing
|
|
5916
|
+
doubleSided = false
|
|
5917
|
+
// Optional: disable backface culling for double-sided materials
|
|
5704
5918
|
}) {
|
|
5705
5919
|
let texture = textures[0];
|
|
5706
5920
|
const { canvas, device, canvasFormat, options } = engine;
|
|
@@ -5762,7 +5976,7 @@ class Pipeline {
|
|
|
5762
5976
|
geometry.vertexBufferLayout,
|
|
5763
5977
|
geometry.instanceBufferLayout
|
|
5764
5978
|
];
|
|
5765
|
-
pipelineDescriptor.primitive.cullMode = "back";
|
|
5979
|
+
pipelineDescriptor.primitive.cullMode = doubleSided ? "none" : "back";
|
|
5766
5980
|
pipelineDescriptor.depthStencil = {
|
|
5767
5981
|
depthWriteEnabled: true,
|
|
5768
5982
|
depthCompare: "less",
|
|
@@ -5990,6 +6204,7 @@ class Pipeline {
|
|
|
5990
6204
|
p.tileLightBuffer = tileLightBuffer;
|
|
5991
6205
|
p.lightBuffer = lightBuffer;
|
|
5992
6206
|
p.noiseTexture = noiseTexture;
|
|
6207
|
+
p.doubleSided = doubleSided;
|
|
5993
6208
|
return p;
|
|
5994
6209
|
}
|
|
5995
6210
|
static async pipelineFromTextures(engine, pipelineDescriptor, label, textures, uniformBuffer, skin = null, noiseTexture = null) {
|
|
@@ -6164,7 +6379,7 @@ class Pipeline {
|
|
|
6164
6379
|
}
|
|
6165
6380
|
}
|
|
6166
6381
|
}
|
|
6167
|
-
var geometry_default = "struct VertexInput {\n @location(0) position: vec3f,\n @location(1) uv: vec2f,\n @location(2) normal: vec3f,\n @location(3) color: vec4f,\n @location(4) weights: vec4f,\n @location(5) joints: vec4u,\n}\n\nstruct VertexOutput {\n @invariant @builtin(position) position: vec4f, \n @location(0) worldPos: vec3f,\n @location(1) uv: vec2f,\n @location(2) normal: vec3f,\n @location(3) viewZ: f32, \n @location(4) currClipPos: vec4f, \n @location(5) prevClipPos: vec4f, \n @location(6) instanceColor: vec4f, \n @location(7) anchorY: f32, \n}\n\nstruct Uniforms {\n viewMatrix: mat4x4f,\n projectionMatrix: mat4x4f,\n prevViewProjMatrix: mat4x4f, \n mipBias: f32,\n skinEnabled: f32, \n numJoints: f32, \n near: f32, \n far: f32, \n jitterFadeDistance: f32, \n jitterOffset: vec2f, \n screenSize: vec2f, \n emissionFactor: vec4f,\n clipPlaneY: f32, \n clipPlaneEnabled: f32, \n clipPlaneDirection: f32, \n pixelRounding: f32, \n pixelExpansion: f32, \n positionRounding: f32, \n alphaHashEnabled: f32, \n alphaHashScale: f32, \n luminanceToAlpha: f32, \n noiseSize: f32, \n noiseOffsetX: f32, \n noiseOffsetY: f32, \n cameraPosition: vec3f, \n distanceFadeStart: f32, \n distanceFadeEnd: f32, \n \n billboardMode: f32, \n billboardCameraRight: vec3f, \n _pad1: f32,\n billboardCameraUp: vec3f, \n _pad2: f32,\n billboardCameraForward: vec3f, \n specularBoost: f32, \n}\n\nstruct InstanceData {\n modelMatrix: mat4x4f,\n posRadius: vec4f,\n}\n\n@group(0) @binding(0) var<uniform> uniforms: Uniforms;\n@group(0) @binding(1) var albedoTexture: texture_2d<f32>;\n@group(0) @binding(2) var albedoSampler: sampler;\n@group(0) @binding(3) var normalTexture: texture_2d<f32>;\n@group(0) @binding(4) var normalSampler: sampler;\n@group(0) @binding(5) var ambientTexture: texture_2d<f32>;\n@group(0) @binding(6) var ambientSampler: sampler;\n@group(0) @binding(7) var rmTexture: texture_2d<f32>;\n@group(0) @binding(8) var rmSampler: sampler;\n@group(0) @binding(9) var emissionTexture: texture_2d<f32>;\n@group(0) @binding(10) var emissionSampler: sampler;\n@group(0) @binding(11) var jointTexture: texture_2d<f32>;\n@group(0) @binding(12) var jointSampler: sampler;\n@group(0) @binding(13) var prevJointTexture: texture_2d<f32>; \n@group(0) @binding(14) var noiseTexture: texture_2d<f32>; \n\nfn sampleNoise(screenPos: vec2f) -> f32 {\n let noiseSize = i32(uniforms.noiseSize);\n let noiseOffsetX = i32(uniforms.noiseOffsetX * f32(noiseSize));\n let noiseOffsetY = i32(uniforms.noiseOffsetY * f32(noiseSize));\n\n let texCoord = vec2i(\n (i32(screenPos.x) + noiseOffsetX) % noiseSize,\n (i32(screenPos.y) + noiseOffsetY) % noiseSize\n );\n return textureLoad(noiseTexture, texCoord, 0).r;\n}\n\nfn getJointMatrix(jointIndex: u32) -> mat4x4f {\n let row = i32(jointIndex);\n let col0 = textureLoad(jointTexture, vec2i(0, row), 0);\n let col1 = textureLoad(jointTexture, vec2i(1, row), 0);\n let col2 = textureLoad(jointTexture, vec2i(2, row), 0);\n let col3 = textureLoad(jointTexture, vec2i(3, row), 0);\n return mat4x4f(col0, col1, col2, col3);\n}\n\nfn getPrevJointMatrix(jointIndex: u32) -> mat4x4f {\n let row = i32(jointIndex);\n let col0 = textureLoad(prevJointTexture, vec2i(0, row), 0);\n let col1 = textureLoad(prevJointTexture, vec2i(1, row), 0);\n let col2 = textureLoad(prevJointTexture, vec2i(2, row), 0);\n let col3 = textureLoad(prevJointTexture, vec2i(3, row), 0);\n return mat4x4f(col0, col1, col2, col3);\n}\n\nfn applySkinning(position: vec3f, joints: vec4u, weights: vec4f) -> vec3f {\n var skinnedPos = vec3f(0.0);\n\n let m0 = getJointMatrix(joints.x);\n let m1 = getJointMatrix(joints.y);\n let m2 = getJointMatrix(joints.z);\n let m3 = getJointMatrix(joints.w);\n\n skinnedPos += (m0 * vec4f(position, 1.0)).xyz * weights.x;\n skinnedPos += (m1 * vec4f(position, 1.0)).xyz * weights.y;\n skinnedPos += (m2 * vec4f(position, 1.0)).xyz * weights.z;\n skinnedPos += (m3 * vec4f(position, 1.0)).xyz * weights.w;\n\n return skinnedPos;\n}\n\nfn applySkinningPrev(position: vec3f, joints: vec4u, weights: vec4f) -> vec3f {\n var skinnedPos = vec3f(0.0);\n\n let m0 = getPrevJointMatrix(joints.x);\n let m1 = getPrevJointMatrix(joints.y);\n let m2 = getPrevJointMatrix(joints.z);\n let m3 = getPrevJointMatrix(joints.w);\n\n skinnedPos += (m0 * vec4f(position, 1.0)).xyz * weights.x;\n skinnedPos += (m1 * vec4f(position, 1.0)).xyz * weights.y;\n skinnedPos += (m2 * vec4f(position, 1.0)).xyz * weights.z;\n skinnedPos += (m3 * vec4f(position, 1.0)).xyz * weights.w;\n\n return skinnedPos;\n}\n\nfn applySkinningNormal(normal: vec3f, joints: vec4u, weights: vec4f) -> vec3f {\n var skinnedNormal = vec3f(0.0);\n\n let m0 = getJointMatrix(joints.x);\n let m1 = getJointMatrix(joints.y);\n let m2 = getJointMatrix(joints.z);\n let m3 = getJointMatrix(joints.w);\n\n \n let r0 = mat3x3f(m0[0].xyz, m0[1].xyz, m0[2].xyz);\n let r1 = mat3x3f(m1[0].xyz, m1[1].xyz, m1[2].xyz);\n let r2 = mat3x3f(m2[0].xyz, m2[1].xyz, m2[2].xyz);\n let r3 = mat3x3f(m3[0].xyz, m3[1].xyz, m3[2].xyz);\n\n skinnedNormal += (r0 * normal) * weights.x;\n skinnedNormal += (r1 * normal) * weights.y;\n skinnedNormal += (r2 * normal) * weights.z;\n skinnedNormal += (r3 * normal) * weights.w;\n\n return normalize(skinnedNormal);\n}\n\n@vertex\nfn vertexMain(\n input: VertexInput,\n @builtin(instance_index) instanceIdx : u32,\n @location(6) instanceModelMatrix0: vec4f,\n @location(7) instanceModelMatrix1: vec4f,\n @location(8) instanceModelMatrix2: vec4f,\n @location(9) instanceModelMatrix3: vec4f,\n @location(10) posRadius: vec4f,\n @location(11) uvTransform: vec4f, \n @location(12) instanceColor: vec4f, \n) -> VertexOutput {\n var output: VertexOutput;\n\n \n let instanceModelMatrix = mat4x4f(\n instanceModelMatrix0,\n instanceModelMatrix1,\n instanceModelMatrix2,\n instanceModelMatrix3\n );\n\n var localPos = input.position;\n var prevLocalPos = input.position; \n var localNormal = input.normal;\n var billboardWorldPos: vec3f;\n var useBillboardWorldPos = false;\n\n \n if (uniforms.billboardMode > 0.5) {\n \n let entityPos = instanceModelMatrix[3].xyz;\n\n \n let scaleX = length(instanceModelMatrix[0].xyz);\n let scaleY = length(instanceModelMatrix[1].xyz);\n\n if (uniforms.billboardMode < 1.5) {\n \n \n let toCamera = uniforms.cameraPosition - entityPos;\n let toCameraDist = length(toCamera);\n\n var right: vec3f;\n var up: vec3f;\n\n if (toCameraDist > 0.001) {\n let forward = toCamera / toCameraDist;\n \n right = cross(vec3f(0.0, 1.0, 0.0), forward);\n let rightLen = length(right);\n if (rightLen < 0.001) {\n \n right = uniforms.billboardCameraRight;\n up = vec3f(0.0, 0.0, 1.0); \n } else {\n right = right / rightLen;\n \n up = cross(forward, right);\n }\n } else {\n right = uniforms.billboardCameraRight;\n up = vec3f(0.0, 1.0, 0.0);\n }\n\n let offset = right * input.position.x * scaleX + up * input.position.y * scaleY;\n billboardWorldPos = entityPos + offset;\n useBillboardWorldPos = true;\n localNormal = vec3f(0.0, 1.0, 0.0); \n\n } else if (uniforms.billboardMode < 2.5) {\n \n \n let toCamera = uniforms.cameraPosition - entityPos;\n let toCameraDist = length(toCamera);\n\n var right: vec3f;\n var billUp: vec3f;\n\n if (toCameraDist > 0.001) {\n let forward = toCamera / toCameraDist; \n \n right = cross(vec3f(0.0, 1.0, 0.0), forward);\n let rightLen = length(right);\n if (rightLen < 0.001) {\n \n right = uniforms.billboardCameraRight;\n billUp = uniforms.billboardCameraUp;\n } else {\n right = right / rightLen;\n \n billUp = cross(forward, right);\n }\n } else {\n \n right = uniforms.billboardCameraRight;\n billUp = uniforms.billboardCameraUp;\n }\n\n let offset = right * input.position.x * scaleX + billUp * input.position.y * scaleY;\n billboardWorldPos = entityPos + offset;\n useBillboardWorldPos = true;\n localNormal = vec3f(0.0, 1.0, 0.0); \n\n } else {\n \n \n \n \n localNormal = vec3f(0.0, 1.0, 0.0); \n }\n }\n\n \n if (uniforms.skinEnabled > 0.5) {\n \n let weightSum = input.weights.x + input.weights.y + input.weights.z + input.weights.w;\n if (weightSum > 0.001) {\n localPos = applySkinning(input.position, input.joints, input.weights);\n prevLocalPos = applySkinningPrev(input.position, input.joints, input.weights);\n localNormal = applySkinningNormal(input.normal, input.joints, input.weights);\n }\n }\n\n \n \n \n var worldPos: vec3f;\n if (useBillboardWorldPos) {\n worldPos = billboardWorldPos;\n } else {\n worldPos = (instanceModelMatrix * vec4f(localPos, 1.0)).xyz;\n }\n var viewPos = uniforms.viewMatrix * vec4f(worldPos, 1.0);\n\n \n let allowRounding = posRadius.w >= 0.0;\n\n \n \n if (allowRounding && uniforms.positionRounding > 0.0) {\n let snap = uniforms.positionRounding;\n viewPos = vec4f(\n floor(viewPos.x / snap) * snap,\n floor(viewPos.y / snap) * snap,\n viewPos.z,\n viewPos.w\n );\n }\n\n var clipPos = uniforms.projectionMatrix * viewPos;\n\n \n \n \n var prevWorldPos: vec3f;\n if (useBillboardWorldPos) {\n prevWorldPos = billboardWorldPos; \n } else {\n prevWorldPos = (instanceModelMatrix * vec4f(prevLocalPos, 1.0)).xyz;\n }\n let prevClipPos = uniforms.prevViewProjMatrix * vec4f(prevWorldPos, 1.0);\n\n \n \n let viewDist = -viewPos.z; \n let jitterFade = saturate(1.0 - viewDist / uniforms.jitterFadeDistance);\n\n \n \n let jitterClip = uniforms.jitterOffset * 2.0 / uniforms.screenSize * jitterFade;\n clipPos.x += jitterClip.x * clipPos.w;\n clipPos.y += jitterClip.y * clipPos.w;\n\n \n \n if (allowRounding && uniforms.pixelRounding > 0.5) {\n let gridSize = uniforms.pixelRounding; \n let ndc = clipPos.xy / clipPos.w;\n\n \n let pixelCoords = (ndc + 1.0) * 0.5 * uniforms.screenSize;\n\n \n var snappedPixel = floor(pixelCoords / gridSize) * gridSize;\n\n \n if (uniforms.pixelExpansion > 0.0) {\n let screenCenter = uniforms.screenSize * 0.5;\n let fromCenter = snappedPixel - screenCenter;\n snappedPixel += sign(fromCenter) * uniforms.pixelExpansion;\n }\n\n \n let snappedNDC = (snappedPixel / uniforms.screenSize) * 2.0 - 1.0;\n\n \n clipPos.x = snappedNDC.x * clipPos.w;\n clipPos.y = snappedNDC.y * clipPos.w;\n }\n\n output.position = clipPos;\n output.worldPos = worldPos;\n \n output.uv = input.uv * uvTransform.zw + uvTransform.xy;\n output.viewZ = viewDist;\n output.currClipPos = clipPos;\n output.prevClipPos = prevClipPos;\n output.instanceColor = instanceColor;\n\n \n \n \n \n if (uniforms.billboardMode > 0.5) {\n let entityPos = instanceModelMatrix[3].xyz;\n output.anchorY = entityPos.y;\n } else {\n output.anchorY = worldPos.y; \n }\n\n \n \n if (uniforms.billboardMode > 0.5) {\n output.normal = localNormal; \n } else {\n let normalMatrix = mat3x3f(\n instanceModelMatrix[0].xyz,\n instanceModelMatrix[1].xyz,\n instanceModelMatrix[2].xyz\n );\n output.normal = normalMatrix * localNormal;\n }\n return output;\n}\n\nstruct GBufferOutput {\n @location(0) albedo: vec4f,\n @location(1) normal: vec4f,\n @location(2) arm: vec4f, \n @location(3) emission: vec4f,\n @location(4) velocity: vec2f, \n @builtin(frag_depth) depth: f32, \n}\n\nfn interleavedGradientNoise(screenPos: vec2f) -> f32 {\n let magic = vec3f(0.06711056, 0.00583715, 52.9829189);\n return fract(magic.z * fract(dot(screenPos, magic.xy)));\n}\n\nfn screenHash(screenPos: vec2f) -> f32 {\n let p = fract(screenPos * vec2f(0.1031, 0.1030));\n let p3 = p.xyx * (p.yxy + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\n@fragment\nfn fragmentMain(input: VertexOutput) -> GBufferOutput {\n \n \n \n \n \n if (uniforms.clipPlaneEnabled > 0.5 && uniforms.billboardMode < 0.5) {\n let clipDist = (input.worldPos.y - uniforms.clipPlaneY) * uniforms.clipPlaneDirection;\n if (clipDist < 0.0) {\n discard;\n }\n }\n\n \n \n if (uniforms.distanceFadeEnd > 0.0) {\n let distToCamera = length(input.worldPos - uniforms.cameraPosition);\n if (distToCamera >= uniforms.distanceFadeEnd) {\n discard; \n }\n if (distToCamera > uniforms.distanceFadeStart) {\n \n let fadeRange = uniforms.distanceFadeEnd - uniforms.distanceFadeStart;\n let fadeFactor = 1.0 - (distToCamera - uniforms.distanceFadeStart) / fadeRange;\n \n let noise = sampleNoise(input.position.xy);\n if (fadeFactor < noise) {\n discard; \n }\n }\n }\n\n var output: GBufferOutput;\n\n \n let mipBias = uniforms.mipBias; \n output.albedo = textureSampleBias(albedoTexture, albedoSampler, input.uv, mipBias);\n\n \n output.albedo = output.albedo * input.instanceColor;\n\n \n \n if (uniforms.luminanceToAlpha > 0.5) {\n let luminance = dot(output.albedo.rgb, vec3f(0.299, 0.587, 0.114));\n if (luminance < 0.004) {\n discard; \n }\n output.albedo.a = 1.0; \n }\n\n \n \n if (uniforms.alphaHashEnabled > 0.5 && uniforms.luminanceToAlpha < 0.5) {\n let alpha = output.albedo.a * uniforms.alphaHashScale;\n let noise = sampleNoise(input.position.xy);\n\n \n if (alpha < 0.5) {\n discard;\n }\n \n \n let remappedAlpha = (alpha - 0.5) * 2.0;\n if (remappedAlpha < noise) {\n discard;\n }\n }\n\n \n\n \n let nsample = textureSampleBias(normalTexture, normalSampler, input.uv, mipBias).rgb;\n let tangentNormal = normalize(nsample * 2.0 - 1.0);\n\n \n let N = normalize(input.normal);\n \n let refVec = select(vec3f(0.0, 1.0, 0.0), vec3f(1.0, 0.0, 0.0), abs(N.y) > 0.9);\n let T = normalize(cross(N, refVec));\n let B = cross(N, T);\n let TBN = mat3x3f(T, B, N);\n\n \n let normal = normalize(TBN * tangentNormal);\n \n output.normal = vec4f(normal, input.worldPos.y);\n\n \n var ambient = textureSampleBias(ambientTexture, ambientSampler, input.uv, mipBias).rgb;\n if (ambient.r < 0.04) {\n ambient.r = 1.0;\n }\n let rm = textureSampleBias(rmTexture, rmSampler, input.uv, mipBias).rgb;\n output.arm = vec4f(ambient.r, rm.g, rm.b, uniforms.specularBoost);\n\n \n output.emission = textureSampleBias(emissionTexture, emissionSampler, input.uv, mipBias) * uniforms.emissionFactor;\n\n \n \n let currNDC = input.currClipPos.xy / input.currClipPos.w;\n let prevNDC = input.prevClipPos.xy / input.prevClipPos.w;\n\n \n \n \n let velocityNDC = currNDC - prevNDC;\n output.velocity = velocityNDC * uniforms.screenSize * 0.5;\n\n \n let near = uniforms.near;\n let far = uniforms.far;\n let z = input.viewZ;\n output.depth = (z - near) / (far - near);\n\n return output;\n}";
|
|
6382
|
+
var geometry_default = "struct VertexInput {\n @location(0) position: vec3f,\n @location(1) uv: vec2f,\n @location(2) normal: vec3f,\n @location(3) color: vec4f,\n @location(4) weights: vec4f,\n @location(5) joints: vec4u,\n}\n\nstruct VertexOutput {\n @invariant @builtin(position) position: vec4f, \n @location(0) worldPos: vec3f,\n @location(1) uv: vec2f,\n @location(2) normal: vec3f,\n @location(3) viewZ: f32, \n @location(4) currClipPos: vec4f, \n @location(5) prevClipPos: vec4f, \n @location(6) instanceColor: vec4f, \n @location(7) anchorY: f32, \n}\n\nstruct Uniforms {\n viewMatrix: mat4x4f,\n projectionMatrix: mat4x4f,\n prevViewProjMatrix: mat4x4f, \n mipBias: f32,\n skinEnabled: f32, \n numJoints: f32, \n near: f32, \n far: f32, \n jitterFadeDistance: f32, \n jitterOffset: vec2f, \n screenSize: vec2f, \n emissionFactor: vec4f,\n clipPlaneY: f32, \n clipPlaneEnabled: f32, \n clipPlaneDirection: f32, \n pixelRounding: f32, \n pixelExpansion: f32, \n positionRounding: f32, \n alphaHashEnabled: f32, \n alphaHashScale: f32, \n luminanceToAlpha: f32, \n noiseSize: f32, \n noiseOffsetX: f32, \n noiseOffsetY: f32, \n cameraPosition: vec3f, \n distanceFadeStart: f32, \n distanceFadeEnd: f32, \n \n billboardMode: f32, \n billboardCameraRight: vec3f, \n _pad1: f32,\n billboardCameraUp: vec3f, \n _pad2: f32,\n billboardCameraForward: vec3f, \n specularBoost: f32, \n}\n\nstruct InstanceData {\n modelMatrix: mat4x4f,\n posRadius: vec4f,\n}\n\n@group(0) @binding(0) var<uniform> uniforms: Uniforms;\n@group(0) @binding(1) var albedoTexture: texture_2d<f32>;\n@group(0) @binding(2) var albedoSampler: sampler;\n@group(0) @binding(3) var normalTexture: texture_2d<f32>;\n@group(0) @binding(4) var normalSampler: sampler;\n@group(0) @binding(5) var ambientTexture: texture_2d<f32>;\n@group(0) @binding(6) var ambientSampler: sampler;\n@group(0) @binding(7) var rmTexture: texture_2d<f32>;\n@group(0) @binding(8) var rmSampler: sampler;\n@group(0) @binding(9) var emissionTexture: texture_2d<f32>;\n@group(0) @binding(10) var emissionSampler: sampler;\n@group(0) @binding(11) var jointTexture: texture_2d<f32>;\n@group(0) @binding(12) var jointSampler: sampler;\n@group(0) @binding(13) var prevJointTexture: texture_2d<f32>; \n@group(0) @binding(14) var noiseTexture: texture_2d<f32>; \n\nfn sampleNoise(screenPos: vec2f) -> f32 {\n let noiseSize = i32(uniforms.noiseSize);\n let noiseOffsetX = i32(uniforms.noiseOffsetX * f32(noiseSize));\n let noiseOffsetY = i32(uniforms.noiseOffsetY * f32(noiseSize));\n\n let texCoord = vec2i(\n (i32(screenPos.x) + noiseOffsetX) % noiseSize,\n (i32(screenPos.y) + noiseOffsetY) % noiseSize\n );\n return textureLoad(noiseTexture, texCoord, 0).r;\n}\n\nfn getJointMatrix(jointIndex: u32) -> mat4x4f {\n let row = i32(jointIndex);\n let col0 = textureLoad(jointTexture, vec2i(0, row), 0);\n let col1 = textureLoad(jointTexture, vec2i(1, row), 0);\n let col2 = textureLoad(jointTexture, vec2i(2, row), 0);\n let col3 = textureLoad(jointTexture, vec2i(3, row), 0);\n return mat4x4f(col0, col1, col2, col3);\n}\n\nfn getPrevJointMatrix(jointIndex: u32) -> mat4x4f {\n let row = i32(jointIndex);\n let col0 = textureLoad(prevJointTexture, vec2i(0, row), 0);\n let col1 = textureLoad(prevJointTexture, vec2i(1, row), 0);\n let col2 = textureLoad(prevJointTexture, vec2i(2, row), 0);\n let col3 = textureLoad(prevJointTexture, vec2i(3, row), 0);\n return mat4x4f(col0, col1, col2, col3);\n}\n\nfn applySkinning(position: vec3f, joints: vec4u, weights: vec4f) -> vec3f {\n var skinnedPos = vec3f(0.0);\n\n let m0 = getJointMatrix(joints.x);\n let m1 = getJointMatrix(joints.y);\n let m2 = getJointMatrix(joints.z);\n let m3 = getJointMatrix(joints.w);\n\n skinnedPos += (m0 * vec4f(position, 1.0)).xyz * weights.x;\n skinnedPos += (m1 * vec4f(position, 1.0)).xyz * weights.y;\n skinnedPos += (m2 * vec4f(position, 1.0)).xyz * weights.z;\n skinnedPos += (m3 * vec4f(position, 1.0)).xyz * weights.w;\n\n return skinnedPos;\n}\n\nfn applySkinningPrev(position: vec3f, joints: vec4u, weights: vec4f) -> vec3f {\n var skinnedPos = vec3f(0.0);\n\n let m0 = getPrevJointMatrix(joints.x);\n let m1 = getPrevJointMatrix(joints.y);\n let m2 = getPrevJointMatrix(joints.z);\n let m3 = getPrevJointMatrix(joints.w);\n\n skinnedPos += (m0 * vec4f(position, 1.0)).xyz * weights.x;\n skinnedPos += (m1 * vec4f(position, 1.0)).xyz * weights.y;\n skinnedPos += (m2 * vec4f(position, 1.0)).xyz * weights.z;\n skinnedPos += (m3 * vec4f(position, 1.0)).xyz * weights.w;\n\n return skinnedPos;\n}\n\nfn applySkinningNormal(normal: vec3f, joints: vec4u, weights: vec4f) -> vec3f {\n var skinnedNormal = vec3f(0.0);\n\n let m0 = getJointMatrix(joints.x);\n let m1 = getJointMatrix(joints.y);\n let m2 = getJointMatrix(joints.z);\n let m3 = getJointMatrix(joints.w);\n\n \n let r0 = mat3x3f(m0[0].xyz, m0[1].xyz, m0[2].xyz);\n let r1 = mat3x3f(m1[0].xyz, m1[1].xyz, m1[2].xyz);\n let r2 = mat3x3f(m2[0].xyz, m2[1].xyz, m2[2].xyz);\n let r3 = mat3x3f(m3[0].xyz, m3[1].xyz, m3[2].xyz);\n\n skinnedNormal += (r0 * normal) * weights.x;\n skinnedNormal += (r1 * normal) * weights.y;\n skinnedNormal += (r2 * normal) * weights.z;\n skinnedNormal += (r3 * normal) * weights.w;\n\n return normalize(skinnedNormal);\n}\n\n@vertex\nfn vertexMain(\n input: VertexInput,\n @builtin(instance_index) instanceIdx : u32,\n @location(6) instanceModelMatrix0: vec4f,\n @location(7) instanceModelMatrix1: vec4f,\n @location(8) instanceModelMatrix2: vec4f,\n @location(9) instanceModelMatrix3: vec4f,\n @location(10) posRadius: vec4f,\n @location(11) uvTransform: vec4f, \n @location(12) instanceColor: vec4f, \n) -> VertexOutput {\n var output: VertexOutput;\n\n \n let instanceModelMatrix = mat4x4f(\n instanceModelMatrix0,\n instanceModelMatrix1,\n instanceModelMatrix2,\n instanceModelMatrix3\n );\n\n var localPos = input.position;\n var prevLocalPos = input.position; \n var localNormal = input.normal;\n var billboardWorldPos: vec3f;\n var useBillboardWorldPos = false;\n\n \n if (uniforms.billboardMode > 0.5) {\n \n let entityPos = instanceModelMatrix[3].xyz;\n\n \n let scaleX = length(instanceModelMatrix[0].xyz);\n let scaleY = length(instanceModelMatrix[1].xyz);\n\n if (uniforms.billboardMode < 1.5) {\n \n \n let toCamera = uniforms.cameraPosition - entityPos;\n let toCameraDist = length(toCamera);\n\n var right: vec3f;\n var up: vec3f;\n\n if (toCameraDist > 0.001) {\n let forward = toCamera / toCameraDist;\n \n right = cross(vec3f(0.0, 1.0, 0.0), forward);\n let rightLen = length(right);\n if (rightLen < 0.001) {\n \n right = uniforms.billboardCameraRight;\n up = vec3f(0.0, 0.0, 1.0); \n } else {\n right = right / rightLen;\n \n up = cross(forward, right);\n }\n } else {\n right = uniforms.billboardCameraRight;\n up = vec3f(0.0, 1.0, 0.0);\n }\n\n let offset = right * input.position.x * scaleX + up * input.position.y * scaleY;\n billboardWorldPos = entityPos + offset;\n useBillboardWorldPos = true;\n localNormal = vec3f(0.0, 1.0, 0.0); \n\n } else if (uniforms.billboardMode < 2.5) {\n \n \n let toCamera = uniforms.cameraPosition - entityPos;\n let toCameraDist = length(toCamera);\n\n var right: vec3f;\n var billUp: vec3f;\n\n if (toCameraDist > 0.001) {\n let forward = toCamera / toCameraDist; \n \n right = cross(vec3f(0.0, 1.0, 0.0), forward);\n let rightLen = length(right);\n if (rightLen < 0.001) {\n \n right = uniforms.billboardCameraRight;\n billUp = uniforms.billboardCameraUp;\n } else {\n right = right / rightLen;\n \n billUp = cross(forward, right);\n }\n } else {\n \n right = uniforms.billboardCameraRight;\n billUp = uniforms.billboardCameraUp;\n }\n\n let offset = right * input.position.x * scaleX + billUp * input.position.y * scaleY;\n billboardWorldPos = entityPos + offset;\n useBillboardWorldPos = true;\n localNormal = vec3f(0.0, 1.0, 0.0); \n\n } else {\n \n \n \n \n localNormal = vec3f(0.0, 1.0, 0.0); \n }\n }\n\n \n if (uniforms.skinEnabled > 0.5) {\n \n let weightSum = input.weights.x + input.weights.y + input.weights.z + input.weights.w;\n if (weightSum > 0.001) {\n localPos = applySkinning(input.position, input.joints, input.weights);\n prevLocalPos = applySkinningPrev(input.position, input.joints, input.weights);\n localNormal = applySkinningNormal(input.normal, input.joints, input.weights);\n }\n }\n\n \n \n \n var worldPos: vec3f;\n if (useBillboardWorldPos) {\n worldPos = billboardWorldPos;\n } else {\n worldPos = (instanceModelMatrix * vec4f(localPos, 1.0)).xyz;\n }\n var viewPos = uniforms.viewMatrix * vec4f(worldPos, 1.0);\n\n \n let allowRounding = posRadius.w >= 0.0;\n\n \n \n if (allowRounding && uniforms.positionRounding > 0.0) {\n let snap = uniforms.positionRounding;\n viewPos = vec4f(\n floor(viewPos.x / snap) * snap,\n floor(viewPos.y / snap) * snap,\n viewPos.z,\n viewPos.w\n );\n }\n\n var clipPos = uniforms.projectionMatrix * viewPos;\n\n \n \n \n var prevWorldPos: vec3f;\n if (useBillboardWorldPos) {\n prevWorldPos = billboardWorldPos; \n } else {\n prevWorldPos = (instanceModelMatrix * vec4f(prevLocalPos, 1.0)).xyz;\n }\n let prevClipPos = uniforms.prevViewProjMatrix * vec4f(prevWorldPos, 1.0);\n\n \n \n let viewDist = -viewPos.z; \n let jitterFade = saturate(1.0 - viewDist / uniforms.jitterFadeDistance);\n\n \n \n let jitterClip = uniforms.jitterOffset * 2.0 / uniforms.screenSize * jitterFade;\n clipPos.x += jitterClip.x * clipPos.w;\n clipPos.y += jitterClip.y * clipPos.w;\n\n \n \n if (allowRounding && uniforms.pixelRounding > 0.5) {\n let gridSize = uniforms.pixelRounding; \n let ndc = clipPos.xy / clipPos.w;\n\n \n let pixelCoords = (ndc + 1.0) * 0.5 * uniforms.screenSize;\n\n \n var snappedPixel = floor(pixelCoords / gridSize) * gridSize;\n\n \n if (uniforms.pixelExpansion > 0.0) {\n let screenCenter = uniforms.screenSize * 0.5;\n let fromCenter = snappedPixel - screenCenter;\n snappedPixel += sign(fromCenter) * uniforms.pixelExpansion;\n }\n\n \n let snappedNDC = (snappedPixel / uniforms.screenSize) * 2.0 - 1.0;\n\n \n clipPos.x = snappedNDC.x * clipPos.w;\n clipPos.y = snappedNDC.y * clipPos.w;\n }\n\n output.position = clipPos;\n output.worldPos = worldPos;\n \n output.uv = input.uv * uvTransform.zw + uvTransform.xy;\n output.viewZ = viewDist;\n output.currClipPos = clipPos;\n output.prevClipPos = prevClipPos;\n output.instanceColor = instanceColor;\n\n \n \n \n \n if (uniforms.billboardMode > 0.5) {\n let entityPos = instanceModelMatrix[3].xyz;\n output.anchorY = entityPos.y;\n } else {\n output.anchorY = worldPos.y; \n }\n\n \n \n if (uniforms.billboardMode > 0.5) {\n output.normal = localNormal; \n } else {\n let normalMatrix = mat3x3f(\n instanceModelMatrix[0].xyz,\n instanceModelMatrix[1].xyz,\n instanceModelMatrix[2].xyz\n );\n output.normal = normalMatrix * localNormal;\n }\n return output;\n}\n\nstruct GBufferOutput {\n @location(0) albedo: vec4f,\n @location(1) normal: vec4f,\n @location(2) arm: vec4f, \n @location(3) emission: vec4f,\n @location(4) velocity: vec2f, \n @builtin(frag_depth) depth: f32, \n}\n\nfn interleavedGradientNoise(screenPos: vec2f) -> f32 {\n let magic = vec3f(0.06711056, 0.00583715, 52.9829189);\n return fract(magic.z * fract(dot(screenPos, magic.xy)));\n}\n\nfn screenHash(screenPos: vec2f) -> f32 {\n let p = fract(screenPos * vec2f(0.1031, 0.1030));\n let p3 = p.xyx * (p.yxy + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\n@fragment\nfn fragmentMain(input: VertexOutput, @builtin(front_facing) frontFacing: bool) -> GBufferOutput {\n \n \n \n \n \n if (uniforms.clipPlaneEnabled > 0.5 && uniforms.billboardMode < 0.5) {\n let clipDist = (input.worldPos.y - uniforms.clipPlaneY) * uniforms.clipPlaneDirection;\n if (clipDist < 0.0) {\n discard;\n }\n }\n\n \n \n if (uniforms.distanceFadeEnd > 0.0) {\n let distToCamera = length(input.worldPos - uniforms.cameraPosition);\n if (distToCamera >= uniforms.distanceFadeEnd) {\n discard; \n }\n if (distToCamera > uniforms.distanceFadeStart) {\n \n let fadeRange = uniforms.distanceFadeEnd - uniforms.distanceFadeStart;\n let fadeFactor = 1.0 - (distToCamera - uniforms.distanceFadeStart) / fadeRange;\n \n let noise = sampleNoise(input.position.xy);\n if (fadeFactor < noise) {\n discard; \n }\n }\n }\n\n var output: GBufferOutput;\n\n \n let mipBias = uniforms.mipBias; \n output.albedo = textureSampleBias(albedoTexture, albedoSampler, input.uv, mipBias);\n\n \n output.albedo = output.albedo * input.instanceColor;\n\n \n \n if (uniforms.luminanceToAlpha > 0.5) {\n let luminance = dot(output.albedo.rgb, vec3f(0.299, 0.587, 0.114));\n if (luminance < 0.004) {\n discard; \n }\n output.albedo.a = 1.0; \n }\n\n \n \n if (uniforms.alphaHashEnabled > 0.5 && uniforms.luminanceToAlpha < 0.5) {\n let alpha = output.albedo.a * uniforms.alphaHashScale;\n let noise = sampleNoise(input.position.xy);\n\n \n if (alpha < 0.5) {\n discard;\n }\n \n \n let remappedAlpha = (alpha - 0.5) * 2.0;\n if (remappedAlpha < noise) {\n discard;\n }\n }\n\n \n\n \n let nsample = textureSampleBias(normalTexture, normalSampler, input.uv, mipBias).rgb;\n let tangentNormal = normalize(nsample * 2.0 - 1.0);\n\n \n \n let dPdx = dpdx(input.worldPos);\n let dPdy = dpdy(input.worldPos);\n let dUVdx = dpdx(input.uv);\n let dUVdy = dpdy(input.uv);\n\n let N = normalize(input.normal);\n\n \n let dp2perp = cross(dPdy, N);\n let dp1perp = cross(N, dPdx);\n\n \n \n var T = -(dp2perp * dUVdx.x + dp1perp * dUVdy.x);\n var B = dp2perp * dUVdx.y + dp1perp * dUVdy.y;\n\n \n let invmax = inverseSqrt(max(dot(T, T), dot(B, B)));\n T = T * invmax;\n B = B * invmax;\n\n \n if (length(T) < 0.001 || length(B) < 0.001) {\n let refVec = select(vec3f(0.0, 1.0, 0.0), vec3f(1.0, 0.0, 0.0), abs(N.y) > 0.9);\n T = normalize(cross(N, refVec));\n B = cross(N, T);\n }\n\n \n if (!frontFacing) {\n T = -T;\n B = -B;\n }\n\n let TBN = mat3x3f(T, B, N);\n\n \n let normal = normalize(TBN * tangentNormal);\n \n output.normal = vec4f(normal, input.worldPos.y);\n\n \n var ambient = textureSampleBias(ambientTexture, ambientSampler, input.uv, mipBias).rgb;\n if (ambient.r < 0.04) {\n ambient.r = 1.0;\n }\n let rm = textureSampleBias(rmTexture, rmSampler, input.uv, mipBias).rgb;\n output.arm = vec4f(ambient.r, rm.g, rm.b, uniforms.specularBoost);\n\n \n output.emission = textureSampleBias(emissionTexture, emissionSampler, input.uv, mipBias) * uniforms.emissionFactor;\n\n \n \n let currNDC = input.currClipPos.xy / input.currClipPos.w;\n let prevNDC = input.prevClipPos.xy / input.prevClipPos.w;\n\n \n \n \n let velocityNDC = currNDC - prevNDC;\n output.velocity = velocityNDC * uniforms.screenSize * 0.5;\n\n \n let near = uniforms.near;\n let far = uniforms.far;\n let z = input.viewZ;\n output.depth = (z - near) / (far - near);\n\n return output;\n}";
|
|
6168
6383
|
class GBuffer {
|
|
6169
6384
|
constructor() {
|
|
6170
6385
|
this.isGBuffer = true;
|
|
@@ -6385,7 +6600,8 @@ class GBufferPass extends BasePass {
|
|
|
6385
6600
|
const isSkinned = mesh.hasSkin && mesh.skin;
|
|
6386
6601
|
const meshId = mesh.uid || mesh.geometry?.uid || "default";
|
|
6387
6602
|
const forceEmissive = mesh.material?.forceEmissive ? "_emissive" : "";
|
|
6388
|
-
|
|
6603
|
+
const doubleSided = mesh.material?.doubleSided ? "_dbl" : "";
|
|
6604
|
+
return `${mesh.material.uid}_${meshId}${isSkinned ? "_skinned" : ""}${forceEmissive}${doubleSided}`;
|
|
6389
6605
|
}
|
|
6390
6606
|
/**
|
|
6391
6607
|
* Check if pipeline is ready for a mesh (non-blocking)
|
|
@@ -6425,7 +6641,8 @@ class GBufferPass extends BasePass {
|
|
|
6425
6641
|
textures: mesh.material.textures,
|
|
6426
6642
|
renderTarget: this.gbuffer,
|
|
6427
6643
|
skin: isSkinned ? mesh.skin : null,
|
|
6428
|
-
noiseTexture: this.noiseTexture
|
|
6644
|
+
noiseTexture: this.noiseTexture,
|
|
6645
|
+
doubleSided: mesh.material?.doubleSided ?? false
|
|
6429
6646
|
}).then((pipeline) => {
|
|
6430
6647
|
this.pendingPipelines.delete(key);
|
|
6431
6648
|
pipeline._warmupFrames = 2;
|
|
@@ -6460,7 +6677,8 @@ class GBufferPass extends BasePass {
|
|
|
6460
6677
|
textures: mesh.material.textures,
|
|
6461
6678
|
renderTarget: this.gbuffer,
|
|
6462
6679
|
skin: isSkinned ? mesh.skin : null,
|
|
6463
|
-
noiseTexture: this.noiseTexture
|
|
6680
|
+
noiseTexture: this.noiseTexture,
|
|
6681
|
+
doubleSided: mesh.material?.doubleSided ?? false
|
|
6464
6682
|
});
|
|
6465
6683
|
pipeline._warmupFrames = 2;
|
|
6466
6684
|
pipelinesMap.set(key, pipeline);
|
|
@@ -6483,6 +6701,8 @@ class GBufferPass extends BasePass {
|
|
|
6483
6701
|
const prevViewProjMatrix = prevData?.hasValidHistory ? prevData.viewProj : camera.viewProj;
|
|
6484
6702
|
const emissionFactor = this.settings?.environment?.emissionFactor ?? [1, 1, 1, 4];
|
|
6485
6703
|
const mipBias = this.settings?.rendering?.mipBias ?? options.mipBias ?? 0;
|
|
6704
|
+
const animationSpeed = this.settings?.animation?.speed ?? 1;
|
|
6705
|
+
const globalAnimTime = performance.now() / 1e3 * animationSpeed;
|
|
6486
6706
|
stats.drawCalls = 0;
|
|
6487
6707
|
stats.triangles = 0;
|
|
6488
6708
|
camera.aspect = canvas.width / canvas.height;
|
|
@@ -6491,12 +6711,18 @@ class GBufferPass extends BasePass {
|
|
|
6491
6711
|
this._extractCameraVectors(camera.view);
|
|
6492
6712
|
let commandEncoder = null;
|
|
6493
6713
|
let passEncoder = null;
|
|
6714
|
+
const updatedSkins = /* @__PURE__ */ new Set();
|
|
6494
6715
|
if (batches && batches.size > 0) {
|
|
6495
6716
|
for (const [modelId, batch] of batches) {
|
|
6496
6717
|
const mesh = batch.mesh;
|
|
6497
6718
|
if (!mesh) continue;
|
|
6498
|
-
if (batch.hasSkin && batch.skin && !batch.skin.externallyManaged) {
|
|
6499
|
-
batch.skin.
|
|
6719
|
+
if (batch.hasSkin && batch.skin && !batch.skin.externallyManaged && !updatedSkins.has(batch.skin)) {
|
|
6720
|
+
if (batch.skin._animStartTime === void 0) {
|
|
6721
|
+
batch.skin._animStartTime = globalAnimTime;
|
|
6722
|
+
}
|
|
6723
|
+
const skinAnimTime = globalAnimTime - batch.skin._animStartTime;
|
|
6724
|
+
batch.skin.updateAtTime(skinAnimTime);
|
|
6725
|
+
updatedSkins.add(batch.skin);
|
|
6500
6726
|
}
|
|
6501
6727
|
const pipeline = await this._getOrCreatePipeline(mesh);
|
|
6502
6728
|
if (pipeline._warmupFrames > 0) {
|
|
@@ -6615,8 +6841,13 @@ class GBufferPass extends BasePass {
|
|
|
6615
6841
|
pipeline._warmupFrames--;
|
|
6616
6842
|
}
|
|
6617
6843
|
this.legacyCullingStats.rendered++;
|
|
6618
|
-
if (mesh.skin && mesh.hasSkin && !mesh.skin.externallyManaged) {
|
|
6619
|
-
mesh.skin.
|
|
6844
|
+
if (mesh.skin && mesh.hasSkin && !mesh.skin.externallyManaged && !updatedSkins.has(mesh.skin)) {
|
|
6845
|
+
if (mesh.skin._animStartTime === void 0) {
|
|
6846
|
+
mesh.skin._animStartTime = globalAnimTime;
|
|
6847
|
+
}
|
|
6848
|
+
const skinAnimTime = globalAnimTime - mesh.skin._animStartTime;
|
|
6849
|
+
mesh.skin.updateAtTime(skinAnimTime);
|
|
6850
|
+
updatedSkins.add(mesh.skin);
|
|
6620
6851
|
}
|
|
6621
6852
|
if (pipeline.geometry !== mesh.geometry) {
|
|
6622
6853
|
pipeline.geometry = mesh.geometry;
|
|
@@ -7239,9 +7470,21 @@ class LightingPass extends BasePass {
|
|
|
7239
7470
|
getOutputTexture() {
|
|
7240
7471
|
return this.outputTexture;
|
|
7241
7472
|
}
|
|
7473
|
+
/**
|
|
7474
|
+
* Get the light buffer for volumetric fog
|
|
7475
|
+
*/
|
|
7476
|
+
getLightBuffer() {
|
|
7477
|
+
return this.lightBuffer;
|
|
7478
|
+
}
|
|
7479
|
+
/**
|
|
7480
|
+
* Get the current light count
|
|
7481
|
+
*/
|
|
7482
|
+
getLightCount() {
|
|
7483
|
+
return this.lights?.length ?? 0;
|
|
7484
|
+
}
|
|
7242
7485
|
}
|
|
7243
7486
|
var particle_simulate_default = "const WORKGROUP_SIZE: u32 = 64u;\nconst MAX_EMITTERS: u32 = 16u;\nconst CASCADE_COUNT = 3;\nconst MAX_LIGHTS = 64u;\nconst MAX_SPOT_SHADOWS = 8;\nconst LIGHTING_FADE_TIME: f32 = 0.3; \n\nconst SPOT_ATLAS_WIDTH: f32 = 2048.0;\nconst SPOT_ATLAS_HEIGHT: f32 = 2048.0;\nconst SPOT_TILE_SIZE: f32 = 512.0;\nconst SPOT_TILES_PER_ROW: i32 = 4;\n\nstruct Particle {\n position: vec3f, \n lifetime: f32, \n velocity: vec3f, \n maxLifetime: f32, \n color: vec4f, \n size: vec2f, \n rotation: f32, \n flags: u32, \n lighting: vec3f, \n lightingPad: f32, \n}\n\nstruct Light {\n enabled: u32,\n position: vec3f,\n color: vec4f,\n direction: vec3f,\n geom: vec4f, \n shadowIndex: i32,\n}\n\nstruct CascadeMatrices {\n matrices: array<mat4x4<f32>, CASCADE_COUNT>,\n}\n\nstruct SpotShadowMatrices {\n matrices: array<mat4x4<f32>, MAX_SPOT_SHADOWS>,\n}\n\nstruct SpawnRequest {\n position: vec3f,\n lifetime: f32,\n velocity: vec3f,\n maxLifetime: f32,\n color: vec4f,\n startSize: f32,\n endSize: f32,\n rotation: f32, \n flags: u32, \n}\n\nstruct EmitterSettings {\n gravity: vec3f,\n drag: f32,\n turbulence: f32,\n fadeIn: f32,\n fadeOut: f32,\n rotationSpeed: f32,\n startSize: f32,\n endSize: f32,\n baseAlpha: f32,\n padding: f32,\n}\n\nstruct SimulationUniforms {\n dt: f32,\n time: f32,\n maxParticles: u32,\n emitterCount: u32,\n \n cameraPosition: vec3f,\n shadowBias: f32,\n lightDir: vec3f,\n shadowStrength: f32,\n lightColor: vec4f,\n ambientColor: vec4f,\n cascadeSizes: vec4f,\n lightCount: u32,\n pad1: u32,\n pad2: u32,\n pad3: u32,\n}\n\nstruct Counters {\n aliveCount: atomic<u32>,\n nextFreeIndex: atomic<u32>,\n spawnCount: u32,\n frameCount: u32,\n}\n\n@group(0) @binding(0) var<uniform> uniforms: SimulationUniforms;\n@group(0) @binding(1) var<storage, read_write> particles: array<Particle>;\n@group(0) @binding(2) var<storage, read_write> counters: Counters;\n@group(0) @binding(3) var<storage, read> spawnRequests: array<SpawnRequest>;\n@group(0) @binding(4) var<storage, read> emitterSettings: array<EmitterSettings>;\n\n@group(0) @binding(5) var<storage, read> emitterRenderSettings: array<EmitterRenderSettings>;\n@group(0) @binding(6) var shadowMapArray: texture_depth_2d_array;\n@group(0) @binding(7) var shadowSampler: sampler_comparison;\n@group(0) @binding(8) var<storage, read> cascadeMatrices: CascadeMatrices;\n@group(0) @binding(9) var<storage, read> lights: array<Light, MAX_LIGHTS>;\n@group(0) @binding(10) var spotShadowAtlas: texture_depth_2d;\n@group(0) @binding(11) var<storage, read> spotMatrices: SpotShadowMatrices;\n\nstruct EmitterRenderSettings {\n lit: f32,\n emissive: f32,\n softness: f32,\n zOffset: f32,\n}\n\nfn hash(p: vec3f) -> f32 {\n var p3 = fract(p * 0.1031);\n p3 += dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn noise3D(p: vec3f) -> vec3f {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n\n return vec3f(\n mix(hash(i), hash(i + vec3f(1.0, 0.0, 0.0)), u.x),\n mix(hash(i + vec3f(0.0, 1.0, 0.0)), hash(i + vec3f(1.0, 1.0, 0.0)), u.x),\n mix(hash(i + vec3f(0.0, 0.0, 1.0)), hash(i + vec3f(1.0, 0.0, 1.0)), u.x)\n ) * 2.0 - 1.0;\n}\n\nfn squircleDistanceXZ(offset: vec2f, size: f32) -> f32 {\n let normalized = offset / size;\n let absNorm = abs(normalized);\n return pow(pow(absNorm.x, 4.0) + pow(absNorm.y, 4.0), 0.25);\n}\n\nfn sampleCascadeShadow(worldPos: vec3f, cascadeIndex: i32) -> f32 {\n let bias = uniforms.shadowBias;\n let lightMatrix = cascadeMatrices.matrices[cascadeIndex];\n let lightSpacePos = lightMatrix * vec4f(worldPos, 1.0);\n let projCoords = lightSpacePos.xyz / lightSpacePos.w;\n let shadowUV = vec2f(projCoords.x * 0.5 + 0.5, 0.5 - projCoords.y * 0.5);\n let currentDepth = projCoords.z - bias;\n\n \n if (shadowUV.x < 0.0 || shadowUV.x > 1.0 || shadowUV.y < 0.0 || shadowUV.y > 1.0 ||\n currentDepth < 0.0 || currentDepth > 1.0) {\n return 1.0;\n }\n\n \n return textureSampleCompareLevel(shadowMapArray, shadowSampler, shadowUV, cascadeIndex, currentDepth);\n}\n\nfn calculateParticleShadow(worldPos: vec3f) -> f32 {\n let camXZ = vec2f(uniforms.cameraPosition.x, uniforms.cameraPosition.z);\n let posXZ = vec2f(worldPos.x, worldPos.z);\n let offsetXZ = posXZ - camXZ;\n\n let dist0 = squircleDistanceXZ(offsetXZ, uniforms.cascadeSizes.x);\n let dist1 = squircleDistanceXZ(offsetXZ, uniforms.cascadeSizes.y);\n let dist2 = squircleDistanceXZ(offsetXZ, uniforms.cascadeSizes.z);\n\n var shadow = 1.0;\n if (dist0 < 0.95) {\n shadow = sampleCascadeShadow(worldPos, 0);\n } else if (dist1 < 0.95) {\n shadow = sampleCascadeShadow(worldPos, 1);\n } else if (dist2 < 0.95) {\n shadow = sampleCascadeShadow(worldPos, 2);\n }\n\n return mix(1.0 - uniforms.shadowStrength, 1.0, shadow);\n}\n\nfn calculateSpotShadow(worldPos: vec3f, slotIndex: i32) -> f32 {\n if (slotIndex < 0 || slotIndex >= MAX_SPOT_SHADOWS) {\n return 1.0;\n }\n\n let lightMatrix = spotMatrices.matrices[slotIndex];\n let lightSpacePos = lightMatrix * vec4f(worldPos, 1.0);\n let w = max(abs(lightSpacePos.w), 0.0001) * sign(lightSpacePos.w + 0.0001);\n let projCoords = lightSpacePos.xyz / w;\n\n if (projCoords.z < 0.0 || projCoords.z > 1.0 || abs(projCoords.x) > 1.0 || abs(projCoords.y) > 1.0) {\n return 1.0;\n }\n\n let col = slotIndex % SPOT_TILES_PER_ROW;\n let row = slotIndex / SPOT_TILES_PER_ROW;\n let localUV = vec2f(projCoords.x * 0.5 + 0.5, 0.5 - projCoords.y * 0.5);\n let tileOffset = vec2f(f32(col), f32(row)) * SPOT_TILE_SIZE;\n let atlasUV = (tileOffset + localUV * SPOT_TILE_SIZE) / vec2f(SPOT_ATLAS_WIDTH, SPOT_ATLAS_HEIGHT);\n let currentDepth = clamp(projCoords.z - uniforms.shadowBias * 3.0, 0.001, 0.999);\n\n return textureSampleCompareLevel(spotShadowAtlas, shadowSampler, atlasUV, currentDepth);\n}\n\nfn calculateParticleLighting(worldPos: vec3f, emitterIdx: u32) -> vec3f {\n let renderSettings = emitterRenderSettings[min(emitterIdx, MAX_EMITTERS - 1u)];\n\n \n if (renderSettings.lit < 0.5) {\n return vec3f(renderSettings.emissive);\n }\n\n \n let normal = vec3f(0.0, 1.0, 0.0);\n\n \n var lighting = uniforms.ambientColor.rgb * uniforms.ambientColor.a;\n\n \n let shadow = calculateParticleShadow(worldPos);\n let NdotL = max(dot(normal, uniforms.lightDir), 0.0);\n lighting += uniforms.lightColor.rgb * uniforms.lightColor.a * NdotL * shadow;\n\n \n let lightCount = uniforms.lightCount;\n for (var i = 0u; i < min(lightCount, MAX_LIGHTS); i++) {\n let light = lights[i];\n if (light.enabled == 0u) { continue; }\n\n let lightVec = light.position - worldPos;\n let dist = length(lightVec);\n let lightDir = normalize(lightVec);\n let distanceFade = light.geom.w;\n\n \n let radius = max(light.geom.x, 0.001);\n let attenuation = max(0.0, 1.0 - dist / radius);\n let attenuationSq = attenuation * attenuation * distanceFade;\n if (attenuationSq <= 0.001) { continue; }\n\n \n let innerCone = light.geom.y;\n let outerCone = light.geom.z;\n var spotAttenuation = 1.0;\n let isSpotlight = outerCone > 0.0;\n if (isSpotlight) {\n let spotCos = dot(-lightDir, normalize(light.direction));\n spotAttenuation = smoothstep(outerCone, innerCone, spotCos);\n }\n if (spotAttenuation <= 0.001) { continue; }\n\n \n var lightShadow = 1.0;\n if (isSpotlight && light.shadowIndex >= 0) {\n lightShadow = calculateSpotShadow(worldPos, light.shadowIndex);\n }\n\n \n let pNdotL = max(dot(normal, lightDir), 0.0);\n let pIntensity = light.color.a * attenuationSq * spotAttenuation * lightShadow * pNdotL;\n lighting += pIntensity * light.color.rgb;\n }\n\n return lighting * renderSettings.emissive;\n}\n\n@compute @workgroup_size(WORKGROUP_SIZE)\nfn spawn(@builtin(global_invocation_id) globalId: vec3u) {\n let idx = globalId.x;\n\n \n if (idx >= counters.spawnCount) {\n return;\n }\n\n \n let maxAttempts = 8u; \n var particleIdx = 0u;\n var foundSlot = false;\n\n for (var attempt = 0u; attempt < maxAttempts; attempt++) {\n let rawIdx = atomicAdd(&counters.nextFreeIndex, 1u);\n particleIdx = rawIdx % uniforms.maxParticles;\n\n \n let existing = particles[particleIdx];\n if ((existing.flags & 1u) == 0u || existing.lifetime <= 0.0) {\n foundSlot = true;\n break;\n }\n }\n\n \n if (!foundSlot) {\n return;\n }\n\n \n let req = spawnRequests[idx];\n\n \n var p: Particle;\n p.position = req.position;\n p.lifetime = req.lifetime;\n p.velocity = req.velocity;\n p.maxLifetime = req.maxLifetime;\n p.color = req.color;\n p.size = vec2f(req.startSize, req.startSize);\n p.rotation = req.rotation; \n p.flags = req.flags;\n \n p.lighting = uniforms.ambientColor.rgb * uniforms.ambientColor.a;\n p.lightingPad = 0.0;\n\n \n particles[particleIdx] = p;\n\n \n atomicAdd(&counters.aliveCount, 1u);\n}\n\n@compute @workgroup_size(WORKGROUP_SIZE)\nfn simulate(@builtin(global_invocation_id) globalId: vec3u) {\n let idx = globalId.x;\n\n \n if (idx >= uniforms.maxParticles) {\n return;\n }\n\n var p = particles[idx];\n\n \n if ((p.flags & 1u) == 0u || p.lifetime <= 0.0) {\n return;\n }\n\n \n let emitterIdx = (p.flags >> 8u) & 0xFFu;\n let settings = emitterSettings[min(emitterIdx, MAX_EMITTERS - 1u)];\n\n let dt = uniforms.dt;\n\n \n p.lifetime -= dt;\n\n \n if (p.lifetime <= 0.0) {\n p.flags = 0u; \n p.lifetime = 0.0;\n atomicSub(&counters.aliveCount, 1u);\n particles[idx] = p;\n return;\n }\n\n \n let lifeProgress = 1.0 - (p.lifetime / p.maxLifetime);\n\n \n p.velocity += settings.gravity * dt;\n\n \n let dragFactor = 1.0 - settings.drag * dt;\n p.velocity *= max(dragFactor, 0.0);\n\n \n if (settings.turbulence > 0.0) {\n let noisePos = p.position * 0.5 + vec3f(uniforms.time * 0.1, 0.0, 0.0);\n let turbulenceForce = noise3D(noisePos + vec3f(p.rotation * 10.0)) * settings.turbulence;\n p.velocity += turbulenceForce * dt * 10.0;\n }\n\n \n p.position += p.velocity * dt;\n\n \n let currentSize = mix(settings.startSize, settings.endSize, lifeProgress);\n p.size = vec2f(currentSize, currentSize);\n\n \n let rotationDir = select(-1.0, 1.0, p.rotation >= 0.0);\n p.rotation += rotationDir * settings.rotationSpeed * dt;\n\n \n var alpha = settings.baseAlpha;\n\n \n let fadeInDuration = settings.fadeIn / p.maxLifetime;\n if (lifeProgress < fadeInDuration && fadeInDuration > 0.0) {\n alpha *= lifeProgress / fadeInDuration;\n }\n\n \n let fadeOutStart = 1.0 - (settings.fadeOut / p.maxLifetime);\n if (lifeProgress > fadeOutStart && fadeOutStart < 1.0) {\n let fadeOutProgress = (lifeProgress - fadeOutStart) / (1.0 - fadeOutStart);\n alpha *= 1.0 - fadeOutProgress;\n }\n\n \n p.color.a = alpha;\n\n \n let targetLighting = calculateParticleLighting(p.position, emitterIdx);\n\n \n \n let lerpFactor = clamp(dt / LIGHTING_FADE_TIME, 0.0, 1.0);\n p.lighting = mix(p.lighting, targetLighting, lerpFactor);\n\n \n particles[idx] = p;\n}\n\n@compute @workgroup_size(1)\nfn resetCounters() {\n \n atomicStore(&counters.nextFreeIndex, 0u);\n}";
|
|
7244
|
-
var particle_render_default = "const PI = 3.14159265359;\nconst CASCADE_COUNT = 3;\nconst MAX_EMITTERS = 16u;\nconst MAX_LIGHTS = 64u;\nconst MAX_SPOT_SHADOWS = 8;\n\nconst SPOT_ATLAS_WIDTH: f32 = 2048.0;\nconst SPOT_ATLAS_HEIGHT: f32 = 2048.0;\nconst SPOT_TILE_SIZE: f32 = 512.0;\nconst SPOT_TILES_PER_ROW: i32 = 4;\n\nstruct Particle {\n position: vec3f,\n lifetime: f32,\n velocity: vec3f,\n maxLifetime: f32,\n color: vec4f,\n size: vec2f,\n rotation: f32, \n flags: u32,\n lighting: vec3f, \n lightingPad: f32,\n}\n\nstruct Light {\n enabled: u32,\n position: vec3f,\n color: vec4f,\n direction: vec3f,\n geom: vec4f, \n shadowIndex: i32, \n}\n\nstruct EmitterRenderSettings {\n lit: f32, \n emissive: f32, \n softness: f32,\n zOffset: f32,\n}\n\nstruct SpotShadowMatrices {\n matrices: array<mat4x4<f32>, MAX_SPOT_SHADOWS>,\n}\n\nstruct ParticleUniforms {\n viewMatrix: mat4x4f,\n projectionMatrix: mat4x4f,\n cameraPosition: vec3f,\n time: f32,\n cameraRight: vec3f,\n softness: f32,\n cameraUp: vec3f,\n zOffset: f32,\n screenSize: vec2f,\n near: f32,\n far: f32,\n blendMode: f32, \n lit: f32, \n shadowBias: f32,\n shadowStrength: f32,\n \n lightDir: vec3f,\n shadowMapSize: f32,\n lightColor: vec4f,\n ambientColor: vec4f,\n cascadeSizes: vec4f, \n \n envParams: vec4f, \n \n lightParams: vec4u, \n \n fogColor: vec3f,\n fogEnabled: f32,\n fogDistances: vec3f, \n fogBrightResist: f32,\n fogAlphas: vec3f, \n fogPad1: f32,\n fogHeightFade: vec2f, \n fogPad2: vec2f,\n}\n\nstruct CascadeMatrices {\n matrices: array<mat4x4<f32>, CASCADE_COUNT>,\n}\n\nstruct VertexOutput {\n @builtin(position) position: vec4f,\n @location(0) uv: vec2f,\n @location(1) color: vec4f,\n @location(2) viewZ: f32,\n @location(3) linearDepth: f32, \n @location(4) lighting: vec3f, \n @location(5) @interpolate(flat) emitterIdx: u32, \n @location(6) worldPos: vec3f, \n}\n\n@group(0) @binding(0) var<uniform> uniforms: ParticleUniforms;\n@group(0) @binding(1) var<storage, read> particles: array<Particle>;\n@group(0) @binding(2) var particleTexture: texture_2d<f32>;\n@group(0) @binding(3) var particleSampler: sampler;\n@group(0) @binding(4) var depthTexture: texture_depth_2d;\n@group(0) @binding(5) var shadowMapArray: texture_depth_2d_array;\n@group(0) @binding(6) var shadowSampler: sampler_comparison;\n@group(0) @binding(7) var<storage, read> cascadeMatrices: CascadeMatrices;\n@group(0) @binding(8) var<storage, read> emitterSettings: array<EmitterRenderSettings, MAX_EMITTERS>;\n@group(0) @binding(9) var envMap: texture_2d<f32>;\n@group(0) @binding(10) var envSampler: sampler;\n\n@group(0) @binding(11) var<storage, read> lights: array<Light, MAX_LIGHTS>;\n\n@group(0) @binding(12) var spotShadowAtlas: texture_depth_2d;\n@group(0) @binding(13) var spotShadowSampler: sampler_comparison;\n@group(0) @binding(14) var<storage, read> spotMatrices: SpotShadowMatrices;\n\nfn getQuadVertex(vertexId: u32) -> vec2f {\n \n \n \n var corners = array<vec2f, 6>(\n vec2f(-0.5, -0.5), \n vec2f(0.5, -0.5), \n vec2f(-0.5, 0.5), \n vec2f(0.5, -0.5), \n vec2f(0.5, 0.5), \n vec2f(-0.5, 0.5) \n );\n return corners[vertexId];\n}\n\nfn getQuadUV(vertexId: u32) -> vec2f {\n var uvs = array<vec2f, 6>(\n vec2f(0.0, 1.0), \n vec2f(1.0, 1.0), \n vec2f(0.0, 0.0), \n vec2f(1.0, 1.0), \n vec2f(1.0, 0.0), \n vec2f(0.0, 0.0) \n );\n return uvs[vertexId];\n}\n\n@vertex\nfn vertexMain(\n @builtin(vertex_index) vertexIndex: u32,\n @builtin(instance_index) instanceIndex: u32\n) -> VertexOutput {\n var output: VertexOutput;\n\n \n let particle = particles[instanceIndex];\n\n \n let emitterIdx = (particle.flags >> 8u) & 0xFFu;\n output.emitterIdx = emitterIdx;\n\n \n if ((particle.flags & 1u) == 0u || particle.lifetime <= 0.0) {\n \n output.position = vec4f(0.0, 0.0, 0.0, 0.0);\n output.uv = vec2f(0.0, 0.0);\n output.color = vec4f(0.0, 0.0, 0.0, 0.0);\n output.viewZ = 0.0;\n output.linearDepth = 0.0;\n output.lighting = vec3f(0.0);\n output.worldPos = vec3f(0.0);\n return output;\n }\n\n \n \n let particleIsAdditive = (particle.flags & 2u) != 0u;\n let renderingAdditive = uniforms.blendMode > 0.5;\n if (particleIsAdditive != renderingAdditive) {\n \n output.position = vec4f(0.0, 0.0, 0.0, 0.0);\n output.uv = vec2f(0.0, 0.0);\n output.color = vec4f(0.0, 0.0, 0.0, 0.0);\n output.viewZ = 0.0;\n output.linearDepth = 0.0;\n output.lighting = vec3f(0.0);\n output.worldPos = vec3f(0.0);\n return output;\n }\n\n \n let localVertexId = vertexIndex % 6u;\n var quadPos = getQuadVertex(localVertexId);\n output.uv = getQuadUV(localVertexId);\n\n \n let cosR = cos(particle.rotation);\n let sinR = sin(particle.rotation);\n let rotatedPos = vec2f(\n quadPos.x * cosR - quadPos.y * sinR,\n quadPos.x * sinR + quadPos.y * cosR\n );\n\n \n let particleWorldPos = particle.position;\n\n \n let scaledOffset = rotatedPos * particle.size;\n\n \n let right = uniforms.cameraRight;\n let up = uniforms.cameraUp;\n let billboardPos = particleWorldPos + right * scaledOffset.x + up * scaledOffset.y;\n\n \n let toCamera = normalize(uniforms.cameraPosition - particleWorldPos);\n let offsetPos = billboardPos + toCamera * uniforms.zOffset;\n\n \n let viewPos = uniforms.viewMatrix * vec4f(offsetPos, 1.0);\n output.position = uniforms.projectionMatrix * viewPos;\n output.viewZ = -viewPos.z; \n\n \n let z = -viewPos.z; \n output.linearDepth = (z - uniforms.near) / (uniforms.far - uniforms.near);\n\n \n output.lighting = particle.lighting;\n\n \n output.color = particle.color;\n\n \n output.worldPos = particleWorldPos;\n\n return output;\n}\n\nfn SphToUV(n: vec3f) -> vec2f {\n var uv: vec2f;\n uv.x = atan2(-n.x, n.z);\n uv.x = (uv.x + PI / 2.0) / (PI * 2.0) + PI * (28.670 / 360.0);\n uv.y = acos(n.y) / PI;\n return uv;\n}\n\nfn octEncode(n: vec3f) -> vec2f {\n var n2 = n / (abs(n.x) + abs(n.y) + abs(n.z));\n if (n2.y < 0.0) {\n let signX = select(-1.0, 1.0, n2.x >= 0.0);\n let signZ = select(-1.0, 1.0, n2.z >= 0.0);\n n2 = vec3f(\n (1.0 - abs(n2.z)) * signX,\n n2.y,\n (1.0 - abs(n2.x)) * signZ\n );\n }\n return n2.xz * 0.5 + 0.5;\n}\n\nfn getEnvUV(dir: vec3f) -> vec2f {\n if (uniforms.envParams.z > 0.5) {\n return octEncode(dir);\n }\n return SphToUV(dir);\n}\n\nfn getIBLSample(dir: vec3f, lod: f32) -> vec3f {\n let envRGBE = textureSampleLevel(envMap, envSampler, getEnvUV(dir), lod);\n \n let envColor = envRGBE.rgb * pow(2.0, envRGBE.a * 255.0 - 128.0);\n return envColor;\n}\n\nfn squircleDistanceXZ(offset: vec2f, size: f32) -> f32 {\n let normalized = offset / size;\n let absNorm = abs(normalized);\n return pow(pow(absNorm.x, 4.0) + pow(absNorm.y, 4.0), 0.25);\n}\n\nfn sampleCascadeShadow(worldPos: vec3f, normal: vec3f, cascadeIndex: i32) -> f32 {\n let bias = uniforms.shadowBias;\n let shadowMapSize = uniforms.shadowMapSize;\n\n \n let biasedPos = worldPos + normal * bias * 0.5;\n\n \n let lightMatrix = cascadeMatrices.matrices[cascadeIndex];\n\n \n let lightSpacePos = lightMatrix * vec4f(biasedPos, 1.0);\n let projCoords = lightSpacePos.xyz / lightSpacePos.w;\n\n \n let shadowUV = vec2f(projCoords.x * 0.5 + 0.5, 0.5 - projCoords.y * 0.5);\n let currentDepth = projCoords.z - bias;\n\n \n let inBoundsX = shadowUV.x >= 0.0 && shadowUV.x <= 1.0;\n let inBoundsY = shadowUV.y >= 0.0 && shadowUV.y <= 1.0;\n let inBoundsZ = currentDepth >= 0.0 && currentDepth <= 1.0;\n\n if (!inBoundsX || !inBoundsY || !inBoundsZ) {\n return 1.0; \n }\n\n let clampedUV = clamp(shadowUV, vec2f(0.001), vec2f(0.999));\n let clampedDepth = clamp(currentDepth, 0.001, 0.999);\n\n \n let texelSize = 1.0 / shadowMapSize;\n var shadow = 0.0;\n shadow += textureSampleCompareLevel(shadowMapArray, shadowSampler, clampedUV + vec2f(-texelSize, 0.0), cascadeIndex, clampedDepth);\n shadow += textureSampleCompareLevel(shadowMapArray, shadowSampler, clampedUV + vec2f(texelSize, 0.0), cascadeIndex, clampedDepth);\n shadow += textureSampleCompareLevel(shadowMapArray, shadowSampler, clampedUV + vec2f(0.0, -texelSize), cascadeIndex, clampedDepth);\n shadow += textureSampleCompareLevel(shadowMapArray, shadowSampler, clampedUV + vec2f(0.0, texelSize), cascadeIndex, clampedDepth);\n shadow /= 4.0;\n\n return shadow;\n}\n\nfn calculateParticleShadow(worldPos: vec3f, normal: vec3f) -> f32 {\n let shadowStrength = uniforms.shadowStrength;\n\n \n let camXZ = vec2f(uniforms.cameraPosition.x, uniforms.cameraPosition.z);\n let posXZ = vec2f(worldPos.x, worldPos.z);\n let offsetXZ = posXZ - camXZ;\n\n \n let cascade0Size = uniforms.cascadeSizes.x;\n let cascade1Size = uniforms.cascadeSizes.y;\n let cascade2Size = uniforms.cascadeSizes.z;\n\n let dist0 = squircleDistanceXZ(offsetXZ, cascade0Size);\n let dist1 = squircleDistanceXZ(offsetXZ, cascade1Size);\n let dist2 = squircleDistanceXZ(offsetXZ, cascade2Size);\n\n var shadow = 1.0;\n\n \n if (dist0 < 0.95) {\n shadow = sampleCascadeShadow(worldPos, normal, 0);\n } else if (dist1 < 0.95) {\n shadow = sampleCascadeShadow(worldPos, normal, 1);\n } else if (dist2 < 0.95) {\n shadow = sampleCascadeShadow(worldPos, normal, 2);\n }\n\n \n return mix(1.0 - shadowStrength, 1.0, shadow);\n}\n\nfn calculateSpotShadow(worldPos: vec3f, normal: vec3f, slotIndex: i32) -> f32 {\n if (slotIndex < 0 || slotIndex >= MAX_SPOT_SHADOWS) {\n return 1.0; \n }\n\n let bias = uniforms.shadowBias;\n let normalBias = bias * 2.0; \n\n \n let biasedPos = worldPos + normal * normalBias;\n\n \n let lightMatrix = spotMatrices.matrices[slotIndex];\n\n \n let lightSpacePos = lightMatrix * vec4f(biasedPos, 1.0);\n\n \n let w = max(abs(lightSpacePos.w), 0.0001) * sign(lightSpacePos.w + 0.0001);\n let projCoords = lightSpacePos.xyz / w;\n\n \n if (projCoords.z < 0.0 || projCoords.z > 1.0 ||\n abs(projCoords.x) > 1.0 || abs(projCoords.y) > 1.0) {\n return 1.0; \n }\n\n \n let col = slotIndex % SPOT_TILES_PER_ROW;\n let row = slotIndex / SPOT_TILES_PER_ROW;\n\n \n let localUV = vec2f(projCoords.x * 0.5 + 0.5, 0.5 - projCoords.y * 0.5);\n\n \n let tileOffset = vec2f(f32(col), f32(row)) * SPOT_TILE_SIZE;\n let atlasUV = (tileOffset + localUV * SPOT_TILE_SIZE) / vec2f(SPOT_ATLAS_WIDTH, SPOT_ATLAS_HEIGHT);\n\n \n let texelSize = 1.0 / SPOT_TILE_SIZE;\n let currentDepth = clamp(projCoords.z - bias * 3.0, 0.001, 0.999);\n\n var shadowSample = 0.0;\n shadowSample += textureSampleCompareLevel(spotShadowAtlas, spotShadowSampler, atlasUV + vec2f(-texelSize, 0.0), currentDepth);\n shadowSample += textureSampleCompareLevel(spotShadowAtlas, spotShadowSampler, atlasUV + vec2f(texelSize, 0.0), currentDepth);\n shadowSample += textureSampleCompareLevel(spotShadowAtlas, spotShadowSampler, atlasUV + vec2f(0.0, -texelSize), currentDepth);\n shadowSample += textureSampleCompareLevel(spotShadowAtlas, spotShadowSampler, atlasUV + vec2f(0.0, texelSize), currentDepth);\n shadowSample /= 4.0;\n\n return shadowSample;\n}\n\nfn applyLighting(baseColor: vec3f, lighting: vec3f) -> vec3f {\n \n \n return baseColor * lighting;\n}\n\nstruct FragmentOutput {\n @location(0) color: vec4f,\n @builtin(frag_depth) depth: f32,\n}\n\nfn calcSoftFade(fragPos: vec4f, particleLinearDepth: f32) -> f32 {\n if (uniforms.softness <= 0.0) {\n return 1.0;\n }\n\n \n let screenPos = vec2i(fragPos.xy);\n let screenSize = vec2i(uniforms.screenSize);\n\n \n if (screenPos.x < 0 || screenPos.x >= screenSize.x ||\n screenPos.y < 0 || screenPos.y >= screenSize.y) {\n return 1.0;\n }\n\n \n let sceneDepthNorm = textureLoad(depthTexture, screenPos, 0);\n\n \n if (sceneDepthNorm <= 0.0) {\n return 1.0;\n }\n\n \n let sceneDepth = uniforms.near + sceneDepthNorm * (uniforms.far - uniforms.near);\n let particleDepth = uniforms.near + particleLinearDepth * (uniforms.far - uniforms.near);\n\n \n let depthDiff = sceneDepth - particleDepth;\n return saturate(depthDiff / uniforms.softness);\n}\n\n@fragment\nfn fragmentMainAlpha(input: VertexOutput) -> FragmentOutput {\n var output: FragmentOutput;\n\n \n let texColor = textureSample(particleTexture, particleSampler, input.uv);\n\n \n var alpha = texColor.a * input.color.a;\n\n \n alpha *= calcSoftFade(input.position, input.linearDepth);\n\n \n if (alpha < 0.001) {\n discard;\n }\n\n \n let baseColor = texColor.rgb * input.color.rgb;\n let litColor = applyLighting(baseColor, input.lighting);\n\n \n output.color = vec4f(litColor, alpha);\n output.depth = input.linearDepth;\n return output;\n}\n\n@fragment\nfn fragmentMainAdditive(input: VertexOutput) -> FragmentOutput {\n var output: FragmentOutput;\n\n \n let texColor = textureSample(particleTexture, particleSampler, input.uv);\n\n \n var alpha = texColor.a * input.color.a;\n\n \n alpha *= calcSoftFade(input.position, input.linearDepth);\n\n \n if (alpha < 0.001) {\n discard;\n }\n\n \n let baseColor = texColor.rgb * input.color.rgb;\n let litColor = applyLighting(baseColor, input.lighting);\n\n \n \n let rgb = litColor * alpha;\n output.color = vec4f(rgb, alpha);\n output.depth = input.linearDepth;\n return output;\n}";
|
|
7487
|
+
var particle_render_default = "const PI = 3.14159265359;\nconst CASCADE_COUNT = 3;\nconst MAX_EMITTERS = 16u;\nconst MAX_LIGHTS = 64u;\nconst MAX_SPOT_SHADOWS = 8;\n\nconst SPOT_ATLAS_WIDTH: f32 = 2048.0;\nconst SPOT_ATLAS_HEIGHT: f32 = 2048.0;\nconst SPOT_TILE_SIZE: f32 = 512.0;\nconst SPOT_TILES_PER_ROW: i32 = 4;\n\nstruct Particle {\n position: vec3f,\n lifetime: f32,\n velocity: vec3f,\n maxLifetime: f32,\n color: vec4f,\n size: vec2f,\n rotation: f32, \n flags: u32,\n lighting: vec3f, \n lightingPad: f32,\n}\n\nstruct Light {\n enabled: u32,\n position: vec3f,\n color: vec4f,\n direction: vec3f,\n geom: vec4f, \n shadowIndex: i32, \n}\n\nstruct EmitterRenderSettings {\n lit: f32, \n emissive: f32, \n softness: f32,\n zOffset: f32,\n}\n\nstruct SpotShadowMatrices {\n matrices: array<mat4x4<f32>, MAX_SPOT_SHADOWS>,\n}\n\nstruct ParticleUniforms {\n viewMatrix: mat4x4f,\n projectionMatrix: mat4x4f,\n cameraPosition: vec3f,\n time: f32,\n cameraRight: vec3f,\n softness: f32,\n cameraUp: vec3f,\n zOffset: f32,\n screenSize: vec2f,\n near: f32,\n far: f32,\n blendMode: f32, \n lit: f32, \n shadowBias: f32,\n shadowStrength: f32,\n \n lightDir: vec3f,\n shadowMapSize: f32,\n lightColor: vec4f,\n ambientColor: vec4f,\n cascadeSizes: vec4f, \n \n envParams: vec4f, \n \n lightParams: vec4u, \n \n fogColor: vec3f,\n fogEnabled: f32,\n fogDistances: vec3f, \n fogBrightResist: f32,\n fogAlphas: vec3f, \n fogPad1: f32,\n fogHeightFade: vec2f, \n fogDebug: f32, \n fogPad2: f32,\n}\n\nstruct CascadeMatrices {\n matrices: array<mat4x4<f32>, CASCADE_COUNT>,\n}\n\nstruct VertexOutput {\n @builtin(position) position: vec4f,\n @location(0) uv: vec2f,\n @location(1) color: vec4f,\n @location(2) viewZ: f32,\n @location(3) linearDepth: f32, \n @location(4) lighting: vec3f, \n @location(5) @interpolate(flat) emitterIdx: u32, \n @location(6) worldPos: vec3f, \n @location(7) @interpolate(flat) centerViewZ: f32, \n}\n\n@group(0) @binding(0) var<uniform> uniforms: ParticleUniforms;\n@group(0) @binding(1) var<storage, read> particles: array<Particle>;\n@group(0) @binding(2) var particleTexture: texture_2d<f32>;\n@group(0) @binding(3) var particleSampler: sampler;\n@group(0) @binding(4) var depthTexture: texture_depth_2d;\n@group(0) @binding(5) var shadowMapArray: texture_depth_2d_array;\n@group(0) @binding(6) var shadowSampler: sampler_comparison;\n@group(0) @binding(7) var<storage, read> cascadeMatrices: CascadeMatrices;\n@group(0) @binding(8) var<storage, read> emitterSettings: array<EmitterRenderSettings, MAX_EMITTERS>;\n@group(0) @binding(9) var envMap: texture_2d<f32>;\n@group(0) @binding(10) var envSampler: sampler;\n\n@group(0) @binding(11) var<storage, read> lights: array<Light, MAX_LIGHTS>;\n\n@group(0) @binding(12) var spotShadowAtlas: texture_depth_2d;\n@group(0) @binding(13) var spotShadowSampler: sampler_comparison;\n@group(0) @binding(14) var<storage, read> spotMatrices: SpotShadowMatrices;\n\nfn getQuadVertex(vertexId: u32) -> vec2f {\n \n \n \n var corners = array<vec2f, 6>(\n vec2f(-0.5, -0.5), \n vec2f(0.5, -0.5), \n vec2f(-0.5, 0.5), \n vec2f(0.5, -0.5), \n vec2f(0.5, 0.5), \n vec2f(-0.5, 0.5) \n );\n return corners[vertexId];\n}\n\nfn getQuadUV(vertexId: u32) -> vec2f {\n var uvs = array<vec2f, 6>(\n vec2f(0.0, 1.0), \n vec2f(1.0, 1.0), \n vec2f(0.0, 0.0), \n vec2f(1.0, 1.0), \n vec2f(1.0, 0.0), \n vec2f(0.0, 0.0) \n );\n return uvs[vertexId];\n}\n\n@vertex\nfn vertexMain(\n @builtin(vertex_index) vertexIndex: u32,\n @builtin(instance_index) instanceIndex: u32\n) -> VertexOutput {\n var output: VertexOutput;\n\n \n let particle = particles[instanceIndex];\n\n \n let emitterIdx = (particle.flags >> 8u) & 0xFFu;\n output.emitterIdx = emitterIdx;\n\n \n if ((particle.flags & 1u) == 0u || particle.lifetime <= 0.0) {\n \n output.position = vec4f(0.0, 0.0, 0.0, 0.0);\n output.uv = vec2f(0.0, 0.0);\n output.color = vec4f(0.0, 0.0, 0.0, 0.0);\n output.viewZ = 0.0;\n output.linearDepth = 0.0;\n output.lighting = vec3f(0.0);\n output.worldPos = vec3f(0.0);\n output.centerViewZ = 0.0;\n return output;\n }\n\n \n \n let particleIsAdditive = (particle.flags & 2u) != 0u;\n let renderingAdditive = uniforms.blendMode > 0.5;\n\n if (particleIsAdditive != renderingAdditive) {\n \n output.position = vec4f(0.0, 0.0, 0.0, 0.0);\n output.uv = vec2f(0.0, 0.0);\n output.color = vec4f(0.0, 0.0, 0.0, 0.0);\n output.viewZ = 0.0;\n output.linearDepth = 0.0;\n output.lighting = vec3f(0.0);\n output.worldPos = vec3f(0.0);\n output.centerViewZ = 0.0;\n return output;\n }\n\n \n let localVertexId = vertexIndex % 6u;\n var quadPos = getQuadVertex(localVertexId);\n output.uv = getQuadUV(localVertexId);\n\n \n let cosR = cos(particle.rotation);\n let sinR = sin(particle.rotation);\n let rotatedPos = vec2f(\n quadPos.x * cosR - quadPos.y * sinR,\n quadPos.x * sinR + quadPos.y * cosR\n );\n\n \n let particleWorldPos = particle.position;\n\n \n let scaledOffset = rotatedPos * particle.size;\n\n \n let right = uniforms.cameraRight;\n let up = uniforms.cameraUp;\n let billboardPos = particleWorldPos + right * scaledOffset.x + up * scaledOffset.y;\n\n \n let toCamera = normalize(uniforms.cameraPosition - particleWorldPos);\n let offsetPos = billboardPos + toCamera * uniforms.zOffset;\n\n \n let viewPos = uniforms.viewMatrix * vec4f(offsetPos, 1.0);\n output.position = uniforms.projectionMatrix * viewPos;\n output.viewZ = -viewPos.z; \n\n \n let z = -viewPos.z; \n output.linearDepth = (z - uniforms.near) / (uniforms.far - uniforms.near);\n\n \n output.lighting = particle.lighting;\n\n \n output.color = particle.color;\n\n \n output.worldPos = particleWorldPos;\n\n \n \n \n output.centerViewZ = output.viewZ;\n\n return output;\n}\n\nfn SphToUV(n: vec3f) -> vec2f {\n var uv: vec2f;\n uv.x = atan2(-n.x, n.z);\n uv.x = (uv.x + PI / 2.0) / (PI * 2.0) + PI * (28.670 / 360.0);\n uv.y = acos(n.y) / PI;\n return uv;\n}\n\nfn octEncode(n: vec3f) -> vec2f {\n var n2 = n / (abs(n.x) + abs(n.y) + abs(n.z));\n if (n2.y < 0.0) {\n let signX = select(-1.0, 1.0, n2.x >= 0.0);\n let signZ = select(-1.0, 1.0, n2.z >= 0.0);\n n2 = vec3f(\n (1.0 - abs(n2.z)) * signX,\n n2.y,\n (1.0 - abs(n2.x)) * signZ\n );\n }\n return n2.xz * 0.5 + 0.5;\n}\n\nfn getEnvUV(dir: vec3f) -> vec2f {\n if (uniforms.envParams.z > 0.5) {\n return octEncode(dir);\n }\n return SphToUV(dir);\n}\n\nfn getIBLSample(dir: vec3f, lod: f32) -> vec3f {\n let envRGBE = textureSampleLevel(envMap, envSampler, getEnvUV(dir), lod);\n \n let envColor = envRGBE.rgb * pow(2.0, envRGBE.a * 255.0 - 128.0);\n return envColor;\n}\n\nfn squircleDistanceXZ(offset: vec2f, size: f32) -> f32 {\n let normalized = offset / size;\n let absNorm = abs(normalized);\n return pow(pow(absNorm.x, 4.0) + pow(absNorm.y, 4.0), 0.25);\n}\n\nfn sampleCascadeShadow(worldPos: vec3f, normal: vec3f, cascadeIndex: i32) -> f32 {\n let bias = uniforms.shadowBias;\n let shadowMapSize = uniforms.shadowMapSize;\n\n \n let biasedPos = worldPos + normal * bias * 0.5;\n\n \n let lightMatrix = cascadeMatrices.matrices[cascadeIndex];\n\n \n let lightSpacePos = lightMatrix * vec4f(biasedPos, 1.0);\n let projCoords = lightSpacePos.xyz / lightSpacePos.w;\n\n \n let shadowUV = vec2f(projCoords.x * 0.5 + 0.5, 0.5 - projCoords.y * 0.5);\n let currentDepth = projCoords.z - bias;\n\n \n let inBoundsX = shadowUV.x >= 0.0 && shadowUV.x <= 1.0;\n let inBoundsY = shadowUV.y >= 0.0 && shadowUV.y <= 1.0;\n let inBoundsZ = currentDepth >= 0.0 && currentDepth <= 1.0;\n\n if (!inBoundsX || !inBoundsY || !inBoundsZ) {\n return 1.0; \n }\n\n let clampedUV = clamp(shadowUV, vec2f(0.001), vec2f(0.999));\n let clampedDepth = clamp(currentDepth, 0.001, 0.999);\n\n \n let texelSize = 1.0 / shadowMapSize;\n var shadow = 0.0;\n shadow += textureSampleCompareLevel(shadowMapArray, shadowSampler, clampedUV + vec2f(-texelSize, 0.0), cascadeIndex, clampedDepth);\n shadow += textureSampleCompareLevel(shadowMapArray, shadowSampler, clampedUV + vec2f(texelSize, 0.0), cascadeIndex, clampedDepth);\n shadow += textureSampleCompareLevel(shadowMapArray, shadowSampler, clampedUV + vec2f(0.0, -texelSize), cascadeIndex, clampedDepth);\n shadow += textureSampleCompareLevel(shadowMapArray, shadowSampler, clampedUV + vec2f(0.0, texelSize), cascadeIndex, clampedDepth);\n shadow /= 4.0;\n\n return shadow;\n}\n\nfn calculateParticleShadow(worldPos: vec3f, normal: vec3f) -> f32 {\n let shadowStrength = uniforms.shadowStrength;\n\n \n let camXZ = vec2f(uniforms.cameraPosition.x, uniforms.cameraPosition.z);\n let posXZ = vec2f(worldPos.x, worldPos.z);\n let offsetXZ = posXZ - camXZ;\n\n \n let cascade0Size = uniforms.cascadeSizes.x;\n let cascade1Size = uniforms.cascadeSizes.y;\n let cascade2Size = uniforms.cascadeSizes.z;\n\n let dist0 = squircleDistanceXZ(offsetXZ, cascade0Size);\n let dist1 = squircleDistanceXZ(offsetXZ, cascade1Size);\n let dist2 = squircleDistanceXZ(offsetXZ, cascade2Size);\n\n var shadow = 1.0;\n\n \n if (dist0 < 0.95) {\n shadow = sampleCascadeShadow(worldPos, normal, 0);\n } else if (dist1 < 0.95) {\n shadow = sampleCascadeShadow(worldPos, normal, 1);\n } else if (dist2 < 0.95) {\n shadow = sampleCascadeShadow(worldPos, normal, 2);\n }\n\n \n return mix(1.0 - shadowStrength, 1.0, shadow);\n}\n\nfn calculateSpotShadow(worldPos: vec3f, normal: vec3f, slotIndex: i32) -> f32 {\n if (slotIndex < 0 || slotIndex >= MAX_SPOT_SHADOWS) {\n return 1.0; \n }\n\n let bias = uniforms.shadowBias;\n let normalBias = bias * 2.0; \n\n \n let biasedPos = worldPos + normal * normalBias;\n\n \n let lightMatrix = spotMatrices.matrices[slotIndex];\n\n \n let lightSpacePos = lightMatrix * vec4f(biasedPos, 1.0);\n\n \n let w = max(abs(lightSpacePos.w), 0.0001) * sign(lightSpacePos.w + 0.0001);\n let projCoords = lightSpacePos.xyz / w;\n\n \n if (projCoords.z < 0.0 || projCoords.z > 1.0 ||\n abs(projCoords.x) > 1.0 || abs(projCoords.y) > 1.0) {\n return 1.0; \n }\n\n \n let col = slotIndex % SPOT_TILES_PER_ROW;\n let row = slotIndex / SPOT_TILES_PER_ROW;\n\n \n let localUV = vec2f(projCoords.x * 0.5 + 0.5, 0.5 - projCoords.y * 0.5);\n\n \n let tileOffset = vec2f(f32(col), f32(row)) * SPOT_TILE_SIZE;\n let atlasUV = (tileOffset + localUV * SPOT_TILE_SIZE) / vec2f(SPOT_ATLAS_WIDTH, SPOT_ATLAS_HEIGHT);\n\n \n let texelSize = 1.0 / SPOT_TILE_SIZE;\n let currentDepth = clamp(projCoords.z - bias * 3.0, 0.001, 0.999);\n\n var shadowSample = 0.0;\n shadowSample += textureSampleCompareLevel(spotShadowAtlas, spotShadowSampler, atlasUV + vec2f(-texelSize, 0.0), currentDepth);\n shadowSample += textureSampleCompareLevel(spotShadowAtlas, spotShadowSampler, atlasUV + vec2f(texelSize, 0.0), currentDepth);\n shadowSample += textureSampleCompareLevel(spotShadowAtlas, spotShadowSampler, atlasUV + vec2f(0.0, -texelSize), currentDepth);\n shadowSample += textureSampleCompareLevel(spotShadowAtlas, spotShadowSampler, atlasUV + vec2f(0.0, texelSize), currentDepth);\n shadowSample /= 4.0;\n\n return shadowSample;\n}\n\nfn applyLighting(baseColor: vec3f, lighting: vec3f) -> vec3f {\n \n \n return baseColor * lighting;\n}\n\nstruct FragmentOutput {\n @location(0) color: vec4f,\n @builtin(frag_depth) depth: f32,\n}\n\nfn calcSoftFade(fragPos: vec4f, particleLinearDepth: f32) -> f32 {\n if (uniforms.softness <= 0.0) {\n return 1.0;\n }\n\n \n let screenPos = vec2i(fragPos.xy);\n let screenSize = vec2i(uniforms.screenSize);\n\n \n if (screenPos.x < 0 || screenPos.x >= screenSize.x ||\n screenPos.y < 0 || screenPos.y >= screenSize.y) {\n return 1.0;\n }\n\n \n let sceneDepthNorm = textureLoad(depthTexture, screenPos, 0);\n\n \n if (sceneDepthNorm <= 0.0) {\n return 1.0;\n }\n\n \n let sceneDepth = uniforms.near + sceneDepthNorm * (uniforms.far - uniforms.near);\n let particleDepth = uniforms.near + particleLinearDepth * (uniforms.far - uniforms.near);\n\n \n let depthDiff = sceneDepth - particleDepth;\n return saturate(depthDiff / uniforms.softness);\n}\n\nfn calcFog(cameraDistance: f32, worldPosY: f32) -> f32 {\n if (uniforms.fogEnabled < 0.5) {\n return 0.0;\n }\n\n \n var distanceFog: f32;\n let d0 = uniforms.fogDistances.x;\n let d1 = uniforms.fogDistances.y;\n let d2 = uniforms.fogDistances.z;\n let a0 = uniforms.fogAlphas.x;\n let a1 = uniforms.fogAlphas.y;\n let a2 = uniforms.fogAlphas.z;\n\n if (cameraDistance <= d0) {\n distanceFog = a0;\n } else if (cameraDistance <= d1) {\n let t = (cameraDistance - d0) / max(d1 - d0, 0.001);\n distanceFog = mix(a0, a1, t);\n } else if (cameraDistance <= d2) {\n let t = (cameraDistance - d1) / max(d2 - d1, 0.001);\n distanceFog = mix(a1, a2, t);\n } else {\n distanceFog = a2;\n }\n\n \n let bottomY = uniforms.fogHeightFade.x;\n let topY = uniforms.fogHeightFade.y;\n var heightFactor = clamp((worldPosY - bottomY) / max(topY - bottomY, 0.001), 0.0, 1.0);\n if (worldPosY < bottomY) {\n heightFactor = 0.0;\n }\n\n return distanceFog * (1.0 - heightFactor);\n}\n\nfn applyFog(color: vec3f, cameraDistance: f32, worldPosY: f32) -> vec3f {\n let fogAlpha = calcFog(cameraDistance, worldPosY);\n if (fogAlpha <= 0.0) {\n return color;\n }\n\n return mix(color, uniforms.fogColor, fogAlpha);\n}\n\n@fragment\nfn fragmentMainAlpha(input: VertexOutput) -> FragmentOutput {\n var output: FragmentOutput;\n\n \n let texColor = textureSample(particleTexture, particleSampler, input.uv);\n\n \n var alpha = texColor.a * input.color.a;\n\n \n alpha *= calcSoftFade(input.position, input.linearDepth);\n\n \n if (alpha < 0.001) {\n discard;\n }\n\n \n let baseColor = texColor.rgb * input.color.rgb;\n let litColor = applyLighting(baseColor, input.lighting);\n\n \n let centerViewZ = input.centerViewZ;\n\n \n \n if (uniforms.fogDebug > 0.5) {\n \n if (uniforms.fogDebug < 1.5) {\n let dist = clamp(centerViewZ / 100.0, 0.0, 1.0);\n output.color = vec4f(vec3f(dist), 1.0);\n }\n \n \n else if (uniforms.fogDebug < 2.5) {\n let r = clamp(centerViewZ / 10.0, 0.0, 1.0);\n let g = clamp(centerViewZ / 100.0, 0.0, 1.0);\n let b = clamp(centerViewZ / 1000.0, 0.0, 1.0);\n output.color = vec4f(r, g, b, 1.0);\n }\n \n \n else if (uniforms.fogDebug < 3.5) {\n let center = clamp(centerViewZ / 100.0, 0.0, 1.0);\n let vertex = clamp(input.viewZ / 100.0, 0.0, 1.0);\n let diff = abs(center - vertex);\n output.color = vec4f(center, vertex, diff * 10.0, 1.0);\n }\n \n else if (uniforms.fogDebug < 4.5) {\n let r = clamp(abs(input.worldPos.x) / 100.0, 0.0, 1.0);\n let g = clamp(abs(input.worldPos.y) / 100.0, 0.0, 1.0);\n let b = clamp(abs(input.worldPos.z) / 100.0, 0.0, 1.0);\n output.color = vec4f(r, g, b, 1.0);\n }\n \n else {\n output.color = vec4f(uniforms.fogColor, 1.0);\n }\n output.depth = input.linearDepth;\n return output;\n }\n\n let foggedColor = applyFog(litColor, centerViewZ, input.worldPos.y);\n\n output.color = vec4f(foggedColor, alpha);\n output.depth = input.linearDepth;\n return output;\n}\n\n@fragment\nfn fragmentMainAdditive(input: VertexOutput) -> FragmentOutput {\n var output: FragmentOutput;\n\n \n let texColor = textureSample(particleTexture, particleSampler, input.uv);\n\n \n var alpha = texColor.a * input.color.a;\n\n \n alpha *= calcSoftFade(input.position, input.linearDepth);\n\n \n if (alpha < 0.001) {\n discard;\n }\n\n \n let baseColor = texColor.rgb * input.color.rgb;\n let litColor = applyLighting(baseColor, input.lighting);\n\n \n let centerViewZ = input.centerViewZ;\n\n \n \n if (uniforms.fogDebug > 0.5) {\n \n if (uniforms.fogDebug < 1.5) {\n let dist = clamp(centerViewZ / 100.0, 0.0, 1.0);\n output.color = vec4f(vec3f(dist), 1.0);\n }\n \n else if (uniforms.fogDebug < 2.5) {\n let r = clamp(centerViewZ / 10.0, 0.0, 1.0);\n let g = clamp(centerViewZ / 100.0, 0.0, 1.0);\n let b = clamp(centerViewZ / 1000.0, 0.0, 1.0);\n output.color = vec4f(r, g, b, 1.0);\n }\n \n else if (uniforms.fogDebug < 3.5) {\n let center = clamp(centerViewZ / 100.0, 0.0, 1.0);\n let vertex = clamp(input.viewZ / 100.0, 0.0, 1.0);\n let diff = abs(center - vertex);\n output.color = vec4f(center, vertex, diff * 10.0, 1.0);\n }\n \n else if (uniforms.fogDebug < 4.5) {\n let r = clamp(abs(input.worldPos.x) / 100.0, 0.0, 1.0);\n let g = clamp(abs(input.worldPos.y) / 100.0, 0.0, 1.0);\n let b = clamp(abs(input.worldPos.z) / 100.0, 0.0, 1.0);\n output.color = vec4f(r, g, b, 1.0);\n }\n \n else {\n output.color = vec4f(uniforms.fogColor, 1.0);\n }\n output.depth = input.linearDepth;\n return output;\n }\n\n \n \n let fogAlpha = calcFog(centerViewZ, input.worldPos.y);\n let fadedColor = litColor * (1.0 - fogAlpha);\n\n \n let rgb = fadedColor * alpha;\n output.color = vec4f(rgb, alpha);\n output.depth = input.linearDepth;\n return output;\n}";
|
|
7245
7488
|
class ParticlePass extends BasePass {
|
|
7246
7489
|
constructor(engine = null) {
|
|
7247
7490
|
super("Particles", engine);
|
|
@@ -7779,7 +8022,7 @@ class ParticlePass extends BasePass {
|
|
|
7779
8022
|
uniformData[87] = 0;
|
|
7780
8023
|
uniformData[88] = fogHeightFade[0];
|
|
7781
8024
|
uniformData[89] = fogHeightFade[1];
|
|
7782
|
-
uniformData[90] = 0;
|
|
8025
|
+
uniformData[90] = fogSettings.debug ?? 0;
|
|
7783
8026
|
uniformData[91] = 0;
|
|
7784
8027
|
const emitterRenderData = new Float32Array(16 * 4);
|
|
7785
8028
|
for (let i = 0; i < Math.min(emitters.length, 16); i++) {
|
|
@@ -7822,7 +8065,8 @@ class ParticlePass extends BasePass {
|
|
|
7822
8065
|
if (hasAlpha) {
|
|
7823
8066
|
uniformData[48] = 0;
|
|
7824
8067
|
device.queue.writeBuffer(this.renderUniformBuffer, 0, uniformData);
|
|
7825
|
-
const
|
|
8068
|
+
const alphaEncoder = device.createCommandEncoder({ label: "Particle Alpha Pass" });
|
|
8069
|
+
const renderPass = alphaEncoder.beginRenderPass({
|
|
7826
8070
|
colorAttachments: [{
|
|
7827
8071
|
view: this.outputTexture.view,
|
|
7828
8072
|
loadOp: "load",
|
|
@@ -7838,11 +8082,13 @@ class ParticlePass extends BasePass {
|
|
|
7838
8082
|
renderPass.setBindGroup(0, renderBindGroup);
|
|
7839
8083
|
renderPass.draw(6, maxParticles, 0, 0);
|
|
7840
8084
|
renderPass.end();
|
|
8085
|
+
device.queue.submit([alphaEncoder.finish()]);
|
|
7841
8086
|
}
|
|
7842
8087
|
if (hasAdditive) {
|
|
7843
8088
|
uniformData[48] = 1;
|
|
7844
8089
|
device.queue.writeBuffer(this.renderUniformBuffer, 0, uniformData);
|
|
7845
|
-
const
|
|
8090
|
+
const additiveEncoder = device.createCommandEncoder({ label: "Particle Additive Pass" });
|
|
8091
|
+
const renderPass = additiveEncoder.beginRenderPass({
|
|
7846
8092
|
colorAttachments: [{
|
|
7847
8093
|
view: this.outputTexture.view,
|
|
7848
8094
|
loadOp: "load",
|
|
@@ -7858,6 +8104,7 @@ class ParticlePass extends BasePass {
|
|
|
7858
8104
|
renderPass.setBindGroup(0, renderBindGroup);
|
|
7859
8105
|
renderPass.draw(6, maxParticles, 0, 0);
|
|
7860
8106
|
renderPass.end();
|
|
8107
|
+
device.queue.submit([additiveEncoder.finish()]);
|
|
7861
8108
|
}
|
|
7862
8109
|
}
|
|
7863
8110
|
async _resize(width, height) {
|
|
@@ -7899,6 +8146,10 @@ class FogPass extends BasePass {
|
|
|
7899
8146
|
get fogBrightResist() {
|
|
7900
8147
|
return this.settings?.environment?.fog?.brightResist ?? 0.8;
|
|
7901
8148
|
}
|
|
8149
|
+
get fogDebug() {
|
|
8150
|
+
return this.settings?.environment?.fog?.debug ?? 0;
|
|
8151
|
+
}
|
|
8152
|
+
// 0=off, 1=show fogAlpha, 2=show distance, 3=show heightFactor
|
|
7902
8153
|
/**
|
|
7903
8154
|
* Set the input texture (HDR lighting output)
|
|
7904
8155
|
*/
|
|
@@ -7975,6 +8226,8 @@ class FogPass extends BasePass {
|
|
|
7975
8226
|
brightResist: f32, // float 47
|
|
7976
8227
|
heightFade: vec2f, // floats 48-49
|
|
7977
8228
|
screenSize: vec2f, // floats 50-51
|
|
8229
|
+
debug: f32, // float 52: 0=off, 1=fogAlpha, 2=distance, 3=heightFactor
|
|
8230
|
+
_pad: vec3f, // floats 53-55
|
|
7978
8231
|
}
|
|
7979
8232
|
|
|
7980
8233
|
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
|
|
@@ -8103,6 +8356,26 @@ class FogPass extends BasePass {
|
|
|
8103
8356
|
let brightnessResist = clamp((luminance - 1.0) / 2.0, 0.0, 1.0);
|
|
8104
8357
|
fogAlpha *= (1.0 - brightnessResist * uniforms.brightResist);
|
|
8105
8358
|
|
|
8359
|
+
// Debug output
|
|
8360
|
+
let debugMode = i32(uniforms.debug);
|
|
8361
|
+
if (debugMode == 1) {
|
|
8362
|
+
// Show fog alpha as grayscale
|
|
8363
|
+
return vec4f(vec3f(fogAlpha), 1.0);
|
|
8364
|
+
} else if (debugMode == 2) {
|
|
8365
|
+
// Show distance (normalized to 0-100m range)
|
|
8366
|
+
let normDist = clamp(cameraDistance / 100.0, 0.0, 1.0);
|
|
8367
|
+
return vec4f(vec3f(normDist), 1.0);
|
|
8368
|
+
} else if (debugMode == 3) {
|
|
8369
|
+
// Show height factor
|
|
8370
|
+
return vec4f(vec3f(heightFactor), 1.0);
|
|
8371
|
+
} else if (debugMode == 4) {
|
|
8372
|
+
// Show distance fog (before height fade)
|
|
8373
|
+
return vec4f(vec3f(distanceFog), 1.0);
|
|
8374
|
+
} else if (debugMode == 5) {
|
|
8375
|
+
// Show the actual fog color being used (to verify it matches particles)
|
|
8376
|
+
return vec4f(uniforms.fogColor, 1.0);
|
|
8377
|
+
}
|
|
8378
|
+
|
|
8106
8379
|
// Apply fog
|
|
8107
8380
|
let foggedColor = mix(color.rgb, uniforms.fogColor, fogAlpha);
|
|
8108
8381
|
|
|
@@ -8177,6 +8450,7 @@ class FogPass extends BasePass {
|
|
|
8177
8450
|
uniformData[49] = heightFade[1];
|
|
8178
8451
|
uniformData[50] = this.width;
|
|
8179
8452
|
uniformData[51] = this.height;
|
|
8453
|
+
uniformData[52] = this.fogDebug;
|
|
8180
8454
|
device.queue.writeBuffer(this.uniformBuffer, 0, uniformData);
|
|
8181
8455
|
const commandEncoder = device.createCommandEncoder({ label: "Fog Pass" });
|
|
8182
8456
|
const renderPass = commandEncoder.beginRenderPass({
|
|
@@ -8575,6 +8849,7 @@ class HiZPass extends BasePass {
|
|
|
8575
8849
|
this.screenWidth = 0;
|
|
8576
8850
|
this.screenHeight = 0;
|
|
8577
8851
|
this._destroyed = false;
|
|
8852
|
+
this._warmupFramesRemaining = 5;
|
|
8578
8853
|
}
|
|
8579
8854
|
/**
|
|
8580
8855
|
* Set the depth texture to read from (from GBuffer)
|
|
@@ -8583,6 +8858,18 @@ class HiZPass extends BasePass {
|
|
|
8583
8858
|
setDepthTexture(depth) {
|
|
8584
8859
|
this.depthTexture = depth;
|
|
8585
8860
|
}
|
|
8861
|
+
/**
|
|
8862
|
+
* Invalidate occlusion culling data and reset warmup period.
|
|
8863
|
+
* Call this after engine creation, scene loading, or major camera changes
|
|
8864
|
+
* to prevent incorrect occlusion culling with stale data.
|
|
8865
|
+
*/
|
|
8866
|
+
invalidate() {
|
|
8867
|
+
this.hasValidHistory = false;
|
|
8868
|
+
this.hizDataReady = false;
|
|
8869
|
+
this._warmupFramesRemaining = 5;
|
|
8870
|
+
vec3$1.set(this.lastCameraPosition, 0, 0, 0);
|
|
8871
|
+
vec3$1.set(this.lastCameraDirection, 0, 0, 0);
|
|
8872
|
+
}
|
|
8586
8873
|
async _init() {
|
|
8587
8874
|
const { device, canvas } = this.engine;
|
|
8588
8875
|
await this._createResources(canvas.width, canvas.height);
|
|
@@ -8666,6 +8953,7 @@ class HiZPass extends BasePass {
|
|
|
8666
8953
|
this._destroyed = false;
|
|
8667
8954
|
this.hizDataReady = false;
|
|
8668
8955
|
this.pendingReadback = null;
|
|
8956
|
+
this._warmupFramesRemaining = 5;
|
|
8669
8957
|
}
|
|
8670
8958
|
/**
|
|
8671
8959
|
* Check if camera has moved significantly, requiring HiZ invalidation
|
|
@@ -8699,6 +8987,9 @@ class HiZPass extends BasePass {
|
|
|
8699
8987
|
const { device } = this.engine;
|
|
8700
8988
|
const { camera } = context;
|
|
8701
8989
|
this._frameCounter++;
|
|
8990
|
+
if (this._warmupFramesRemaining > 0) {
|
|
8991
|
+
this._warmupFramesRemaining--;
|
|
8992
|
+
}
|
|
8702
8993
|
if (!this.settings?.occlusionCulling?.enabled) {
|
|
8703
8994
|
return;
|
|
8704
8995
|
}
|
|
@@ -8849,6 +9140,9 @@ class HiZPass extends BasePass {
|
|
|
8849
9140
|
*/
|
|
8850
9141
|
testSphereOcclusion(bsphere, viewProj, near, far, cameraPos) {
|
|
8851
9142
|
this.debugStats.tested++;
|
|
9143
|
+
if (this._warmupFramesRemaining > 0) {
|
|
9144
|
+
return false;
|
|
9145
|
+
}
|
|
8852
9146
|
if (!this.hizDataReady || !this.hasValidHistory) {
|
|
8853
9147
|
return false;
|
|
8854
9148
|
}
|
|
@@ -9262,6 +9556,8 @@ class SSGITilePass extends BasePass {
|
|
|
9262
9556
|
this.tilePropagateBuffer = null;
|
|
9263
9557
|
this.tileCountX = 0;
|
|
9264
9558
|
this.tileCountY = 0;
|
|
9559
|
+
this.renderWidth = 0;
|
|
9560
|
+
this.renderHeight = 0;
|
|
9265
9561
|
this.prevHDRTexture = null;
|
|
9266
9562
|
this.emissiveTexture = null;
|
|
9267
9563
|
this.uniformBuffer = null;
|
|
@@ -9290,6 +9586,8 @@ class SSGITilePass extends BasePass {
|
|
|
9290
9586
|
}
|
|
9291
9587
|
async _createResources(width, height) {
|
|
9292
9588
|
const { device } = this.engine;
|
|
9589
|
+
this.renderWidth = width;
|
|
9590
|
+
this.renderHeight = height;
|
|
9293
9591
|
this.tileCountX = Math.ceil(width / TILE_SIZE$1);
|
|
9294
9592
|
this.tileCountY = Math.ceil(height / TILE_SIZE$1);
|
|
9295
9593
|
const totalTiles = this.tileCountX * this.tileCountY;
|
|
@@ -9357,13 +9655,13 @@ class SSGITilePass extends BasePass {
|
|
|
9357
9655
|
this._needsRebuild = false;
|
|
9358
9656
|
}
|
|
9359
9657
|
async _execute(context) {
|
|
9360
|
-
const { device
|
|
9658
|
+
const { device } = this.engine;
|
|
9361
9659
|
const ssgiSettings = this.settings?.ssgi;
|
|
9362
9660
|
if (!ssgiSettings?.enabled) {
|
|
9363
9661
|
return;
|
|
9364
9662
|
}
|
|
9365
9663
|
if (this._needsRebuild) {
|
|
9366
|
-
await this._createResources(
|
|
9664
|
+
await this._createResources(this.renderWidth, this.renderHeight);
|
|
9367
9665
|
}
|
|
9368
9666
|
if (!this.prevHDRTexture || !this.emissiveTexture) {
|
|
9369
9667
|
return;
|
|
@@ -9371,8 +9669,8 @@ class SSGITilePass extends BasePass {
|
|
|
9371
9669
|
if (!this.accumulatePipeline || !this.propagatePipeline) {
|
|
9372
9670
|
return;
|
|
9373
9671
|
}
|
|
9374
|
-
const width =
|
|
9375
|
-
const height =
|
|
9672
|
+
const width = this.renderWidth;
|
|
9673
|
+
const height = this.renderHeight;
|
|
9376
9674
|
const emissiveBoost = ssgiSettings.emissiveBoost ?? 2;
|
|
9377
9675
|
const maxBrightness = ssgiSettings.maxBrightness ?? 4;
|
|
9378
9676
|
device.queue.writeBuffer(this.uniformBuffer, 0, new Float32Array([
|
|
@@ -9568,7 +9866,7 @@ class SSGIPass extends BasePass {
|
|
|
9568
9866
|
if (!this.pipeline) {
|
|
9569
9867
|
return;
|
|
9570
9868
|
}
|
|
9571
|
-
this._updateUniforms(ssgiSettings,
|
|
9869
|
+
this._updateUniforms(ssgiSettings, this.width * 2, this.height * 2);
|
|
9572
9870
|
const bindGroup = device.createBindGroup({
|
|
9573
9871
|
label: "ssgiBindGroup",
|
|
9574
9872
|
layout: this.bindGroupLayout,
|
|
@@ -10040,9 +10338,11 @@ class BloomPass extends BasePass {
|
|
|
10040
10338
|
const scale = this.bloomScale;
|
|
10041
10339
|
const bloomWidth = Math.max(1, Math.floor(width * scale));
|
|
10042
10340
|
const bloomHeight = Math.max(1, Math.floor(height * scale));
|
|
10341
|
+
if (this.bloomWidth !== bloomWidth || this.bloomHeight !== bloomHeight) {
|
|
10342
|
+
console.log(`Bloom: ${width}x${height} -> ${bloomWidth}x${bloomHeight} (scale: ${scale})`);
|
|
10343
|
+
}
|
|
10043
10344
|
this.bloomWidth = bloomWidth;
|
|
10044
10345
|
this.bloomHeight = bloomHeight;
|
|
10045
|
-
console.log(`Bloom: ${width}x${height} -> ${bloomWidth}x${bloomHeight} (scale: ${scale})`);
|
|
10046
10346
|
const createBloomTexture = (label) => {
|
|
10047
10347
|
const texture = device.createTexture({
|
|
10048
10348
|
label,
|
|
@@ -11011,150 +11311,842 @@ fn fragmentMain(input: VertexOutput) -> @location(0) vec4f {
|
|
|
11011
11311
|
this.pipelineCache.clear();
|
|
11012
11312
|
}
|
|
11013
11313
|
}
|
|
11014
|
-
var postproc_default = "struct VertexOutput {\n @builtin(position) position : vec4<f32>,\n @location(0) uv : vec2<f32>,\n}\n\nstruct Uniforms {\n canvasSize: vec2f,\n noiseParams: vec4f, \n ditherParams: vec4f, \n bloomParams: vec4f, \n}\n\n@group(0) @binding(0) var<uniform> uniforms: Uniforms;\n@group(0) @binding(1) var inputTexture : texture_2d<f32>;\n@group(0) @binding(2) var inputSampler : sampler;\n@group(0) @binding(3) var noiseTexture : texture_2d<f32>;\n@group(0) @binding(4) var noiseSampler : sampler;\n@group(0) @binding(5) var bloomTexture : texture_2d<f32>;\n@group(0) @binding(6) var bloomSampler : sampler;\n@group(0) @binding(7) var guiTexture : texture_2d<f32>;\n@group(0) @binding(8) var guiSampler : sampler;\n\nconst FXAA_EDGE_THRESHOLD: f32 = 0.125; \nconst FXAA_EDGE_THRESHOLD_MIN: f32 = 0.0156; \nconst FXAA_SUBPIX_QUALITY: f32 = 1.0; \n\nfn sampleNoise(screenPos: vec2f) -> f32 {\n let noiseSize = i32(uniforms.noiseParams.x);\n let noiseOffsetX = i32(uniforms.noiseParams.y * f32(noiseSize));\n let noiseOffsetY = i32(uniforms.noiseParams.z * f32(noiseSize));\n\n let texCoord = vec2i(\n (i32(screenPos.x) + noiseOffsetX) % noiseSize,\n (i32(screenPos.y) + noiseOffsetY) % noiseSize\n );\n return textureLoad(noiseTexture, texCoord, 0).r;\n}\n\nfn rgb2luma(rgb: vec3f) -> f32 {\n return dot(rgb, vec3f(0.299, 0.587, 0.114));\n}\n\nfn loadPixel(coord: vec2i) -> vec3f {\n let size = vec2i(textureDimensions(inputTexture, 0));\n let clampedCoord = clamp(coord, vec2i(0), size - vec2i(1));\n return textureLoad(inputTexture, clampedCoord, 0).rgb;\n}\n\nfn sampleBilinear(uv: vec2f) -> vec3f {\n let texSize = vec2f(textureDimensions(inputTexture, 0));\n let texelPos = uv * texSize - 0.5;\n let baseCoord = vec2i(floor(texelPos));\n let frac = fract(texelPos);\n\n let c00 = loadPixel(baseCoord);\n let c10 = loadPixel(baseCoord + vec2i(1, 0));\n let c01 = loadPixel(baseCoord + vec2i(0, 1));\n let c11 = loadPixel(baseCoord + vec2i(1, 1));\n\n let c0 = mix(c00, c10, frac.x);\n let c1 = mix(c01, c11, frac.x);\n return mix(c0, c1, frac.y);\n}\n\nfn fxaa(uv: vec2f) -> vec3f {\n let texSize = vec2f(textureDimensions(inputTexture, 0));\n let pixelCoord = vec2i(uv * texSize);\n\n \n let rgbM = loadPixel(pixelCoord);\n let rgbN = loadPixel(pixelCoord + vec2i(0, -1));\n let rgbS = loadPixel(pixelCoord + vec2i(0, 1));\n let rgbW = loadPixel(pixelCoord + vec2i(-1, 0));\n let rgbE = loadPixel(pixelCoord + vec2i(1, 0));\n let rgbNW = loadPixel(pixelCoord + vec2i(-1, -1));\n let rgbNE = loadPixel(pixelCoord + vec2i(1, -1));\n let rgbSW = loadPixel(pixelCoord + vec2i(-1, 1));\n let rgbSE = loadPixel(pixelCoord + vec2i(1, 1));\n\n \n let lumaM = rgb2luma(rgbM);\n let lumaN = rgb2luma(rgbN);\n let lumaS = rgb2luma(rgbS);\n let lumaW = rgb2luma(rgbW);\n let lumaE = rgb2luma(rgbE);\n let lumaNW = rgb2luma(rgbNW);\n let lumaNE = rgb2luma(rgbNE);\n let lumaSW = rgb2luma(rgbSW);\n let lumaSE = rgb2luma(rgbSE);\n\n \n let lumaMin = min(lumaM, min(min(lumaN, lumaS), min(lumaW, lumaE)));\n let lumaMax = max(lumaM, max(max(lumaN, lumaS), max(lumaW, lumaE)));\n let lumaRange = lumaMax - lumaMin;\n\n \n let isEdge = lumaRange >= max(FXAA_EDGE_THRESHOLD_MIN, lumaMax * FXAA_EDGE_THRESHOLD);\n\n \n let lumaL = (lumaN + lumaS + lumaW + lumaE) * 0.25;\n let rangeL = abs(lumaL - lumaM);\n var blendL = max(0.0, (rangeL / max(lumaRange, 0.0001)) - 0.25) * (1.0 / 0.75);\n blendL = min(1.0, blendL) * blendL * FXAA_SUBPIX_QUALITY;\n\n \n let edgeHorz = abs((lumaNW + lumaNE) - 2.0 * lumaN) +\n 2.0 * abs((lumaW + lumaE) - 2.0 * lumaM) +\n abs((lumaSW + lumaSE) - 2.0 * lumaS);\n let edgeVert = abs((lumaNW + lumaSW) - 2.0 * lumaW) +\n 2.0 * abs((lumaN + lumaS) - 2.0 * lumaM) +\n abs((lumaNE + lumaSE) - 2.0 * lumaE);\n let isHorizontal = edgeHorz >= edgeVert;\n\n \n let luma1 = select(lumaE, lumaS, isHorizontal);\n let luma2 = select(lumaW, lumaN, isHorizontal);\n let gradient1 = abs(luma1 - lumaM);\n let gradient2 = abs(luma2 - lumaM);\n let is1Steepest = gradient1 >= gradient2;\n let gradientScaled = 0.25 * max(gradient1, gradient2);\n\n \n let stepSign = select(-1.0, 1.0, is1Steepest);\n let lumaLocalAverage = 0.5 * (select(luma2, luma1, is1Steepest) + lumaM);\n\n \n let searchDir = select(vec2i(0, 1), vec2i(1, 0), isHorizontal);\n\n let luma1_1 = rgb2luma(loadPixel(pixelCoord - searchDir)) - lumaLocalAverage;\n let luma2_1 = rgb2luma(loadPixel(pixelCoord + searchDir)) - lumaLocalAverage;\n let luma1_2 = rgb2luma(loadPixel(pixelCoord - searchDir * 2)) - lumaLocalAverage;\n let luma2_2 = rgb2luma(loadPixel(pixelCoord + searchDir * 2)) - lumaLocalAverage;\n let luma1_3 = rgb2luma(loadPixel(pixelCoord - searchDir * 3)) - lumaLocalAverage;\n let luma2_3 = rgb2luma(loadPixel(pixelCoord + searchDir * 3)) - lumaLocalAverage;\n let luma1_4 = rgb2luma(loadPixel(pixelCoord - searchDir * 4)) - lumaLocalAverage;\n let luma2_4 = rgb2luma(loadPixel(pixelCoord + searchDir * 4)) - lumaLocalAverage;\n\n \n let reached1_1 = abs(luma1_1) >= gradientScaled;\n let reached1_2 = abs(luma1_2) >= gradientScaled;\n let reached1_3 = abs(luma1_3) >= gradientScaled;\n let reached2_1 = abs(luma2_1) >= gradientScaled;\n let reached2_2 = abs(luma2_2) >= gradientScaled;\n let reached2_3 = abs(luma2_3) >= gradientScaled;\n\n \n let dist1 = select(select(select(4.0, 3.0, reached1_3), 2.0, reached1_2), 1.0, reached1_1);\n let dist2 = select(select(select(4.0, 3.0, reached2_3), 2.0, reached2_2), 1.0, reached2_1);\n\n \n let lumaEnd1 = select(select(select(luma1_4, luma1_3, reached1_3), luma1_2, reached1_2), luma1_1, reached1_1);\n let lumaEnd2 = select(select(select(luma2_4, luma2_3, reached2_3), luma2_2, reached2_2), luma2_1, reached2_1);\n\n \n let distFinal = min(dist1, dist2);\n let edgeThickness = dist1 + dist2;\n let lumaEndCloser = select(lumaEnd2, lumaEnd1, dist1 < dist2);\n let correctVariation = (lumaEndCloser < 0.0) != (lumaM < lumaLocalAverage);\n var pixelOffset = select(0.0, -distFinal / max(edgeThickness, 0.0001) + 0.5, correctVariation);\n\n \n let finalOffset = max(max(pixelOffset, blendL), 0.5);\n\n \n let inverseVP = 1.0 / texSize;\n var finalUv = uv;\n let offsetAmount = finalOffset * stepSign;\n finalUv.x += select(offsetAmount * inverseVP.x, 0.0, isHorizontal);\n finalUv.y += select(0.0, offsetAmount * inverseVP.y, isHorizontal);\n\n \n let offsetColor = sampleBilinear(finalUv);\n\n \n \n let perpDir = select(vec2i(0, 1), vec2i(1, 0), isHorizontal);\n let perpColor1 = loadPixel(pixelCoord + perpDir);\n let perpColor2 = loadPixel(pixelCoord - perpDir);\n let neighborAvg = (perpColor1 + perpColor2) * 0.5;\n\n \n let fxaaColor = mix(mix(offsetColor, neighborAvg, 0.7), rgbM, 0.1);\n\n \n return select(rgbM, fxaaColor, isEdge);\n}\n\nfn sampleBloom(uv: vec2f, sceneBrightness: f32) -> vec3f {\n let bloom = textureSample(bloomTexture, bloomSampler, uv).rgb;\n\n \n \n let threshold = 0.5; \n let mask = saturate(1.0 - (sceneBrightness - threshold) / (1.0 - threshold));\n\n return bloom * mask * mask; \n}\n\nfn aces_tone_map(hdr: vec3<f32>) -> vec3<f32> {\n let m1 = mat3x3(\n 0.59719, 0.07600, 0.02840,\n 0.35458, 0.90834, 0.13383,\n 0.04823, 0.01566, 0.83777,\n );\n let m2 = mat3x3(\n 1.60475, -0.10208, -0.00327,\n -0.53108, 1.10813, -0.07276,\n -0.07367, -0.00605, 1.07602,\n );\n let v = m1 * hdr;\n let a = v * (v + 0.0245786) - 0.000090537;\n let b = v * (0.983729 * v + 0.4329510) + 0.238081;\n return clamp(m2 * (a / b), vec3(0.0), vec3(1.0));\n}\n\n@vertex\nfn vertexMain(@builtin(vertex_index) vertexIndex : u32) -> VertexOutput {\n var output : VertexOutput;\n let x = f32(vertexIndex & 1u) * 4.0 - 1.0;\n let y = f32(vertexIndex >> 1u) * 4.0 - 1.0;\n output.position = vec4<f32>(x, y, 0.0, 1.0);\n output.uv = vec2<f32>((x + 1.0) * 0.5, (1.0 - y) * 0.5);\n return output;\n}\n\n@fragment\nfn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {\n \n let fxaaEnabled = uniforms.noiseParams.w > 0.5;\n let fxaaColor = fxaa(input.uv);\n let directColor = textureSample(inputTexture, inputSampler, input.uv).rgb;\n var color = select(directColor, fxaaColor, fxaaEnabled);\n\n \n \n if (uniforms.bloomParams.x > 0.5) {\n let sceneBrightness = rgb2luma(color);\n let bloom = sampleBloom(input.uv, sceneBrightness);\n color += bloom * uniforms.bloomParams.y; \n }\n\n var sdr = aces_tone_map(color);\n\n \n \n let gui = textureSample(guiTexture, guiSampler, input.uv);\n sdr = sdr * (1.0 - gui.a) + gui.rgb;\n\n \n \n if (uniforms.ditherParams.x > 0.5 && uniforms.noiseParams.x > 0.0) {\n let levels = uniforms.ditherParams.y;\n let noise = sampleNoise(input.position.xy);\n\n \n \n let dither = noise - 0.5;\n sdr = floor(sdr * (levels - 1.0) + dither + 0.5) / (levels - 1.0);\n sdr = clamp(sdr, vec3f(0.0), vec3f(1.0));\n }\n\n return vec4<f32>(sdr, 1.0);\n}";
|
|
11015
|
-
|
|
11314
|
+
var volumetric_raymarch_default = "const MAX_LIGHTS: u32 = 768u;\nconst MAX_SPOT_SHADOWS: i32 = 16;\nconst SPOT_ATLAS_SIZE: f32 = 2048.0;\nconst SPOT_TILE_SIZE: f32 = 512.0;\nconst SPOT_TILES_PER_ROW: i32 = 4;\n\nstruct Uniforms {\n inverseProjection: mat4x4f,\n inverseView: mat4x4f,\n cameraPosition: vec3f,\n nearPlane: f32,\n farPlane: f32,\n maxSamples: f32,\n time: f32,\n fogDensity: f32,\n fogColor: vec3f,\n shadowsEnabled: f32,\n mainLightDir: vec3f,\n mainLightIntensity: f32,\n mainLightColor: vec3f,\n scatterStrength: f32,\n fogHeightFade: vec2f,\n maxDistance: f32,\n lightCount: f32,\n debugMode: f32,\n noiseStrength: f32, \n noiseAnimated: f32, \n mainLightScatter: f32, \n noiseScale: f32, \n mainLightSaturation: f32, \n}\n\nstruct Light {\n enabled: u32, \n _pad0: u32, \n _pad1: u32, \n _pad2: u32, \n position: vec3f, \n _pad3: f32, \n color: vec4f, \n direction: vec3f, \n _pad4: f32, \n geom: vec4f, \n shadowIndex: i32, \n _pad5: u32, \n _pad6: u32, \n _pad7: u32, \n}\n\nstruct VertexOutput {\n @builtin(position) position: vec4f,\n @location(0) uv: vec2f,\n}\n\n@group(0) @binding(0) var<uniform> uniforms: Uniforms;\n@group(0) @binding(1) var depthTexture: texture_depth_2d;\n@group(0) @binding(2) var cascadeShadowMaps: texture_depth_2d_array;\n@group(0) @binding(3) var shadowSampler: sampler_comparison;\n@group(0) @binding(4) var<storage, read> cascadeMatrices: array<mat4x4f>;\n@group(0) @binding(5) var<storage, read> lights: array<Light, MAX_LIGHTS>;\n@group(0) @binding(6) var spotShadowAtlas: texture_depth_2d;\n@group(0) @binding(7) var<storage, read> spotMatrices: array<mat4x4f, MAX_SPOT_SHADOWS>;\n\n@vertex\nfn vertexMain(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {\n var output: VertexOutput;\n let x = f32(vertexIndex & 1u) * 4.0 - 1.0;\n let y = f32(vertexIndex >> 1u) * 4.0 - 1.0;\n output.position = vec4f(x, y, 0.0, 1.0);\n output.uv = vec2f((x + 1.0) * 0.5, (1.0 - y) * 0.5);\n return output;\n}\n\nfn hash(p: vec3f) -> f32 {\n var p3 = fract(p * 0.1031);\n p3 += dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn noise3d(p: vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n\n return mix(\n mix(mix(hash(i + vec3f(0,0,0)), hash(i + vec3f(1,0,0)), u.x),\n mix(hash(i + vec3f(0,1,0)), hash(i + vec3f(1,1,0)), u.x), u.y),\n mix(mix(hash(i + vec3f(0,0,1)), hash(i + vec3f(1,0,1)), u.x),\n mix(hash(i + vec3f(0,1,1)), hash(i + vec3f(1,1,1)), u.x), u.y),\n u.z\n );\n}\n\nfn fbm(p: vec3f) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var pos = p;\n for (var i = 0; i < 3; i++) {\n value += amplitude * noise3d(pos);\n pos *= 2.0;\n amplitude *= 0.5;\n }\n return value;\n}\n\nfn getFogDensity(worldPos: vec3f, dist: f32) -> f32 {\n \n let heightFade = uniforms.fogHeightFade;\n var heightMod = 1.0;\n if (heightFade.y > heightFade.x) {\n let range = heightFade.y - heightFade.x;\n let fadeZone = range * 0.1; \n let topFade = 1.0 - smoothstep(heightFade.y - fadeZone, heightFade.y, worldPos.y);\n let bottomFade = smoothstep(heightFade.x, heightFade.x + fadeZone, worldPos.y);\n heightMod = topFade * bottomFade;\n }\n\n \n if (uniforms.noiseStrength < 0.001) {\n return uniforms.fogDensity * heightMod;\n }\n\n \n let timeOffset = select(0.0, uniforms.time, uniforms.noiseAnimated > 0.5);\n\n \n \n let scale1 = uniforms.noiseScale; \n let scale2 = uniforms.noiseScale * 0.32; \n\n let noisePos = worldPos * scale1 + vec3f(timeOffset * 0.15, timeOffset * 0.02, timeOffset * 0.08);\n let noiseVal = fbm(noisePos);\n\n \n let noisePos2 = worldPos * scale2 + vec3f(timeOffset * 0.05, 0.0, timeOffset * 0.03);\n let noiseVal2 = fbm(noisePos2);\n\n \n let combinedNoise = noiseVal * 0.6 + noiseVal2 * 0.4;\n\n \n \n \n let noiseRange = combinedNoise * 1.0 + 0.2; \n let noiseMapped = mix(1.0, noiseRange, uniforms.noiseStrength);\n\n return uniforms.fogDensity * heightMod * noiseMapped;\n}\n\nfn sampleShadow(worldPos: vec3f) -> f32 {\n if (uniforms.shadowsEnabled < 0.5) {\n return 1.0;\n }\n\n \n for (var cascade = 0; cascade < 3; cascade++) {\n let lightSpacePos = cascadeMatrices[cascade] * vec4f(worldPos, 1.0);\n let projCoords = lightSpacePos.xyz / lightSpacePos.w;\n\n \n let uv = vec2f(projCoords.x * 0.5 + 0.5, 0.5 - projCoords.y * 0.5);\n\n \n if (uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0 &&\n projCoords.z >= 0.0 && projCoords.z <= 1.0) {\n\n let bias = 0.003 * f32(cascade + 1);\n let depth = projCoords.z - bias;\n let clampedUV = clamp(uv, vec2f(0.002), vec2f(0.998));\n let clampedDepth = clamp(depth, 0.001, 0.999);\n\n return textureSampleCompareLevel(cascadeShadowMaps, shadowSampler, clampedUV, cascade, clampedDepth);\n }\n }\n\n return 1.0;\n}\n\nfn sampleSpotShadow(worldPos: vec3f, slotIndex: i32) -> f32 {\n if (slotIndex < 0 || slotIndex >= MAX_SPOT_SHADOWS) {\n return 1.0;\n }\n\n \n let lightMatrix = spotMatrices[slotIndex];\n\n \n let lightSpacePos = lightMatrix * vec4f(worldPos, 1.0);\n\n \n let w = max(abs(lightSpacePos.w), 0.0001) * sign(lightSpacePos.w + 0.0001);\n let projCoords = lightSpacePos.xyz / w;\n\n \n if (abs(projCoords.x) > 1.0 || abs(projCoords.y) > 1.0 ||\n projCoords.z < 0.0 || projCoords.z > 1.0) {\n return 1.0;\n }\n\n \n let col = slotIndex % SPOT_TILES_PER_ROW;\n let row = slotIndex / SPOT_TILES_PER_ROW;\n\n \n let tileUV = vec2f(projCoords.x * 0.5 + 0.5, 0.5 - projCoords.y * 0.5);\n\n \n let tileOffsetX = f32(col) * SPOT_TILE_SIZE;\n let tileOffsetY = f32(row) * SPOT_TILE_SIZE;\n let atlasUV = vec2f(\n (tileOffsetX + tileUV.x * SPOT_TILE_SIZE) / SPOT_ATLAS_SIZE,\n (tileOffsetY + tileUV.y * SPOT_TILE_SIZE) / SPOT_ATLAS_SIZE\n );\n\n \n let bias = 0.005;\n let depth = clamp(projCoords.z - bias, 0.001, 0.999);\n\n return textureSampleCompareLevel(spotShadowAtlas, shadowSampler, atlasUV, depth);\n}\n\nfn calculateLightContribution(worldPos: vec3f, light: Light, worldRayDir: vec3f) -> vec3f {\n let toLight = light.position - worldPos;\n let dist = length(toLight);\n let radius = light.geom.x;\n\n \n if (dist > radius) {\n return vec3f(0.0);\n }\n\n \n \n let normalizedDist = dist / radius;\n\n \n \n let attenuation = 1.0 - smoothstep(0.0, 1.0, normalizedDist);\n\n \n let innerCone = light.geom.y;\n let outerCone = light.geom.z;\n var spotAttenuation = 1.0;\n let isSpotlight = outerCone > 0.0;\n\n if (isSpotlight) {\n let lightDir = normalize(-toLight);\n let spotCos = dot(lightDir, normalize(light.direction));\n spotAttenuation = smoothstep(outerCone, innerCone, spotCos);\n if (spotAttenuation <= 0.0) {\n return vec3f(0.0);\n }\n }\n\n \n var shadow = 1.0;\n if (isSpotlight && light.shadowIndex >= 0 && uniforms.shadowsEnabled > 0.5) {\n shadow = sampleSpotShadow(worldPos, light.shadowIndex);\n }\n\n \n let phase = 0.25; \n\n \n let intensity = light.color.a;\n return light.color.rgb * intensity * attenuation * spotAttenuation * shadow * phase;\n}\n\nfn phaseHG(cosTheta: f32, g: f32) -> f32 {\n let g2 = g * g;\n let denom = 1.0 + g2 - 2.0 * g * cosTheta;\n return (1.0 - g2) / (4.0 * 3.14159 * pow(denom, 1.5));\n}\n\nfn linearizeDepth(depth: f32) -> f32 {\n let near = uniforms.nearPlane;\n let far = uniforms.farPlane;\n return near + depth * (far - near);\n}\n\n@fragment\nfn fragmentMain(input: VertexOutput) -> @location(0) vec4f {\n let uv = input.uv;\n\n \n let DEBUG = i32(uniforms.debugMode);\n\n \n \n var clipUV = uv;\n clipUV.y = 1.0 - clipUV.y; \n let ndc = vec4f(clipUV * 2.0 - 1.0, 0.0, 1.0);\n\n \n let viewRay4 = uniforms.inverseProjection * ndc;\n let viewDir = normalize(viewRay4.xyz / viewRay4.w);\n\n \n let worldRayDir = normalize((uniforms.inverseView * vec4f(viewDir, 0.0)).xyz);\n\n \n let depthTexSize = vec2f(textureDimensions(depthTexture));\n let depthCoord = vec2i(uv * depthTexSize);\n let rawDepth = textureLoad(depthTexture, depthCoord, 0);\n\n \n if (DEBUG == 1) {\n return vec4f(rawDepth, rawDepth, rawDepth, 1.0);\n }\n\n \n if (DEBUG == 2) {\n return vec4f(worldRayDir * 0.5 + 0.5, 1.0);\n }\n\n \n if (DEBUG == 4) {\n let vzVis = abs(viewDir.z);\n return vec4f(vzVis, vzVis, vzVis, 1.0);\n }\n\n \n \n \n \n if (DEBUG == 7) {\n let numLights = i32(uniforms.lightCount);\n if (numLights > 0) {\n let light = lights[0];\n \n let camToLightDist = length(light.position - uniforms.cameraPosition);\n let radius = light.geom.x;\n \n return vec4f(\n clamp(camToLightDist / 50.0, 0.0, 1.0),\n f32(light.enabled) * 0.5,\n clamp(radius / 50.0, 0.0, 1.0),\n 1.0\n );\n }\n return vec4f(1.0, 0.0, 0.0, 1.0); \n }\n\n \n \n if (DEBUG == 8) {\n let numLights = i32(uniforms.lightCount);\n if (numLights > 0) {\n let light = lights[0];\n \n return vec4f(\n (light.position.x + 100.0) / 200.0,\n (light.position.y + 10.0) / 60.0,\n (light.position.z + 100.0) / 200.0,\n 1.0\n );\n }\n return vec4f(0.0, 0.0, 0.0, 1.0);\n }\n\n \n var geometryDist = 1000000.0;\n if (rawDepth < 0.9999) {\n let linearDepth = linearizeDepth(rawDepth);\n geometryDist = linearDepth / max(0.001, -viewDir.z) * 0.98;\n }\n\n \n \n let fogBottom = uniforms.fogHeightFade.x;\n let fogTop = uniforms.fogHeightFade.y;\n let camY = uniforms.cameraPosition.y;\n\n \n \n \n \n var tStart = 0.0;\n var tEnd = geometryDist;\n\n if (abs(worldRayDir.y) > 0.0001) {\n let tBottom = (fogBottom - camY) / worldRayDir.y;\n let tTop = (fogTop - camY) / worldRayDir.y;\n\n \n let tEnter = min(tBottom, tTop);\n let tExit = max(tBottom, tTop);\n\n \n if (camY < fogBottom || camY > fogTop) {\n tStart = max(0.0, tEnter);\n }\n\n \n tEnd = min(tEnd, max(0.0, tExit));\n } else {\n \n if (camY < fogBottom || camY > fogTop) {\n return vec4f(0.0); \n }\n }\n\n \n if (tStart >= tEnd || (tEnd - tStart) < 0.5) {\n return vec4f(0.0);\n }\n\n \n \n if (DEBUG == 9) {\n let rayLength = tEnd - tStart;\n return vec4f(tStart / 50.0, tEnd / 50.0, rayLength / 50.0, 1.0);\n }\n\n \n \n if (DEBUG == 10) {\n let numLights = i32(uniforms.lightCount);\n if (numLights > 0) {\n let light = lights[0];\n let radius = light.geom.x;\n var minDist = 9999.0;\n var hitCount = 0.0;\n let rayLength = tEnd - tStart;\n for (var ti = 0; ti < 20; ti++) {\n let testT = tStart + rayLength * f32(ti) / 20.0;\n let testPos = uniforms.cameraPosition + worldRayDir * testT;\n let dist = length(light.position - testPos);\n minDist = min(minDist, dist);\n if (dist < radius) {\n hitCount += 1.0;\n }\n }\n \n return vec4f(minDist / 50.0, hitCount / 20.0, radius / 50.0, 1.0);\n }\n return vec4f(1.0, 0.0, 0.0, 1.0);\n }\n\n \n if (DEBUG == 3) {\n let samplePos = uniforms.cameraPosition + worldRayDir * min(10.0, tEnd);\n let n = getFogDensity(samplePos, 10.0);\n return vec4f(n, n, n, 1.0);\n }\n\n \n if (DEBUG == 5) {\n let worldPos = uniforms.cameraPosition + worldRayDir * min(5.0, tEnd);\n return vec4f(fract(worldPos * 0.1), 1.0);\n }\n\n \n \n let numSamples = i32(uniforms.maxSamples);\n let fNumSamples = f32(numSamples);\n\n \n let jitter = fract(sin(dot(input.position.xy, vec2f(12.9898, 78.233))) * 43758.5453);\n\n var accumulatedColor = vec3f(0.0);\n var accumulatedMainLight = vec3f(0.0); \n var accumulatedShadow = 0.0; \n var shadowWeight = 0.0; \n var accumulatedAlpha = 0.0;\n\n \n let lightDir = normalize(uniforms.mainLightDir);\n let viewToLight = dot(worldRayDir, lightDir);\n \n let hgPhase = phaseHG(viewToLight, 0.6);\n let phase = max(0.4, hgPhase); \n\n \n\n let rayLength = tEnd - tStart;\n let minStepSize = 0.25; \n let maxStepSize = 2.0; \n let stepSize = clamp(rayLength / fNumSamples, minStepSize, maxStepSize);\n \n\n \n var t = tStart + jitter * stepSize;\n\n \n var debugMaxLight = vec3f(0.0);\n var debugLightHits = 0.0;\n\n for (var i = 0; i < numSamples; i++) {\n \n if (t > tEnd) { break; }\n\n let samplePos = uniforms.cameraPosition + worldRayDir * t;\n\n \n t += stepSize;\n\n \n let density = getFogDensity(samplePos, t);\n \n \n if (density <= 0.0) { continue; }\n\n \n \n var mainLighting = vec3f(0.0);\n var pointLighting = vec3f(0.0);\n\n \n if (uniforms.mainLightIntensity > 0.0) {\n let rawShadow = sampleShadow(samplePos);\n\n \n let weight = density * stepSize;\n accumulatedShadow += rawShadow * weight;\n shadowWeight += weight;\n\n \n mainLighting = uniforms.mainLightColor * uniforms.mainLightIntensity * phase * uniforms.mainLightScatter;\n }\n\n \n let numLights = i32(uniforms.lightCount);\n for (var li = 0; li < numLights; li++) {\n let light = lights[li];\n if (light.enabled == 0u) { continue; }\n\n let contrib = calculateLightContribution(samplePos, light, worldRayDir);\n pointLighting += contrib;\n\n \n if (length(contrib) > 0.001) {\n debugLightHits += 1.0;\n debugMaxLight = max(debugMaxLight, contrib);\n }\n }\n\n \n let pointColor = pointLighting * uniforms.scatterStrength * uniforms.fogColor;\n let mainColor = mainLighting * uniforms.fogColor;\n\n \n \n \n let rawSampleAlpha = density * stepSize;\n let sampleAlpha = min(rawSampleAlpha, 0.03); \n\n \n let colorWeight = rawSampleAlpha * (1.0 - accumulatedAlpha);\n accumulatedColor += pointColor * colorWeight;\n accumulatedMainLight += mainColor * colorWeight;\n accumulatedAlpha += sampleAlpha * (1.0 - accumulatedAlpha);\n }\n\n \n accumulatedAlpha = min(accumulatedAlpha, 1.0);\n\n \n let avgShadow = select(1.0, accumulatedShadow / shadowWeight, shadowWeight > 0.001);\n\n \n \n \n let contrastShadow = smoothstep(0.25, 0.75, avgShadow);\n \n let finalShadow = mix(avgShadow, contrastShadow, 0.88);\n\n \n let sat = uniforms.mainLightSaturation;\n let saturatedShadow = sat * (1.0 - exp(-finalShadow * 3.0 / max(sat, 0.01)));\n\n \n accumulatedColor += accumulatedMainLight * saturatedShadow;\n\n \n \n if (DEBUG == 11) {\n return vec4f(length(debugMaxLight), debugLightHits / 100.0, 0.0, 1.0);\n }\n\n \n \n if (DEBUG == 12) {\n let distMarched = t - tStart;\n let iterCount = distMarched / stepSize;\n return vec4f(distMarched / 50.0, iterCount / fNumSamples, accumulatedAlpha, 1.0);\n }\n\n \n \n if (DEBUG == 13) {\n let numLights = i32(uniforms.lightCount);\n if (numLights > 0) {\n let light = lights[0];\n let radius = light.geom.x;\n var minDistToLight = 9999.0;\n var closestT = 0.0;\n let testRayLen = t - tStart; \n for (var ti = 0; ti < 32; ti++) {\n let testT = tStart + testRayLen * f32(ti) / 32.0;\n let testPos = uniforms.cameraPosition + worldRayDir * testT;\n let distToLight = length(light.position - testPos);\n if (distToLight < minDistToLight) {\n minDistToLight = distToLight;\n closestT = testT;\n }\n }\n \n \n \n return vec4f(minDistToLight / radius, closestT / 50.0, radius / 50.0, 1.0);\n }\n return vec4f(1.0, 0.0, 0.0, 1.0);\n }\n\n \n if (DEBUG == 6) {\n return vec4f(accumulatedColor, 1.0);\n }\n\n return vec4f(accumulatedColor, accumulatedAlpha);\n}";
|
|
11315
|
+
var volumetric_blur_default = "struct Uniforms {\n direction: vec2f,\n texelSize: vec2f,\n radius: f32,\n _pad0: f32,\n _pad1: f32,\n _pad2: f32,\n}\n\nstruct VertexOutput {\n @builtin(position) position: vec4f,\n @location(0) uv: vec2f,\n}\n\n@group(0) @binding(0) var<uniform> uniforms: Uniforms;\n@group(0) @binding(1) var inputTexture: texture_2d<f32>;\n@group(0) @binding(2) var inputSampler: sampler;\n\n@vertex\nfn vertexMain(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {\n var output: VertexOutput;\n\n let x = f32(vertexIndex & 1u) * 4.0 - 1.0;\n let y = f32(vertexIndex >> 1u) * 4.0 - 1.0;\n\n output.position = vec4f(x, y, 0.0, 1.0);\n output.uv = vec2f((x + 1.0) * 0.5, (1.0 - y) * 0.5);\n\n return output;\n}\n\nfn gaussian(x: f32, sigma: f32) -> f32 {\n return exp(-(x * x) / (2.0 * sigma * sigma));\n}\n\n@fragment\nfn fragmentMain(input: VertexOutput) -> @location(0) vec4f {\n let uv = input.uv;\n let radius = uniforms.radius;\n let sigma = radius / 3.0; \n\n \n var color = textureSample(inputTexture, inputSampler, uv);\n var totalWeight = 1.0;\n\n \n let stepSize = 1.5;\n let numSamples = i32(ceil(radius / stepSize));\n\n \n let dir = uniforms.direction * uniforms.texelSize;\n\n \n for (var i = 1; i <= numSamples; i++) {\n let offset = f32(i) * stepSize;\n let weight = gaussian(offset, sigma);\n\n \n let uvPos = uv + dir * offset;\n let samplePos = textureSample(inputTexture, inputSampler, uvPos);\n color += samplePos * weight;\n\n \n let uvNeg = uv - dir * offset;\n let sampleNeg = textureSample(inputTexture, inputSampler, uvNeg);\n color += sampleNeg * weight;\n\n totalWeight += weight * 2.0;\n }\n\n \n color /= totalWeight;\n\n return color;\n}";
|
|
11316
|
+
var volumetric_composite_default = "struct Uniforms {\n canvasSize: vec2f,\n renderSize: vec2f,\n texelSize: vec2f,\n \n brightnessThreshold: f32, \n minVisibility: f32, \n skyBrightness: f32, \n _pad: f32,\n}\n\nstruct VertexOutput {\n @builtin(position) position: vec4f,\n @location(0) uv: vec2f,\n}\n\n@group(0) @binding(0) var<uniform> uniforms: Uniforms;\n@group(0) @binding(1) var sceneTexture: texture_2d<f32>;\n@group(0) @binding(2) var fogTexture: texture_2d<f32>;\n@group(0) @binding(3) var linearSampler: sampler;\n@group(0) @binding(4) var depthTexture: texture_depth_2d;\n\n@vertex\nfn vertexMain(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {\n var output: VertexOutput;\n\n let x = f32(vertexIndex & 1u) * 4.0 - 1.0;\n let y = f32(vertexIndex >> 1u) * 4.0 - 1.0;\n\n output.position = vec4f(x, y, 0.0, 1.0);\n output.uv = vec2f((x + 1.0) * 0.5, (1.0 - y) * 0.5);\n\n return output;\n}\n\n@fragment\nfn fragmentMain(input: VertexOutput) -> @location(0) vec4f {\n let uv = input.uv;\n\n \n let sceneColor = textureSample(sceneTexture, linearSampler, uv).rgb;\n\n \n let fog = textureSample(fogTexture, linearSampler, uv);\n\n \n let depthCoord = vec2i(input.position.xy);\n let depth = textureLoad(depthTexture, depthCoord, 0);\n\n \n var luminance = dot(sceneColor, vec3f(0.299, 0.587, 0.114));\n\n \n \n let isSky = depth > 0.9999;\n if (isSky) {\n luminance = max(luminance, uniforms.skyBrightness);\n }\n\n \n \n let threshold = uniforms.brightnessThreshold;\n let minVis = uniforms.minVisibility;\n\n \n \n \n \n let falloff = 1.0 / (1.0 + luminance / max(threshold, 0.01));\n let fogVisibility = mix(minVis, 1.0, falloff);\n\n \n let finalColor = sceneColor + fog.rgb * fogVisibility;\n\n return vec4f(finalColor, 1.0);\n}";
|
|
11317
|
+
class VolumetricFogPass extends BasePass {
|
|
11016
11318
|
constructor(engine = null) {
|
|
11017
|
-
super("
|
|
11018
|
-
this.
|
|
11319
|
+
super("VolumetricFog", engine);
|
|
11320
|
+
this.raymarchPipeline = null;
|
|
11321
|
+
this.blurHPipeline = null;
|
|
11322
|
+
this.blurVPipeline = null;
|
|
11323
|
+
this.compositePipeline = null;
|
|
11324
|
+
this.raymarchBGL = null;
|
|
11325
|
+
this.blurBGL = null;
|
|
11326
|
+
this.compositeBGL = null;
|
|
11327
|
+
this.raymarchTexture = null;
|
|
11328
|
+
this.blurTempTexture = null;
|
|
11329
|
+
this.blurredTexture = null;
|
|
11330
|
+
this.outputTexture = null;
|
|
11331
|
+
this.raymarchUniformBuffer = null;
|
|
11332
|
+
this.blurHUniformBuffer = null;
|
|
11333
|
+
this.blurVUniformBuffer = null;
|
|
11334
|
+
this.compositeUniformBuffer = null;
|
|
11335
|
+
this.linearSampler = null;
|
|
11019
11336
|
this.inputTexture = null;
|
|
11020
|
-
this.
|
|
11021
|
-
this.
|
|
11022
|
-
this.
|
|
11023
|
-
this.
|
|
11024
|
-
this.
|
|
11025
|
-
this.
|
|
11026
|
-
this.
|
|
11027
|
-
this.
|
|
11337
|
+
this.gbuffer = null;
|
|
11338
|
+
this.shadowPass = null;
|
|
11339
|
+
this.lightingPass = null;
|
|
11340
|
+
this.canvasWidth = 0;
|
|
11341
|
+
this.canvasHeight = 0;
|
|
11342
|
+
this.renderWidth = 0;
|
|
11343
|
+
this.renderHeight = 0;
|
|
11344
|
+
this._currentMainLightScatter = null;
|
|
11345
|
+
this._lastUpdateTime = 0;
|
|
11346
|
+
this._cameraInShadowSmooth = 0;
|
|
11347
|
+
this._skyVisible = true;
|
|
11348
|
+
this._skyCheckPending = false;
|
|
11349
|
+
this._lastSkyCheckTime = 0;
|
|
11028
11350
|
}
|
|
11029
|
-
//
|
|
11030
|
-
get
|
|
11031
|
-
return this.settings?.
|
|
11351
|
+
// Settings getters
|
|
11352
|
+
get volumetricSettings() {
|
|
11353
|
+
return this.settings?.volumetricFog ?? {};
|
|
11032
11354
|
}
|
|
11033
|
-
|
|
11034
|
-
|
|
11035
|
-
return this.settings?.rendering?.fxaa ?? true;
|
|
11355
|
+
get fogSettings() {
|
|
11356
|
+
return this.settings?.fog ?? {};
|
|
11036
11357
|
}
|
|
11037
|
-
|
|
11038
|
-
|
|
11039
|
-
return this.settings?.dithering?.enabled ?? true;
|
|
11358
|
+
get isVolumetricEnabled() {
|
|
11359
|
+
return this.volumetricSettings.enabled ?? false;
|
|
11040
11360
|
}
|
|
11041
|
-
get
|
|
11042
|
-
return this.
|
|
11361
|
+
get resolution() {
|
|
11362
|
+
return this.volumetricSettings.resolution ?? 0.25;
|
|
11043
11363
|
}
|
|
11044
|
-
|
|
11045
|
-
|
|
11046
|
-
return this.settings?.bloom?.enabled ?? true;
|
|
11364
|
+
get maxSamples() {
|
|
11365
|
+
return this.volumetricSettings.maxSamples ?? 32;
|
|
11047
11366
|
}
|
|
11048
|
-
get
|
|
11049
|
-
return this.
|
|
11367
|
+
get blurRadius() {
|
|
11368
|
+
return this.volumetricSettings.blurRadius ?? 4;
|
|
11050
11369
|
}
|
|
11051
|
-
get
|
|
11052
|
-
return this.
|
|
11370
|
+
get fogDensity() {
|
|
11371
|
+
return this.volumetricSettings.density ?? this.volumetricSettings.densityMultiplier ?? 0.5;
|
|
11372
|
+
}
|
|
11373
|
+
get scatterStrength() {
|
|
11374
|
+
return this.volumetricSettings.scatterStrength ?? 1;
|
|
11375
|
+
}
|
|
11376
|
+
get maxDistance() {
|
|
11377
|
+
return this.volumetricSettings.maxDistance ?? 20;
|
|
11378
|
+
}
|
|
11379
|
+
get heightRange() {
|
|
11380
|
+
return this.volumetricSettings.heightRange ?? [-5, 20];
|
|
11381
|
+
}
|
|
11382
|
+
get shadowsEnabled() {
|
|
11383
|
+
return this.volumetricSettings.shadowsEnabled ?? true;
|
|
11384
|
+
}
|
|
11385
|
+
get noiseStrength() {
|
|
11386
|
+
return this.volumetricSettings.noiseStrength ?? 1;
|
|
11387
|
+
}
|
|
11388
|
+
// 0 = uniform fog, 1 = full noise
|
|
11389
|
+
get noiseAnimated() {
|
|
11390
|
+
return this.volumetricSettings.noiseAnimated ?? true;
|
|
11391
|
+
}
|
|
11392
|
+
get noiseScale() {
|
|
11393
|
+
return this.volumetricSettings.noiseScale ?? 0.25;
|
|
11394
|
+
}
|
|
11395
|
+
// Noise frequency (higher = finer detail)
|
|
11396
|
+
get mainLightScatter() {
|
|
11397
|
+
return this.volumetricSettings.mainLightScatter ?? 1;
|
|
11398
|
+
}
|
|
11399
|
+
// Scatter when camera in light
|
|
11400
|
+
get mainLightScatterDark() {
|
|
11401
|
+
return this.volumetricSettings.mainLightScatterDark ?? 3;
|
|
11402
|
+
}
|
|
11403
|
+
// Scatter when camera in shadow
|
|
11404
|
+
get mainLightSaturation() {
|
|
11405
|
+
return this.volumetricSettings.mainLightSaturation ?? 1;
|
|
11406
|
+
}
|
|
11407
|
+
// Max brightness cap
|
|
11408
|
+
// Brightness-based attenuation (fog less visible over bright surfaces)
|
|
11409
|
+
get brightnessThreshold() {
|
|
11410
|
+
return this.volumetricSettings.brightnessThreshold ?? 1;
|
|
11411
|
+
}
|
|
11412
|
+
// Scene luminance where fog starts fading
|
|
11413
|
+
get minVisibility() {
|
|
11414
|
+
return this.volumetricSettings.minVisibility ?? 0.15;
|
|
11415
|
+
}
|
|
11416
|
+
// Minimum fog visibility over very bright surfaces
|
|
11417
|
+
get skyBrightness() {
|
|
11418
|
+
return this.volumetricSettings.skyBrightness ?? 5;
|
|
11419
|
+
}
|
|
11420
|
+
// Virtual brightness for sky (far depth)
|
|
11421
|
+
// Debug mode: 0=normal, 1=depth, 2=ray dir, 3=noise, 4=viewDir.z, 5=worldPos, 6=accum, 7=light dist, 8=light pos
|
|
11422
|
+
get debugMode() {
|
|
11423
|
+
return this.volumetricSettings.debug ?? 0;
|
|
11053
11424
|
}
|
|
11054
|
-
/**
|
|
11055
|
-
* Set the input texture (HDR image from LightingPass)
|
|
11056
|
-
* @param {Texture} texture - Input HDR texture
|
|
11057
|
-
*/
|
|
11058
11425
|
setInputTexture(texture) {
|
|
11059
|
-
|
|
11060
|
-
this.inputTexture = texture;
|
|
11061
|
-
this._needsRebuild = true;
|
|
11062
|
-
}
|
|
11426
|
+
this.inputTexture = texture;
|
|
11063
11427
|
}
|
|
11064
|
-
|
|
11065
|
-
|
|
11066
|
-
* @param {Object} bloomTexture - Bloom texture with mip levels
|
|
11067
|
-
*/
|
|
11068
|
-
setBloomTexture(bloomTexture) {
|
|
11069
|
-
if (this.bloomTexture !== bloomTexture) {
|
|
11070
|
-
this.bloomTexture = bloomTexture;
|
|
11071
|
-
this._needsRebuild = true;
|
|
11072
|
-
}
|
|
11428
|
+
setGBuffer(gbuffer) {
|
|
11429
|
+
this.gbuffer = gbuffer;
|
|
11073
11430
|
}
|
|
11074
|
-
|
|
11075
|
-
|
|
11076
|
-
* @param {Texture} texture - Noise texture (blue noise or bayer dither)
|
|
11077
|
-
* @param {number} size - Texture size
|
|
11078
|
-
* @param {boolean} animated - Whether to animate noise offset each frame
|
|
11079
|
-
*/
|
|
11080
|
-
setNoise(texture, size = 64, animated = true) {
|
|
11081
|
-
this.noiseTexture = texture;
|
|
11082
|
-
this.noiseSize = size;
|
|
11083
|
-
this.noiseAnimated = animated;
|
|
11084
|
-
this._needsRebuild = true;
|
|
11431
|
+
setShadowPass(shadowPass) {
|
|
11432
|
+
this.shadowPass = shadowPass;
|
|
11085
11433
|
}
|
|
11086
|
-
|
|
11087
|
-
|
|
11088
|
-
|
|
11089
|
-
|
|
11090
|
-
|
|
11091
|
-
|
|
11434
|
+
setLightingPass(lightingPass) {
|
|
11435
|
+
this.lightingPass = lightingPass;
|
|
11436
|
+
}
|
|
11437
|
+
getOutputTexture() {
|
|
11438
|
+
return this.outputTexture;
|
|
11439
|
+
}
|
|
11440
|
+
// Unused setters (kept for API compatibility)
|
|
11441
|
+
setHiZPass() {
|
|
11092
11442
|
}
|
|
11093
11443
|
async _init() {
|
|
11094
11444
|
const { device } = this.engine;
|
|
11095
|
-
|
|
11096
|
-
label: "
|
|
11097
|
-
size: [1, 1, 1],
|
|
11098
|
-
format: "rgba16float",
|
|
11099
|
-
mipLevelCount: 1,
|
|
11100
|
-
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST
|
|
11101
|
-
});
|
|
11102
|
-
device.queue.writeTexture(
|
|
11103
|
-
{ texture: dummyTexture },
|
|
11104
|
-
new Float32Array([0, 0, 0, 0]).buffer,
|
|
11105
|
-
{ bytesPerRow: 8 },
|
|
11106
|
-
{ width: 1, height: 1 }
|
|
11107
|
-
);
|
|
11108
|
-
const dummySampler = device.createSampler({
|
|
11109
|
-
label: "Dummy Bloom Sampler",
|
|
11445
|
+
this.linearSampler = device.createSampler({
|
|
11446
|
+
label: "Volumetric Linear Sampler",
|
|
11110
11447
|
minFilter: "linear",
|
|
11111
|
-
magFilter: "linear"
|
|
11448
|
+
magFilter: "linear",
|
|
11449
|
+
addressModeU: "clamp-to-edge",
|
|
11450
|
+
addressModeV: "clamp-to-edge"
|
|
11112
11451
|
});
|
|
11113
|
-
this.
|
|
11114
|
-
|
|
11115
|
-
|
|
11116
|
-
sampler: dummySampler,
|
|
11117
|
-
mipCount: 1
|
|
11118
|
-
};
|
|
11119
|
-
this.guiSampler = device.createSampler({
|
|
11120
|
-
label: "GUI Sampler",
|
|
11121
|
-
minFilter: "linear",
|
|
11122
|
-
magFilter: "linear"
|
|
11452
|
+
this.fallbackShadowSampler = device.createSampler({
|
|
11453
|
+
label: "Volumetric Fallback Shadow Sampler",
|
|
11454
|
+
compare: "less"
|
|
11123
11455
|
});
|
|
11124
|
-
|
|
11125
|
-
label: "
|
|
11456
|
+
this.fallbackCascadeShadowMap = device.createTexture({
|
|
11457
|
+
label: "Volumetric Fallback Cascade Shadow",
|
|
11458
|
+
size: [1, 1, 3],
|
|
11459
|
+
format: "depth32float",
|
|
11460
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT
|
|
11461
|
+
});
|
|
11462
|
+
const identityMatrix = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
|
|
11463
|
+
const matrixData = new Float32Array(16 * 3);
|
|
11464
|
+
for (let i = 0; i < 3; i++) matrixData.set(identityMatrix, i * 16);
|
|
11465
|
+
this.fallbackCascadeMatrices = device.createBuffer({
|
|
11466
|
+
label: "Volumetric Fallback Cascade Matrices",
|
|
11467
|
+
size: 16 * 4 * 3,
|
|
11468
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
11469
|
+
});
|
|
11470
|
+
device.queue.writeBuffer(this.fallbackCascadeMatrices, 0, matrixData);
|
|
11471
|
+
this.fallbackLightsBuffer = device.createBuffer({
|
|
11472
|
+
label: "Volumetric Fallback Lights",
|
|
11473
|
+
size: 768 * 96,
|
|
11474
|
+
// MAX_LIGHTS * 96 bytes per light
|
|
11475
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
11476
|
+
});
|
|
11477
|
+
this.fallbackSpotShadowAtlas = device.createTexture({
|
|
11478
|
+
label: "Volumetric Fallback Spot Shadow Atlas",
|
|
11126
11479
|
size: [1, 1, 1],
|
|
11127
|
-
format: "
|
|
11128
|
-
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.
|
|
11480
|
+
format: "depth32float",
|
|
11481
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT
|
|
11129
11482
|
});
|
|
11130
|
-
|
|
11131
|
-
|
|
11132
|
-
|
|
11133
|
-
|
|
11134
|
-
|
|
11135
|
-
|
|
11136
|
-
|
|
11137
|
-
|
|
11138
|
-
|
|
11139
|
-
sampler: this.guiSampler
|
|
11140
|
-
};
|
|
11483
|
+
const spotMatrixData = new Float32Array(16 * 16);
|
|
11484
|
+
for (let i = 0; i < 16; i++) spotMatrixData.set(identityMatrix, i * 16);
|
|
11485
|
+
this.fallbackSpotMatrices = device.createBuffer({
|
|
11486
|
+
label: "Volumetric Fallback Spot Matrices",
|
|
11487
|
+
size: 16 * 4 * 16,
|
|
11488
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
11489
|
+
});
|
|
11490
|
+
device.queue.writeBuffer(this.fallbackSpotMatrices, 0, spotMatrixData);
|
|
11491
|
+
await this._createResources(this.engine.canvas.width, this.engine.canvas.height);
|
|
11141
11492
|
}
|
|
11142
|
-
|
|
11143
|
-
|
|
11144
|
-
|
|
11145
|
-
|
|
11146
|
-
|
|
11147
|
-
|
|
11148
|
-
|
|
11149
|
-
|
|
11150
|
-
|
|
11151
|
-
|
|
11152
|
-
|
|
11153
|
-
|
|
11154
|
-
|
|
11155
|
-
|
|
11156
|
-
|
|
11157
|
-
|
|
11493
|
+
async _createResources(width, height) {
|
|
11494
|
+
const { device } = this.engine;
|
|
11495
|
+
this.canvasWidth = width;
|
|
11496
|
+
this.canvasHeight = height;
|
|
11497
|
+
this.renderWidth = Math.max(1, Math.floor(width * this.resolution));
|
|
11498
|
+
this.renderHeight = Math.max(1, Math.floor(height * this.resolution));
|
|
11499
|
+
this._destroyTextures();
|
|
11500
|
+
this.raymarchTexture = this._create2DTexture("Raymarch Output", this.renderWidth, this.renderHeight);
|
|
11501
|
+
this.blurTempTexture = this._create2DTexture("Blur Temp", this.renderWidth, this.renderHeight);
|
|
11502
|
+
this.blurredTexture = this._create2DTexture("Blurred Fog", this.renderWidth, this.renderHeight);
|
|
11503
|
+
this.outputTexture = await Texture.renderTarget(this.engine, "rgba16float", width, height);
|
|
11504
|
+
this.raymarchUniformBuffer = this._createUniformBuffer("Raymarch Uniforms", 256);
|
|
11505
|
+
this.blurHUniformBuffer = this._createUniformBuffer("Blur H Uniforms", 32);
|
|
11506
|
+
this.blurVUniformBuffer = this._createUniformBuffer("Blur V Uniforms", 32);
|
|
11507
|
+
this.compositeUniformBuffer = this._createUniformBuffer("Composite Uniforms", 48);
|
|
11508
|
+
await this._createPipelines();
|
|
11509
|
+
}
|
|
11510
|
+
_create2DTexture(label, width, height) {
|
|
11511
|
+
const texture = this.engine.device.createTexture({
|
|
11512
|
+
label,
|
|
11513
|
+
size: [width, height, 1],
|
|
11514
|
+
format: "rgba16float",
|
|
11515
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING
|
|
11516
|
+
});
|
|
11517
|
+
return { texture, view: texture.createView(), width, height };
|
|
11518
|
+
}
|
|
11519
|
+
_createUniformBuffer(label, size) {
|
|
11520
|
+
return this.engine.device.createBuffer({
|
|
11521
|
+
label,
|
|
11522
|
+
size,
|
|
11523
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
11524
|
+
});
|
|
11525
|
+
}
|
|
11526
|
+
async _createPipelines() {
|
|
11527
|
+
const { device } = this.engine;
|
|
11528
|
+
const raymarchModule = device.createShaderModule({
|
|
11529
|
+
label: "Volumetric Raymarch Shader",
|
|
11530
|
+
code: volumetric_raymarch_default
|
|
11531
|
+
});
|
|
11532
|
+
this.raymarchBGL = device.createBindGroupLayout({
|
|
11533
|
+
label: "Volumetric Raymarch BGL",
|
|
11534
|
+
entries: [
|
|
11535
|
+
{ binding: 0, visibility: GPUShaderStage.FRAGMENT | GPUShaderStage.VERTEX, buffer: { type: "uniform" } },
|
|
11536
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "depth" } },
|
|
11537
|
+
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "depth", viewDimension: "2d-array" } },
|
|
11538
|
+
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "comparison" } },
|
|
11539
|
+
{ binding: 4, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "read-only-storage" } },
|
|
11540
|
+
// cascade matrices
|
|
11541
|
+
{ binding: 5, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "read-only-storage" } },
|
|
11542
|
+
// lights
|
|
11543
|
+
{ binding: 6, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "depth" } },
|
|
11544
|
+
// spot shadow atlas
|
|
11545
|
+
{ binding: 7, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "read-only-storage" } }
|
|
11546
|
+
// spot matrices
|
|
11547
|
+
]
|
|
11548
|
+
});
|
|
11549
|
+
this.raymarchPipeline = await device.createRenderPipelineAsync({
|
|
11550
|
+
label: "Volumetric Raymarch Pipeline",
|
|
11551
|
+
layout: device.createPipelineLayout({ bindGroupLayouts: [this.raymarchBGL] }),
|
|
11552
|
+
vertex: { module: raymarchModule, entryPoint: "vertexMain" },
|
|
11553
|
+
fragment: {
|
|
11554
|
+
module: raymarchModule,
|
|
11555
|
+
entryPoint: "fragmentMain",
|
|
11556
|
+
targets: [{ format: "rgba16float" }]
|
|
11557
|
+
},
|
|
11558
|
+
primitive: { topology: "triangle-list" }
|
|
11559
|
+
});
|
|
11560
|
+
const blurModule = device.createShaderModule({
|
|
11561
|
+
label: "Volumetric Blur Shader",
|
|
11562
|
+
code: volumetric_blur_default
|
|
11563
|
+
});
|
|
11564
|
+
this.blurBGL = device.createBindGroupLayout({
|
|
11565
|
+
label: "Volumetric Blur BGL",
|
|
11566
|
+
entries: [
|
|
11567
|
+
{ binding: 0, visibility: GPUShaderStage.FRAGMENT | GPUShaderStage.VERTEX, buffer: { type: "uniform" } },
|
|
11568
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
11569
|
+
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } }
|
|
11570
|
+
]
|
|
11571
|
+
});
|
|
11572
|
+
const blurPipelineDesc = {
|
|
11573
|
+
layout: device.createPipelineLayout({ bindGroupLayouts: [this.blurBGL] }),
|
|
11574
|
+
vertex: { module: blurModule, entryPoint: "vertexMain" },
|
|
11575
|
+
fragment: {
|
|
11576
|
+
module: blurModule,
|
|
11577
|
+
entryPoint: "fragmentMain",
|
|
11578
|
+
targets: [{ format: "rgba16float" }]
|
|
11579
|
+
},
|
|
11580
|
+
primitive: { topology: "triangle-list" }
|
|
11581
|
+
};
|
|
11582
|
+
this.blurHPipeline = await device.createRenderPipelineAsync({ ...blurPipelineDesc, label: "Volumetric Blur H" });
|
|
11583
|
+
this.blurVPipeline = await device.createRenderPipelineAsync({ ...blurPipelineDesc, label: "Volumetric Blur V" });
|
|
11584
|
+
const compositeModule = device.createShaderModule({
|
|
11585
|
+
label: "Volumetric Composite Shader",
|
|
11586
|
+
code: volumetric_composite_default
|
|
11587
|
+
});
|
|
11588
|
+
this.compositeBGL = device.createBindGroupLayout({
|
|
11589
|
+
label: "Volumetric Composite BGL",
|
|
11590
|
+
entries: [
|
|
11591
|
+
{ binding: 0, visibility: GPUShaderStage.FRAGMENT | GPUShaderStage.VERTEX, buffer: { type: "uniform" } },
|
|
11592
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
11593
|
+
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
11594
|
+
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } },
|
|
11595
|
+
{ binding: 4, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "depth" } }
|
|
11596
|
+
]
|
|
11597
|
+
});
|
|
11598
|
+
this.compositePipeline = await device.createRenderPipelineAsync({
|
|
11599
|
+
label: "Volumetric Composite Pipeline",
|
|
11600
|
+
layout: device.createPipelineLayout({ bindGroupLayouts: [this.compositeBGL] }),
|
|
11601
|
+
vertex: { module: compositeModule, entryPoint: "vertexMain" },
|
|
11602
|
+
fragment: {
|
|
11603
|
+
module: compositeModule,
|
|
11604
|
+
entryPoint: "fragmentMain",
|
|
11605
|
+
targets: [{ format: "rgba16float" }]
|
|
11606
|
+
},
|
|
11607
|
+
primitive: { topology: "triangle-list" }
|
|
11608
|
+
});
|
|
11609
|
+
}
|
|
11610
|
+
async _execute(context) {
|
|
11611
|
+
if (!this.isVolumetricEnabled) return;
|
|
11612
|
+
if (!this.inputTexture || !this.gbuffer) return;
|
|
11613
|
+
const { device } = this.engine;
|
|
11614
|
+
const { camera, mainLight, lights } = context;
|
|
11615
|
+
const time = performance.now() / 1e3;
|
|
11616
|
+
this._updateAdaptiveScatter(camera, mainLight, time);
|
|
11617
|
+
const lightCount = lights?.length ?? this.lightingPass?.lightCount ?? 0;
|
|
11618
|
+
const commandEncoder = device.createCommandEncoder({ label: "Volumetric Fog Pass" });
|
|
11619
|
+
this._updateRaymarchUniforms(camera, mainLight, time, lightCount);
|
|
11620
|
+
const raymarchBindGroup = this._createRaymarchBindGroup();
|
|
11621
|
+
if (raymarchBindGroup) {
|
|
11622
|
+
const raymarchPass = commandEncoder.beginRenderPass({
|
|
11623
|
+
label: "Volumetric Raymarch",
|
|
11624
|
+
colorAttachments: [{
|
|
11625
|
+
view: this.raymarchTexture.view,
|
|
11626
|
+
loadOp: "clear",
|
|
11627
|
+
storeOp: "store",
|
|
11628
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 }
|
|
11629
|
+
}]
|
|
11630
|
+
});
|
|
11631
|
+
raymarchPass.setPipeline(this.raymarchPipeline);
|
|
11632
|
+
raymarchPass.setBindGroup(0, raymarchBindGroup);
|
|
11633
|
+
raymarchPass.draw(3);
|
|
11634
|
+
raymarchPass.end();
|
|
11635
|
+
}
|
|
11636
|
+
this._updateBlurUniforms(this.blurHUniformBuffer, 1, 0);
|
|
11637
|
+
this._updateBlurUniforms(this.blurVUniformBuffer, 0, 1);
|
|
11638
|
+
const blurHBindGroup = this._createBlurBindGroup(this.raymarchTexture, this.blurHUniformBuffer);
|
|
11639
|
+
const blurHPass = commandEncoder.beginRenderPass({
|
|
11640
|
+
label: "Volumetric Blur H",
|
|
11641
|
+
colorAttachments: [{
|
|
11642
|
+
view: this.blurTempTexture.view,
|
|
11643
|
+
loadOp: "clear",
|
|
11644
|
+
storeOp: "store",
|
|
11645
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 }
|
|
11646
|
+
}]
|
|
11647
|
+
});
|
|
11648
|
+
blurHPass.setPipeline(this.blurHPipeline);
|
|
11649
|
+
blurHPass.setBindGroup(0, blurHBindGroup);
|
|
11650
|
+
blurHPass.draw(3);
|
|
11651
|
+
blurHPass.end();
|
|
11652
|
+
const blurVBindGroup = this._createBlurBindGroup(this.blurTempTexture, this.blurVUniformBuffer);
|
|
11653
|
+
const blurVPass = commandEncoder.beginRenderPass({
|
|
11654
|
+
label: "Volumetric Blur V",
|
|
11655
|
+
colorAttachments: [{
|
|
11656
|
+
view: this.blurredTexture.view,
|
|
11657
|
+
loadOp: "clear",
|
|
11658
|
+
storeOp: "store",
|
|
11659
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 }
|
|
11660
|
+
}]
|
|
11661
|
+
});
|
|
11662
|
+
blurVPass.setPipeline(this.blurVPipeline);
|
|
11663
|
+
blurVPass.setBindGroup(0, blurVBindGroup);
|
|
11664
|
+
blurVPass.draw(3);
|
|
11665
|
+
blurVPass.end();
|
|
11666
|
+
this._updateCompositeUniforms();
|
|
11667
|
+
const compositeBindGroup = this._createCompositeBindGroup();
|
|
11668
|
+
if (compositeBindGroup) {
|
|
11669
|
+
const compositePass = commandEncoder.beginRenderPass({
|
|
11670
|
+
label: "Volumetric Composite",
|
|
11671
|
+
colorAttachments: [{
|
|
11672
|
+
view: this.outputTexture.view,
|
|
11673
|
+
loadOp: "clear",
|
|
11674
|
+
storeOp: "store",
|
|
11675
|
+
clearValue: { r: 0, g: 0, b: 0, a: 1 }
|
|
11676
|
+
}]
|
|
11677
|
+
});
|
|
11678
|
+
compositePass.setPipeline(this.compositePipeline);
|
|
11679
|
+
compositePass.setBindGroup(0, compositeBindGroup);
|
|
11680
|
+
compositePass.draw(3);
|
|
11681
|
+
compositePass.end();
|
|
11682
|
+
}
|
|
11683
|
+
device.queue.submit([commandEncoder.finish()]);
|
|
11684
|
+
}
|
|
11685
|
+
/**
|
|
11686
|
+
* Update adaptive main light scatter based on camera shadow state and sky visibility
|
|
11687
|
+
* Smoothly transitions between light/dark scatter values
|
|
11688
|
+
* Only uses dark scatter if camera is in shadow AND there's something overhead (no sky)
|
|
11689
|
+
*/
|
|
11690
|
+
_updateAdaptiveScatter(camera, mainLight, currentTime) {
|
|
11691
|
+
if (this._currentMainLightScatter === null) {
|
|
11692
|
+
this._currentMainLightScatter = this.mainLightScatter;
|
|
11693
|
+
}
|
|
11694
|
+
const deltaTime = this._lastUpdateTime > 0 ? currentTime - this._lastUpdateTime : 0.016;
|
|
11695
|
+
this._lastUpdateTime = currentTime;
|
|
11696
|
+
let cameraInShadow = false;
|
|
11697
|
+
if (this.shadowPass && this.shadowsEnabled && mainLight?.enabled !== false) {
|
|
11698
|
+
const cameraPos = camera.position || [0, 0, 0];
|
|
11699
|
+
cameraInShadow = this._isCameraInShadow(cameraPos);
|
|
11700
|
+
if (!this._skyCheckPending && currentTime - this._lastSkyCheckTime > 0.5) {
|
|
11701
|
+
this._checkSkyVisibility(cameraPos);
|
|
11702
|
+
}
|
|
11703
|
+
}
|
|
11704
|
+
const inDarkArea = cameraInShadow && !this._skyVisible;
|
|
11705
|
+
const targetShadowState = inDarkArea ? 1 : 0;
|
|
11706
|
+
const transitionSpeed = inDarkArea ? 1 / 5 : 1 / 1;
|
|
11707
|
+
const t = 1 - Math.exp(-transitionSpeed * deltaTime * 3);
|
|
11708
|
+
this._cameraInShadowSmooth += (targetShadowState - this._cameraInShadowSmooth) * t;
|
|
11709
|
+
this._cameraInShadowSmooth = Math.max(0, Math.min(1, this._cameraInShadowSmooth));
|
|
11710
|
+
const lightScatter = this.mainLightScatter;
|
|
11711
|
+
const darkScatter = this.mainLightScatterDark;
|
|
11712
|
+
this._currentMainLightScatter = lightScatter + (darkScatter - lightScatter) * this._cameraInShadowSmooth;
|
|
11713
|
+
}
|
|
11714
|
+
/**
|
|
11715
|
+
* Check if sky is visible above the camera using raycaster
|
|
11716
|
+
* This is async and updates _skyVisible when complete
|
|
11717
|
+
*/
|
|
11718
|
+
_checkSkyVisibility(cameraPos) {
|
|
11719
|
+
const raycaster = this.engine?.raycaster;
|
|
11720
|
+
if (!raycaster) return;
|
|
11721
|
+
this._skyCheckPending = true;
|
|
11722
|
+
this._lastSkyCheckTime = this._lastUpdateTime;
|
|
11723
|
+
const skyCheckDistance = this.volumetricSettings.skyCheckDistance ?? 100;
|
|
11724
|
+
const debugSkyCheck = this.volumetricSettings.debugSkyCheck ?? false;
|
|
11725
|
+
raycaster.cast(
|
|
11726
|
+
cameraPos,
|
|
11727
|
+
[0, 1, 0],
|
|
11728
|
+
// Straight up
|
|
11729
|
+
skyCheckDistance,
|
|
11730
|
+
(result) => {
|
|
11731
|
+
this._skyVisible;
|
|
11732
|
+
this._skyVisible = !result.hit;
|
|
11733
|
+
this._skyCheckPending = false;
|
|
11734
|
+
if (debugSkyCheck) {
|
|
11735
|
+
const pos = cameraPos.map((v) => v.toFixed(1)).join(", ");
|
|
11736
|
+
if (result.hit) {
|
|
11737
|
+
console.log(`Sky check from [${pos}]: HIT ${result.meshName || result.candidateId} at dist=${result.distance?.toFixed(1)}`);
|
|
11738
|
+
} else {
|
|
11739
|
+
console.log(`Sky check from [${pos}]: NO HIT (sky visible)`);
|
|
11740
|
+
}
|
|
11741
|
+
}
|
|
11742
|
+
},
|
|
11743
|
+
{ backfaces: true, debug: debugSkyCheck }
|
|
11744
|
+
// Need backfaces to hit ceilings from below
|
|
11745
|
+
);
|
|
11746
|
+
}
|
|
11747
|
+
/**
|
|
11748
|
+
* Check if camera position is in shadow
|
|
11749
|
+
* Uses the shadow pass's isCameraInShadow method if available,
|
|
11750
|
+
* otherwise falls back to checking fog height bounds
|
|
11751
|
+
*/
|
|
11752
|
+
_isCameraInShadow(cameraPos) {
|
|
11753
|
+
if (typeof this.shadowPass?.isCameraInShadow === "function") {
|
|
11754
|
+
return this.shadowPass.isCameraInShadow(cameraPos);
|
|
11755
|
+
}
|
|
11756
|
+
const heightRange = this.heightRange;
|
|
11757
|
+
const fogBottom = heightRange[0];
|
|
11758
|
+
const fogTop = heightRange[1];
|
|
11759
|
+
const lowerThird = fogBottom + (fogTop - fogBottom) * 0.33;
|
|
11760
|
+
if (cameraPos[1] < lowerThird) {
|
|
11761
|
+
return true;
|
|
11762
|
+
}
|
|
11763
|
+
const mainLightDir = this.engine?.settings?.mainLight?.direction;
|
|
11764
|
+
if (mainLightDir) {
|
|
11765
|
+
const sunAngle = mainLightDir[1];
|
|
11766
|
+
if (sunAngle > 0.7) {
|
|
11767
|
+
return true;
|
|
11768
|
+
}
|
|
11769
|
+
}
|
|
11770
|
+
return false;
|
|
11771
|
+
}
|
|
11772
|
+
_updateRaymarchUniforms(camera, mainLight, time, lightCount) {
|
|
11773
|
+
const { device } = this.engine;
|
|
11774
|
+
if (!camera.iProj || !camera.iView) {
|
|
11775
|
+
console.warn("VolumetricFogPass: Camera missing iProj or iView matrices");
|
|
11776
|
+
return;
|
|
11777
|
+
}
|
|
11778
|
+
const invProj = camera.iProj;
|
|
11779
|
+
const invView = camera.iView;
|
|
11780
|
+
const cameraPos = camera.position || [0, 0, 0];
|
|
11781
|
+
const mainLightEnabled = mainLight?.enabled !== false;
|
|
11782
|
+
const mainLightDir = mainLight?.direction ?? [-1, 1, -0.5];
|
|
11783
|
+
const mainLightColor = mainLight?.color ?? [1, 0.95, 0.9];
|
|
11784
|
+
const mainLightIntensity = mainLightEnabled ? mainLight?.intensity ?? 1 : 0;
|
|
11785
|
+
const fogColor = this.fogSettings.color ?? [0.8, 0.85, 0.9];
|
|
11786
|
+
const heightFade = this.heightRange;
|
|
11787
|
+
const shadowsEnabled = this.shadowsEnabled && this.shadowPass != null;
|
|
11788
|
+
const data = new Float32Array(64);
|
|
11789
|
+
let offset = 0;
|
|
11790
|
+
data.set(invProj, offset);
|
|
11791
|
+
offset += 16;
|
|
11792
|
+
data.set(invView, offset);
|
|
11793
|
+
offset += 16;
|
|
11794
|
+
data[offset++] = cameraPos[0];
|
|
11795
|
+
data[offset++] = cameraPos[1];
|
|
11796
|
+
data[offset++] = cameraPos[2];
|
|
11797
|
+
data[offset++] = camera.near ?? 0.1;
|
|
11798
|
+
data[offset++] = camera.far ?? 1e3;
|
|
11799
|
+
data[offset++] = this.maxSamples;
|
|
11800
|
+
data[offset++] = time;
|
|
11801
|
+
data[offset++] = this.fogDensity;
|
|
11802
|
+
data[offset++] = fogColor[0];
|
|
11803
|
+
data[offset++] = fogColor[1];
|
|
11804
|
+
data[offset++] = fogColor[2];
|
|
11805
|
+
data[offset++] = shadowsEnabled ? 1 : 0;
|
|
11806
|
+
data[offset++] = mainLightDir[0];
|
|
11807
|
+
data[offset++] = mainLightDir[1];
|
|
11808
|
+
data[offset++] = mainLightDir[2];
|
|
11809
|
+
data[offset++] = mainLightIntensity;
|
|
11810
|
+
data[offset++] = mainLightColor[0];
|
|
11811
|
+
data[offset++] = mainLightColor[1];
|
|
11812
|
+
data[offset++] = mainLightColor[2];
|
|
11813
|
+
data[offset++] = this.scatterStrength;
|
|
11814
|
+
data[offset++] = heightFade[0];
|
|
11815
|
+
data[offset++] = heightFade[1];
|
|
11816
|
+
data[offset++] = this.maxDistance;
|
|
11817
|
+
data[offset++] = lightCount;
|
|
11818
|
+
data[offset++] = this.debugMode;
|
|
11819
|
+
data[offset++] = this.noiseStrength;
|
|
11820
|
+
data[offset++] = this.noiseAnimated ? 1 : 0;
|
|
11821
|
+
data[offset++] = this._currentMainLightScatter;
|
|
11822
|
+
data[offset++] = this.noiseScale;
|
|
11823
|
+
data[offset++] = this.mainLightSaturation;
|
|
11824
|
+
device.queue.writeBuffer(this.raymarchUniformBuffer, 0, data);
|
|
11825
|
+
}
|
|
11826
|
+
_updateBlurUniforms(buffer, dirX, dirY) {
|
|
11827
|
+
const data = new Float32Array([
|
|
11828
|
+
dirX,
|
|
11829
|
+
dirY,
|
|
11830
|
+
1 / this.renderWidth,
|
|
11831
|
+
1 / this.renderHeight,
|
|
11832
|
+
this.blurRadius,
|
|
11833
|
+
0,
|
|
11834
|
+
0,
|
|
11835
|
+
0
|
|
11836
|
+
]);
|
|
11837
|
+
this.engine.device.queue.writeBuffer(buffer, 0, data);
|
|
11838
|
+
}
|
|
11839
|
+
_updateCompositeUniforms() {
|
|
11840
|
+
const data = new Float32Array([
|
|
11841
|
+
this.canvasWidth,
|
|
11842
|
+
this.canvasHeight,
|
|
11843
|
+
this.renderWidth,
|
|
11844
|
+
this.renderHeight,
|
|
11845
|
+
1 / this.canvasWidth,
|
|
11846
|
+
1 / this.canvasHeight,
|
|
11847
|
+
this.brightnessThreshold,
|
|
11848
|
+
this.minVisibility,
|
|
11849
|
+
this.skyBrightness,
|
|
11850
|
+
0,
|
|
11851
|
+
// skyBrightness + padding
|
|
11852
|
+
0,
|
|
11853
|
+
0
|
|
11854
|
+
// extra padding to 48 bytes
|
|
11855
|
+
]);
|
|
11856
|
+
this.engine.device.queue.writeBuffer(this.compositeUniformBuffer, 0, data);
|
|
11857
|
+
}
|
|
11858
|
+
_createRaymarchBindGroup() {
|
|
11859
|
+
const { device } = this.engine;
|
|
11860
|
+
const depthTexture = this.gbuffer?.depth;
|
|
11861
|
+
if (!depthTexture) return null;
|
|
11862
|
+
const cascadeShadows = this.shadowPass?.getShadowMap?.() ?? this.fallbackCascadeShadowMap;
|
|
11863
|
+
const cascadeMatrices = this.shadowPass?.getCascadeMatricesBuffer?.() ?? this.fallbackCascadeMatrices;
|
|
11864
|
+
const shadowSampler = this.shadowPass?.getShadowSampler?.() ?? this.fallbackShadowSampler;
|
|
11865
|
+
const lightsBuffer = this.lightingPass?.getLightBuffer?.() ?? this.fallbackLightsBuffer;
|
|
11866
|
+
const spotShadowAtlas = this.shadowPass?.getSpotShadowAtlasView?.() ?? this.fallbackSpotShadowAtlas.createView();
|
|
11867
|
+
const spotMatrices = this.shadowPass?.getSpotMatricesBuffer?.() ?? this.fallbackSpotMatrices;
|
|
11868
|
+
return device.createBindGroup({
|
|
11869
|
+
label: "Volumetric Raymarch Bind Group",
|
|
11870
|
+
layout: this.raymarchBGL,
|
|
11871
|
+
entries: [
|
|
11872
|
+
{ binding: 0, resource: { buffer: this.raymarchUniformBuffer } },
|
|
11873
|
+
{ binding: 1, resource: depthTexture.texture.createView({ aspect: "depth-only" }) },
|
|
11874
|
+
{ binding: 2, resource: cascadeShadows.createView({ dimension: "2d-array", aspect: "depth-only" }) },
|
|
11875
|
+
{ binding: 3, resource: shadowSampler },
|
|
11876
|
+
{ binding: 4, resource: { buffer: cascadeMatrices } },
|
|
11877
|
+
{ binding: 5, resource: { buffer: lightsBuffer } },
|
|
11878
|
+
{ binding: 6, resource: spotShadowAtlas },
|
|
11879
|
+
{ binding: 7, resource: { buffer: spotMatrices } }
|
|
11880
|
+
]
|
|
11881
|
+
});
|
|
11882
|
+
}
|
|
11883
|
+
_createBlurBindGroup(inputTexture, uniformBuffer) {
|
|
11884
|
+
return this.engine.device.createBindGroup({
|
|
11885
|
+
label: "Volumetric Blur Bind Group",
|
|
11886
|
+
layout: this.blurBGL,
|
|
11887
|
+
entries: [
|
|
11888
|
+
{ binding: 0, resource: { buffer: uniformBuffer } },
|
|
11889
|
+
{ binding: 1, resource: inputTexture.view },
|
|
11890
|
+
{ binding: 2, resource: this.linearSampler }
|
|
11891
|
+
]
|
|
11892
|
+
});
|
|
11893
|
+
}
|
|
11894
|
+
_createCompositeBindGroup() {
|
|
11895
|
+
if (!this.inputTexture) return null;
|
|
11896
|
+
const depthTexture = this.gbuffer?.depth;
|
|
11897
|
+
if (!depthTexture) return null;
|
|
11898
|
+
return this.engine.device.createBindGroup({
|
|
11899
|
+
label: "Volumetric Composite Bind Group",
|
|
11900
|
+
layout: this.compositeBGL,
|
|
11901
|
+
entries: [
|
|
11902
|
+
{ binding: 0, resource: { buffer: this.compositeUniformBuffer } },
|
|
11903
|
+
{ binding: 1, resource: this.inputTexture.view },
|
|
11904
|
+
{ binding: 2, resource: this.blurredTexture.view },
|
|
11905
|
+
{ binding: 3, resource: this.linearSampler },
|
|
11906
|
+
{ binding: 4, resource: depthTexture.texture.createView({ aspect: "depth-only" }) }
|
|
11907
|
+
]
|
|
11908
|
+
});
|
|
11909
|
+
}
|
|
11910
|
+
_destroyTextures() {
|
|
11911
|
+
const textures = [this.raymarchTexture, this.blurTempTexture, this.blurredTexture, this.outputTexture];
|
|
11912
|
+
for (const tex of textures) {
|
|
11913
|
+
if (tex?.texture) tex.texture.destroy();
|
|
11914
|
+
}
|
|
11915
|
+
this.raymarchTexture = null;
|
|
11916
|
+
this.blurTempTexture = null;
|
|
11917
|
+
this.blurredTexture = null;
|
|
11918
|
+
this.outputTexture = null;
|
|
11919
|
+
}
|
|
11920
|
+
async _resize(width, height) {
|
|
11921
|
+
await this._createResources(width, height);
|
|
11922
|
+
}
|
|
11923
|
+
_destroy() {
|
|
11924
|
+
this._destroyTextures();
|
|
11925
|
+
const buffers = [this.raymarchUniformBuffer, this.blurHUniformBuffer, this.blurVUniformBuffer, this.compositeUniformBuffer];
|
|
11926
|
+
for (const buf of buffers) {
|
|
11927
|
+
if (buf) buf.destroy();
|
|
11928
|
+
}
|
|
11929
|
+
this.raymarchPipeline = null;
|
|
11930
|
+
this.blurHPipeline = null;
|
|
11931
|
+
this.blurVPipeline = null;
|
|
11932
|
+
this.compositePipeline = null;
|
|
11933
|
+
}
|
|
11934
|
+
}
|
|
11935
|
+
var postproc_default = "struct VertexOutput {\n @builtin(position) position : vec4<f32>,\n @location(0) uv : vec2<f32>,\n}\n\nstruct Uniforms {\n canvasSize: vec2f,\n noiseParams: vec4f, \n ditherParams: vec4f, \n bloomParams: vec4f, \n}\n\n@group(0) @binding(0) var<uniform> uniforms: Uniforms;\n@group(0) @binding(1) var inputTexture : texture_2d<f32>;\n@group(0) @binding(2) var inputSampler : sampler;\n@group(0) @binding(3) var noiseTexture : texture_2d<f32>;\n@group(0) @binding(4) var noiseSampler : sampler;\n@group(0) @binding(5) var bloomTexture : texture_2d<f32>;\n@group(0) @binding(6) var bloomSampler : sampler;\n@group(0) @binding(7) var guiTexture : texture_2d<f32>;\n@group(0) @binding(8) var guiSampler : sampler;\n\nconst FXAA_EDGE_THRESHOLD: f32 = 0.125; \nconst FXAA_EDGE_THRESHOLD_MIN: f32 = 0.0156; \nconst FXAA_SUBPIX_QUALITY: f32 = 1.0; \n\nfn sampleNoise(screenPos: vec2f) -> f32 {\n let noiseSize = i32(uniforms.noiseParams.x);\n let noiseOffsetX = i32(uniforms.noiseParams.y * f32(noiseSize));\n let noiseOffsetY = i32(uniforms.noiseParams.z * f32(noiseSize));\n\n let texCoord = vec2i(\n (i32(screenPos.x) + noiseOffsetX) % noiseSize,\n (i32(screenPos.y) + noiseOffsetY) % noiseSize\n );\n return textureLoad(noiseTexture, texCoord, 0).r;\n}\n\nfn rgb2luma(rgb: vec3f) -> f32 {\n return dot(rgb, vec3f(0.299, 0.587, 0.114));\n}\n\nfn loadPixel(coord: vec2i) -> vec3f {\n let size = vec2i(textureDimensions(inputTexture, 0));\n let clampedCoord = clamp(coord, vec2i(0), size - vec2i(1));\n return textureLoad(inputTexture, clampedCoord, 0).rgb;\n}\n\nfn sampleBilinear(uv: vec2f) -> vec3f {\n let texSize = vec2f(textureDimensions(inputTexture, 0));\n let texelPos = uv * texSize - 0.5;\n let baseCoord = vec2i(floor(texelPos));\n let frac = fract(texelPos);\n\n let c00 = loadPixel(baseCoord);\n let c10 = loadPixel(baseCoord + vec2i(1, 0));\n let c01 = loadPixel(baseCoord + vec2i(0, 1));\n let c11 = loadPixel(baseCoord + vec2i(1, 1));\n\n let c0 = mix(c00, c10, frac.x);\n let c1 = mix(c01, c11, frac.x);\n return mix(c0, c1, frac.y);\n}\n\nfn fxaa(uv: vec2f) -> vec3f {\n let texSize = vec2f(textureDimensions(inputTexture, 0));\n let pixelCoord = vec2i(uv * texSize);\n\n \n let rgbM = loadPixel(pixelCoord);\n let rgbN = loadPixel(pixelCoord + vec2i(0, -1));\n let rgbS = loadPixel(pixelCoord + vec2i(0, 1));\n let rgbW = loadPixel(pixelCoord + vec2i(-1, 0));\n let rgbE = loadPixel(pixelCoord + vec2i(1, 0));\n let rgbNW = loadPixel(pixelCoord + vec2i(-1, -1));\n let rgbNE = loadPixel(pixelCoord + vec2i(1, -1));\n let rgbSW = loadPixel(pixelCoord + vec2i(-1, 1));\n let rgbSE = loadPixel(pixelCoord + vec2i(1, 1));\n\n \n let lumaM = rgb2luma(rgbM);\n let lumaN = rgb2luma(rgbN);\n let lumaS = rgb2luma(rgbS);\n let lumaW = rgb2luma(rgbW);\n let lumaE = rgb2luma(rgbE);\n let lumaNW = rgb2luma(rgbNW);\n let lumaNE = rgb2luma(rgbNE);\n let lumaSW = rgb2luma(rgbSW);\n let lumaSE = rgb2luma(rgbSE);\n\n \n let lumaMin = min(lumaM, min(min(lumaN, lumaS), min(lumaW, lumaE)));\n let lumaMax = max(lumaM, max(max(lumaN, lumaS), max(lumaW, lumaE)));\n let lumaRange = lumaMax - lumaMin;\n\n \n let isEdge = lumaRange >= max(FXAA_EDGE_THRESHOLD_MIN, lumaMax * FXAA_EDGE_THRESHOLD);\n\n \n let lumaL = (lumaN + lumaS + lumaW + lumaE) * 0.25;\n let rangeL = abs(lumaL - lumaM);\n var blendL = max(0.0, (rangeL / max(lumaRange, 0.0001)) - 0.25) * (1.0 / 0.75);\n blendL = min(1.0, blendL) * blendL * FXAA_SUBPIX_QUALITY;\n\n \n let edgeHorz = abs((lumaNW + lumaNE) - 2.0 * lumaN) +\n 2.0 * abs((lumaW + lumaE) - 2.0 * lumaM) +\n abs((lumaSW + lumaSE) - 2.0 * lumaS);\n let edgeVert = abs((lumaNW + lumaSW) - 2.0 * lumaW) +\n 2.0 * abs((lumaN + lumaS) - 2.0 * lumaM) +\n abs((lumaNE + lumaSE) - 2.0 * lumaE);\n let isHorizontal = edgeHorz >= edgeVert;\n\n \n let luma1 = select(lumaE, lumaS, isHorizontal);\n let luma2 = select(lumaW, lumaN, isHorizontal);\n let gradient1 = abs(luma1 - lumaM);\n let gradient2 = abs(luma2 - lumaM);\n let is1Steepest = gradient1 >= gradient2;\n let gradientScaled = 0.25 * max(gradient1, gradient2);\n\n \n let stepSign = select(-1.0, 1.0, is1Steepest);\n let lumaLocalAverage = 0.5 * (select(luma2, luma1, is1Steepest) + lumaM);\n\n \n let searchDir = select(vec2i(0, 1), vec2i(1, 0), isHorizontal);\n\n let luma1_1 = rgb2luma(loadPixel(pixelCoord - searchDir)) - lumaLocalAverage;\n let luma2_1 = rgb2luma(loadPixel(pixelCoord + searchDir)) - lumaLocalAverage;\n let luma1_2 = rgb2luma(loadPixel(pixelCoord - searchDir * 2)) - lumaLocalAverage;\n let luma2_2 = rgb2luma(loadPixel(pixelCoord + searchDir * 2)) - lumaLocalAverage;\n let luma1_3 = rgb2luma(loadPixel(pixelCoord - searchDir * 3)) - lumaLocalAverage;\n let luma2_3 = rgb2luma(loadPixel(pixelCoord + searchDir * 3)) - lumaLocalAverage;\n let luma1_4 = rgb2luma(loadPixel(pixelCoord - searchDir * 4)) - lumaLocalAverage;\n let luma2_4 = rgb2luma(loadPixel(pixelCoord + searchDir * 4)) - lumaLocalAverage;\n\n \n let reached1_1 = abs(luma1_1) >= gradientScaled;\n let reached1_2 = abs(luma1_2) >= gradientScaled;\n let reached1_3 = abs(luma1_3) >= gradientScaled;\n let reached2_1 = abs(luma2_1) >= gradientScaled;\n let reached2_2 = abs(luma2_2) >= gradientScaled;\n let reached2_3 = abs(luma2_3) >= gradientScaled;\n\n \n let dist1 = select(select(select(4.0, 3.0, reached1_3), 2.0, reached1_2), 1.0, reached1_1);\n let dist2 = select(select(select(4.0, 3.0, reached2_3), 2.0, reached2_2), 1.0, reached2_1);\n\n \n let lumaEnd1 = select(select(select(luma1_4, luma1_3, reached1_3), luma1_2, reached1_2), luma1_1, reached1_1);\n let lumaEnd2 = select(select(select(luma2_4, luma2_3, reached2_3), luma2_2, reached2_2), luma2_1, reached2_1);\n\n \n let distFinal = min(dist1, dist2);\n let edgeThickness = dist1 + dist2;\n let lumaEndCloser = select(lumaEnd2, lumaEnd1, dist1 < dist2);\n let correctVariation = (lumaEndCloser < 0.0) != (lumaM < lumaLocalAverage);\n var pixelOffset = select(0.0, -distFinal / max(edgeThickness, 0.0001) + 0.5, correctVariation);\n\n \n let finalOffset = max(max(pixelOffset, blendL), 0.5);\n\n \n let inverseVP = 1.0 / texSize;\n var finalUv = uv;\n let offsetAmount = finalOffset * stepSign;\n finalUv.x += select(offsetAmount * inverseVP.x, 0.0, isHorizontal);\n finalUv.y += select(0.0, offsetAmount * inverseVP.y, isHorizontal);\n\n \n let offsetColor = sampleBilinear(finalUv);\n\n \n \n let perpDir = select(vec2i(0, 1), vec2i(1, 0), isHorizontal);\n let perpColor1 = loadPixel(pixelCoord + perpDir);\n let perpColor2 = loadPixel(pixelCoord - perpDir);\n let neighborAvg = (perpColor1 + perpColor2) * 0.5;\n\n \n let fxaaColor = mix(mix(offsetColor, neighborAvg, 0.7), rgbM, 0.1);\n\n \n return select(rgbM, fxaaColor, isEdge);\n}\n\nfn sampleBloom(uv: vec2f, sceneBrightness: f32) -> vec3f {\n let bloom = textureSample(bloomTexture, bloomSampler, uv).rgb;\n\n \n \n let threshold = 0.5; \n let mask = saturate(1.0 - (sceneBrightness - threshold) / (1.0 - threshold));\n\n return bloom * mask * mask; \n}\n\nfn aces_tone_map(hdr: vec3<f32>) -> vec3<f32> {\n let m1 = mat3x3(\n 0.59719, 0.07600, 0.02840,\n 0.35458, 0.90834, 0.13383,\n 0.04823, 0.01566, 0.83777,\n );\n let m2 = mat3x3(\n 1.60475, -0.10208, -0.00327,\n -0.53108, 1.10813, -0.07276,\n -0.07367, -0.00605, 1.07602,\n );\n let v = m1 * hdr;\n let a = v * (v + 0.0245786) - 0.000090537;\n let b = v * (0.983729 * v + 0.4329510) + 0.238081;\n return clamp(m2 * (a / b), vec3(0.0), vec3(1.0));\n}\n\nfn reinhard_tone_map(hdr: vec3<f32>) -> vec3<f32> {\n return hdr / (hdr + vec3(1.0));\n}\n\nfn linear_tone_map(hdr: vec3<f32>) -> vec3<f32> {\n return clamp(hdr, vec3(0.0), vec3(1.0));\n}\n\nfn apply_tone_map(hdr: vec3<f32>, mode: i32) -> vec3<f32> {\n if (mode == 1) {\n return reinhard_tone_map(hdr);\n } else if (mode == 2) {\n return linear_tone_map(hdr);\n }\n return aces_tone_map(hdr); \n}\n\n@vertex\nfn vertexMain(@builtin(vertex_index) vertexIndex : u32) -> VertexOutput {\n var output : VertexOutput;\n let x = f32(vertexIndex & 1u) * 4.0 - 1.0;\n let y = f32(vertexIndex >> 1u) * 4.0 - 1.0;\n output.position = vec4<f32>(x, y, 0.0, 1.0);\n output.uv = vec2<f32>((x + 1.0) * 0.5, (1.0 - y) * 0.5);\n return output;\n}\n\n@fragment\nfn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {\n \n let fxaaEnabled = uniforms.noiseParams.w > 0.5;\n let fxaaColor = fxaa(input.uv);\n let directColor = textureSample(inputTexture, inputSampler, input.uv).rgb;\n var color = select(directColor, fxaaColor, fxaaEnabled);\n\n \n \n if (uniforms.bloomParams.x > 0.5) {\n let sceneBrightness = rgb2luma(color);\n let bloom = sampleBloom(input.uv, sceneBrightness);\n color += bloom * uniforms.bloomParams.y; \n }\n\n let tonemapMode = i32(uniforms.ditherParams.z);\n var sdr = apply_tone_map(color, tonemapMode);\n\n \n \n let gui = textureSample(guiTexture, guiSampler, input.uv);\n sdr = sdr * (1.0 - gui.a) + gui.rgb;\n\n \n \n if (uniforms.ditherParams.x > 0.5 && uniforms.noiseParams.x > 0.0) {\n let levels = uniforms.ditherParams.y;\n let noise = sampleNoise(input.position.xy);\n\n \n \n let dither = noise - 0.5;\n sdr = floor(sdr * (levels - 1.0) + dither + 0.5) / (levels - 1.0);\n sdr = clamp(sdr, vec3f(0.0), vec3f(1.0));\n }\n\n return vec4<f32>(sdr, 1.0);\n}";
|
|
11936
|
+
class PostProcessPass extends BasePass {
|
|
11937
|
+
constructor(engine = null) {
|
|
11938
|
+
super("PostProcess", engine);
|
|
11939
|
+
this.pipeline = null;
|
|
11940
|
+
this.inputTexture = null;
|
|
11941
|
+
this.bloomTexture = null;
|
|
11942
|
+
this.dummyBloomTexture = null;
|
|
11943
|
+
this.noiseTexture = null;
|
|
11944
|
+
this.noiseSize = 64;
|
|
11945
|
+
this.noiseAnimated = true;
|
|
11946
|
+
this.guiCanvas = null;
|
|
11947
|
+
this.guiTexture = null;
|
|
11948
|
+
this.guiSampler = null;
|
|
11949
|
+
this.intermediateTexture = null;
|
|
11950
|
+
this._outputWidth = 0;
|
|
11951
|
+
this._outputHeight = 0;
|
|
11952
|
+
this._lastOutputToTexture = false;
|
|
11953
|
+
this._resizeWidth = 0;
|
|
11954
|
+
this._resizeHeight = 0;
|
|
11955
|
+
}
|
|
11956
|
+
// Convenience getter for exposure setting
|
|
11957
|
+
get exposure() {
|
|
11958
|
+
return this.settings?.environment?.exposure ?? 1.6;
|
|
11959
|
+
}
|
|
11960
|
+
// Convenience getter for fxaa setting
|
|
11961
|
+
get fxaa() {
|
|
11962
|
+
return this.settings?.rendering?.fxaa ?? true;
|
|
11963
|
+
}
|
|
11964
|
+
// Convenience getter for dithering settings
|
|
11965
|
+
get ditheringEnabled() {
|
|
11966
|
+
return this.settings?.dithering?.enabled ?? true;
|
|
11967
|
+
}
|
|
11968
|
+
get colorLevels() {
|
|
11969
|
+
return this.settings?.dithering?.colorLevels ?? 32;
|
|
11970
|
+
}
|
|
11971
|
+
// Tonemap mode: 0=ACES, 1=Reinhard, 2=None/Linear
|
|
11972
|
+
get tonemapMode() {
|
|
11973
|
+
return this.settings?.rendering?.tonemapMode ?? 0;
|
|
11974
|
+
}
|
|
11975
|
+
// Convenience getters for bloom settings
|
|
11976
|
+
get bloomEnabled() {
|
|
11977
|
+
return this.settings?.bloom?.enabled ?? true;
|
|
11978
|
+
}
|
|
11979
|
+
get bloomIntensity() {
|
|
11980
|
+
return this.settings?.bloom?.intensity ?? 1;
|
|
11981
|
+
}
|
|
11982
|
+
get bloomRadius() {
|
|
11983
|
+
return this.settings?.bloom?.radius ?? 5;
|
|
11984
|
+
}
|
|
11985
|
+
// CRT settings (determines if we output to intermediate texture)
|
|
11986
|
+
get crtEnabled() {
|
|
11987
|
+
return this.settings?.crt?.enabled ?? false;
|
|
11988
|
+
}
|
|
11989
|
+
get crtUpscaleEnabled() {
|
|
11990
|
+
return this.settings?.crt?.upscaleEnabled ?? false;
|
|
11991
|
+
}
|
|
11992
|
+
get shouldOutputToTexture() {
|
|
11993
|
+
return this.crtEnabled || this.crtUpscaleEnabled;
|
|
11994
|
+
}
|
|
11995
|
+
/**
|
|
11996
|
+
* Set the input texture (HDR image from LightingPass)
|
|
11997
|
+
* @param {Texture} texture - Input HDR texture
|
|
11998
|
+
*/
|
|
11999
|
+
setInputTexture(texture) {
|
|
12000
|
+
if (this.inputTexture !== texture) {
|
|
12001
|
+
this.inputTexture = texture;
|
|
12002
|
+
this._needsRebuild = true;
|
|
12003
|
+
}
|
|
12004
|
+
}
|
|
12005
|
+
/**
|
|
12006
|
+
* Set the bloom texture (from BloomPass)
|
|
12007
|
+
* @param {Object} bloomTexture - Bloom texture with mip levels
|
|
12008
|
+
*/
|
|
12009
|
+
setBloomTexture(bloomTexture) {
|
|
12010
|
+
if (this.bloomTexture !== bloomTexture) {
|
|
12011
|
+
this.bloomTexture = bloomTexture;
|
|
12012
|
+
this._needsRebuild = true;
|
|
12013
|
+
}
|
|
12014
|
+
}
|
|
12015
|
+
/**
|
|
12016
|
+
* Set the noise texture for dithering
|
|
12017
|
+
* @param {Texture} texture - Noise texture (blue noise or bayer dither)
|
|
12018
|
+
* @param {number} size - Texture size
|
|
12019
|
+
* @param {boolean} animated - Whether to animate noise offset each frame
|
|
12020
|
+
*/
|
|
12021
|
+
setNoise(texture, size = 64, animated = true) {
|
|
12022
|
+
this.noiseTexture = texture;
|
|
12023
|
+
this.noiseSize = size;
|
|
12024
|
+
this.noiseAnimated = animated;
|
|
12025
|
+
this._needsRebuild = true;
|
|
12026
|
+
}
|
|
12027
|
+
/**
|
|
12028
|
+
* Set the GUI canvas for overlay rendering
|
|
12029
|
+
* @param {HTMLCanvasElement} canvas - 2D canvas with GUI content
|
|
12030
|
+
*/
|
|
12031
|
+
setGuiCanvas(canvas) {
|
|
12032
|
+
this.guiCanvas = canvas;
|
|
12033
|
+
}
|
|
12034
|
+
/**
|
|
12035
|
+
* Get the output texture (for CRT pass to use)
|
|
12036
|
+
* Returns null if outputting directly to canvas
|
|
12037
|
+
*/
|
|
12038
|
+
getOutputTexture() {
|
|
12039
|
+
return this.shouldOutputToTexture ? this.intermediateTexture : null;
|
|
12040
|
+
}
|
|
12041
|
+
/**
|
|
12042
|
+
* Create or resize the intermediate texture for CRT
|
|
12043
|
+
*/
|
|
12044
|
+
async _createIntermediateTexture(width, height) {
|
|
12045
|
+
if (!this.shouldOutputToTexture) {
|
|
12046
|
+
if (this.intermediateTexture?.texture) {
|
|
12047
|
+
this.intermediateTexture.texture.destroy();
|
|
12048
|
+
this.intermediateTexture = null;
|
|
12049
|
+
}
|
|
12050
|
+
return;
|
|
12051
|
+
}
|
|
12052
|
+
if (this._outputWidth === width && this._outputHeight === height && this.intermediateTexture) {
|
|
12053
|
+
return;
|
|
12054
|
+
}
|
|
12055
|
+
const { device } = this.engine;
|
|
12056
|
+
if (this.intermediateTexture?.texture) {
|
|
12057
|
+
this.intermediateTexture.texture.destroy();
|
|
12058
|
+
}
|
|
12059
|
+
const texture = device.createTexture({
|
|
12060
|
+
label: "PostProcess Intermediate",
|
|
12061
|
+
size: [width, height, 1],
|
|
12062
|
+
format: "rgba8unorm",
|
|
12063
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC
|
|
12064
|
+
});
|
|
12065
|
+
const sampler = device.createSampler({
|
|
12066
|
+
label: "PostProcess Intermediate Sampler",
|
|
12067
|
+
minFilter: "nearest",
|
|
12068
|
+
magFilter: "nearest"
|
|
12069
|
+
});
|
|
12070
|
+
this.intermediateTexture = {
|
|
12071
|
+
texture,
|
|
12072
|
+
view: texture.createView(),
|
|
12073
|
+
sampler,
|
|
12074
|
+
width,
|
|
12075
|
+
height,
|
|
12076
|
+
format: "rgba8unorm"
|
|
12077
|
+
// Required by Pipeline.create()
|
|
12078
|
+
};
|
|
12079
|
+
this._outputWidth = width;
|
|
12080
|
+
this._outputHeight = height;
|
|
12081
|
+
this._needsRebuild = true;
|
|
12082
|
+
console.log(`PostProcessPass: Created intermediate texture ${width}x${height} for CRT`);
|
|
12083
|
+
}
|
|
12084
|
+
async _init() {
|
|
12085
|
+
const { device } = this.engine;
|
|
12086
|
+
const dummyTexture = device.createTexture({
|
|
12087
|
+
label: "Dummy Bloom Texture",
|
|
12088
|
+
size: [1, 1, 1],
|
|
12089
|
+
format: "rgba16float",
|
|
12090
|
+
mipLevelCount: 1,
|
|
12091
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST
|
|
12092
|
+
});
|
|
12093
|
+
device.queue.writeTexture(
|
|
12094
|
+
{ texture: dummyTexture },
|
|
12095
|
+
new Float32Array([0, 0, 0, 0]).buffer,
|
|
12096
|
+
{ bytesPerRow: 8 },
|
|
12097
|
+
{ width: 1, height: 1 }
|
|
12098
|
+
);
|
|
12099
|
+
const dummySampler = device.createSampler({
|
|
12100
|
+
label: "Dummy Bloom Sampler",
|
|
12101
|
+
minFilter: "linear",
|
|
12102
|
+
magFilter: "linear"
|
|
12103
|
+
});
|
|
12104
|
+
this.dummyBloomTexture = {
|
|
12105
|
+
texture: dummyTexture,
|
|
12106
|
+
view: dummyTexture.createView(),
|
|
12107
|
+
sampler: dummySampler,
|
|
12108
|
+
mipCount: 1
|
|
12109
|
+
};
|
|
12110
|
+
this.guiSampler = device.createSampler({
|
|
12111
|
+
label: "GUI Sampler",
|
|
12112
|
+
minFilter: "linear",
|
|
12113
|
+
magFilter: "linear"
|
|
12114
|
+
});
|
|
12115
|
+
const dummyGuiTexture = device.createTexture({
|
|
12116
|
+
label: "Dummy GUI Texture",
|
|
12117
|
+
size: [1, 1, 1],
|
|
12118
|
+
format: "rgba8unorm",
|
|
12119
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST
|
|
12120
|
+
});
|
|
12121
|
+
device.queue.writeTexture(
|
|
12122
|
+
{ texture: dummyGuiTexture },
|
|
12123
|
+
new Uint8Array([0, 0, 0, 0]),
|
|
12124
|
+
{ bytesPerRow: 4 },
|
|
12125
|
+
{ width: 1, height: 1 }
|
|
12126
|
+
);
|
|
12127
|
+
this.dummyGuiTexture = {
|
|
12128
|
+
texture: dummyGuiTexture,
|
|
12129
|
+
view: dummyGuiTexture.createView(),
|
|
12130
|
+
sampler: this.guiSampler
|
|
12131
|
+
};
|
|
12132
|
+
}
|
|
12133
|
+
/**
|
|
12134
|
+
* Build or rebuild the pipeline
|
|
12135
|
+
*/
|
|
12136
|
+
async _buildPipeline() {
|
|
12137
|
+
if (!this.inputTexture) {
|
|
12138
|
+
return;
|
|
12139
|
+
}
|
|
12140
|
+
const textures = [this.inputTexture];
|
|
12141
|
+
if (this.noiseTexture) {
|
|
12142
|
+
textures.push(this.noiseTexture);
|
|
12143
|
+
}
|
|
12144
|
+
const effectiveBloomTexture = this.bloomTexture || this.dummyBloomTexture;
|
|
12145
|
+
textures.push(effectiveBloomTexture);
|
|
12146
|
+
const effectiveGuiTexture = this.guiTexture || this.dummyGuiTexture;
|
|
12147
|
+
textures.push(effectiveGuiTexture);
|
|
12148
|
+
const hasBloom = this.bloomTexture && this.bloomEnabled;
|
|
12149
|
+
const renderTarget = this.shouldOutputToTexture && this.intermediateTexture ? this.intermediateTexture : null;
|
|
11158
12150
|
this.pipeline = await Pipeline.create(this.engine, {
|
|
11159
12151
|
label: "postProcess",
|
|
11160
12152
|
wgslSource: postproc_default,
|
|
@@ -11162,75 +12154,662 @@ class PostProcessPass extends BasePass {
|
|
|
11162
12154
|
textures,
|
|
11163
12155
|
uniforms: () => ({
|
|
11164
12156
|
noiseParams: [this.noiseSize, this.noiseAnimated ? Math.random() : 0, this.noiseAnimated ? Math.random() : 0, this.fxaa ? 1 : 0],
|
|
11165
|
-
ditherParams: [this.ditheringEnabled ? 1 : 0, this.colorLevels,
|
|
12157
|
+
ditherParams: [this.ditheringEnabled ? 1 : 0, this.colorLevels, this.tonemapMode, 0],
|
|
11166
12158
|
bloomParams: [hasBloom ? 1 : 0, this.bloomIntensity, this.bloomRadius, effectiveBloomTexture?.mipCount ?? 1]
|
|
11167
|
-
})
|
|
11168
|
-
|
|
12159
|
+
}),
|
|
12160
|
+
renderTarget
|
|
12161
|
+
});
|
|
12162
|
+
this._needsRebuild = false;
|
|
12163
|
+
}
|
|
12164
|
+
async _execute(context) {
|
|
12165
|
+
const { device, canvas } = this.engine;
|
|
12166
|
+
const needsOutputToTexture = this.shouldOutputToTexture;
|
|
12167
|
+
const hasIntermediateTexture = !!this.intermediateTexture;
|
|
12168
|
+
if (needsOutputToTexture !== this._lastOutputToTexture) {
|
|
12169
|
+
this._lastOutputToTexture = needsOutputToTexture;
|
|
12170
|
+
this._needsRebuild = true;
|
|
12171
|
+
if (needsOutputToTexture && !hasIntermediateTexture) {
|
|
12172
|
+
const w = this._resizeWidth || canvas.width;
|
|
12173
|
+
const h = this._resizeHeight || canvas.height;
|
|
12174
|
+
await this._createIntermediateTexture(w, h);
|
|
12175
|
+
} else if (!needsOutputToTexture && hasIntermediateTexture) {
|
|
12176
|
+
if (this.intermediateTexture?.texture) {
|
|
12177
|
+
this.intermediateTexture.texture.destroy();
|
|
12178
|
+
this.intermediateTexture = null;
|
|
12179
|
+
}
|
|
12180
|
+
}
|
|
12181
|
+
}
|
|
12182
|
+
if (this.guiCanvas && this.guiCanvas.width > 0 && this.guiCanvas.height > 0) {
|
|
12183
|
+
const needsNewTexture = !this.guiTexture || this.guiTexture.width !== this.guiCanvas.width || this.guiTexture.height !== this.guiCanvas.height;
|
|
12184
|
+
if (needsNewTexture) {
|
|
12185
|
+
if (this.guiTexture?.texture) {
|
|
12186
|
+
this.guiTexture.texture.destroy();
|
|
12187
|
+
}
|
|
12188
|
+
const texture = device.createTexture({
|
|
12189
|
+
label: "GUI Texture",
|
|
12190
|
+
size: [this.guiCanvas.width, this.guiCanvas.height, 1],
|
|
12191
|
+
format: "rgba8unorm",
|
|
12192
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
|
|
12193
|
+
});
|
|
12194
|
+
this.guiTexture = {
|
|
12195
|
+
texture,
|
|
12196
|
+
view: texture.createView(),
|
|
12197
|
+
sampler: this.guiSampler,
|
|
12198
|
+
width: this.guiCanvas.width,
|
|
12199
|
+
height: this.guiCanvas.height
|
|
12200
|
+
};
|
|
12201
|
+
this._needsRebuild = true;
|
|
12202
|
+
}
|
|
12203
|
+
device.queue.copyExternalImageToTexture(
|
|
12204
|
+
{ source: this.guiCanvas },
|
|
12205
|
+
{ texture: this.guiTexture.texture },
|
|
12206
|
+
[this.guiCanvas.width, this.guiCanvas.height]
|
|
12207
|
+
);
|
|
12208
|
+
}
|
|
12209
|
+
if (this._needsRebuild) {
|
|
12210
|
+
await this._buildPipeline();
|
|
12211
|
+
}
|
|
12212
|
+
if (!this.pipeline || this._needsRebuild) {
|
|
12213
|
+
console.warn("PostProcessPass: Pipeline not ready");
|
|
12214
|
+
return;
|
|
12215
|
+
}
|
|
12216
|
+
const hasBloom = this.bloomTexture && this.bloomEnabled;
|
|
12217
|
+
const effectiveBloomTexture = this.bloomTexture || this.dummyBloomTexture;
|
|
12218
|
+
this.pipeline.uniformValues.set({
|
|
12219
|
+
noiseParams: [this.noiseSize, this.noiseAnimated ? Math.random() : 0, this.noiseAnimated ? Math.random() : 0, this.fxaa ? 1 : 0],
|
|
12220
|
+
ditherParams: [this.ditheringEnabled ? 1 : 0, this.colorLevels, this.tonemapMode, 0],
|
|
12221
|
+
bloomParams: [hasBloom ? 1 : 0, this.bloomIntensity, this.bloomRadius, effectiveBloomTexture?.mipCount ?? 1]
|
|
12222
|
+
});
|
|
12223
|
+
this.pipeline.render();
|
|
12224
|
+
}
|
|
12225
|
+
async _resize(width, height) {
|
|
12226
|
+
this._resizeWidth = width;
|
|
12227
|
+
this._resizeHeight = height;
|
|
12228
|
+
await this._createIntermediateTexture(width, height);
|
|
12229
|
+
this._needsRebuild = true;
|
|
12230
|
+
}
|
|
12231
|
+
_destroy() {
|
|
12232
|
+
this.pipeline = null;
|
|
12233
|
+
if (this.dummyBloomTexture?.texture) {
|
|
12234
|
+
this.dummyBloomTexture.texture.destroy();
|
|
12235
|
+
this.dummyBloomTexture = null;
|
|
12236
|
+
}
|
|
12237
|
+
if (this.guiTexture?.texture) {
|
|
12238
|
+
this.guiTexture.texture.destroy();
|
|
12239
|
+
this.guiTexture = null;
|
|
12240
|
+
}
|
|
12241
|
+
if (this.dummyGuiTexture?.texture) {
|
|
12242
|
+
this.dummyGuiTexture.texture.destroy();
|
|
12243
|
+
this.dummyGuiTexture = null;
|
|
12244
|
+
}
|
|
12245
|
+
if (this.intermediateTexture?.texture) {
|
|
12246
|
+
this.intermediateTexture.texture.destroy();
|
|
12247
|
+
this.intermediateTexture = null;
|
|
12248
|
+
}
|
|
12249
|
+
}
|
|
12250
|
+
}
|
|
12251
|
+
var crt_default = "struct VertexOutput {\n @builtin(position) position: vec4f,\n @location(0) uv: vec2f,\n}\n\nstruct Uniforms {\n canvasSize: vec2f, \n inputSize: vec2f, \n renderSize: vec2f, \n\n \n curvature: f32, \n cornerRadius: f32, \n zoom: f32, \n _padGeom: f32,\n\n \n scanlineIntensity: f32, \n scanlineWidth: f32, \n scanlineBrightBoost: f32, \n scanlineHeight: f32, \n\n \n convergence: vec3f,\n _pad2: f32,\n\n \n maskType: f32, \n maskIntensity: f32, \n maskScale: f32, \n maskCompensation: f32, \n\n \n vignetteIntensity: f32,\n vignetteSize: f32,\n\n \n blurSize: f32, \n\n \n crtEnabled: f32, \n upscaleEnabled: f32, \n}\n\n@group(0) @binding(0) var<uniform> uniforms: Uniforms;\n@group(0) @binding(1) var inputTexture: texture_2d<f32>;\n@group(0) @binding(2) var inputSampler: sampler; \n@group(0) @binding(3) var nearestSampler: sampler; \n@group(0) @binding(4) var phosphorMask: texture_2d<f32>;\n@group(0) @binding(5) var phosphorSampler: sampler;\n\nconst PI: f32 = 3.14159265359;\n\nfn applyBarrelDistortion(uv: vec2f, curvature: f32) -> vec2f {\n \n let centered = uv - 0.5;\n\n \n let r2 = dot(centered, centered);\n\n \n let distorted = centered * (1.0 + curvature * r2);\n\n return distorted + 0.5;\n}\n\nfn getCornerMask(uv: vec2f, radius: f32) -> f32 {\n \n let d = abs(uv - 0.5) * 2.0;\n\n \n let corner = max(d.x, d.y);\n let roundedCorner = length(max(d - (1.0 - radius), vec2f(0.0)));\n\n \n let mask = 1.0 - smoothstep(1.0 - radius * 0.5, 1.0, max(corner, roundedCorner));\n\n return mask;\n}\n\nfn applyScanlines(color: vec3f, scanlinePos: f32, intensity: f32, width: f32, brightBoost: f32) -> vec3f {\n if (intensity <= 0.0) {\n return color;\n }\n\n \n \n let pos = fract(scanlinePos);\n\n \n \n let distFromCenter = abs(pos - 0.5);\n\n \n let brightness = dot(color, vec3f(0.299, 0.587, 0.114));\n\n \n \n \n let baseWidth = width;\n let brightWidening = brightBoost * brightness * (1.0 - width);\n let effectiveWidth = clamp(baseWidth + brightWidening, 0.0, 1.0);\n\n \n \n \n let sigma = mix(0.08, 0.8, effectiveWidth);\n let gaussian = exp(-0.5 * pow(distFromCenter / sigma, 2.0));\n\n \n let scanline = clamp(gaussian, 0.0, 1.0);\n let darkening = mix(1.0, scanline, intensity);\n\n return color * darkening;\n}\n\nfn sampleWithConvergence(uv: vec2f, convergence: vec3f, texelSize: vec2f) -> vec3f {\n \n \n let offsetR = vec2f(convergence.r * texelSize.x, 0.0);\n let offsetG = vec2f(convergence.g * texelSize.x, 0.0);\n let offsetB = vec2f(convergence.b * texelSize.x, 0.0);\n\n let r = textureSampleLevel(inputTexture, inputSampler, uv + offsetR, 0.0).r;\n let g = textureSampleLevel(inputTexture, inputSampler, uv + offsetG, 0.0).g;\n let b = textureSampleLevel(inputTexture, inputSampler, uv + offsetB, 0.0).b;\n\n return vec3f(r, g, b);\n}\n\nfn applyPhosphorMask(color: vec3f, screenPos: vec2f, maskType: f32, intensity: f32, scale: f32) -> vec3f {\n if (intensity <= 0.0 || maskType < 0.5) {\n return color;\n }\n\n \n \n \n let maskUV = screenPos / (3.0 * scale);\n let mask = textureSampleLevel(phosphorMask, phosphorSampler, maskUV, 0.0).rgb;\n\n \n \n return mix(color, color * mask, intensity);\n}\n\nfn proceduralApertureGrille(screenX: f32, scale: f32) -> vec3f {\n let phase = fract(screenX / (3.0 * scale));\n\n \n var mask = vec3f(0.0);\n if (phase < 0.333) {\n mask = vec3f(1.0, 0.2, 0.2); \n } else if (phase < 0.666) {\n mask = vec3f(0.2, 1.0, 0.2); \n } else {\n mask = vec3f(0.2, 0.2, 1.0); \n }\n\n return mask;\n}\n\nfn proceduralSlotMask(screenPos: vec2f, scale: f32, scanlineHeight: f32) -> vec3f {\n \n let cellWidth = 3.0 * scale;\n let cellHeight = max(scanlineHeight, 1.0);\n let cellPos = fract(vec2f(screenPos.x / cellWidth, screenPos.y / cellHeight));\n\n \n var adjusted = cellPos;\n if (fract(screenPos.y / (cellHeight * 2.0)) > 0.5) {\n adjusted.x = fract(adjusted.x + 0.5);\n }\n\n \n var mask = vec3f(0.0);\n if (adjusted.x < 0.333) {\n mask = vec3f(1.0, 0.1, 0.1);\n } else if (adjusted.x < 0.666) {\n mask = vec3f(0.1, 1.0, 0.1);\n } else {\n mask = vec3f(0.1, 0.1, 1.0);\n }\n\n \n mask *= smoothstep(0.0, 0.15, cellPos.y) * smoothstep(1.0, 0.85, cellPos.y);\n\n return mask;\n}\n\nfn proceduralShadowMask(screenPos: vec2f, scale: f32) -> vec3f {\n let cellSize = 2.0 * scale;\n let cell = floor(screenPos / cellSize);\n let cellPos = fract(screenPos / cellSize);\n\n \n let rowOffset = select(0.0, 0.5, fract(cell.y * 0.5) > 0.25);\n let adjustedX = fract(cellPos.x + rowOffset);\n\n \n let dist = length(cellPos - 0.5) * 2.0;\n let circle = 1.0 - smoothstep(0.6, 0.8, dist);\n\n \n let phase = fract((cell.x + rowOffset) / 3.0);\n var mask = vec3f(0.1);\n if (phase < 0.333) {\n mask = vec3f(1.0, 0.1, 0.1) * circle;\n } else if (phase < 0.666) {\n mask = vec3f(0.1, 1.0, 0.1) * circle;\n } else {\n mask = vec3f(0.1, 0.1, 1.0) * circle;\n }\n\n return max(mask, vec3f(0.1));\n}\n\nfn applyVignette(color: vec3f, uv: vec2f, intensity: f32, size: f32) -> vec3f {\n if (intensity <= 0.0) {\n return color;\n }\n\n \n let centered = uv - 0.5;\n let dist = length(centered);\n\n \n let vignette = 1.0 - smoothstep(size * 0.5, size, dist);\n let darkening = mix(1.0 - intensity, 1.0, vignette);\n\n return color * darkening;\n}\n\nfn sampleWithHorizontalBlur(uv: vec2f, blurSize: f32, texelSize: vec2f) -> vec3f {\n if (blurSize <= 0.0) {\n return textureSampleLevel(inputTexture, inputSampler, uv, 0.0).rgb;\n }\n\n \n \n let w0 = 0.0625;\n let w1 = 0.25;\n let w2 = 0.375;\n\n let offset = blurSize * texelSize.x;\n\n let c0 = textureSampleLevel(inputTexture, inputSampler, uv + vec2f(-2.0 * offset, 0.0), 0.0).rgb;\n let c1 = textureSampleLevel(inputTexture, inputSampler, uv + vec2f(-1.0 * offset, 0.0), 0.0).rgb;\n let c2 = textureSampleLevel(inputTexture, inputSampler, uv, 0.0).rgb;\n let c3 = textureSampleLevel(inputTexture, inputSampler, uv + vec2f(1.0 * offset, 0.0), 0.0).rgb;\n let c4 = textureSampleLevel(inputTexture, inputSampler, uv + vec2f(2.0 * offset, 0.0), 0.0).rgb;\n\n return c0 * w0 + c1 * w1 + c2 * w2 + c3 * w1 + c4 * w0;\n}\n\nfn sampleWithBlurAndConvergence(uv: vec2f, convergence: vec3f, blurSize: f32, texelSize: vec2f) -> vec3f {\n if (blurSize <= 0.0) {\n \n let offsetR = vec2f(convergence.r * texelSize.x, 0.0);\n let offsetG = vec2f(convergence.g * texelSize.x, 0.0);\n let offsetB = vec2f(convergence.b * texelSize.x, 0.0);\n let r = textureSampleLevel(inputTexture, inputSampler, uv + offsetR, 0.0).r;\n let g = textureSampleLevel(inputTexture, inputSampler, uv + offsetG, 0.0).g;\n let b = textureSampleLevel(inputTexture, inputSampler, uv + offsetB, 0.0).b;\n return vec3f(r, g, b);\n }\n\n \n let w0 = 0.0625;\n let w1 = 0.25;\n let w2 = 0.375;\n let offset = blurSize * texelSize.x;\n\n \n var r = 0.0;\n var g = 0.0;\n var b = 0.0;\n\n for (var i = -2; i <= 2; i++) {\n let weight = select(select(w1, w2, i == 0), w0, abs(i) == 2);\n let blurOffset = f32(i) * offset;\n\n r += textureSampleLevel(inputTexture, inputSampler, uv + vec2f(convergence.r * texelSize.x + blurOffset, 0.0), 0.0).r * weight;\n g += textureSampleLevel(inputTexture, inputSampler, uv + vec2f(convergence.g * texelSize.x + blurOffset, 0.0), 0.0).g * weight;\n b += textureSampleLevel(inputTexture, inputSampler, uv + vec2f(convergence.b * texelSize.x + blurOffset, 0.0), 0.0).b * weight;\n }\n\n return vec3f(r, g, b);\n}\n\n@vertex\nfn vertexMain(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {\n var output: VertexOutput;\n\n \n let x = f32(vertexIndex & 1u) * 4.0 - 1.0;\n let y = f32(vertexIndex >> 1u) * 4.0 - 1.0;\n output.position = vec4f(x, y, 0.0, 1.0);\n output.uv = vec2f((x + 1.0) * 0.5, (1.0 - y) * 0.5);\n\n return output;\n}\n\n@fragment\nfn fragmentMain(input: VertexOutput) -> @location(0) vec4f {\n \n let pixelPos = input.position.xy;\n var uv = input.uv;\n\n \n let crtEnabled = uniforms.crtEnabled > 0.5;\n\n \n \n if (!crtEnabled && uniforms.upscaleEnabled < 0.5) {\n return textureSampleLevel(inputTexture, nearestSampler, uv, 0.0);\n }\n\n \n var distortedUV = uv;\n var cornerMask = 1.0;\n\n if (crtEnabled && uniforms.curvature > 0.0) {\n \n var zoomedUV = uv;\n if (uniforms.zoom > 1.0) {\n let centered = uv - 0.5;\n zoomedUV = centered / uniforms.zoom + 0.5;\n }\n distortedUV = applyBarrelDistortion(zoomedUV, uniforms.curvature);\n cornerMask = getCornerMask(distortedUV, uniforms.cornerRadius);\n\n \n \n if (distortedUV.x < 0.0 || distortedUV.x > 1.0 ||\n distortedUV.y < 0.0 || distortedUV.y > 1.0) {\n return vec4f(0.0, 0.0, 0.0, 1.0);\n }\n }\n\n \n let texelSize = 1.0 / uniforms.inputSize;\n\n \n var color: vec3f;\n if (crtEnabled) {\n let hasConvergence = any(uniforms.convergence != vec3f(0.0));\n if (hasConvergence) {\n \n color = sampleWithBlurAndConvergence(distortedUV, uniforms.convergence, uniforms.blurSize, texelSize);\n } else {\n \n color = sampleWithHorizontalBlur(distortedUV, uniforms.blurSize, texelSize);\n }\n } else {\n \n \n let hasUpscaling = uniforms.inputSize.x > uniforms.canvasSize.x + 0.5 ||\n uniforms.inputSize.y > uniforms.canvasSize.y + 0.5;\n\n if (hasUpscaling) {\n \n color = textureSampleLevel(inputTexture, inputSampler, distortedUV, 0.0).rgb;\n } else {\n \n \n color = textureSampleLevel(inputTexture, nearestSampler, distortedUV, 0.0).rgb;\n }\n }\n\n \n if (crtEnabled) {\n \n \n let scanlinePos = pixelPos.y / max(uniforms.scanlineHeight, 1.0);\n\n \n color = applyScanlines(\n color,\n scanlinePos,\n uniforms.scanlineIntensity,\n uniforms.scanlineWidth,\n uniforms.scanlineBrightBoost\n );\n\n \n if (uniforms.maskType > 0.5 && uniforms.maskIntensity > 0.0) {\n var mask: vec3f;\n let maskPos = pixelPos / uniforms.maskScale;\n if (uniforms.maskType < 1.5) {\n \n mask = proceduralApertureGrille(maskPos.x, 1.0);\n } else if (uniforms.maskType < 2.5) {\n \n mask = proceduralSlotMask(maskPos, 1.0, uniforms.scanlineHeight / uniforms.maskScale);\n } else {\n \n mask = proceduralShadowMask(maskPos, 1.0);\n }\n\n \n let maskedColor = color * mask;\n color = mix(color, maskedColor, uniforms.maskIntensity);\n color *= uniforms.maskCompensation;\n }\n\n \n color = applyVignette(color, uv, uniforms.vignetteIntensity, uniforms.vignetteSize);\n\n \n color *= cornerMask;\n }\n\n return vec4f(color, 1.0);\n}";
|
|
12252
|
+
class CRTPass extends BasePass {
|
|
12253
|
+
constructor(engine = null) {
|
|
12254
|
+
super("CRT", engine);
|
|
12255
|
+
this.pipeline = null;
|
|
12256
|
+
this.inputTexture = null;
|
|
12257
|
+
this.upscaledTexture = null;
|
|
12258
|
+
this.upscaledWidth = 0;
|
|
12259
|
+
this.upscaledHeight = 0;
|
|
12260
|
+
this.linearSampler = null;
|
|
12261
|
+
this.nearestSampler = null;
|
|
12262
|
+
this.phosphorMaskTexture = null;
|
|
12263
|
+
this.canvasWidth = 0;
|
|
12264
|
+
this.canvasHeight = 0;
|
|
12265
|
+
this.renderWidth = 0;
|
|
12266
|
+
this.renderHeight = 0;
|
|
12267
|
+
}
|
|
12268
|
+
// Settings getters
|
|
12269
|
+
get crtSettings() {
|
|
12270
|
+
return this.engine?.settings?.crt ?? {};
|
|
12271
|
+
}
|
|
12272
|
+
get crtEnabled() {
|
|
12273
|
+
return this.crtSettings.enabled ?? false;
|
|
12274
|
+
}
|
|
12275
|
+
get upscaleEnabled() {
|
|
12276
|
+
return this.crtSettings.upscaleEnabled ?? false;
|
|
12277
|
+
}
|
|
12278
|
+
get upscaleTarget() {
|
|
12279
|
+
return this.crtSettings.upscaleTarget ?? 4;
|
|
12280
|
+
}
|
|
12281
|
+
get maxTextureSize() {
|
|
12282
|
+
return this.crtSettings.maxTextureSize ?? 4096;
|
|
12283
|
+
}
|
|
12284
|
+
// Geometry
|
|
12285
|
+
get curvature() {
|
|
12286
|
+
return this.crtSettings.curvature ?? 0.03;
|
|
12287
|
+
}
|
|
12288
|
+
get cornerRadius() {
|
|
12289
|
+
return this.crtSettings.cornerRadius ?? 0.03;
|
|
12290
|
+
}
|
|
12291
|
+
get zoom() {
|
|
12292
|
+
return this.crtSettings.zoom ?? 1;
|
|
12293
|
+
}
|
|
12294
|
+
// Scanlines
|
|
12295
|
+
get scanlineIntensity() {
|
|
12296
|
+
return this.crtSettings.scanlineIntensity ?? 0.25;
|
|
12297
|
+
}
|
|
12298
|
+
get scanlineWidth() {
|
|
12299
|
+
return this.crtSettings.scanlineWidth ?? 0.5;
|
|
12300
|
+
}
|
|
12301
|
+
get scanlineBrightBoost() {
|
|
12302
|
+
return this.crtSettings.scanlineBrightBoost ?? 1;
|
|
12303
|
+
}
|
|
12304
|
+
get scanlineHeight() {
|
|
12305
|
+
return this.crtSettings.scanlineHeight ?? 3;
|
|
12306
|
+
}
|
|
12307
|
+
// pixels per scanline
|
|
12308
|
+
// Convergence
|
|
12309
|
+
get convergence() {
|
|
12310
|
+
return this.crtSettings.convergence ?? [0.5, 0, -0.5];
|
|
12311
|
+
}
|
|
12312
|
+
// Phosphor mask
|
|
12313
|
+
get maskType() {
|
|
12314
|
+
const type = this.crtSettings.maskType ?? "aperture";
|
|
12315
|
+
switch (type) {
|
|
12316
|
+
case "none":
|
|
12317
|
+
return 0;
|
|
12318
|
+
case "aperture":
|
|
12319
|
+
return 1;
|
|
12320
|
+
case "slot":
|
|
12321
|
+
return 2;
|
|
12322
|
+
case "shadow":
|
|
12323
|
+
return 3;
|
|
12324
|
+
default:
|
|
12325
|
+
return 1;
|
|
12326
|
+
}
|
|
12327
|
+
}
|
|
12328
|
+
get maskIntensity() {
|
|
12329
|
+
return this.crtSettings.maskIntensity ?? 0.15;
|
|
12330
|
+
}
|
|
12331
|
+
get maskScale() {
|
|
12332
|
+
return this.crtSettings.maskScale ?? 1;
|
|
12333
|
+
}
|
|
12334
|
+
// Vignette
|
|
12335
|
+
get vignetteIntensity() {
|
|
12336
|
+
return this.crtSettings.vignetteIntensity ?? 0.15;
|
|
12337
|
+
}
|
|
12338
|
+
get vignetteSize() {
|
|
12339
|
+
return this.crtSettings.vignetteSize ?? 0.4;
|
|
12340
|
+
}
|
|
12341
|
+
// Blur
|
|
12342
|
+
get blurSize() {
|
|
12343
|
+
return this.crtSettings.blurSize ?? 0.5;
|
|
12344
|
+
}
|
|
12345
|
+
/**
|
|
12346
|
+
* Calculate brightness compensation for phosphor mask
|
|
12347
|
+
* Pre-computed on CPU to avoid per-pixel calculation in shader
|
|
12348
|
+
* @param {number} maskType - 0=none, 1=aperture, 2=slot, 3=shadow
|
|
12349
|
+
* @param {number} intensity - mask intensity 0-1
|
|
12350
|
+
* @returns {number} compensation multiplier
|
|
12351
|
+
*/
|
|
12352
|
+
_calculateMaskCompensation(maskType, intensity) {
|
|
12353
|
+
if (maskType < 0.5 || intensity <= 0) {
|
|
12354
|
+
return 1;
|
|
12355
|
+
}
|
|
12356
|
+
let darkening;
|
|
12357
|
+
let useLinearOnly = false;
|
|
12358
|
+
if (maskType < 1.5) {
|
|
12359
|
+
darkening = 0.25;
|
|
12360
|
+
} else if (maskType < 2.5) {
|
|
12361
|
+
darkening = 0.27;
|
|
12362
|
+
} else {
|
|
12363
|
+
darkening = 0.82;
|
|
12364
|
+
useLinearOnly = true;
|
|
12365
|
+
}
|
|
12366
|
+
const linearComp = 1 / Math.max(1 - intensity * darkening, 0.1);
|
|
12367
|
+
if (useLinearOnly) {
|
|
12368
|
+
return linearComp;
|
|
12369
|
+
}
|
|
12370
|
+
const expComp = Math.exp(intensity * darkening);
|
|
12371
|
+
const t = Math.max(0, Math.min(1, (intensity - 0.4) / 0.2));
|
|
12372
|
+
const blendFactor = t * t * (3 - 2 * t);
|
|
12373
|
+
return linearComp * (1 - blendFactor) + expComp * blendFactor;
|
|
12374
|
+
}
|
|
12375
|
+
/**
|
|
12376
|
+
* Set the input texture (from PostProcessPass intermediate output)
|
|
12377
|
+
* @param {Object} texture - Input texture object
|
|
12378
|
+
*/
|
|
12379
|
+
setInputTexture(texture) {
|
|
12380
|
+
if (this.inputTexture !== texture) {
|
|
12381
|
+
this.inputTexture = texture;
|
|
12382
|
+
this._needsRebuild = true;
|
|
12383
|
+
this._blitPipeline = null;
|
|
12384
|
+
}
|
|
12385
|
+
}
|
|
12386
|
+
/**
|
|
12387
|
+
* Set the render size (before upscaling)
|
|
12388
|
+
* @param {number} width - Render width
|
|
12389
|
+
* @param {number} height - Render height
|
|
12390
|
+
*/
|
|
12391
|
+
setRenderSize(width, height) {
|
|
12392
|
+
if (this.renderWidth !== width || this.renderHeight !== height) {
|
|
12393
|
+
this.renderWidth = width;
|
|
12394
|
+
this.renderHeight = height;
|
|
12395
|
+
this._needsUpscaleRebuild = true;
|
|
12396
|
+
}
|
|
12397
|
+
}
|
|
12398
|
+
/**
|
|
12399
|
+
* Calculate the upscaled texture size
|
|
12400
|
+
* @returns {{width: number, height: number, scale: number}}
|
|
12401
|
+
*/
|
|
12402
|
+
_calculateUpscaledSize() {
|
|
12403
|
+
const renderW = this.renderWidth || this.canvasWidth;
|
|
12404
|
+
const renderH = this.renderHeight || this.canvasHeight;
|
|
12405
|
+
const maxSize = this.maxTextureSize;
|
|
12406
|
+
let scale = this.upscaleTarget;
|
|
12407
|
+
while (scale > 1 && (renderW * scale > maxSize || renderH * scale > maxSize)) {
|
|
12408
|
+
scale--;
|
|
12409
|
+
}
|
|
12410
|
+
const maxCanvasScale = 2;
|
|
12411
|
+
while (scale > 1 && (renderW * scale > this.canvasWidth * maxCanvasScale || renderH * scale > this.canvasHeight * maxCanvasScale)) {
|
|
12412
|
+
scale--;
|
|
12413
|
+
}
|
|
12414
|
+
scale = Math.max(scale, 1);
|
|
12415
|
+
const targetW = renderW * scale;
|
|
12416
|
+
const targetH = renderH * scale;
|
|
12417
|
+
return { width: targetW, height: targetH, scale };
|
|
12418
|
+
}
|
|
12419
|
+
/**
|
|
12420
|
+
* Check if actual upscaling is needed
|
|
12421
|
+
* Returns true only if the upscaled texture would be larger than the input
|
|
12422
|
+
*/
|
|
12423
|
+
_needsUpscaling() {
|
|
12424
|
+
if (!this.upscaleEnabled) return false;
|
|
12425
|
+
const { scale } = this._calculateUpscaledSize();
|
|
12426
|
+
return scale > 1;
|
|
12427
|
+
}
|
|
12428
|
+
async _init() {
|
|
12429
|
+
const { device } = this.engine;
|
|
12430
|
+
this.linearSampler = device.createSampler({
|
|
12431
|
+
label: "CRT Linear Sampler",
|
|
12432
|
+
minFilter: "linear",
|
|
12433
|
+
magFilter: "linear",
|
|
12434
|
+
addressModeU: "mirror-repeat",
|
|
12435
|
+
addressModeV: "mirror-repeat"
|
|
11169
12436
|
});
|
|
11170
|
-
this.
|
|
12437
|
+
this.nearestSampler = device.createSampler({
|
|
12438
|
+
label: "CRT Nearest Sampler",
|
|
12439
|
+
minFilter: "nearest",
|
|
12440
|
+
magFilter: "nearest",
|
|
12441
|
+
addressModeU: "mirror-repeat",
|
|
12442
|
+
addressModeV: "mirror-repeat"
|
|
12443
|
+
});
|
|
12444
|
+
await this._createPhosphorMaskTexture();
|
|
11171
12445
|
}
|
|
11172
|
-
|
|
12446
|
+
/**
|
|
12447
|
+
* Create phosphor mask texture
|
|
12448
|
+
* This is a simple procedural texture for the aperture grille pattern
|
|
12449
|
+
*/
|
|
12450
|
+
async _createPhosphorMaskTexture() {
|
|
11173
12451
|
const { device } = this.engine;
|
|
11174
|
-
|
|
11175
|
-
|
|
11176
|
-
|
|
11177
|
-
|
|
11178
|
-
|
|
12452
|
+
const size = 6;
|
|
12453
|
+
const data = new Uint8Array(size * 2 * 4);
|
|
12454
|
+
for (let y = 0; y < 2; y++) {
|
|
12455
|
+
for (let x = 0; x < size; x++) {
|
|
12456
|
+
const idx = (y * size + x) * 4;
|
|
12457
|
+
const phase = x % 3;
|
|
12458
|
+
if (phase === 0) {
|
|
12459
|
+
data[idx] = 255;
|
|
12460
|
+
data[idx + 1] = 50;
|
|
12461
|
+
data[idx + 2] = 50;
|
|
12462
|
+
} else if (phase === 1) {
|
|
12463
|
+
data[idx] = 50;
|
|
12464
|
+
data[idx + 1] = 255;
|
|
12465
|
+
data[idx + 2] = 50;
|
|
12466
|
+
} else {
|
|
12467
|
+
data[idx] = 50;
|
|
12468
|
+
data[idx + 1] = 50;
|
|
12469
|
+
data[idx + 2] = 255;
|
|
11179
12470
|
}
|
|
11180
|
-
|
|
11181
|
-
label: "GUI Texture",
|
|
11182
|
-
size: [this.guiCanvas.width, this.guiCanvas.height, 1],
|
|
11183
|
-
format: "rgba8unorm",
|
|
11184
|
-
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
|
|
11185
|
-
});
|
|
11186
|
-
this.guiTexture = {
|
|
11187
|
-
texture,
|
|
11188
|
-
view: texture.createView(),
|
|
11189
|
-
sampler: this.guiSampler,
|
|
11190
|
-
width: this.guiCanvas.width,
|
|
11191
|
-
height: this.guiCanvas.height
|
|
11192
|
-
};
|
|
11193
|
-
this._needsRebuild = true;
|
|
12471
|
+
data[idx + 3] = 255;
|
|
11194
12472
|
}
|
|
11195
|
-
device.queue.copyExternalImageToTexture(
|
|
11196
|
-
{ source: this.guiCanvas },
|
|
11197
|
-
{ texture: this.guiTexture.texture },
|
|
11198
|
-
[this.guiCanvas.width, this.guiCanvas.height]
|
|
11199
|
-
);
|
|
11200
12473
|
}
|
|
11201
|
-
|
|
11202
|
-
|
|
12474
|
+
const texture = device.createTexture({
|
|
12475
|
+
label: "Phosphor Mask",
|
|
12476
|
+
size: [size, 2, 1],
|
|
12477
|
+
format: "rgba8unorm",
|
|
12478
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST
|
|
12479
|
+
});
|
|
12480
|
+
device.queue.writeTexture(
|
|
12481
|
+
{ texture },
|
|
12482
|
+
data,
|
|
12483
|
+
{ bytesPerRow: size * 4 },
|
|
12484
|
+
{ width: size, height: 2 }
|
|
12485
|
+
);
|
|
12486
|
+
const sampler = device.createSampler({
|
|
12487
|
+
label: "Phosphor Mask Sampler",
|
|
12488
|
+
minFilter: "nearest",
|
|
12489
|
+
magFilter: "nearest",
|
|
12490
|
+
addressModeU: "repeat",
|
|
12491
|
+
addressModeV: "repeat"
|
|
12492
|
+
});
|
|
12493
|
+
this.phosphorMaskTexture = {
|
|
12494
|
+
texture,
|
|
12495
|
+
view: texture.createView(),
|
|
12496
|
+
sampler
|
|
12497
|
+
};
|
|
12498
|
+
}
|
|
12499
|
+
/**
|
|
12500
|
+
* Create or resize the upscaled texture
|
|
12501
|
+
*/
|
|
12502
|
+
async _createUpscaledTexture() {
|
|
12503
|
+
const { device } = this.engine;
|
|
12504
|
+
const { width, height, scale } = this._calculateUpscaledSize();
|
|
12505
|
+
if (this.upscaledWidth === width && this.upscaledHeight === height) {
|
|
12506
|
+
return;
|
|
11203
12507
|
}
|
|
11204
|
-
if (
|
|
11205
|
-
|
|
12508
|
+
if (this.upscaledTexture?.texture) {
|
|
12509
|
+
this.upscaledTexture.texture.destroy();
|
|
12510
|
+
}
|
|
12511
|
+
const texture = device.createTexture({
|
|
12512
|
+
label: "CRT Upscaled Texture",
|
|
12513
|
+
size: [width, height, 1],
|
|
12514
|
+
format: "rgba8unorm",
|
|
12515
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_DST
|
|
12516
|
+
});
|
|
12517
|
+
this.upscaledTexture = {
|
|
12518
|
+
texture,
|
|
12519
|
+
view: texture.createView(),
|
|
12520
|
+
width,
|
|
12521
|
+
height,
|
|
12522
|
+
scale,
|
|
12523
|
+
format: "rgba8unorm"
|
|
12524
|
+
};
|
|
12525
|
+
this.upscaledWidth = width;
|
|
12526
|
+
this.upscaledHeight = height;
|
|
12527
|
+
console.log(`CRTPass: Created upscaled texture ${width}x${height} (${scale.toFixed(1)}x)`);
|
|
12528
|
+
this._needsRebuild = true;
|
|
12529
|
+
this._blitPipeline = null;
|
|
12530
|
+
}
|
|
12531
|
+
/**
|
|
12532
|
+
* Build or rebuild the CRT pipeline
|
|
12533
|
+
*/
|
|
12534
|
+
async _buildPipeline() {
|
|
12535
|
+
if (!this.inputTexture && !this.upscaledTexture) {
|
|
11206
12536
|
return;
|
|
11207
12537
|
}
|
|
11208
|
-
const
|
|
11209
|
-
const
|
|
11210
|
-
this.
|
|
11211
|
-
|
|
11212
|
-
|
|
11213
|
-
|
|
12538
|
+
const { device } = this.engine;
|
|
12539
|
+
const needsUpscaling = this._needsUpscaling();
|
|
12540
|
+
const effectiveInput = (this.crtEnabled || this.upscaleEnabled) && needsUpscaling && this.upscaledTexture ? this.upscaledTexture : this.inputTexture;
|
|
12541
|
+
if (!effectiveInput) return;
|
|
12542
|
+
const bindGroupLayout = device.createBindGroupLayout({
|
|
12543
|
+
label: "CRT Bind Group Layout",
|
|
12544
|
+
entries: [
|
|
12545
|
+
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
12546
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
12547
|
+
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } },
|
|
12548
|
+
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } },
|
|
12549
|
+
{ binding: 4, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
12550
|
+
{ binding: 5, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } }
|
|
12551
|
+
]
|
|
11214
12552
|
});
|
|
11215
|
-
|
|
12553
|
+
const uniformBuffer = device.createBuffer({
|
|
12554
|
+
label: "CRT Uniforms",
|
|
12555
|
+
size: 128,
|
|
12556
|
+
// Padded for alignment
|
|
12557
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
12558
|
+
});
|
|
12559
|
+
const bindGroup = device.createBindGroup({
|
|
12560
|
+
label: "CRT Bind Group",
|
|
12561
|
+
layout: bindGroupLayout,
|
|
12562
|
+
entries: [
|
|
12563
|
+
{ binding: 0, resource: { buffer: uniformBuffer } },
|
|
12564
|
+
{ binding: 1, resource: effectiveInput.view },
|
|
12565
|
+
{ binding: 2, resource: this.linearSampler },
|
|
12566
|
+
{ binding: 3, resource: this.nearestSampler },
|
|
12567
|
+
{ binding: 4, resource: this.phosphorMaskTexture.view },
|
|
12568
|
+
{ binding: 5, resource: this.phosphorMaskTexture.sampler }
|
|
12569
|
+
]
|
|
12570
|
+
});
|
|
12571
|
+
const pipelineLayout = device.createPipelineLayout({
|
|
12572
|
+
label: "CRT Pipeline Layout",
|
|
12573
|
+
bindGroupLayouts: [bindGroupLayout]
|
|
12574
|
+
});
|
|
12575
|
+
const shaderModule = device.createShaderModule({
|
|
12576
|
+
label: "CRT Shader",
|
|
12577
|
+
code: crt_default
|
|
12578
|
+
});
|
|
12579
|
+
const pipeline = device.createRenderPipeline({
|
|
12580
|
+
label: "CRT Pipeline",
|
|
12581
|
+
layout: pipelineLayout,
|
|
12582
|
+
vertex: {
|
|
12583
|
+
module: shaderModule,
|
|
12584
|
+
entryPoint: "vertexMain"
|
|
12585
|
+
},
|
|
12586
|
+
fragment: {
|
|
12587
|
+
module: shaderModule,
|
|
12588
|
+
entryPoint: "fragmentMain",
|
|
12589
|
+
targets: [{
|
|
12590
|
+
format: navigator.gpu.getPreferredCanvasFormat()
|
|
12591
|
+
}]
|
|
12592
|
+
},
|
|
12593
|
+
primitive: {
|
|
12594
|
+
topology: "triangle-list"
|
|
12595
|
+
}
|
|
12596
|
+
});
|
|
12597
|
+
this.pipeline = {
|
|
12598
|
+
pipeline,
|
|
12599
|
+
bindGroup,
|
|
12600
|
+
uniformBuffer
|
|
12601
|
+
};
|
|
12602
|
+
this._pipelineInputTexture = this.inputTexture;
|
|
12603
|
+
this._pipelineUpscaledTexture = needsUpscaling ? this.upscaledTexture : null;
|
|
12604
|
+
this._needsRebuild = false;
|
|
12605
|
+
}
|
|
12606
|
+
/**
|
|
12607
|
+
* Upscale the input texture using nearest-neighbor filtering
|
|
12608
|
+
*/
|
|
12609
|
+
_upscaleInput() {
|
|
12610
|
+
if (!this.inputTexture || !this.upscaledTexture) return;
|
|
12611
|
+
const { device } = this.engine;
|
|
12612
|
+
const commandEncoder = device.createCommandEncoder({ label: "CRT Upscale" });
|
|
12613
|
+
if (this.inputTexture.width === this.upscaledTexture.width && this.inputTexture.height === this.upscaledTexture.height) {
|
|
12614
|
+
commandEncoder.copyTextureToTexture(
|
|
12615
|
+
{ texture: this.inputTexture.texture },
|
|
12616
|
+
{ texture: this.upscaledTexture.texture },
|
|
12617
|
+
[this.inputTexture.width, this.inputTexture.height]
|
|
12618
|
+
);
|
|
12619
|
+
} else {
|
|
12620
|
+
if (!this._blitPipeline || this._blitInputTexture !== this.inputTexture) {
|
|
12621
|
+
this._createBlitPipeline();
|
|
12622
|
+
}
|
|
12623
|
+
if (this._blitPipeline) {
|
|
12624
|
+
const renderPass = commandEncoder.beginRenderPass({
|
|
12625
|
+
colorAttachments: [{
|
|
12626
|
+
view: this.upscaledTexture.view,
|
|
12627
|
+
loadOp: "clear",
|
|
12628
|
+
storeOp: "store",
|
|
12629
|
+
clearValue: { r: 0, g: 0, b: 0, a: 1 }
|
|
12630
|
+
}]
|
|
12631
|
+
});
|
|
12632
|
+
renderPass.setPipeline(this._blitPipeline.pipeline);
|
|
12633
|
+
renderPass.setBindGroup(0, this._blitPipeline.bindGroup);
|
|
12634
|
+
renderPass.draw(3, 1, 0, 0);
|
|
12635
|
+
renderPass.end();
|
|
12636
|
+
}
|
|
12637
|
+
}
|
|
12638
|
+
device.queue.submit([commandEncoder.finish()]);
|
|
12639
|
+
}
|
|
12640
|
+
/**
|
|
12641
|
+
* Create a simple nearest-neighbor blit pipeline
|
|
12642
|
+
*/
|
|
12643
|
+
_createBlitPipeline() {
|
|
12644
|
+
if (!this.inputTexture) return;
|
|
12645
|
+
const { device } = this.engine;
|
|
12646
|
+
this._blitInputTexture = this.inputTexture;
|
|
12647
|
+
const blitShader = `
|
|
12648
|
+
struct VertexOutput {
|
|
12649
|
+
@builtin(position) position: vec4f,
|
|
12650
|
+
@location(0) uv: vec2f,
|
|
12651
|
+
}
|
|
12652
|
+
|
|
12653
|
+
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
12654
|
+
@group(0) @binding(1) var inputSampler: sampler;
|
|
12655
|
+
|
|
12656
|
+
@vertex
|
|
12657
|
+
fn vertexMain(@builtin(vertex_index) idx: u32) -> VertexOutput {
|
|
12658
|
+
var output: VertexOutput;
|
|
12659
|
+
let x = f32(idx & 1u) * 4.0 - 1.0;
|
|
12660
|
+
let y = f32(idx >> 1u) * 4.0 - 1.0;
|
|
12661
|
+
output.position = vec4f(x, y, 0.0, 1.0);
|
|
12662
|
+
output.uv = vec2f((x + 1.0) * 0.5, (1.0 - y) * 0.5);
|
|
12663
|
+
return output;
|
|
12664
|
+
}
|
|
12665
|
+
|
|
12666
|
+
@fragment
|
|
12667
|
+
fn fragmentMain(input: VertexOutput) -> @location(0) vec4f {
|
|
12668
|
+
return textureSample(inputTexture, inputSampler, input.uv);
|
|
12669
|
+
}
|
|
12670
|
+
`;
|
|
12671
|
+
const shaderModule = device.createShaderModule({
|
|
12672
|
+
label: "Blit Shader",
|
|
12673
|
+
code: blitShader
|
|
12674
|
+
});
|
|
12675
|
+
const bindGroupLayout = device.createBindGroupLayout({
|
|
12676
|
+
label: "Blit Bind Group Layout",
|
|
12677
|
+
entries: [
|
|
12678
|
+
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
12679
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } }
|
|
12680
|
+
]
|
|
12681
|
+
});
|
|
12682
|
+
const bindGroup = device.createBindGroup({
|
|
12683
|
+
label: "Blit Bind Group",
|
|
12684
|
+
layout: bindGroupLayout,
|
|
12685
|
+
entries: [
|
|
12686
|
+
{ binding: 0, resource: this.inputTexture.view },
|
|
12687
|
+
{ binding: 1, resource: this.nearestSampler }
|
|
12688
|
+
]
|
|
12689
|
+
});
|
|
12690
|
+
const pipeline = device.createRenderPipeline({
|
|
12691
|
+
label: "Blit Pipeline",
|
|
12692
|
+
layout: device.createPipelineLayout({ bindGroupLayouts: [bindGroupLayout] }),
|
|
12693
|
+
vertex: {
|
|
12694
|
+
module: shaderModule,
|
|
12695
|
+
entryPoint: "vertexMain"
|
|
12696
|
+
},
|
|
12697
|
+
fragment: {
|
|
12698
|
+
module: shaderModule,
|
|
12699
|
+
entryPoint: "fragmentMain",
|
|
12700
|
+
targets: [{ format: "rgba8unorm" }]
|
|
12701
|
+
},
|
|
12702
|
+
primitive: { topology: "triangle-list" }
|
|
12703
|
+
});
|
|
12704
|
+
this._blitPipeline = { pipeline, bindGroup };
|
|
12705
|
+
}
|
|
12706
|
+
async _execute(context) {
|
|
12707
|
+
const { device, canvas } = this.engine;
|
|
12708
|
+
const needsUpscaling = this._needsUpscaling();
|
|
12709
|
+
const effectiveUpscaledTexture = needsUpscaling ? this.upscaledTexture : null;
|
|
12710
|
+
if (this.pipeline && (this._pipelineInputTexture !== this.inputTexture || this._pipelineUpscaledTexture !== effectiveUpscaledTexture)) {
|
|
12711
|
+
this._needsRebuild = true;
|
|
12712
|
+
this._blitPipeline = null;
|
|
12713
|
+
}
|
|
12714
|
+
if (!this.crtEnabled && !this.upscaleEnabled) {
|
|
12715
|
+
if (!this.inputTexture) return;
|
|
12716
|
+
if (this._needsRebuild || !this.pipeline) {
|
|
12717
|
+
await this._buildPipeline();
|
|
12718
|
+
}
|
|
12719
|
+
} else {
|
|
12720
|
+
if (needsUpscaling) {
|
|
12721
|
+
if (this._needsUpscaleRebuild) {
|
|
12722
|
+
await this._createUpscaledTexture();
|
|
12723
|
+
this._needsUpscaleRebuild = false;
|
|
12724
|
+
}
|
|
12725
|
+
if (this.inputTexture && this.upscaledTexture) {
|
|
12726
|
+
this._upscaleInput();
|
|
12727
|
+
}
|
|
12728
|
+
}
|
|
12729
|
+
if (this._needsRebuild || !this.pipeline) {
|
|
12730
|
+
await this._buildPipeline();
|
|
12731
|
+
}
|
|
12732
|
+
}
|
|
12733
|
+
if (!this.pipeline) return;
|
|
12734
|
+
const uniformData = new Float32Array(32);
|
|
12735
|
+
uniformData[0] = this.canvasWidth;
|
|
12736
|
+
uniformData[1] = this.canvasHeight;
|
|
12737
|
+
const inputW = needsUpscaling && this.upscaledTexture ? this.upscaledTexture.width : this.inputTexture?.width || this.canvasWidth;
|
|
12738
|
+
const inputH = needsUpscaling && this.upscaledTexture ? this.upscaledTexture.height : this.inputTexture?.height || this.canvasHeight;
|
|
12739
|
+
uniformData[2] = inputW;
|
|
12740
|
+
uniformData[3] = inputH;
|
|
12741
|
+
const renderW = this.renderWidth || this.canvasWidth;
|
|
12742
|
+
const renderH = this.renderHeight || this.canvasHeight;
|
|
12743
|
+
uniformData[4] = renderW;
|
|
12744
|
+
uniformData[5] = renderH;
|
|
12745
|
+
if (!this._loggedDimensions) {
|
|
12746
|
+
const renderScale = renderW > 0 ? (renderW / this.canvasWidth).toFixed(2) : "?";
|
|
12747
|
+
const upscaleInfo = needsUpscaling ? `upscale=${this.upscaledTexture?.scale || "?"}x` : "no-upscale (direct)";
|
|
12748
|
+
console.log(`CRT: canvas=${this.canvasWidth}x${this.canvasHeight}, render=${renderW}x${renderH} (scale ${renderScale}), input=${inputW}x${inputH}, ${upscaleInfo}`);
|
|
12749
|
+
console.log(`CRT: Scanlines use fragment coords (${this.canvasHeight}px), should repeat every ${this.scanlineHeight}px = ${Math.floor(this.canvasHeight / this.scanlineHeight)} scanlines`);
|
|
12750
|
+
this._loggedDimensions = true;
|
|
12751
|
+
}
|
|
12752
|
+
uniformData[6] = this.curvature;
|
|
12753
|
+
uniformData[7] = this.cornerRadius;
|
|
12754
|
+
uniformData[8] = this.zoom;
|
|
12755
|
+
uniformData[9] = 0;
|
|
12756
|
+
uniformData[10] = this.scanlineIntensity;
|
|
12757
|
+
uniformData[11] = this.scanlineWidth;
|
|
12758
|
+
uniformData[12] = this.scanlineBrightBoost;
|
|
12759
|
+
uniformData[13] = this.scanlineHeight;
|
|
12760
|
+
uniformData[14] = 0;
|
|
12761
|
+
uniformData[15] = 0;
|
|
12762
|
+
const conv = this.convergence;
|
|
12763
|
+
uniformData[16] = conv[0];
|
|
12764
|
+
uniformData[17] = conv[1];
|
|
12765
|
+
uniformData[18] = conv[2];
|
|
12766
|
+
uniformData[19] = 0;
|
|
12767
|
+
uniformData[20] = this.maskType;
|
|
12768
|
+
uniformData[21] = this.maskIntensity;
|
|
12769
|
+
uniformData[22] = this.maskScale;
|
|
12770
|
+
const maskCompensation = this._calculateMaskCompensation(this.maskType, this.maskIntensity);
|
|
12771
|
+
uniformData[23] = maskCompensation;
|
|
12772
|
+
uniformData[24] = this.vignetteIntensity;
|
|
12773
|
+
uniformData[25] = this.vignetteSize;
|
|
12774
|
+
uniformData[26] = this.blurSize;
|
|
12775
|
+
uniformData[27] = this.crtEnabled ? 1 : 0;
|
|
12776
|
+
uniformData[28] = this.upscaleEnabled ? 1 : 0;
|
|
12777
|
+
device.queue.writeBuffer(this.pipeline.uniformBuffer, 0, uniformData);
|
|
12778
|
+
const commandEncoder = device.createCommandEncoder({ label: "CRT Render" });
|
|
12779
|
+
const canvasTexture = this.engine.context.getCurrentTexture();
|
|
12780
|
+
const renderPass = commandEncoder.beginRenderPass({
|
|
12781
|
+
colorAttachments: [{
|
|
12782
|
+
view: canvasTexture.createView(),
|
|
12783
|
+
loadOp: "clear",
|
|
12784
|
+
storeOp: "store",
|
|
12785
|
+
clearValue: { r: 0, g: 0, b: 0, a: 1 }
|
|
12786
|
+
}]
|
|
12787
|
+
});
|
|
12788
|
+
renderPass.setPipeline(this.pipeline.pipeline);
|
|
12789
|
+
renderPass.setBindGroup(0, this.pipeline.bindGroup);
|
|
12790
|
+
renderPass.draw(3, 1, 0, 0);
|
|
12791
|
+
renderPass.end();
|
|
12792
|
+
device.queue.submit([commandEncoder.finish()]);
|
|
11216
12793
|
}
|
|
11217
12794
|
async _resize(width, height) {
|
|
12795
|
+
this.canvasWidth = width;
|
|
12796
|
+
this.canvasHeight = height;
|
|
12797
|
+
this._needsUpscaleRebuild = true;
|
|
11218
12798
|
this._needsRebuild = true;
|
|
12799
|
+
this._loggedDimensions = false;
|
|
11219
12800
|
}
|
|
11220
12801
|
_destroy() {
|
|
11221
|
-
this.pipeline
|
|
11222
|
-
|
|
11223
|
-
this.dummyBloomTexture.texture.destroy();
|
|
11224
|
-
this.dummyBloomTexture = null;
|
|
12802
|
+
if (this.pipeline?.uniformBuffer) {
|
|
12803
|
+
this.pipeline.uniformBuffer.destroy();
|
|
11225
12804
|
}
|
|
11226
|
-
if (this.
|
|
11227
|
-
this.
|
|
11228
|
-
this.guiTexture = null;
|
|
12805
|
+
if (this.upscaledTexture?.texture) {
|
|
12806
|
+
this.upscaledTexture.texture.destroy();
|
|
11229
12807
|
}
|
|
11230
|
-
if (this.
|
|
11231
|
-
this.
|
|
11232
|
-
this.dummyGuiTexture = null;
|
|
12808
|
+
if (this.phosphorMaskTexture?.texture) {
|
|
12809
|
+
this.phosphorMaskTexture.texture.destroy();
|
|
11233
12810
|
}
|
|
12811
|
+
this.pipeline = null;
|
|
12812
|
+
this._blitPipeline = null;
|
|
11234
12813
|
}
|
|
11235
12814
|
}
|
|
11236
12815
|
class AmbientCapturePass extends BasePass {
|
|
@@ -13394,8 +14973,10 @@ class RenderGraph {
|
|
|
13394
14973
|
// Pass 11: Forward transparent objects
|
|
13395
14974
|
particles: null,
|
|
13396
14975
|
// Pass 12: GPU particle rendering
|
|
13397
|
-
postProcess: null
|
|
14976
|
+
postProcess: null,
|
|
13398
14977
|
// Pass 13: Tone mapping + bloom composite
|
|
14978
|
+
crt: null
|
|
14979
|
+
// Pass 14: CRT effect (optional)
|
|
13399
14980
|
};
|
|
13400
14981
|
this.historyManager = null;
|
|
13401
14982
|
this.cullingSystem = new CullingSystem(engine);
|
|
@@ -13468,7 +15049,9 @@ class RenderGraph {
|
|
|
13468
15049
|
this.passes.transparent = new TransparentPass(this.engine);
|
|
13469
15050
|
this.passes.particles = new ParticlePass(this.engine);
|
|
13470
15051
|
this.passes.fog = new FogPass(this.engine);
|
|
15052
|
+
this.passes.volumetricFog = new VolumetricFogPass(this.engine);
|
|
13471
15053
|
this.passes.postProcess = new PostProcessPass(this.engine);
|
|
15054
|
+
this.passes.crt = new CRTPass(this.engine);
|
|
13472
15055
|
const { canvas } = this.engine;
|
|
13473
15056
|
this.historyManager = new HistoryBufferManager(this.engine);
|
|
13474
15057
|
start = performance.now();
|
|
@@ -13520,9 +15103,15 @@ class RenderGraph {
|
|
|
13520
15103
|
await this.passes.fog.initialize();
|
|
13521
15104
|
timings.push({ name: "init:fog", time: performance.now() - start });
|
|
13522
15105
|
start = performance.now();
|
|
15106
|
+
await this.passes.volumetricFog.initialize();
|
|
15107
|
+
timings.push({ name: "init:volumetricFog", time: performance.now() - start });
|
|
15108
|
+
start = performance.now();
|
|
13523
15109
|
await this.passes.postProcess.initialize();
|
|
13524
15110
|
timings.push({ name: "init:postProcess", time: performance.now() - start });
|
|
13525
15111
|
start = performance.now();
|
|
15112
|
+
await this.passes.crt.initialize();
|
|
15113
|
+
timings.push({ name: "init:crt", time: performance.now() - start });
|
|
15114
|
+
start = performance.now();
|
|
13526
15115
|
this.passes.reflection.setFallbackEnvironment(environmentMap, this.environmentEncoding);
|
|
13527
15116
|
this.passes.lighting.setEnvironmentMap(environmentMap, this.environmentEncoding);
|
|
13528
15117
|
await this.passes.lighting.setGBuffer(this.passes.gbuffer.getGBuffer());
|
|
@@ -13586,11 +15175,16 @@ class RenderGraph {
|
|
|
13586
15175
|
this.passes.particles.setShadowPass(this.passes.shadow);
|
|
13587
15176
|
this.passes.particles.setEnvironmentMap(environmentMap, this.environmentEncoding);
|
|
13588
15177
|
this.passes.particles.setLightingPass(this.passes.lighting);
|
|
15178
|
+
this.passes.volumetricFog.setGBuffer(this.passes.gbuffer.getGBuffer());
|
|
15179
|
+
this.passes.volumetricFog.setShadowPass(this.passes.shadow);
|
|
15180
|
+
this.passes.volumetricFog.setLightingPass(this.passes.lighting);
|
|
15181
|
+
this.passes.volumetricFog.setHiZPass(this.passes.hiz);
|
|
13589
15182
|
this.passes.postProcess.setInputTexture(this.passes.lighting.getOutputTexture());
|
|
13590
15183
|
this.passes.postProcess.setNoise(this.noiseTexture, this.noiseSize, this.noiseAnimated);
|
|
13591
15184
|
if (this.engine?.guiCanvas) {
|
|
13592
15185
|
this.passes.postProcess.setGuiCanvas(this.engine.guiCanvas);
|
|
13593
15186
|
}
|
|
15187
|
+
this.invalidateOcclusionCulling();
|
|
13594
15188
|
}
|
|
13595
15189
|
/**
|
|
13596
15190
|
* Render a frame using the new entity/asset system
|
|
@@ -13843,10 +15437,6 @@ class RenderGraph {
|
|
|
13843
15437
|
this.passes.transparent.distanceFadeStart = transparentMaxDist * transparentFadeStart;
|
|
13844
15438
|
await this.passes.transparent.execute(passContext);
|
|
13845
15439
|
}
|
|
13846
|
-
if (this.passes.particles && this.particleSystem.getActiveEmitters().length > 0) {
|
|
13847
|
-
this.passes.particles.setOutputTexture(hdrSource);
|
|
13848
|
-
await this.passes.particles.execute(passContext);
|
|
13849
|
-
}
|
|
13850
15440
|
const fogEnabled = this.engine?.settings?.environment?.fog?.enabled;
|
|
13851
15441
|
if (this.passes.fog && fogEnabled) {
|
|
13852
15442
|
this.passes.fog.setInputTexture(hdrSource);
|
|
@@ -13857,6 +15447,20 @@ class RenderGraph {
|
|
|
13857
15447
|
hdrSource = fogOutput;
|
|
13858
15448
|
}
|
|
13859
15449
|
}
|
|
15450
|
+
if (this.passes.particles && this.particleSystem.getActiveEmitters().length > 0) {
|
|
15451
|
+
this.passes.particles.setOutputTexture(hdrSource);
|
|
15452
|
+
await this.passes.particles.execute(passContext);
|
|
15453
|
+
}
|
|
15454
|
+
const volumetricFogEnabled = this.engine?.settings?.volumetricFog?.enabled;
|
|
15455
|
+
if (this.passes.volumetricFog && volumetricFogEnabled) {
|
|
15456
|
+
this.passes.volumetricFog.setInputTexture(hdrSource);
|
|
15457
|
+
this.passes.volumetricFog.setGBuffer(gbuffer);
|
|
15458
|
+
await this.passes.volumetricFog.execute(passContext);
|
|
15459
|
+
const volFogOutput = this.passes.volumetricFog.getOutputTexture();
|
|
15460
|
+
if (volFogOutput && volFogOutput !== hdrSource) {
|
|
15461
|
+
hdrSource = volFogOutput;
|
|
15462
|
+
}
|
|
15463
|
+
}
|
|
13860
15464
|
const bloomEnabled = this.engine?.settings?.bloom?.enabled;
|
|
13861
15465
|
if (this.passes.bloom && bloomEnabled) {
|
|
13862
15466
|
this.passes.bloom.setInputTexture(hdrSource);
|
|
@@ -13867,6 +15471,19 @@ class RenderGraph {
|
|
|
13867
15471
|
}
|
|
13868
15472
|
this.passes.postProcess.setInputTexture(hdrSource);
|
|
13869
15473
|
await this.passes.postProcess.execute(passContext);
|
|
15474
|
+
const crtEnabled = this.engine?.settings?.crt?.enabled;
|
|
15475
|
+
const crtUpscaleEnabled = this.engine?.settings?.crt?.upscaleEnabled;
|
|
15476
|
+
if (crtEnabled || crtUpscaleEnabled) {
|
|
15477
|
+
const postProcessOutput = this.passes.postProcess.getOutputTexture();
|
|
15478
|
+
if (postProcessOutput) {
|
|
15479
|
+
this.passes.crt.setInputTexture(postProcessOutput);
|
|
15480
|
+
this.passes.crt.setRenderSize(
|
|
15481
|
+
this.passes.gbuffer.getGBuffer()?.depth?.width || canvas.width,
|
|
15482
|
+
this.passes.gbuffer.getGBuffer()?.depth?.height || canvas.height
|
|
15483
|
+
);
|
|
15484
|
+
await this.passes.crt.execute(passContext);
|
|
15485
|
+
}
|
|
15486
|
+
}
|
|
13870
15487
|
this.historyManager.swap(camera);
|
|
13871
15488
|
this._lastRenderContext = {
|
|
13872
15489
|
meshes,
|
|
@@ -13931,6 +15548,40 @@ class RenderGraph {
|
|
|
13931
15548
|
}
|
|
13932
15549
|
}
|
|
13933
15550
|
const asset = assetManager.get(modelId);
|
|
15551
|
+
if (asset?.meshNames && !asset.mesh) {
|
|
15552
|
+
for (const meshName of asset.meshNames) {
|
|
15553
|
+
const submeshId = assetManager.createModelId(modelId, meshName);
|
|
15554
|
+
const submeshAsset = assetManager.get(submeshId);
|
|
15555
|
+
if (!submeshAsset?.mesh) continue;
|
|
15556
|
+
if (submeshAsset.hasSkin && submeshAsset.skin) {
|
|
15557
|
+
for (const item of entities) {
|
|
15558
|
+
const entity = item.entity;
|
|
15559
|
+
const animation = entity.animation || "default";
|
|
15560
|
+
const phase = entity.phase || 0;
|
|
15561
|
+
const quantizedPhase = Math.floor(phase / 0.05) * 0.05;
|
|
15562
|
+
const key = `${submeshId}|${animation}|${quantizedPhase.toFixed(2)}`;
|
|
15563
|
+
if (!skinnedInstancedGroups.has(key)) {
|
|
15564
|
+
skinnedInstancedGroups.set(key, {
|
|
15565
|
+
modelId: submeshId,
|
|
15566
|
+
animation,
|
|
15567
|
+
phase: quantizedPhase,
|
|
15568
|
+
asset: submeshAsset,
|
|
15569
|
+
entities: []
|
|
15570
|
+
});
|
|
15571
|
+
}
|
|
15572
|
+
skinnedInstancedGroups.get(key).entities.push(item);
|
|
15573
|
+
}
|
|
15574
|
+
} else {
|
|
15575
|
+
if (!nonSkinnedGroups.has(submeshId)) {
|
|
15576
|
+
nonSkinnedGroups.set(submeshId, { asset: submeshAsset, entities: [] });
|
|
15577
|
+
}
|
|
15578
|
+
for (const item of entities) {
|
|
15579
|
+
nonSkinnedGroups.get(submeshId).entities.push(item);
|
|
15580
|
+
}
|
|
15581
|
+
}
|
|
15582
|
+
}
|
|
15583
|
+
continue;
|
|
15584
|
+
}
|
|
13934
15585
|
if (!asset?.mesh) continue;
|
|
13935
15586
|
if (asset.hasSkin && asset.skin) {
|
|
13936
15587
|
for (const item of entities) {
|
|
@@ -14028,7 +15679,8 @@ class RenderGraph {
|
|
|
14028
15679
|
geometry._instanceDataDirty = true;
|
|
14029
15680
|
updatedMeshes.add(meshName);
|
|
14030
15681
|
}
|
|
14031
|
-
const
|
|
15682
|
+
const animationSpeed = this.engine?.settings?.animation?.speed ?? 1;
|
|
15683
|
+
const globalTime = performance.now() / 1e3 * animationSpeed;
|
|
14032
15684
|
for (const { id: entityId, entity, asset, modelId } of skinnedIndividualEntities) {
|
|
14033
15685
|
const entityAnimation = entity.animation || "default";
|
|
14034
15686
|
const entityPhase = entity.phase || 0;
|
|
@@ -14071,7 +15723,9 @@ class RenderGraph {
|
|
|
14071
15723
|
material: asset.mesh.material,
|
|
14072
15724
|
skin: individualSkin2,
|
|
14073
15725
|
hasSkin: true,
|
|
14074
|
-
uid: `individual_${entityId}
|
|
15726
|
+
uid: `individual_${entityId}`,
|
|
15727
|
+
// Use asset's combined bsphere for culling (all skinned submeshes share one sphere)
|
|
15728
|
+
combinedBsphere: asset.bsphere || null
|
|
14075
15729
|
};
|
|
14076
15730
|
cached = {
|
|
14077
15731
|
skin: individualSkin2,
|
|
@@ -14200,7 +15854,9 @@ class RenderGraph {
|
|
|
14200
15854
|
material: asset.mesh.material,
|
|
14201
15855
|
skin: clonedSkin2,
|
|
14202
15856
|
hasSkin: true,
|
|
14203
|
-
uid: asset.mesh.uid + "_phase_" + key.replace(/[^a-zA-Z0-9]/g, "_")
|
|
15857
|
+
uid: asset.mesh.uid + "_phase_" + key.replace(/[^a-zA-Z0-9]/g, "_"),
|
|
15858
|
+
// Use asset's combined bsphere for culling (all skinned submeshes share one sphere)
|
|
15859
|
+
combinedBsphere: asset.bsphere || null
|
|
14204
15860
|
};
|
|
14205
15861
|
cached = { skin: clonedSkin2, mesh: phaseMesh2, geometry: phaseGeometry2 };
|
|
14206
15862
|
this._skinnedPhaseCache.set(key, cached);
|
|
@@ -14335,45 +15991,62 @@ class RenderGraph {
|
|
|
14335
15991
|
}
|
|
14336
15992
|
/**
|
|
14337
15993
|
* Handle window resize
|
|
14338
|
-
* @param {number} width -
|
|
14339
|
-
* @param {number} height -
|
|
15994
|
+
* @param {number} width - Canvas width (full device pixels)
|
|
15995
|
+
* @param {number} height - Canvas height (full device pixels)
|
|
15996
|
+
* @param {number} renderScale - Scale for internal rendering (1.0 = full resolution)
|
|
14340
15997
|
*/
|
|
14341
|
-
async resize(width, height) {
|
|
15998
|
+
async resize(width, height, renderScale = 1) {
|
|
14342
15999
|
const timings = [];
|
|
14343
16000
|
performance.now();
|
|
16001
|
+
this.canvasWidth = width;
|
|
16002
|
+
this.canvasHeight = height;
|
|
16003
|
+
const renderWidth = Math.max(1, Math.round(width * renderScale));
|
|
16004
|
+
const renderHeight = Math.max(1, Math.round(height * renderScale));
|
|
16005
|
+
this.renderWidth = renderWidth;
|
|
16006
|
+
this.renderHeight = renderHeight;
|
|
16007
|
+
this.renderScale = renderScale;
|
|
14344
16008
|
const autoScale = this.engine?.settings?.rendering?.autoScale;
|
|
14345
16009
|
let effectScale = 1;
|
|
14346
16010
|
if (autoScale && !autoScale.enabled && autoScale.enabledForEffects) {
|
|
14347
|
-
if (
|
|
16011
|
+
if (renderHeight > (autoScale.maxHeight ?? 1536)) {
|
|
14348
16012
|
effectScale = autoScale.scaleFactor ?? 0.5;
|
|
14349
16013
|
if (!this._effectScaleWarned) {
|
|
14350
|
-
console.log(`Effect auto-scale: Reducing effect resolution by ${effectScale} (height: ${
|
|
16014
|
+
console.log(`Effect auto-scale: Reducing effect resolution by ${effectScale} (height: ${renderHeight}px > ${autoScale.maxHeight}px)`);
|
|
14351
16015
|
this._effectScaleWarned = true;
|
|
14352
16016
|
}
|
|
14353
16017
|
} else if (this._effectScaleWarned) {
|
|
14354
|
-
console.log(`Effect auto-scale: Restoring full effect resolution (height: ${
|
|
16018
|
+
console.log(`Effect auto-scale: Restoring full effect resolution (height: ${renderHeight}px <= ${autoScale.maxHeight}px)`);
|
|
14355
16019
|
this._effectScaleWarned = false;
|
|
14356
16020
|
}
|
|
14357
16021
|
}
|
|
14358
|
-
const
|
|
14359
|
-
const
|
|
14360
|
-
const
|
|
16022
|
+
const fullResPasses = /* @__PURE__ */ new Set(["crt"]);
|
|
16023
|
+
const effectScaledPasses = /* @__PURE__ */ new Set(["bloom", "ao", "planarReflection"]);
|
|
16024
|
+
const effectWidth = Math.max(1, Math.floor(renderWidth * effectScale));
|
|
16025
|
+
const effectHeight = Math.max(1, Math.floor(renderHeight * effectScale));
|
|
14361
16026
|
this.effectWidth = effectWidth;
|
|
14362
16027
|
this.effectHeight = effectHeight;
|
|
14363
16028
|
this.effectScale = effectScale;
|
|
14364
16029
|
for (const passName in this.passes) {
|
|
14365
16030
|
if (this.passes[passName]) {
|
|
14366
16031
|
const start2 = performance.now();
|
|
14367
|
-
|
|
14368
|
-
|
|
14369
|
-
|
|
16032
|
+
let w, h;
|
|
16033
|
+
if (fullResPasses.has(passName)) {
|
|
16034
|
+
w = width;
|
|
16035
|
+
h = height;
|
|
16036
|
+
} else if (effectScaledPasses.has(passName) && effectScale < 1) {
|
|
16037
|
+
w = effectWidth;
|
|
16038
|
+
h = effectHeight;
|
|
16039
|
+
} else {
|
|
16040
|
+
w = renderWidth;
|
|
16041
|
+
h = renderHeight;
|
|
16042
|
+
}
|
|
14370
16043
|
await this.passes[passName].resize(w, h);
|
|
14371
16044
|
timings.push({ name: `pass:${passName}`, time: performance.now() - start2 });
|
|
14372
16045
|
}
|
|
14373
16046
|
}
|
|
14374
16047
|
if (this.historyManager) {
|
|
14375
16048
|
const start2 = performance.now();
|
|
14376
|
-
await this.historyManager.resize(
|
|
16049
|
+
await this.historyManager.resize(renderWidth, renderHeight);
|
|
14377
16050
|
timings.push({ name: "historyManager", time: performance.now() - start2 });
|
|
14378
16051
|
}
|
|
14379
16052
|
let start = performance.now();
|
|
@@ -14392,8 +16065,18 @@ class RenderGraph {
|
|
|
14392
16065
|
this.passes.lighting.setHiZPass(this.passes.hiz);
|
|
14393
16066
|
this.passes.transparent.setHiZPass(this.passes.hiz);
|
|
14394
16067
|
this.passes.shadow.setHiZPass(this.passes.hiz);
|
|
16068
|
+
if (this.passes.volumetricFog) {
|
|
16069
|
+
this.passes.volumetricFog.setHiZPass(this.passes.hiz);
|
|
16070
|
+
}
|
|
14395
16071
|
}
|
|
14396
16072
|
timings.push({ name: "rewire:hiz", time: performance.now() - start });
|
|
16073
|
+
if (this.passes.volumetricFog) {
|
|
16074
|
+
start = performance.now();
|
|
16075
|
+
this.passes.volumetricFog.setGBuffer(this.passes.gbuffer.getGBuffer());
|
|
16076
|
+
this.passes.volumetricFog.setShadowPass(this.passes.shadow);
|
|
16077
|
+
this.passes.volumetricFog.setLightingPass(this.passes.lighting);
|
|
16078
|
+
timings.push({ name: "rewire:volumetricFog", time: performance.now() - start });
|
|
16079
|
+
}
|
|
14397
16080
|
start = performance.now();
|
|
14398
16081
|
this.passes.shadow.setNoise(this.noiseTexture, this.noiseSize, this.noiseAnimated);
|
|
14399
16082
|
timings.push({ name: "rewire:shadow.setNoise", time: performance.now() - start });
|
|
@@ -14771,8 +16454,18 @@ class RenderGraph {
|
|
|
14771
16454
|
* @param {string} passName - Name of pass
|
|
14772
16455
|
* @returns {BasePass} The pass instance
|
|
14773
16456
|
*/
|
|
14774
|
-
getPass(passName) {
|
|
14775
|
-
return this.passes[passName];
|
|
16457
|
+
getPass(passName) {
|
|
16458
|
+
return this.passes[passName];
|
|
16459
|
+
}
|
|
16460
|
+
/**
|
|
16461
|
+
* Invalidate occlusion culling data and reset warmup period.
|
|
16462
|
+
* Call this after scene loading or major camera changes to prevent
|
|
16463
|
+
* incorrect occlusion culling with stale depth buffer data.
|
|
16464
|
+
*/
|
|
16465
|
+
invalidateOcclusionCulling() {
|
|
16466
|
+
if (this.passes.hiz) {
|
|
16467
|
+
this.passes.hiz.invalidate();
|
|
16468
|
+
}
|
|
14776
16469
|
}
|
|
14777
16470
|
/**
|
|
14778
16471
|
* Load noise texture based on settings
|
|
@@ -14910,6 +16603,38 @@ class RenderGraph {
|
|
|
14910
16603
|
height: 8
|
|
14911
16604
|
};
|
|
14912
16605
|
}
|
|
16606
|
+
/**
|
|
16607
|
+
* Reload noise texture and update all passes that use it
|
|
16608
|
+
* Called when noise settings change at runtime
|
|
16609
|
+
*/
|
|
16610
|
+
async reloadNoiseTexture() {
|
|
16611
|
+
if (this.noiseTexture?.texture) {
|
|
16612
|
+
this.noiseTexture.texture.destroy();
|
|
16613
|
+
}
|
|
16614
|
+
await this._loadNoiseTexture();
|
|
16615
|
+
if (this.passes.gbuffer) {
|
|
16616
|
+
this.passes.gbuffer.setNoise(this.noiseTexture, this.noiseSize, this.noiseAnimated);
|
|
16617
|
+
}
|
|
16618
|
+
if (this.passes.shadow) {
|
|
16619
|
+
this.passes.shadow.setNoise(this.noiseTexture, this.noiseSize, this.noiseAnimated);
|
|
16620
|
+
}
|
|
16621
|
+
if (this.passes.lighting) {
|
|
16622
|
+
this.passes.lighting.setNoise(this.noiseTexture, this.noiseSize, this.noiseAnimated);
|
|
16623
|
+
}
|
|
16624
|
+
if (this.passes.ao) {
|
|
16625
|
+
this.passes.ao.setNoise(this.noiseTexture, this.noiseSize, this.noiseAnimated);
|
|
16626
|
+
}
|
|
16627
|
+
if (this.passes.transparent) {
|
|
16628
|
+
this.passes.transparent.setNoise(this.noiseTexture, this.noiseSize, this.noiseAnimated);
|
|
16629
|
+
}
|
|
16630
|
+
if (this.passes.postProcess) {
|
|
16631
|
+
this.passes.postProcess.setNoise(this.noiseTexture, this.noiseSize, this.noiseAnimated);
|
|
16632
|
+
}
|
|
16633
|
+
if (this.passes.renderPost) {
|
|
16634
|
+
this.passes.renderPost.setNoise(this.noiseTexture, this.noiseSize, this.noiseAnimated);
|
|
16635
|
+
}
|
|
16636
|
+
console.log(`RenderGraph: Reloaded noise texture (${this.engine?.settings?.noise?.type || "bluenoise"})`);
|
|
16637
|
+
}
|
|
14913
16638
|
/**
|
|
14914
16639
|
* Destroy all resources
|
|
14915
16640
|
*/
|
|
@@ -15537,6 +17262,7 @@ class Joint {
|
|
|
15537
17262
|
function expandTriangles(attributes, expansion) {
|
|
15538
17263
|
const positions = attributes.position;
|
|
15539
17264
|
const normals = attributes.normal;
|
|
17265
|
+
const tangents = attributes.tangent;
|
|
15540
17266
|
const uvs = attributes.uv;
|
|
15541
17267
|
const indices = attributes.indices;
|
|
15542
17268
|
const weights = attributes.weights;
|
|
@@ -15545,6 +17271,7 @@ function expandTriangles(attributes, expansion) {
|
|
|
15545
17271
|
const triCount = indices.length / 3;
|
|
15546
17272
|
const newPositions = new Float32Array(triCount * 3 * 3);
|
|
15547
17273
|
const newNormals = normals ? new Float32Array(triCount * 3 * 3) : null;
|
|
17274
|
+
const newTangents = tangents ? new Float32Array(triCount * 3 * 4) : null;
|
|
15548
17275
|
const newUvs = uvs ? new Float32Array(triCount * 3 * 2) : null;
|
|
15549
17276
|
const newWeights = weights ? new Float32Array(triCount * 3 * 4) : null;
|
|
15550
17277
|
const newJoints = joints ? new Uint16Array(triCount * 3 * 4) : null;
|
|
@@ -15580,6 +17307,12 @@ function expandTriangles(attributes, expansion) {
|
|
|
15580
17307
|
newNormals[newIdx * 3 + 1] = normals[origIdx * 3 + 1];
|
|
15581
17308
|
newNormals[newIdx * 3 + 2] = normals[origIdx * 3 + 2];
|
|
15582
17309
|
}
|
|
17310
|
+
if (newTangents) {
|
|
17311
|
+
newTangents[newIdx * 4 + 0] = tangents[origIdx * 4 + 0];
|
|
17312
|
+
newTangents[newIdx * 4 + 1] = tangents[origIdx * 4 + 1];
|
|
17313
|
+
newTangents[newIdx * 4 + 2] = tangents[origIdx * 4 + 2];
|
|
17314
|
+
newTangents[newIdx * 4 + 3] = tangents[origIdx * 4 + 3];
|
|
17315
|
+
}
|
|
15583
17316
|
if (newUvs) {
|
|
15584
17317
|
newUvs[newIdx * 2 + 0] = uvs[origIdx * 2 + 0];
|
|
15585
17318
|
newUvs[newIdx * 2 + 1] = uvs[origIdx * 2 + 1];
|
|
@@ -15602,6 +17335,7 @@ function expandTriangles(attributes, expansion) {
|
|
|
15602
17335
|
return {
|
|
15603
17336
|
position: newPositions,
|
|
15604
17337
|
normal: newNormals,
|
|
17338
|
+
tangent: newTangents,
|
|
15605
17339
|
uv: newUvs,
|
|
15606
17340
|
indices: newIndices,
|
|
15607
17341
|
weights: newWeights,
|
|
@@ -15817,6 +17551,7 @@ async function loadGltfData(engine, url, options = {}) {
|
|
|
15817
17551
|
let attrs = {
|
|
15818
17552
|
position: getAccessor(attributes.POSITION),
|
|
15819
17553
|
normal: getAccessor(attributes.NORMAL),
|
|
17554
|
+
tangent: getAccessor(attributes.TANGENT),
|
|
15820
17555
|
uv: getAccessor(attributes.TEXCOORD_0),
|
|
15821
17556
|
indices: getAccessor(primitive.indices),
|
|
15822
17557
|
weights: getAccessor(attributes.WEIGHTS_0),
|
|
@@ -15935,6 +17670,9 @@ async function loadGltf(engine, url, options = {}) {
|
|
|
15935
17670
|
const transmission = mesh.material.extensions.KHR_materials_transmission;
|
|
15936
17671
|
material.opacity = 1 - (transmission.transmissionFactor ?? 0);
|
|
15937
17672
|
}
|
|
17673
|
+
if (mesh.material.doubleSided) {
|
|
17674
|
+
material.doubleSided = true;
|
|
17675
|
+
}
|
|
15938
17676
|
const nmesh = new Mesh(geometry, material, name);
|
|
15939
17677
|
if (mesh.skinIndex !== void 0 && mesh.skinIndex !== null && skins[mesh.skinIndex]) {
|
|
15940
17678
|
nmesh.skin = skins[mesh.skinIndex];
|
|
@@ -16444,6 +18182,22 @@ class AssetManager {
|
|
|
16444
18182
|
try {
|
|
16445
18183
|
const result = await loadGltf(this.engine, path, options);
|
|
16446
18184
|
const meshNames = Object.keys(result.meshes);
|
|
18185
|
+
let combinedBsphere = null;
|
|
18186
|
+
const hasAnySkin = Object.values(result.meshes).some((m) => m.hasSkin);
|
|
18187
|
+
if (hasAnySkin) {
|
|
18188
|
+
const allPositions = [];
|
|
18189
|
+
for (const mesh of Object.values(result.meshes)) {
|
|
18190
|
+
if (mesh.geometry?.attributes?.position) {
|
|
18191
|
+
const positions = mesh.geometry.attributes.position;
|
|
18192
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
18193
|
+
allPositions.push(positions[i], positions[i + 1], positions[i + 2]);
|
|
18194
|
+
}
|
|
18195
|
+
}
|
|
18196
|
+
}
|
|
18197
|
+
if (allPositions.length > 0) {
|
|
18198
|
+
combinedBsphere = calculateBoundingSphere(new Float32Array(allPositions));
|
|
18199
|
+
}
|
|
18200
|
+
}
|
|
16447
18201
|
this.assets[path] = {
|
|
16448
18202
|
gltf: result,
|
|
16449
18203
|
meshes: result.meshes,
|
|
@@ -16451,12 +18205,17 @@ class AssetManager {
|
|
|
16451
18205
|
animations: result.animations,
|
|
16452
18206
|
nodes: result.nodes,
|
|
16453
18207
|
meshNames,
|
|
18208
|
+
bsphere: combinedBsphere,
|
|
18209
|
+
// Combined bsphere for parent path entities
|
|
18210
|
+
hasSkin: hasAnySkin,
|
|
18211
|
+
// Flag for skinned model detection
|
|
16454
18212
|
ready: true,
|
|
16455
18213
|
loading: false
|
|
16456
18214
|
};
|
|
16457
18215
|
for (const meshName of meshNames) {
|
|
16458
|
-
const
|
|
16459
|
-
|
|
18216
|
+
const mesh = result.meshes[meshName];
|
|
18217
|
+
const bsphere = hasAnySkin && combinedBsphere ? combinedBsphere : null;
|
|
18218
|
+
await this._registerMesh(path, meshName, mesh, bsphere);
|
|
16460
18219
|
}
|
|
16461
18220
|
this._triggerReady(path);
|
|
16462
18221
|
return this.assets[path];
|
|
@@ -16472,10 +18231,14 @@ class AssetManager {
|
|
|
16472
18231
|
}
|
|
16473
18232
|
/**
|
|
16474
18233
|
* Register a mesh asset (internal)
|
|
18234
|
+
* @param {string} path - GLTF file path
|
|
18235
|
+
* @param {string} meshName - Mesh name
|
|
18236
|
+
* @param {Object} mesh - Mesh object
|
|
18237
|
+
* @param {Object|null} overrideBsphere - Optional bounding sphere (for skinned mesh combined sphere)
|
|
16475
18238
|
*/
|
|
16476
|
-
async _registerMesh(path, meshName, mesh) {
|
|
18239
|
+
async _registerMesh(path, meshName, mesh, overrideBsphere = null) {
|
|
16477
18240
|
const modelId = this.createModelId(path, meshName);
|
|
16478
|
-
const bsphere = calculateBoundingSphere(mesh.geometry.attributes.position);
|
|
18241
|
+
const bsphere = overrideBsphere || calculateBoundingSphere(mesh.geometry.attributes.position);
|
|
16479
18242
|
this.assets[modelId] = {
|
|
16480
18243
|
mesh,
|
|
16481
18244
|
geometry: mesh.geometry,
|
|
@@ -16672,19 +18435,20 @@ class DebugUI {
|
|
|
16672
18435
|
this._addStyles();
|
|
16673
18436
|
this._createStatsFolder();
|
|
16674
18437
|
this._createRenderingFolder();
|
|
16675
|
-
this.
|
|
16676
|
-
this._createShadowFolder();
|
|
16677
|
-
this._createMainLightFolder();
|
|
18438
|
+
this._createCameraFolder();
|
|
16678
18439
|
this._createEnvironmentFolder();
|
|
16679
18440
|
this._createLightingFolder();
|
|
16680
|
-
this.
|
|
16681
|
-
this.
|
|
16682
|
-
this._createBloomFolder();
|
|
18441
|
+
this._createMainLightFolder();
|
|
18442
|
+
this._createShadowFolder();
|
|
16683
18443
|
this._createPlanarReflectionFolder();
|
|
18444
|
+
this._createAOFolder();
|
|
16684
18445
|
this._createAmbientCaptureFolder();
|
|
16685
|
-
this.
|
|
18446
|
+
this._createSSGIFolder();
|
|
18447
|
+
this._createVolumetricFogFolder();
|
|
18448
|
+
this._createBloomFolder();
|
|
18449
|
+
this._createTonemapFolder();
|
|
16686
18450
|
this._createDitheringFolder();
|
|
16687
|
-
this.
|
|
18451
|
+
this._createCRTFolder();
|
|
16688
18452
|
this._createDebugFolder();
|
|
16689
18453
|
for (const folder of Object.values(this.folders)) {
|
|
16690
18454
|
folder.close();
|
|
@@ -16828,6 +18592,33 @@ Lights: ${fmt(visibleLights)} (occ:${fmt(lightOccCulled)})`;
|
|
|
16828
18592
|
this.gui.domElement.style.display = "none";
|
|
16829
18593
|
}
|
|
16830
18594
|
});
|
|
18595
|
+
if (s.culling) {
|
|
18596
|
+
const cullFolder = folder.addFolder("Culling");
|
|
18597
|
+
cullFolder.add(s.culling, "frustumEnabled").name("Frustum Culling");
|
|
18598
|
+
if (s.occlusionCulling) {
|
|
18599
|
+
cullFolder.add(s.occlusionCulling, "enabled").name("Occlusion Culling");
|
|
18600
|
+
cullFolder.add(s.occlusionCulling, "threshold", 0.1, 2, 0.1).name("Occlusion Threshold");
|
|
18601
|
+
}
|
|
18602
|
+
if (s.culling.planarReflection) {
|
|
18603
|
+
const prFolder = cullFolder.addFolder("Planar Reflection");
|
|
18604
|
+
prFolder.add(s.culling.planarReflection, "frustum").name("Frustum Culling");
|
|
18605
|
+
prFolder.add(s.culling.planarReflection, "maxDistance", 10, 200, 10).name("Max Distance");
|
|
18606
|
+
prFolder.add(s.culling.planarReflection, "maxSkinned", 0, 100, 1).name("Max Skinned");
|
|
18607
|
+
prFolder.add(s.culling.planarReflection, "minPixelSize", 0, 16, 1).name("Min Pixel Size");
|
|
18608
|
+
prFolder.close();
|
|
18609
|
+
}
|
|
18610
|
+
cullFolder.close();
|
|
18611
|
+
}
|
|
18612
|
+
if (s.noise) {
|
|
18613
|
+
const noiseFolder = folder.addFolder("Noise");
|
|
18614
|
+
noiseFolder.add(s.noise, "type", ["bluenoise", "bayer8"]).name("Type").onChange(() => {
|
|
18615
|
+
if (this.engine.renderer?.renderGraph) {
|
|
18616
|
+
this.engine.renderer.renderGraph.reloadNoiseTexture();
|
|
18617
|
+
}
|
|
18618
|
+
});
|
|
18619
|
+
noiseFolder.add(s.noise, "animated").name("Animated");
|
|
18620
|
+
noiseFolder.close();
|
|
18621
|
+
}
|
|
16831
18622
|
}
|
|
16832
18623
|
_createAOFolder() {
|
|
16833
18624
|
const s = this.engine.settings;
|
|
@@ -16906,6 +18697,8 @@ Lights: ${fmt(visibleLights)} (occ:${fmt(lightOccCulled)})`;
|
|
|
16906
18697
|
fogFolder.add(s.environment.fog.heightFade, "0", -100, 100, 1).name("Bottom Y");
|
|
16907
18698
|
fogFolder.add(s.environment.fog.heightFade, "1", -50, 200, 5).name("Top Y");
|
|
16908
18699
|
fogFolder.add(s.environment.fog, "brightResist", 0, 1, 0.05).name("Bright Resist");
|
|
18700
|
+
if (s.environment.fog.debug === void 0) s.environment.fog.debug = 0;
|
|
18701
|
+
fogFolder.add(s.environment.fog, "debug", 0, 10, 1).name("Debug Mode");
|
|
16909
18702
|
}
|
|
16910
18703
|
}
|
|
16911
18704
|
_createLightingFolder() {
|
|
@@ -16922,25 +18715,6 @@ Lights: ${fmt(visibleLights)} (occ:${fmt(lightOccCulled)})`;
|
|
|
16922
18715
|
folder.add(s.lighting, "specularBoostRoughnessCutoff", 0.1, 1, 0.05).name("Boost Roughness Cutoff");
|
|
16923
18716
|
}
|
|
16924
18717
|
}
|
|
16925
|
-
_createCullingFolder() {
|
|
16926
|
-
const s = this.engine.settings;
|
|
16927
|
-
if (!s.culling) return;
|
|
16928
|
-
const folder = this.gui.addFolder("Culling");
|
|
16929
|
-
this.folders.culling = folder;
|
|
16930
|
-
folder.add(s.culling, "frustumEnabled").name("Frustum Culling");
|
|
16931
|
-
if (s.occlusionCulling) {
|
|
16932
|
-
folder.add(s.occlusionCulling, "enabled").name("Occlusion Culling");
|
|
16933
|
-
folder.add(s.occlusionCulling, "threshold", 0.1, 2, 0.1).name("Occlusion Threshold");
|
|
16934
|
-
}
|
|
16935
|
-
if (s.culling.planarReflection) {
|
|
16936
|
-
const prFolder = folder.addFolder("Planar Reflection");
|
|
16937
|
-
prFolder.add(s.culling.planarReflection, "frustum").name("Frustum Culling");
|
|
16938
|
-
prFolder.add(s.culling.planarReflection, "maxDistance", 10, 200, 10).name("Max Distance");
|
|
16939
|
-
prFolder.add(s.culling.planarReflection, "maxSkinned", 0, 100, 1).name("Max Skinned");
|
|
16940
|
-
prFolder.add(s.culling.planarReflection, "minPixelSize", 0, 16, 1).name("Min Pixel Size");
|
|
16941
|
-
prFolder.close();
|
|
16942
|
-
}
|
|
16943
|
-
}
|
|
16944
18718
|
_createSSGIFolder() {
|
|
16945
18719
|
const s = this.engine.settings;
|
|
16946
18720
|
if (!s.ssgi) return;
|
|
@@ -16953,6 +18727,57 @@ Lights: ${fmt(visibleLights)} (occ:${fmt(lightOccCulled)})`;
|
|
|
16953
18727
|
folder.add(s.ssgi, "sampleRadius", 0.5, 4, 0.5).name("Sample Radius");
|
|
16954
18728
|
folder.add(s.ssgi, "saturateLevel", 0.1, 2, 0.1).name("Saturate Level");
|
|
16955
18729
|
}
|
|
18730
|
+
_createVolumetricFogFolder() {
|
|
18731
|
+
const s = this.engine.settings;
|
|
18732
|
+
if (!s.volumetricFog) return;
|
|
18733
|
+
const vf = s.volumetricFog;
|
|
18734
|
+
if (vf.density === void 0 && vf.densityMultiplier === void 0) vf.density = 0.5;
|
|
18735
|
+
if (vf.scatterStrength === void 0) vf.scatterStrength = 1;
|
|
18736
|
+
if (!vf.heightRange) vf.heightRange = [-5, 20];
|
|
18737
|
+
if (vf.resolution === void 0) vf.resolution = 0.25;
|
|
18738
|
+
if (vf.maxSamples === void 0) vf.maxSamples = 32;
|
|
18739
|
+
if (vf.blurRadius === void 0) vf.blurRadius = 4;
|
|
18740
|
+
if (vf.noiseStrength === void 0) vf.noiseStrength = 1;
|
|
18741
|
+
if (vf.noiseScale === void 0) vf.noiseScale = 0.25;
|
|
18742
|
+
if (vf.noiseAnimated === void 0) vf.noiseAnimated = true;
|
|
18743
|
+
if (vf.shadowsEnabled === void 0) vf.shadowsEnabled = true;
|
|
18744
|
+
if (vf.mainLightScatter === void 0) vf.mainLightScatter = 1;
|
|
18745
|
+
if (vf.mainLightScatterDark === void 0) vf.mainLightScatterDark = 3;
|
|
18746
|
+
if (vf.mainLightSaturation === void 0) vf.mainLightSaturation = 1;
|
|
18747
|
+
if (vf.brightnessThreshold === void 0) vf.brightnessThreshold = 1;
|
|
18748
|
+
if (vf.minVisibility === void 0) vf.minVisibility = 0.15;
|
|
18749
|
+
if (vf.skyBrightness === void 0) vf.skyBrightness = 5;
|
|
18750
|
+
if (vf.debug === void 0) vf.debug = 0;
|
|
18751
|
+
const folder = this.gui.addFolder("Volumetric Fog");
|
|
18752
|
+
this.folders.volumetricFog = folder;
|
|
18753
|
+
folder.add(vf, "enabled").name("Enabled");
|
|
18754
|
+
if (vf.density !== void 0) {
|
|
18755
|
+
folder.add(vf, "density", 0, 2, 0.05).name("Density");
|
|
18756
|
+
} else if (vf.densityMultiplier !== void 0) {
|
|
18757
|
+
folder.add(vf, "densityMultiplier", 0, 2, 0.05).name("Density");
|
|
18758
|
+
}
|
|
18759
|
+
folder.add(vf, "scatterStrength", 0, 10, 0.1).name("Scatter (Lights)");
|
|
18760
|
+
folder.add(vf, "mainLightScatter", 0, 5, 0.1).name("Sun Scatter (Light)");
|
|
18761
|
+
folder.add(vf, "mainLightScatterDark", 0, 10, 0.1).name("Sun Scatter (Dark)");
|
|
18762
|
+
folder.add(vf, "mainLightSaturation", 0, 1, 0.01).name("Sun Saturation");
|
|
18763
|
+
folder.add(vf, "brightnessThreshold", 0.1, 5, 0.1).name("Bright Threshold");
|
|
18764
|
+
folder.add(vf, "minVisibility", 0, 1, 0.05).name("Min Visibility");
|
|
18765
|
+
folder.add(vf, "skyBrightness", 0, 10, 0.5).name("Sky Brightness");
|
|
18766
|
+
folder.add(vf.heightRange, "0", -50, 50, 1).name("Height Bottom");
|
|
18767
|
+
folder.add(vf.heightRange, "1", -10, 100, 1).name("Height Top");
|
|
18768
|
+
const qualityFolder = folder.addFolder("Quality");
|
|
18769
|
+
qualityFolder.add(vf, "resolution", 0.125, 0.5, 0.125).name("Resolution");
|
|
18770
|
+
qualityFolder.add(vf, "maxSamples", 16, 128, 8).name("Max Samples");
|
|
18771
|
+
qualityFolder.add(vf, "blurRadius", 0, 8, 1).name("Blur Radius");
|
|
18772
|
+
qualityFolder.close();
|
|
18773
|
+
const noiseFolder = folder.addFolder("Noise");
|
|
18774
|
+
noiseFolder.add(vf, "noiseStrength", 0, 1, 0.1).name("Strength");
|
|
18775
|
+
noiseFolder.add(vf, "noiseScale", 0.05, 1, 0.05).name("Scale (Detail)");
|
|
18776
|
+
noiseFolder.add(vf, "noiseAnimated").name("Animated");
|
|
18777
|
+
noiseFolder.close();
|
|
18778
|
+
folder.add(vf, "shadowsEnabled").name("Shadows");
|
|
18779
|
+
folder.add(vf, "debug", 0, 12, 1).name("Debug Mode");
|
|
18780
|
+
}
|
|
16956
18781
|
_createBloomFolder() {
|
|
16957
18782
|
const s = this.engine.settings;
|
|
16958
18783
|
if (!s.bloom) return;
|
|
@@ -16969,6 +18794,14 @@ Lights: ${fmt(visibleLights)} (occ:${fmt(lightOccCulled)})`;
|
|
|
16969
18794
|
folder.add(s.bloom, "scale", 0.25, 1, 0.25).name("Resolution Scale");
|
|
16970
18795
|
}
|
|
16971
18796
|
}
|
|
18797
|
+
_createTonemapFolder() {
|
|
18798
|
+
const s = this.engine.settings;
|
|
18799
|
+
if (!s.rendering) return;
|
|
18800
|
+
if (s.rendering.tonemapMode === void 0) s.rendering.tonemapMode = 0;
|
|
18801
|
+
const folder = this.gui.addFolder("Tone Mapping");
|
|
18802
|
+
this.folders.tonemap = folder;
|
|
18803
|
+
folder.add(s.rendering, "tonemapMode", { "ACES": 0, "Reinhard": 1, "None (Linear)": 2 }).name("Mode");
|
|
18804
|
+
}
|
|
16972
18805
|
_createPlanarReflectionFolder() {
|
|
16973
18806
|
const s = this.engine.settings;
|
|
16974
18807
|
if (!s.planarReflection) return;
|
|
@@ -16984,7 +18817,7 @@ Lights: ${fmt(visibleLights)} (occ:${fmt(lightOccCulled)})`;
|
|
|
16984
18817
|
_createAmbientCaptureFolder() {
|
|
16985
18818
|
const s = this.engine.settings;
|
|
16986
18819
|
if (!s.ambientCapture) return;
|
|
16987
|
-
const folder = this.gui.addFolder("Ambient Capture");
|
|
18820
|
+
const folder = this.gui.addFolder("Probe GI (Ambient Capture)");
|
|
16988
18821
|
this.folders.ambientCapture = folder;
|
|
16989
18822
|
folder.add(s.ambientCapture, "enabled").name("Enabled");
|
|
16990
18823
|
folder.add(s.ambientCapture, "intensity", 0, 2, 0.05).name("Intensity");
|
|
@@ -16992,14 +18825,6 @@ Lights: ${fmt(visibleLights)} (occ:${fmt(lightOccCulled)})`;
|
|
|
16992
18825
|
folder.add(s.ambientCapture, "emissiveBoost", 0, 10, 0.1).name("Emissive Boost");
|
|
16993
18826
|
folder.add(s.ambientCapture, "saturateLevel", 0, 2, 0.05).name("Saturate Level");
|
|
16994
18827
|
}
|
|
16995
|
-
_createNoiseFolder() {
|
|
16996
|
-
const s = this.engine.settings;
|
|
16997
|
-
if (!s.noise) return;
|
|
16998
|
-
const folder = this.gui.addFolder("Noise");
|
|
16999
|
-
this.folders.noise = folder;
|
|
17000
|
-
folder.add(s.noise, "type", ["bluenoise", "bayer8"]).name("Type");
|
|
17001
|
-
folder.add(s.noise, "animated").name("Animated");
|
|
17002
|
-
}
|
|
17003
18828
|
_createDitheringFolder() {
|
|
17004
18829
|
const s = this.engine.settings;
|
|
17005
18830
|
if (!s.dithering) return;
|
|
@@ -17008,6 +18833,40 @@ Lights: ${fmt(visibleLights)} (occ:${fmt(lightOccCulled)})`;
|
|
|
17008
18833
|
folder.add(s.dithering, "enabled").name("Enabled");
|
|
17009
18834
|
folder.add(s.dithering, "colorLevels", 4, 256, 1).name("Color Levels");
|
|
17010
18835
|
}
|
|
18836
|
+
_createCRTFolder() {
|
|
18837
|
+
const s = this.engine.settings;
|
|
18838
|
+
if (!s.crt) return;
|
|
18839
|
+
const folder = this.gui.addFolder("CRT Effect");
|
|
18840
|
+
this.folders.crt = folder;
|
|
18841
|
+
folder.add(s.crt, "enabled").name("CRT Enabled");
|
|
18842
|
+
folder.add(s.crt, "upscaleEnabled").name("Upscale Only");
|
|
18843
|
+
folder.add(s.crt, "upscaleTarget", 1, 8, 1).name("Upscale Target");
|
|
18844
|
+
const geomFolder = folder.addFolder("Geometry");
|
|
18845
|
+
geomFolder.add(s.crt, "curvature", 0, 0.25, 5e-3).name("Curvature");
|
|
18846
|
+
geomFolder.add(s.crt, "cornerRadius", 0, 0.2, 5e-3).name("Corner Radius");
|
|
18847
|
+
geomFolder.add(s.crt, "zoom", 1, 1.25, 5e-3).name("Zoom");
|
|
18848
|
+
geomFolder.close();
|
|
18849
|
+
const scanFolder = folder.addFolder("Scanlines");
|
|
18850
|
+
scanFolder.add(s.crt, "scanlineIntensity", 0, 1, 0.05).name("Intensity");
|
|
18851
|
+
scanFolder.add(s.crt, "scanlineWidth", 0, 1, 0.05).name("Width");
|
|
18852
|
+
scanFolder.add(s.crt, "scanlineBrightBoost", 0, 2, 0.05).name("Bright Boost");
|
|
18853
|
+
scanFolder.add(s.crt, "scanlineHeight", 1, 10, 1).name("Height (px)");
|
|
18854
|
+
scanFolder.close();
|
|
18855
|
+
const convFolder = folder.addFolder("RGB Convergence");
|
|
18856
|
+
convFolder.add(s.crt.convergence, "0", -3, 3, 0.1).name("Red X Offset");
|
|
18857
|
+
convFolder.add(s.crt.convergence, "1", -3, 3, 0.1).name("Green X Offset");
|
|
18858
|
+
convFolder.add(s.crt.convergence, "2", -3, 3, 0.1).name("Blue X Offset");
|
|
18859
|
+
convFolder.close();
|
|
18860
|
+
const maskFolder = folder.addFolder("Phosphor Mask");
|
|
18861
|
+
maskFolder.add(s.crt, "maskType", ["none", "aperture", "slot", "shadow"]).name("Type");
|
|
18862
|
+
maskFolder.add(s.crt, "maskIntensity", 0, 1, 0.05).name("Intensity");
|
|
18863
|
+
maskFolder.close();
|
|
18864
|
+
const vigFolder = folder.addFolder("Vignette");
|
|
18865
|
+
vigFolder.add(s.crt, "vignetteIntensity", 0, 1, 0.05).name("Intensity");
|
|
18866
|
+
vigFolder.add(s.crt, "vignetteSize", 0.1, 1, 0.05).name("Size");
|
|
18867
|
+
vigFolder.close();
|
|
18868
|
+
folder.add(s.crt, "blurSize", 0, 8, 0.1).name("H-Blur (px)");
|
|
18869
|
+
}
|
|
17011
18870
|
_createCameraFolder() {
|
|
17012
18871
|
const s = this.engine.settings;
|
|
17013
18872
|
if (!s.camera) return;
|
|
@@ -17087,6 +18946,619 @@ Lights: ${fmt(visibleLights)} (occ:${fmt(lightOccCulled)})`;
|
|
|
17087
18946
|
} : { r: 1, g: 1, b: 1 };
|
|
17088
18947
|
}
|
|
17089
18948
|
}
|
|
18949
|
+
class Raycaster {
|
|
18950
|
+
constructor(engine) {
|
|
18951
|
+
this.engine = engine;
|
|
18952
|
+
this.worker = null;
|
|
18953
|
+
this._pendingCallbacks = /* @__PURE__ */ new Map();
|
|
18954
|
+
this._nextRequestId = 0;
|
|
18955
|
+
this._initialized = false;
|
|
18956
|
+
}
|
|
18957
|
+
async initialize() {
|
|
18958
|
+
const workerCode = this._getWorkerCode();
|
|
18959
|
+
const blob = new Blob([workerCode], { type: "application/javascript" });
|
|
18960
|
+
const workerUrl = URL.createObjectURL(blob);
|
|
18961
|
+
this.worker = new Worker(workerUrl);
|
|
18962
|
+
this.worker.onmessage = this._handleWorkerMessage.bind(this);
|
|
18963
|
+
this.worker.onerror = (e) => console.error("Raycaster worker error:", e);
|
|
18964
|
+
this._initialized = true;
|
|
18965
|
+
URL.revokeObjectURL(workerUrl);
|
|
18966
|
+
}
|
|
18967
|
+
/**
|
|
18968
|
+
* Cast a ray and get the closest intersection
|
|
18969
|
+
* @param {Array|vec3} origin - Ray start point [x, y, z]
|
|
18970
|
+
* @param {Array|vec3} direction - Ray direction (will be normalized)
|
|
18971
|
+
* @param {number} maxDistance - Maximum ray length
|
|
18972
|
+
* @param {Function} callback - Called with result: { hit, distance, point, normal, entity, mesh, triangleIndex }
|
|
18973
|
+
* @param {Object} options - Optional settings
|
|
18974
|
+
* @param {Array} options.entities - Specific entities to test (default: all scene entities)
|
|
18975
|
+
* @param {Array} options.meshes - Specific meshes to test (default: all scene meshes)
|
|
18976
|
+
* @param {boolean} options.backfaces - Test backfaces (default: false)
|
|
18977
|
+
* @param {Array} options.exclude - Entities/meshes to exclude
|
|
18978
|
+
*/
|
|
18979
|
+
cast(origin, direction, maxDistance, callback, options = {}) {
|
|
18980
|
+
if (!this._initialized) {
|
|
18981
|
+
console.warn("Raycaster not initialized");
|
|
18982
|
+
callback({ hit: false, error: "not initialized" });
|
|
18983
|
+
return;
|
|
18984
|
+
}
|
|
18985
|
+
const ray = {
|
|
18986
|
+
origin: Array.from(origin),
|
|
18987
|
+
direction: this._normalize(Array.from(direction)),
|
|
18988
|
+
maxDistance
|
|
18989
|
+
};
|
|
18990
|
+
const candidates = this._collectCandidates(ray, options);
|
|
18991
|
+
if (candidates.length === 0) {
|
|
18992
|
+
callback({ hit: false });
|
|
18993
|
+
return;
|
|
18994
|
+
}
|
|
18995
|
+
const requestId = this._nextRequestId++;
|
|
18996
|
+
this._pendingCallbacks.set(requestId, { callback, candidates });
|
|
18997
|
+
this.worker.postMessage({
|
|
18998
|
+
type: "raycast",
|
|
18999
|
+
requestId,
|
|
19000
|
+
ray,
|
|
19001
|
+
debug: options.debug ?? false,
|
|
19002
|
+
candidates: candidates.map((c) => ({
|
|
19003
|
+
id: c.id,
|
|
19004
|
+
vertices: c.vertices,
|
|
19005
|
+
indices: c.indices,
|
|
19006
|
+
matrix: c.matrix,
|
|
19007
|
+
backfaces: options.backfaces ?? false
|
|
19008
|
+
}))
|
|
19009
|
+
});
|
|
19010
|
+
}
|
|
19011
|
+
/**
|
|
19012
|
+
* Cast a ray upward from a position to check for sky visibility
|
|
19013
|
+
* Useful for determining if camera is under cover
|
|
19014
|
+
* @param {Array|vec3} position - Position to test from
|
|
19015
|
+
* @param {number} maxDistance - How far to check (default: 100)
|
|
19016
|
+
* @param {Function} callback - Called with { hitSky: boolean, distance?: number, entity?: object }
|
|
19017
|
+
*/
|
|
19018
|
+
castToSky(position, maxDistance, callback) {
|
|
19019
|
+
this.cast(
|
|
19020
|
+
position,
|
|
19021
|
+
[0, 1, 0],
|
|
19022
|
+
// Straight up
|
|
19023
|
+
maxDistance ?? 100,
|
|
19024
|
+
(result) => {
|
|
19025
|
+
callback({
|
|
19026
|
+
hitSky: !result.hit,
|
|
19027
|
+
distance: result.distance,
|
|
19028
|
+
entity: result.entity,
|
|
19029
|
+
mesh: result.mesh
|
|
19030
|
+
});
|
|
19031
|
+
}
|
|
19032
|
+
);
|
|
19033
|
+
}
|
|
19034
|
+
/**
|
|
19035
|
+
* Cast a ray from screen coordinates (mouse picking)
|
|
19036
|
+
* @param {number} screenX - Screen X coordinate
|
|
19037
|
+
* @param {number} screenY - Screen Y coordinate
|
|
19038
|
+
* @param {Object} camera - Camera with projection/view matrices
|
|
19039
|
+
* @param {Function} callback - Called with intersection result
|
|
19040
|
+
* @param {Object} options - Cast options
|
|
19041
|
+
*/
|
|
19042
|
+
castFromScreen(screenX, screenY, camera, callback, options = {}) {
|
|
19043
|
+
const { width, height } = this.engine.canvas;
|
|
19044
|
+
const ndcX = screenX / width * 2 - 1;
|
|
19045
|
+
const ndcY = 1 - screenY / height * 2;
|
|
19046
|
+
const invViewProj = mat4$1.create();
|
|
19047
|
+
mat4$1.multiply(invViewProj, camera.proj, camera.view);
|
|
19048
|
+
mat4$1.invert(invViewProj, invViewProj);
|
|
19049
|
+
const nearPoint = this._unproject([ndcX, ndcY, 0], invViewProj);
|
|
19050
|
+
const farPoint = this._unproject([ndcX, ndcY, 1], invViewProj);
|
|
19051
|
+
const direction = [
|
|
19052
|
+
farPoint[0] - nearPoint[0],
|
|
19053
|
+
farPoint[1] - nearPoint[1],
|
|
19054
|
+
farPoint[2] - nearPoint[2]
|
|
19055
|
+
];
|
|
19056
|
+
const maxDistance = options.maxDistance ?? camera.far ?? 1e3;
|
|
19057
|
+
this.cast(nearPoint, direction, maxDistance, callback, options);
|
|
19058
|
+
}
|
|
19059
|
+
/**
|
|
19060
|
+
* Collect candidate geometries that pass bounding sphere test
|
|
19061
|
+
*/
|
|
19062
|
+
_collectCandidates(ray, options) {
|
|
19063
|
+
const candidates = [];
|
|
19064
|
+
const exclude = new Set(options.exclude ?? []);
|
|
19065
|
+
const debug = options.debug;
|
|
19066
|
+
const entities = options.entities ?? this._getAllEntities();
|
|
19067
|
+
const assetManager = this.engine.assetManager;
|
|
19068
|
+
for (const entity of entities) {
|
|
19069
|
+
if (exclude.has(entity)) continue;
|
|
19070
|
+
if (!entity.model) continue;
|
|
19071
|
+
const asset = assetManager?.get(entity.model);
|
|
19072
|
+
if (!asset?.geometry) continue;
|
|
19073
|
+
const bsphere = this._getEntityBoundingSphere(entity);
|
|
19074
|
+
if (!bsphere) continue;
|
|
19075
|
+
if (this._raySphereIntersect(ray, bsphere)) {
|
|
19076
|
+
const geometryData = this._extractGeometry(asset.geometry);
|
|
19077
|
+
if (geometryData) {
|
|
19078
|
+
const matrix = entity._matrix ?? mat4$1.create();
|
|
19079
|
+
candidates.push({
|
|
19080
|
+
id: entity.id ?? entity.name ?? `entity_${candidates.length}`,
|
|
19081
|
+
type: "entity",
|
|
19082
|
+
entity,
|
|
19083
|
+
asset,
|
|
19084
|
+
vertices: geometryData.vertices,
|
|
19085
|
+
indices: geometryData.indices,
|
|
19086
|
+
matrix: Array.from(matrix),
|
|
19087
|
+
bsphereDistance: this._raySphereDistance(ray, bsphere)
|
|
19088
|
+
});
|
|
19089
|
+
}
|
|
19090
|
+
}
|
|
19091
|
+
}
|
|
19092
|
+
const meshes = options.meshes ?? this._getAllMeshes();
|
|
19093
|
+
let debugStats = debug ? { total: 0, noGeom: 0, noBsphere: 0, noData: 0, sphereMiss: 0, candidates: 0 } : null;
|
|
19094
|
+
for (const [name, mesh] of Object.entries(meshes)) {
|
|
19095
|
+
if (exclude.has(mesh)) continue;
|
|
19096
|
+
if (!mesh.geometry) {
|
|
19097
|
+
if (debug) debugStats.noGeom++;
|
|
19098
|
+
continue;
|
|
19099
|
+
}
|
|
19100
|
+
const bsphere = this._getMeshBoundingSphere(mesh);
|
|
19101
|
+
if (!bsphere) {
|
|
19102
|
+
if (debug) debugStats.noBsphere++;
|
|
19103
|
+
continue;
|
|
19104
|
+
}
|
|
19105
|
+
const geometryData = this._extractGeometry(mesh.geometry);
|
|
19106
|
+
if (!geometryData) {
|
|
19107
|
+
if (debug) debugStats.noData++;
|
|
19108
|
+
continue;
|
|
19109
|
+
}
|
|
19110
|
+
if (debug) debugStats.total++;
|
|
19111
|
+
let instanceCount = mesh.geometry.instanceCount ?? 0;
|
|
19112
|
+
if (instanceCount === 0) {
|
|
19113
|
+
if (mesh.geometry.instanceData) {
|
|
19114
|
+
instanceCount = mesh.static ? mesh.geometry.maxInstances ?? 1 : 1;
|
|
19115
|
+
} else {
|
|
19116
|
+
instanceCount = 1;
|
|
19117
|
+
}
|
|
19118
|
+
}
|
|
19119
|
+
for (let i = 0; i < instanceCount; i++) {
|
|
19120
|
+
const matrix = this._getInstanceMatrix(mesh.geometry, i);
|
|
19121
|
+
const instanceBsphere = this._transformBoundingSphere(bsphere, matrix);
|
|
19122
|
+
if (this._raySphereIntersect(ray, instanceBsphere)) {
|
|
19123
|
+
if (debug) debugStats.candidates++;
|
|
19124
|
+
candidates.push({
|
|
19125
|
+
id: `${name}_${i}`,
|
|
19126
|
+
type: "mesh",
|
|
19127
|
+
mesh,
|
|
19128
|
+
meshName: name,
|
|
19129
|
+
instanceIndex: i,
|
|
19130
|
+
vertices: geometryData.vertices,
|
|
19131
|
+
indices: geometryData.indices,
|
|
19132
|
+
matrix: Array.from(matrix),
|
|
19133
|
+
bsphereDistance: this._raySphereDistance(ray, instanceBsphere)
|
|
19134
|
+
});
|
|
19135
|
+
} else {
|
|
19136
|
+
if (debug) debugStats.sphereMiss++;
|
|
19137
|
+
}
|
|
19138
|
+
}
|
|
19139
|
+
}
|
|
19140
|
+
if (debug && debugStats) {
|
|
19141
|
+
console.log(`Raycaster: meshes=${debugStats.total}, sphereHit=${debugStats.candidates}, sphereMiss=${debugStats.sphereMiss}`);
|
|
19142
|
+
if (candidates.length > 0 && candidates.length < 50) {
|
|
19143
|
+
const candInfo = candidates.map((c) => {
|
|
19144
|
+
const m = c.matrix;
|
|
19145
|
+
const pos = [m[12], m[13], m[14]];
|
|
19146
|
+
return `${c.id}@[${pos.map((v) => v.toFixed(1)).join(",")}]`;
|
|
19147
|
+
}).join(", ");
|
|
19148
|
+
console.log(`Candidates: ${candInfo}`);
|
|
19149
|
+
}
|
|
19150
|
+
}
|
|
19151
|
+
candidates.sort((a, b) => a.bsphereDistance - b.bsphereDistance);
|
|
19152
|
+
return candidates;
|
|
19153
|
+
}
|
|
19154
|
+
_getAllEntities() {
|
|
19155
|
+
const entities = this.engine.entities;
|
|
19156
|
+
if (!entities) return [];
|
|
19157
|
+
return Object.values(entities);
|
|
19158
|
+
}
|
|
19159
|
+
_getAllMeshes() {
|
|
19160
|
+
return this.engine.meshes ?? {};
|
|
19161
|
+
}
|
|
19162
|
+
_getEntityBoundingSphere(entity) {
|
|
19163
|
+
if (entity._bsphere && entity._bsphere.radius > 0) {
|
|
19164
|
+
return {
|
|
19165
|
+
center: Array.from(entity._bsphere.center),
|
|
19166
|
+
radius: entity._bsphere.radius
|
|
19167
|
+
};
|
|
19168
|
+
}
|
|
19169
|
+
const geometry = entity.mesh?.geometry;
|
|
19170
|
+
if (!geometry) return null;
|
|
19171
|
+
const localBsphere = geometry.getBoundingSphere?.();
|
|
19172
|
+
if (!localBsphere || localBsphere.radius <= 0) return null;
|
|
19173
|
+
const matrix = entity._matrix ?? entity.matrix ?? mat4$1.create();
|
|
19174
|
+
return this._transformBoundingSphere(localBsphere, matrix);
|
|
19175
|
+
}
|
|
19176
|
+
_getMeshBoundingSphere(mesh) {
|
|
19177
|
+
const geometry = mesh.geometry;
|
|
19178
|
+
if (!geometry) return null;
|
|
19179
|
+
return geometry.getBoundingSphere?.() ?? null;
|
|
19180
|
+
}
|
|
19181
|
+
_transformBoundingSphere(bsphere, matrix) {
|
|
19182
|
+
const center = vec3$1.create();
|
|
19183
|
+
vec3$1.transformMat4(center, bsphere.center, matrix);
|
|
19184
|
+
const scaleX = Math.sqrt(matrix[0] * matrix[0] + matrix[1] * matrix[1] + matrix[2] * matrix[2]);
|
|
19185
|
+
const scaleY = Math.sqrt(matrix[4] * matrix[4] + matrix[5] * matrix[5] + matrix[6] * matrix[6]);
|
|
19186
|
+
const scaleZ = Math.sqrt(matrix[8] * matrix[8] + matrix[9] * matrix[9] + matrix[10] * matrix[10]);
|
|
19187
|
+
const maxScale = Math.max(scaleX, scaleY, scaleZ);
|
|
19188
|
+
return {
|
|
19189
|
+
center: Array.from(center),
|
|
19190
|
+
radius: bsphere.radius * maxScale
|
|
19191
|
+
};
|
|
19192
|
+
}
|
|
19193
|
+
_getInstanceMatrix(geometry, instanceIndex) {
|
|
19194
|
+
if (!geometry.instanceData) {
|
|
19195
|
+
return mat4$1.create();
|
|
19196
|
+
}
|
|
19197
|
+
const stride = 28;
|
|
19198
|
+
const offset = instanceIndex * stride;
|
|
19199
|
+
if (offset + 16 > geometry.instanceData.length) {
|
|
19200
|
+
return mat4$1.create();
|
|
19201
|
+
}
|
|
19202
|
+
const matrix = mat4$1.create();
|
|
19203
|
+
for (let i = 0; i < 16; i++) {
|
|
19204
|
+
matrix[i] = geometry.instanceData[offset + i];
|
|
19205
|
+
}
|
|
19206
|
+
return matrix;
|
|
19207
|
+
}
|
|
19208
|
+
_extractGeometry(geometry) {
|
|
19209
|
+
if (!geometry.vertexArray || !geometry.indexArray) {
|
|
19210
|
+
return null;
|
|
19211
|
+
}
|
|
19212
|
+
const stride = 20;
|
|
19213
|
+
const vertexCount = geometry.vertexArray.length / stride;
|
|
19214
|
+
const vertices = new Float32Array(vertexCount * 3);
|
|
19215
|
+
for (let i = 0; i < vertexCount; i++) {
|
|
19216
|
+
vertices[i * 3] = geometry.vertexArray[i * stride];
|
|
19217
|
+
vertices[i * 3 + 1] = geometry.vertexArray[i * stride + 1];
|
|
19218
|
+
vertices[i * 3 + 2] = geometry.vertexArray[i * stride + 2];
|
|
19219
|
+
}
|
|
19220
|
+
return {
|
|
19221
|
+
vertices,
|
|
19222
|
+
indices: geometry.indexArray
|
|
19223
|
+
};
|
|
19224
|
+
}
|
|
19225
|
+
/**
|
|
19226
|
+
* Ray-sphere intersection test
|
|
19227
|
+
* Returns true if ray intersects sphere within maxDistance
|
|
19228
|
+
* Handles case where ray origin is inside the sphere
|
|
19229
|
+
*/
|
|
19230
|
+
_raySphereIntersect(ray, sphere) {
|
|
19231
|
+
const oc = [
|
|
19232
|
+
ray.origin[0] - sphere.center[0],
|
|
19233
|
+
ray.origin[1] - sphere.center[1],
|
|
19234
|
+
ray.origin[2] - sphere.center[2]
|
|
19235
|
+
];
|
|
19236
|
+
const distToCenter = Math.sqrt(oc[0] * oc[0] + oc[1] * oc[1] + oc[2] * oc[2]);
|
|
19237
|
+
if (distToCenter < sphere.radius) {
|
|
19238
|
+
return true;
|
|
19239
|
+
}
|
|
19240
|
+
const a = this._dot(ray.direction, ray.direction);
|
|
19241
|
+
const b = 2 * this._dot(oc, ray.direction);
|
|
19242
|
+
const c = this._dot(oc, oc) - sphere.radius * sphere.radius;
|
|
19243
|
+
const discriminant = b * b - 4 * a * c;
|
|
19244
|
+
if (discriminant < 0) return false;
|
|
19245
|
+
const sqrtDisc = Math.sqrt(discriminant);
|
|
19246
|
+
const t1 = (-b - sqrtDisc) / (2 * a);
|
|
19247
|
+
const t2 = (-b + sqrtDisc) / (2 * a);
|
|
19248
|
+
if (t1 >= 0 && t1 <= ray.maxDistance) return true;
|
|
19249
|
+
if (t2 >= 0 && t2 <= ray.maxDistance) return true;
|
|
19250
|
+
return false;
|
|
19251
|
+
}
|
|
19252
|
+
/**
|
|
19253
|
+
* Get distance to sphere along ray (for sorting)
|
|
19254
|
+
*/
|
|
19255
|
+
_raySphereDistance(ray, sphere) {
|
|
19256
|
+
const oc = [
|
|
19257
|
+
ray.origin[0] - sphere.center[0],
|
|
19258
|
+
ray.origin[1] - sphere.center[1],
|
|
19259
|
+
ray.origin[2] - sphere.center[2]
|
|
19260
|
+
];
|
|
19261
|
+
const a = this._dot(ray.direction, ray.direction);
|
|
19262
|
+
const b = 2 * this._dot(oc, ray.direction);
|
|
19263
|
+
const c = this._dot(oc, oc) - sphere.radius * sphere.radius;
|
|
19264
|
+
const discriminant = b * b - 4 * a * c;
|
|
19265
|
+
if (discriminant < 0) return Infinity;
|
|
19266
|
+
const t = (-b - Math.sqrt(discriminant)) / (2 * a);
|
|
19267
|
+
return Math.max(0, t);
|
|
19268
|
+
}
|
|
19269
|
+
_handleWorkerMessage(event) {
|
|
19270
|
+
const { type, requestId, result } = event.data;
|
|
19271
|
+
if (type === "raycastResult") {
|
|
19272
|
+
const pending = this._pendingCallbacks.get(requestId);
|
|
19273
|
+
if (pending) {
|
|
19274
|
+
this._pendingCallbacks.delete(requestId);
|
|
19275
|
+
if (result.hit && pending.candidates) {
|
|
19276
|
+
const candidate = pending.candidates.find((c) => c.id === result.candidateId);
|
|
19277
|
+
if (candidate) {
|
|
19278
|
+
result.entity = candidate.entity;
|
|
19279
|
+
result.mesh = candidate.mesh;
|
|
19280
|
+
result.meshName = candidate.meshName;
|
|
19281
|
+
result.instanceIndex = candidate.instanceIndex;
|
|
19282
|
+
}
|
|
19283
|
+
}
|
|
19284
|
+
pending.callback(result);
|
|
19285
|
+
}
|
|
19286
|
+
}
|
|
19287
|
+
}
|
|
19288
|
+
_normalize(v) {
|
|
19289
|
+
const len = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
|
|
19290
|
+
if (len === 0) return [0, 0, 1];
|
|
19291
|
+
return [v[0] / len, v[1] / len, v[2] / len];
|
|
19292
|
+
}
|
|
19293
|
+
_dot(a, b) {
|
|
19294
|
+
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
19295
|
+
}
|
|
19296
|
+
/**
|
|
19297
|
+
* Get perpendicular distance from a point to a ray
|
|
19298
|
+
*/
|
|
19299
|
+
_pointToRayDistance(point, ray) {
|
|
19300
|
+
const op = [
|
|
19301
|
+
point[0] - ray.origin[0],
|
|
19302
|
+
point[1] - ray.origin[1],
|
|
19303
|
+
point[2] - ray.origin[2]
|
|
19304
|
+
];
|
|
19305
|
+
const t = this._dot(op, ray.direction);
|
|
19306
|
+
const closest = [
|
|
19307
|
+
ray.origin[0] + ray.direction[0] * t,
|
|
19308
|
+
ray.origin[1] + ray.direction[1] * t,
|
|
19309
|
+
ray.origin[2] + ray.direction[2] * t
|
|
19310
|
+
];
|
|
19311
|
+
const dx = point[0] - closest[0];
|
|
19312
|
+
const dy = point[1] - closest[1];
|
|
19313
|
+
const dz = point[2] - closest[2];
|
|
19314
|
+
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
19315
|
+
}
|
|
19316
|
+
_unproject(ndc, invViewProj) {
|
|
19317
|
+
const x = ndc[0];
|
|
19318
|
+
const y = ndc[1];
|
|
19319
|
+
const z = ndc[2];
|
|
19320
|
+
const w = invViewProj[3] * x + invViewProj[7] * y + invViewProj[11] * z + invViewProj[15];
|
|
19321
|
+
return [
|
|
19322
|
+
(invViewProj[0] * x + invViewProj[4] * y + invViewProj[8] * z + invViewProj[12]) / w,
|
|
19323
|
+
(invViewProj[1] * x + invViewProj[5] * y + invViewProj[9] * z + invViewProj[13]) / w,
|
|
19324
|
+
(invViewProj[2] * x + invViewProj[6] * y + invViewProj[10] * z + invViewProj[14]) / w
|
|
19325
|
+
];
|
|
19326
|
+
}
|
|
19327
|
+
/**
|
|
19328
|
+
* Generate Web Worker code as string
|
|
19329
|
+
*/
|
|
19330
|
+
_getWorkerCode() {
|
|
19331
|
+
return `
|
|
19332
|
+
// Raycaster Web Worker
|
|
19333
|
+
// Performs triangle intersection tests off the main thread
|
|
19334
|
+
|
|
19335
|
+
self.onmessage = function(event) {
|
|
19336
|
+
const { type, requestId, ray, candidates, debug } = event.data
|
|
19337
|
+
|
|
19338
|
+
if (type === 'raycast') {
|
|
19339
|
+
const result = raycastTriangles(ray, candidates, debug)
|
|
19340
|
+
self.postMessage({ type: 'raycastResult', requestId, result })
|
|
19341
|
+
}
|
|
19342
|
+
}
|
|
19343
|
+
|
|
19344
|
+
function raycastTriangles(ray, candidates, debug) {
|
|
19345
|
+
let closestHit = null
|
|
19346
|
+
let closestDistance = ray.maxDistance
|
|
19347
|
+
let debugInfo = debug ? { totalTris: 0, testedCandidates: 0, scales: [] } : null
|
|
19348
|
+
|
|
19349
|
+
for (const candidate of candidates) {
|
|
19350
|
+
if (debug) debugInfo.testedCandidates++
|
|
19351
|
+
const result = testCandidate(ray, candidate, closestDistance, debug ? debugInfo : null)
|
|
19352
|
+
|
|
19353
|
+
if (result && result.distance < closestDistance) {
|
|
19354
|
+
closestDistance = result.distance
|
|
19355
|
+
closestHit = {
|
|
19356
|
+
hit: true,
|
|
19357
|
+
distance: result.distance,
|
|
19358
|
+
point: result.point,
|
|
19359
|
+
normal: result.normal,
|
|
19360
|
+
triangleIndex: result.triangleIndex,
|
|
19361
|
+
candidateId: candidate.id,
|
|
19362
|
+
localT: result.localT,
|
|
19363
|
+
scale: result.scale
|
|
19364
|
+
}
|
|
19365
|
+
}
|
|
19366
|
+
}
|
|
19367
|
+
|
|
19368
|
+
if (debug) {
|
|
19369
|
+
let msg = 'Worker: candidates=' + debugInfo.testedCandidates + ', triangles=' + debugInfo.totalTris
|
|
19370
|
+
if (closestHit) {
|
|
19371
|
+
msg += ', hit=' + closestHit.distance.toFixed(2) + ' (localT=' + closestHit.localT.toFixed(2) + ', scale=' + closestHit.scale.toFixed(2) + ')'
|
|
19372
|
+
} else {
|
|
19373
|
+
msg += ', hit=none'
|
|
19374
|
+
}
|
|
19375
|
+
console.log(msg)
|
|
19376
|
+
}
|
|
19377
|
+
|
|
19378
|
+
return closestHit ?? { hit: false }
|
|
19379
|
+
}
|
|
19380
|
+
|
|
19381
|
+
function testCandidate(ray, candidate, maxDistance, debugInfo) {
|
|
19382
|
+
const { vertices, indices, matrix, backfaces } = candidate
|
|
19383
|
+
|
|
19384
|
+
// Compute inverse matrix for transforming ray to local space
|
|
19385
|
+
const invMatrix = invertMatrix4(matrix)
|
|
19386
|
+
|
|
19387
|
+
// Transform ray to local space
|
|
19388
|
+
const localOrigin = transformPoint(ray.origin, invMatrix)
|
|
19389
|
+
const localDir = transformDirection(ray.direction, invMatrix)
|
|
19390
|
+
|
|
19391
|
+
// Calculate the scale factor of the transformation (for correct distance)
|
|
19392
|
+
const dirScale = Math.sqrt(localDir[0]*localDir[0] + localDir[1]*localDir[1] + localDir[2]*localDir[2])
|
|
19393
|
+
const localDirNorm = [localDir[0]/dirScale, localDir[1]/dirScale, localDir[2]/dirScale]
|
|
19394
|
+
|
|
19395
|
+
let closestHit = null
|
|
19396
|
+
let closestT = maxDistance
|
|
19397
|
+
|
|
19398
|
+
// Test each triangle
|
|
19399
|
+
const triangleCount = indices.length / 3
|
|
19400
|
+
if (debugInfo) debugInfo.totalTris += triangleCount
|
|
19401
|
+
for (let i = 0; i < triangleCount; i++) {
|
|
19402
|
+
const i0 = indices[i * 3]
|
|
19403
|
+
const i1 = indices[i * 3 + 1]
|
|
19404
|
+
const i2 = indices[i * 3 + 2]
|
|
19405
|
+
|
|
19406
|
+
const v0 = [vertices[i0 * 3], vertices[i0 * 3 + 1], vertices[i0 * 3 + 2]]
|
|
19407
|
+
const v1 = [vertices[i1 * 3], vertices[i1 * 3 + 1], vertices[i1 * 3 + 2]]
|
|
19408
|
+
const v2 = [vertices[i2 * 3], vertices[i2 * 3 + 1], vertices[i2 * 3 + 2]]
|
|
19409
|
+
|
|
19410
|
+
const hit = rayTriangleIntersect(localOrigin, localDirNorm, v0, v1, v2, backfaces)
|
|
19411
|
+
|
|
19412
|
+
if (hit && hit.t > 0) {
|
|
19413
|
+
// Transform hit point and normal back to world space
|
|
19414
|
+
const worldPoint = transformPoint(hit.point, matrix)
|
|
19415
|
+
|
|
19416
|
+
// Calculate world-space distance (local t may be wrong due to matrix scale)
|
|
19417
|
+
const worldDist = Math.sqrt(
|
|
19418
|
+
(worldPoint[0] - ray.origin[0]) ** 2 +
|
|
19419
|
+
(worldPoint[1] - ray.origin[1]) ** 2 +
|
|
19420
|
+
(worldPoint[2] - ray.origin[2]) ** 2
|
|
19421
|
+
)
|
|
19422
|
+
|
|
19423
|
+
if (worldDist < closestT) {
|
|
19424
|
+
closestT = worldDist
|
|
19425
|
+
const worldNormal = transformDirection(hit.normal, matrix)
|
|
19426
|
+
|
|
19427
|
+
closestHit = {
|
|
19428
|
+
distance: worldDist,
|
|
19429
|
+
point: worldPoint,
|
|
19430
|
+
normal: normalize(worldNormal),
|
|
19431
|
+
triangleIndex: i,
|
|
19432
|
+
localT: hit.t,
|
|
19433
|
+
scale: dirScale
|
|
19434
|
+
}
|
|
19435
|
+
}
|
|
19436
|
+
}
|
|
19437
|
+
}
|
|
19438
|
+
|
|
19439
|
+
return closestHit
|
|
19440
|
+
}
|
|
19441
|
+
|
|
19442
|
+
// Möller–Trumbore intersection algorithm
|
|
19443
|
+
function rayTriangleIntersect(origin, dir, v0, v1, v2, backfaces) {
|
|
19444
|
+
const EPSILON = 0.0000001
|
|
19445
|
+
|
|
19446
|
+
const edge1 = sub(v1, v0)
|
|
19447
|
+
const edge2 = sub(v2, v0)
|
|
19448
|
+
const h = cross(dir, edge2)
|
|
19449
|
+
const a = dot(edge1, h)
|
|
19450
|
+
|
|
19451
|
+
// Check if ray is parallel to triangle
|
|
19452
|
+
if (a > -EPSILON && a < EPSILON) return null
|
|
19453
|
+
|
|
19454
|
+
// Check backface
|
|
19455
|
+
if (!backfaces && a < 0) return null
|
|
19456
|
+
|
|
19457
|
+
const f = 1.0 / a
|
|
19458
|
+
const s = sub(origin, v0)
|
|
19459
|
+
const u = f * dot(s, h)
|
|
19460
|
+
|
|
19461
|
+
if (u < 0.0 || u > 1.0) return null
|
|
19462
|
+
|
|
19463
|
+
const q = cross(s, edge1)
|
|
19464
|
+
const v = f * dot(dir, q)
|
|
19465
|
+
|
|
19466
|
+
if (v < 0.0 || u + v > 1.0) return null
|
|
19467
|
+
|
|
19468
|
+
const t = f * dot(edge2, q)
|
|
19469
|
+
|
|
19470
|
+
if (t > EPSILON) {
|
|
19471
|
+
const point = [
|
|
19472
|
+
origin[0] + dir[0] * t,
|
|
19473
|
+
origin[1] + dir[1] * t,
|
|
19474
|
+
origin[2] + dir[2] * t
|
|
19475
|
+
]
|
|
19476
|
+
const normal = normalize(cross(edge1, edge2))
|
|
19477
|
+
return { t, point, normal, u, v }
|
|
19478
|
+
}
|
|
19479
|
+
|
|
19480
|
+
return null
|
|
19481
|
+
}
|
|
19482
|
+
|
|
19483
|
+
// Matrix and vector utilities
|
|
19484
|
+
function invertMatrix4(m) {
|
|
19485
|
+
const inv = new Array(16)
|
|
19486
|
+
|
|
19487
|
+
inv[0] = m[5]*m[10]*m[15] - m[5]*m[11]*m[14] - m[9]*m[6]*m[15] + m[9]*m[7]*m[14] + m[13]*m[6]*m[11] - m[13]*m[7]*m[10]
|
|
19488
|
+
inv[4] = -m[4]*m[10]*m[15] + m[4]*m[11]*m[14] + m[8]*m[6]*m[15] - m[8]*m[7]*m[14] - m[12]*m[6]*m[11] + m[12]*m[7]*m[10]
|
|
19489
|
+
inv[8] = m[4]*m[9]*m[15] - m[4]*m[11]*m[13] - m[8]*m[5]*m[15] + m[8]*m[7]*m[13] + m[12]*m[5]*m[11] - m[12]*m[7]*m[9]
|
|
19490
|
+
inv[12] = -m[4]*m[9]*m[14] + m[4]*m[10]*m[13] + m[8]*m[5]*m[14] - m[8]*m[6]*m[13] - m[12]*m[5]*m[10] + m[12]*m[6]*m[9]
|
|
19491
|
+
inv[1] = -m[1]*m[10]*m[15] + m[1]*m[11]*m[14] + m[9]*m[2]*m[15] - m[9]*m[3]*m[14] - m[13]*m[2]*m[11] + m[13]*m[3]*m[10]
|
|
19492
|
+
inv[5] = m[0]*m[10]*m[15] - m[0]*m[11]*m[14] - m[8]*m[2]*m[15] + m[8]*m[3]*m[14] + m[12]*m[2]*m[11] - m[12]*m[3]*m[10]
|
|
19493
|
+
inv[9] = -m[0]*m[9]*m[15] + m[0]*m[11]*m[13] + m[8]*m[1]*m[15] - m[8]*m[3]*m[13] - m[12]*m[1]*m[11] + m[12]*m[3]*m[9]
|
|
19494
|
+
inv[13] = m[0]*m[9]*m[14] - m[0]*m[10]*m[13] - m[8]*m[1]*m[14] + m[8]*m[2]*m[13] + m[12]*m[1]*m[10] - m[12]*m[2]*m[9]
|
|
19495
|
+
inv[2] = m[1]*m[6]*m[15] - m[1]*m[7]*m[14] - m[5]*m[2]*m[15] + m[5]*m[3]*m[14] + m[13]*m[2]*m[7] - m[13]*m[3]*m[6]
|
|
19496
|
+
inv[6] = -m[0]*m[6]*m[15] + m[0]*m[7]*m[14] + m[4]*m[2]*m[15] - m[4]*m[3]*m[14] - m[12]*m[2]*m[7] + m[12]*m[3]*m[6]
|
|
19497
|
+
inv[10] = m[0]*m[5]*m[15] - m[0]*m[7]*m[13] - m[4]*m[1]*m[15] + m[4]*m[3]*m[13] + m[12]*m[1]*m[7] - m[12]*m[3]*m[5]
|
|
19498
|
+
inv[14] = -m[0]*m[5]*m[14] + m[0]*m[6]*m[13] + m[4]*m[1]*m[14] - m[4]*m[2]*m[13] - m[12]*m[1]*m[6] + m[12]*m[2]*m[5]
|
|
19499
|
+
inv[3] = -m[1]*m[6]*m[11] + m[1]*m[7]*m[10] + m[5]*m[2]*m[11] - m[5]*m[3]*m[10] - m[9]*m[2]*m[7] + m[9]*m[3]*m[6]
|
|
19500
|
+
inv[7] = m[0]*m[6]*m[11] - m[0]*m[7]*m[10] - m[4]*m[2]*m[11] + m[4]*m[3]*m[10] + m[8]*m[2]*m[7] - m[8]*m[3]*m[6]
|
|
19501
|
+
inv[11] = -m[0]*m[5]*m[11] + m[0]*m[7]*m[9] + m[4]*m[1]*m[11] - m[4]*m[3]*m[9] - m[8]*m[1]*m[7] + m[8]*m[3]*m[5]
|
|
19502
|
+
inv[15] = m[0]*m[5]*m[10] - m[0]*m[6]*m[9] - m[4]*m[1]*m[10] + m[4]*m[2]*m[9] + m[8]*m[1]*m[6] - m[8]*m[2]*m[5]
|
|
19503
|
+
|
|
19504
|
+
let det = m[0]*inv[0] + m[1]*inv[4] + m[2]*inv[8] + m[3]*inv[12]
|
|
19505
|
+
if (det === 0) return m // Return original if singular
|
|
19506
|
+
|
|
19507
|
+
det = 1.0 / det
|
|
19508
|
+
for (let i = 0; i < 16; i++) inv[i] *= det
|
|
19509
|
+
|
|
19510
|
+
return inv
|
|
19511
|
+
}
|
|
19512
|
+
|
|
19513
|
+
function transformPoint(p, m) {
|
|
19514
|
+
const w = m[3]*p[0] + m[7]*p[1] + m[11]*p[2] + m[15]
|
|
19515
|
+
return [
|
|
19516
|
+
(m[0]*p[0] + m[4]*p[1] + m[8]*p[2] + m[12]) / w,
|
|
19517
|
+
(m[1]*p[0] + m[5]*p[1] + m[9]*p[2] + m[13]) / w,
|
|
19518
|
+
(m[2]*p[0] + m[6]*p[1] + m[10]*p[2] + m[14]) / w
|
|
19519
|
+
]
|
|
19520
|
+
}
|
|
19521
|
+
|
|
19522
|
+
function transformDirection(d, m) {
|
|
19523
|
+
return [
|
|
19524
|
+
m[0]*d[0] + m[4]*d[1] + m[8]*d[2],
|
|
19525
|
+
m[1]*d[0] + m[5]*d[1] + m[9]*d[2],
|
|
19526
|
+
m[2]*d[0] + m[6]*d[1] + m[10]*d[2]
|
|
19527
|
+
]
|
|
19528
|
+
}
|
|
19529
|
+
|
|
19530
|
+
function normalize(v) {
|
|
19531
|
+
const len = Math.sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2])
|
|
19532
|
+
if (len === 0) return [0, 0, 1]
|
|
19533
|
+
return [v[0]/len, v[1]/len, v[2]/len]
|
|
19534
|
+
}
|
|
19535
|
+
|
|
19536
|
+
function dot(a, b) {
|
|
19537
|
+
return a[0]*b[0] + a[1]*b[1] + a[2]*b[2]
|
|
19538
|
+
}
|
|
19539
|
+
|
|
19540
|
+
function cross(a, b) {
|
|
19541
|
+
return [
|
|
19542
|
+
a[1]*b[2] - a[2]*b[1],
|
|
19543
|
+
a[2]*b[0] - a[0]*b[2],
|
|
19544
|
+
a[0]*b[1] - a[1]*b[0]
|
|
19545
|
+
]
|
|
19546
|
+
}
|
|
19547
|
+
|
|
19548
|
+
function sub(a, b) {
|
|
19549
|
+
return [a[0]-b[0], a[1]-b[1], a[2]-b[2]]
|
|
19550
|
+
}
|
|
19551
|
+
`;
|
|
19552
|
+
}
|
|
19553
|
+
destroy() {
|
|
19554
|
+
if (this.worker) {
|
|
19555
|
+
this.worker.terminate();
|
|
19556
|
+
this.worker = null;
|
|
19557
|
+
}
|
|
19558
|
+
this._pendingCallbacks.clear();
|
|
19559
|
+
this._initialized = false;
|
|
19560
|
+
}
|
|
19561
|
+
}
|
|
17090
19562
|
function fail(engine, msg, data) {
|
|
17091
19563
|
if (engine?.canvas) {
|
|
17092
19564
|
engine.canvas.style.display = "none";
|
|
@@ -17152,7 +19624,7 @@ const DEFAULT_SETTINGS = {
|
|
|
17152
19624
|
renderScale: 1,
|
|
17153
19625
|
// Render resolution multiplier (1.5-2.0 for supersampling AA)
|
|
17154
19626
|
autoScale: {
|
|
17155
|
-
enabled:
|
|
19627
|
+
enabled: true,
|
|
17156
19628
|
// Auto-reduce renderScale for high resolutions
|
|
17157
19629
|
enabledForEffects: true,
|
|
17158
19630
|
// Auto scale effects at high resolutions (when main autoScale disabled)
|
|
@@ -17177,8 +19649,10 @@ const DEFAULT_SETTINGS = {
|
|
|
17177
19649
|
// Enable alpha hashing/dithering for cutout transparency (global default)
|
|
17178
19650
|
alphaHashScale: 1,
|
|
17179
19651
|
// Scale factor for alpha hash threshold (higher = more opaque)
|
|
17180
|
-
luminanceToAlpha: false
|
|
19652
|
+
luminanceToAlpha: false,
|
|
17181
19653
|
// Derive alpha from color luminance (for old game assets where black=transparent)
|
|
19654
|
+
tonemapMode: 0
|
|
19655
|
+
// 0=ACES, 1=Reinhard, 2=None (linear clamp)
|
|
17182
19656
|
},
|
|
17183
19657
|
// Noise settings for dithering, jittering, etc.
|
|
17184
19658
|
noise: {
|
|
@@ -17225,13 +19699,14 @@ const DEFAULT_SETTINGS = {
|
|
|
17225
19699
|
exposure: 1.6,
|
|
17226
19700
|
fog: {
|
|
17227
19701
|
enabled: true,
|
|
17228
|
-
color: [
|
|
17229
|
-
distances: [
|
|
19702
|
+
color: [100 / 255, 135 / 255, 170 / 255],
|
|
19703
|
+
distances: [0, 15, 50],
|
|
17230
19704
|
alpha: [0, 0.5, 0.9],
|
|
17231
19705
|
heightFade: [-2, 185],
|
|
17232
19706
|
// [bottomY, topY] - full fog at bottomY, zero at topY
|
|
17233
|
-
brightResist: 0
|
|
19707
|
+
brightResist: 0,
|
|
17234
19708
|
// How much bright/emissive colors resist fog (0-1)
|
|
19709
|
+
debug: 0
|
|
17235
19710
|
}
|
|
17236
19711
|
},
|
|
17237
19712
|
// Main directional light
|
|
@@ -17261,6 +19736,8 @@ const DEFAULT_SETTINGS = {
|
|
|
17261
19736
|
surfaceBias: 0,
|
|
17262
19737
|
// Scale shadow projection larger (0.01 = 1% larger)
|
|
17263
19738
|
strength: 1
|
|
19739
|
+
//frustum: false,
|
|
19740
|
+
//hiZ: false,
|
|
17264
19741
|
},
|
|
17265
19742
|
// Ambient Occlusion settings
|
|
17266
19743
|
ao: {
|
|
@@ -17368,6 +19845,54 @@ const DEFAULT_SETTINGS = {
|
|
|
17368
19845
|
saturateLevel: 0.5
|
|
17369
19846
|
// Logarithmic saturation level for indirect light
|
|
17370
19847
|
},
|
|
19848
|
+
// Volumetric Fog (light scattering through particles)
|
|
19849
|
+
volumetricFog: {
|
|
19850
|
+
enabled: false,
|
|
19851
|
+
// Disabled by default (performance impact)
|
|
19852
|
+
resolution: 0.125,
|
|
19853
|
+
// 1/4 render resolution for ray marching
|
|
19854
|
+
maxSamples: 32,
|
|
19855
|
+
// Ray march samples (8-32)
|
|
19856
|
+
blurRadius: 8,
|
|
19857
|
+
// Gaussian blur radius
|
|
19858
|
+
densityMultiplier: 1,
|
|
19859
|
+
// Multiplies base fog density
|
|
19860
|
+
scatterStrength: 0.35,
|
|
19861
|
+
// Light scattering intensity
|
|
19862
|
+
mainLightScatter: 1.4,
|
|
19863
|
+
// Main directional light scattering boost
|
|
19864
|
+
mainLightScatterDark: 5,
|
|
19865
|
+
// Main directional light scattering boost
|
|
19866
|
+
mainLightSaturation: 0.15,
|
|
19867
|
+
// Main light color saturation in fog
|
|
19868
|
+
maxFogOpacity: 0.3,
|
|
19869
|
+
// Maximum fog opacity (0-1)
|
|
19870
|
+
heightRange: [-2, 8],
|
|
19871
|
+
// [bottom, top] Y bounds for fog (low ground fog)
|
|
19872
|
+
windDirection: [1, 0, 0.2],
|
|
19873
|
+
// Wind direction for fog animation
|
|
19874
|
+
windSpeed: 0.5,
|
|
19875
|
+
// Wind speed multiplier
|
|
19876
|
+
noiseScale: 0.9,
|
|
19877
|
+
// 3D noise frequency (higher = finer detail)
|
|
19878
|
+
noiseStrength: 0.8,
|
|
19879
|
+
// Noise intensity (0 = uniform, 1 = full variation)
|
|
19880
|
+
noiseOctaves: 6,
|
|
19881
|
+
// Noise detail layers
|
|
19882
|
+
noiseEnabled: true,
|
|
19883
|
+
// Enable 3D noise (disable for debug)
|
|
19884
|
+
lightingEnabled: true,
|
|
19885
|
+
// Light fog from scene lights
|
|
19886
|
+
shadowsEnabled: true,
|
|
19887
|
+
// Apply shadows to fog
|
|
19888
|
+
brightnessThreshold: 0.8,
|
|
19889
|
+
// Scene luminance where fog starts fading (like bloom)
|
|
19890
|
+
minVisibility: 0.15,
|
|
19891
|
+
// Minimum fog visibility over bright surfaces (0-1)
|
|
19892
|
+
skyBrightness: 1.2
|
|
19893
|
+
// Virtual brightness for sky pixels (depth at far plane)
|
|
19894
|
+
//debugSkyCheck: true
|
|
19895
|
+
},
|
|
17371
19896
|
// Planar Reflections (alternative to SSR for water/floor)
|
|
17372
19897
|
planarReflection: {
|
|
17373
19898
|
enabled: true,
|
|
@@ -17423,15 +19948,68 @@ const DEFAULT_SETTINGS = {
|
|
|
17423
19948
|
// FPS threshold for auto-disable
|
|
17424
19949
|
disableDelay: 3
|
|
17425
19950
|
// Seconds below threshold before disabling
|
|
19951
|
+
},
|
|
19952
|
+
// CRT effect (retro monitor simulation)
|
|
19953
|
+
crt: {
|
|
19954
|
+
enabled: false,
|
|
19955
|
+
// Enable CRT effect (geometry, scanlines, etc.)
|
|
19956
|
+
upscaleEnabled: false,
|
|
19957
|
+
// Enable upscaling (pixelated look) even when CRT disabled
|
|
19958
|
+
upscaleTarget: 4,
|
|
19959
|
+
// Target upscale multiplier (4x render resolution)
|
|
19960
|
+
maxTextureSize: 4096,
|
|
19961
|
+
// Max upscaled texture dimension
|
|
19962
|
+
// Geometry distortion
|
|
19963
|
+
curvature: 0.14,
|
|
19964
|
+
// Screen curvature amount (0-0.15)
|
|
19965
|
+
cornerRadius: 0.055,
|
|
19966
|
+
// Rounded corner radius (0-0.1)
|
|
19967
|
+
zoom: 1.06,
|
|
19968
|
+
// Zoom to compensate for curvature shrinkage
|
|
19969
|
+
// Scanlines (electron beam simulation - Gaussian profile)
|
|
19970
|
+
scanlineIntensity: 0.4,
|
|
19971
|
+
// Scanline effect strength (0-1)
|
|
19972
|
+
scanlineWidth: 0,
|
|
19973
|
+
// Beam width (0=thin/center only, 1=no gap)
|
|
19974
|
+
scanlineBrightBoost: 0.8,
|
|
19975
|
+
// Bright pixels widen beam to fill gaps (0-1)
|
|
19976
|
+
scanlineHeight: 5,
|
|
19977
|
+
// Scanline height in canvas pixels
|
|
19978
|
+
// RGB convergence error (color channel misalignment)
|
|
19979
|
+
convergence: [0.79, 0, -0.77],
|
|
19980
|
+
// RGB X offset in source pixels
|
|
19981
|
+
// Phosphor mask
|
|
19982
|
+
maskType: "aperture",
|
|
19983
|
+
// 'aperture', 'slot', 'shadow', 'none'
|
|
19984
|
+
maskIntensity: 0.25,
|
|
19985
|
+
// Mask strength (0-1)
|
|
19986
|
+
maskScale: 1,
|
|
19987
|
+
// Mask size multiplier
|
|
19988
|
+
// Vignette (edge darkening)
|
|
19989
|
+
vignetteIntensity: 0.54,
|
|
19990
|
+
// Edge darkening strength (0-1)
|
|
19991
|
+
vignetteSize: 0.85,
|
|
19992
|
+
// Vignette size (larger = more visible)
|
|
19993
|
+
// Horizontal blur (beam softness)
|
|
19994
|
+
blurSize: 0.79
|
|
19995
|
+
// Horizontal blur in pixels (0-2)
|
|
17426
19996
|
}
|
|
17427
19997
|
};
|
|
17428
19998
|
async function createWebGPUContext(engine, canvasId) {
|
|
17429
19999
|
try {
|
|
17430
20000
|
let configureContext = function() {
|
|
17431
|
-
|
|
17432
|
-
|
|
17433
|
-
|
|
17434
|
-
|
|
20001
|
+
let pixelWidth, pixelHeight;
|
|
20002
|
+
if (engine._devicePixelSize) {
|
|
20003
|
+
pixelWidth = engine._devicePixelSize.width;
|
|
20004
|
+
pixelHeight = engine._devicePixelSize.height;
|
|
20005
|
+
} else {
|
|
20006
|
+
const devicePixelRatio = window.devicePixelRatio || 1;
|
|
20007
|
+
pixelWidth = Math.round(canvas.clientWidth * devicePixelRatio);
|
|
20008
|
+
pixelHeight = Math.round(canvas.clientHeight * devicePixelRatio);
|
|
20009
|
+
}
|
|
20010
|
+
canvas.width = pixelWidth;
|
|
20011
|
+
canvas.height = pixelHeight;
|
|
20012
|
+
engine._canvasPixelSize = { width: pixelWidth, height: pixelHeight };
|
|
17435
20013
|
context.configure({
|
|
17436
20014
|
device,
|
|
17437
20015
|
format: canvasFormat,
|
|
@@ -17604,9 +20182,34 @@ class Engine {
|
|
|
17604
20182
|
this.stats.avg_fps = 60;
|
|
17605
20183
|
this.stats.avg_dt_render = 0.1;
|
|
17606
20184
|
requestAnimationFrame(() => this._frame());
|
|
17607
|
-
|
|
17608
|
-
|
|
17609
|
-
|
|
20185
|
+
this._devicePixelSize = null;
|
|
20186
|
+
try {
|
|
20187
|
+
const resizeObserver = new ResizeObserver((entries) => {
|
|
20188
|
+
for (const entry of entries) {
|
|
20189
|
+
if (entry.devicePixelContentBoxSize) {
|
|
20190
|
+
const size = entry.devicePixelContentBoxSize[0];
|
|
20191
|
+
this._devicePixelSize = {
|
|
20192
|
+
width: size.inlineSize,
|
|
20193
|
+
height: size.blockSize
|
|
20194
|
+
};
|
|
20195
|
+
} else if (entry.contentBoxSize) {
|
|
20196
|
+
const size = entry.contentBoxSize[0];
|
|
20197
|
+
const dpr = window.devicePixelRatio || 1;
|
|
20198
|
+
this._devicePixelSize = {
|
|
20199
|
+
width: Math.round(size.inlineSize * dpr),
|
|
20200
|
+
height: Math.round(size.blockSize * dpr)
|
|
20201
|
+
};
|
|
20202
|
+
}
|
|
20203
|
+
this.needsResize = true;
|
|
20204
|
+
}
|
|
20205
|
+
});
|
|
20206
|
+
resizeObserver.observe(this.canvas, { box: "device-pixel-content-box" });
|
|
20207
|
+
} catch (e) {
|
|
20208
|
+
console.log("ResizeObserver device-pixel-content-box not supported, falling back to window resize");
|
|
20209
|
+
window.addEventListener("resize", () => {
|
|
20210
|
+
this.needsResize = true;
|
|
20211
|
+
});
|
|
20212
|
+
}
|
|
17610
20213
|
setInterval(() => {
|
|
17611
20214
|
if (this.needsResize && !this._resizing) {
|
|
17612
20215
|
this.needsResize = false;
|
|
@@ -17726,11 +20329,19 @@ class Engine {
|
|
|
17726
20329
|
* @param {Array} options.position - Optional position offset [x, y, z]
|
|
17727
20330
|
* @param {Array} options.rotation - Optional rotation offset [x, y, z] in radians
|
|
17728
20331
|
* @param {number} options.scale - Optional uniform scale multiplier
|
|
20332
|
+
* @param {boolean} options.doubleSided - Optional: force all materials to be double-sided
|
|
17729
20333
|
* @returns {Promise<Object>} Object containing { meshes, nodes, skins, animations }
|
|
17730
20334
|
*/
|
|
17731
20335
|
async loadScene(url, options = {}) {
|
|
17732
20336
|
const result = await loadGltf(this, url, options);
|
|
17733
20337
|
const { meshes, nodes } = result;
|
|
20338
|
+
if (options.doubleSided) {
|
|
20339
|
+
for (const mesh of Object.values(meshes)) {
|
|
20340
|
+
if (mesh.material) {
|
|
20341
|
+
mesh.material.doubleSided = true;
|
|
20342
|
+
}
|
|
20343
|
+
}
|
|
20344
|
+
}
|
|
17734
20345
|
for (const node of nodes) {
|
|
17735
20346
|
if (!node.parent) {
|
|
17736
20347
|
node.updateMatrix(null);
|
|
@@ -17750,6 +20361,23 @@ class Engine {
|
|
|
17750
20361
|
[scl, scl, scl]
|
|
17751
20362
|
);
|
|
17752
20363
|
}
|
|
20364
|
+
let combinedBsphere = null;
|
|
20365
|
+
const hasAnySkin = Object.values(meshes).some((m) => m.hasSkin);
|
|
20366
|
+
if (hasAnySkin) {
|
|
20367
|
+
const allPositions = [];
|
|
20368
|
+
for (const mesh of Object.values(meshes)) {
|
|
20369
|
+
const positions = mesh.geometry?.attributes?.position;
|
|
20370
|
+
if (positions) {
|
|
20371
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
20372
|
+
allPositions.push(positions[i], positions[i + 1], positions[i + 2]);
|
|
20373
|
+
}
|
|
20374
|
+
}
|
|
20375
|
+
}
|
|
20376
|
+
if (allPositions.length > 0) {
|
|
20377
|
+
const { calculateBoundingSphere: calculateBoundingSphere2 } = await Promise.resolve().then(() => BoundingSphere);
|
|
20378
|
+
combinedBsphere = calculateBoundingSphere2(new Float32Array(allPositions));
|
|
20379
|
+
}
|
|
20380
|
+
}
|
|
17753
20381
|
for (const [name, mesh] of Object.entries(meshes)) {
|
|
17754
20382
|
let meshNode = null;
|
|
17755
20383
|
if (mesh.nodeIndex !== null && mesh.nodeIndex !== void 0) {
|
|
@@ -17762,7 +20390,7 @@ class Engine {
|
|
|
17762
20390
|
if (options.position || options.rotation || options.scale) {
|
|
17763
20391
|
mat4.multiply(worldMatrix, rootTransform, worldMatrix);
|
|
17764
20392
|
}
|
|
17765
|
-
const localBsphere = mesh.geometry.getBoundingSphere?.();
|
|
20393
|
+
const localBsphere = hasAnySkin && combinedBsphere ? combinedBsphere : mesh.geometry.getBoundingSphere?.();
|
|
17766
20394
|
let worldCenter = [0, 0, 0];
|
|
17767
20395
|
let worldRadius = 1;
|
|
17768
20396
|
if (localBsphere && localBsphere.radius > 0) {
|
|
@@ -17777,6 +20405,9 @@ class Engine {
|
|
|
17777
20405
|
const scaleZ = Math.sqrt(worldMatrix[8] ** 2 + worldMatrix[9] ** 2 + worldMatrix[10] ** 2);
|
|
17778
20406
|
worldRadius = localBsphere.radius * Math.max(scaleX, scaleY, scaleZ);
|
|
17779
20407
|
}
|
|
20408
|
+
if (hasAnySkin && combinedBsphere) {
|
|
20409
|
+
mesh.combinedBsphere = combinedBsphere;
|
|
20410
|
+
}
|
|
17780
20411
|
mesh.addInstance(worldCenter, worldRadius);
|
|
17781
20412
|
mesh.updateInstance(0, worldMatrix);
|
|
17782
20413
|
mesh.static = true;
|
|
@@ -17845,6 +20476,16 @@ class Engine {
|
|
|
17845
20476
|
getEntity(id) {
|
|
17846
20477
|
return this.entityManager.get(id);
|
|
17847
20478
|
}
|
|
20479
|
+
/**
|
|
20480
|
+
* Invalidate occlusion culling data and reset warmup period.
|
|
20481
|
+
* Call this after scene loading or major camera teleportation to prevent
|
|
20482
|
+
* incorrect occlusion culling with stale depth buffer data.
|
|
20483
|
+
*/
|
|
20484
|
+
invalidateOcclusionCulling() {
|
|
20485
|
+
if (this.renderer) {
|
|
20486
|
+
this.renderer.invalidateOcclusionCulling();
|
|
20487
|
+
}
|
|
20488
|
+
}
|
|
17848
20489
|
async _create() {
|
|
17849
20490
|
let camera = new Camera(this);
|
|
17850
20491
|
camera.updateMatrix();
|
|
@@ -17867,6 +20508,8 @@ class Engine {
|
|
|
17867
20508
|
}
|
|
17868
20509
|
async _after_create() {
|
|
17869
20510
|
this.renderer = await RenderGraph.create(this, this.environment, this.environmentEncoding);
|
|
20511
|
+
this.raycaster = new Raycaster(this);
|
|
20512
|
+
await this.raycaster.initialize();
|
|
17870
20513
|
}
|
|
17871
20514
|
_update(dt) {
|
|
17872
20515
|
this._updateInput();
|
|
@@ -17922,7 +20565,7 @@ class Engine {
|
|
|
17922
20565
|
this.guiCanvas.height = canvas.height;
|
|
17923
20566
|
this.guiCtx.clearRect(0, 0, canvas.width, canvas.height);
|
|
17924
20567
|
}
|
|
17925
|
-
await this.renderer.resize(canvas.width, canvas.height);
|
|
20568
|
+
await this.renderer.resize(canvas.width, canvas.height, this.renderScale);
|
|
17926
20569
|
this.resize();
|
|
17927
20570
|
await new Promise((resolve) => setTimeout(resolve, 16));
|
|
17928
20571
|
this._resizing = false;
|
|
@@ -18176,6 +20819,7 @@ export {
|
|
|
18176
20819
|
Mesh,
|
|
18177
20820
|
ParticleEmitter,
|
|
18178
20821
|
ParticleSystem,
|
|
20822
|
+
Raycaster,
|
|
18179
20823
|
RenderGraph,
|
|
18180
20824
|
Texture,
|
|
18181
20825
|
fail
|