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.cjs
CHANGED
|
@@ -770,6 +770,13 @@ function sphereInCascade(bsphere, cascadeMatrix) {
|
|
|
770
770
|
if (clipZ + clipRadius < 0 || clipZ - clipRadius > 1) return false;
|
|
771
771
|
return true;
|
|
772
772
|
}
|
|
773
|
+
const BoundingSphere = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
774
|
+
__proto__: null,
|
|
775
|
+
calculateBoundingSphere,
|
|
776
|
+
calculateShadowBoundingSphere,
|
|
777
|
+
sphereInCascade,
|
|
778
|
+
transformBoundingSphere
|
|
779
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
773
780
|
var _UID$2 = 30001;
|
|
774
781
|
class Geometry {
|
|
775
782
|
constructor(engine, attributes) {
|
|
@@ -1932,6 +1939,7 @@ class Material {
|
|
|
1932
1939
|
this.luminanceToAlpha = false;
|
|
1933
1940
|
this.forceEmissive = false;
|
|
1934
1941
|
this.specularBoost = 0;
|
|
1942
|
+
this.doubleSided = false;
|
|
1935
1943
|
}
|
|
1936
1944
|
/**
|
|
1937
1945
|
* Get textures array, substituting albedo for emission if forceEmissive is true
|
|
@@ -2667,6 +2675,13 @@ class ShadowPass extends BasePass {
|
|
|
2667
2675
|
this.noiseAnimated = true;
|
|
2668
2676
|
this.hizPass = null;
|
|
2669
2677
|
this._meshBindGroups = /* @__PURE__ */ new WeakMap();
|
|
2678
|
+
this._cameraShadowBuffer = null;
|
|
2679
|
+
this._cameraShadowReadBuffer = null;
|
|
2680
|
+
this._cameraShadowPipeline = null;
|
|
2681
|
+
this._cameraShadowBindGroup = null;
|
|
2682
|
+
this._cameraShadowUniformBuffer = null;
|
|
2683
|
+
this._cameraInShadow = false;
|
|
2684
|
+
this._cameraShadowPending = false;
|
|
2670
2685
|
}
|
|
2671
2686
|
/**
|
|
2672
2687
|
* Set the HiZ pass for occlusion culling of static meshes
|
|
@@ -2783,6 +2798,7 @@ class ShadowPass extends BasePass {
|
|
|
2783
2798
|
});
|
|
2784
2799
|
this._createPlaceholderTextures();
|
|
2785
2800
|
await this._createPipeline();
|
|
2801
|
+
await this._createCameraShadowDetection();
|
|
2786
2802
|
}
|
|
2787
2803
|
async _createPipeline() {
|
|
2788
2804
|
const { device } = this.engine;
|
|
@@ -3226,12 +3242,13 @@ class ShadowPass extends BasePass {
|
|
|
3226
3242
|
* @param {mat4} cascadeMatrix - Cascade's view-projection matrix
|
|
3227
3243
|
* @param {Array} lightDir - Normalized light direction (pointing to light)
|
|
3228
3244
|
* @param {number} groundLevel - Ground plane Y coordinate
|
|
3245
|
+
* @param {Object|null} combinedBsphere - Combined bsphere for skinned models (optional)
|
|
3229
3246
|
* @returns {{ data: Float32Array, count: number }}
|
|
3230
3247
|
*/
|
|
3231
|
-
_buildCascadeFilteredInstances(geometry, cascadeMatrix, lightDir, groundLevel) {
|
|
3248
|
+
_buildCascadeFilteredInstances(geometry, cascadeMatrix, lightDir, groundLevel, combinedBsphere = null) {
|
|
3232
3249
|
const instanceStride = 28;
|
|
3233
3250
|
const visibleIndices = [];
|
|
3234
|
-
const localBsphere = geometry.getBoundingSphere?.();
|
|
3251
|
+
const localBsphere = combinedBsphere || geometry.getBoundingSphere?.();
|
|
3235
3252
|
for (let i = 0; i < geometry.instanceCount; i++) {
|
|
3236
3253
|
const offset = i * instanceStride;
|
|
3237
3254
|
let bsphere = {
|
|
@@ -3279,12 +3296,13 @@ class ShadowPass extends BasePass {
|
|
|
3279
3296
|
* @param {Array} lightDir - Normalized light direction
|
|
3280
3297
|
* @param {number} maxDistance - Max shadow distance (min of light radius and spotMaxDistance)
|
|
3281
3298
|
* @param {number} coneAngle - Half-angle of spotlight cone in radians
|
|
3299
|
+
* @param {Object|null} combinedBsphere - Combined bsphere for skinned models (optional)
|
|
3282
3300
|
* @returns {{ data: Float32Array, count: number }}
|
|
3283
3301
|
*/
|
|
3284
|
-
_buildFilteredInstances(geometry, lightPos, lightDir, maxDistance, coneAngle) {
|
|
3302
|
+
_buildFilteredInstances(geometry, lightPos, lightDir, maxDistance, coneAngle, combinedBsphere = null) {
|
|
3285
3303
|
const instanceStride = 28;
|
|
3286
3304
|
const visibleIndices = [];
|
|
3287
|
-
const localBsphere = geometry.getBoundingSphere?.();
|
|
3305
|
+
const localBsphere = combinedBsphere || geometry.getBoundingSphere?.();
|
|
3288
3306
|
for (let i = 0; i < geometry.instanceCount; i++) {
|
|
3289
3307
|
const offset = i * instanceStride;
|
|
3290
3308
|
let bsphere = {
|
|
@@ -3504,6 +3522,10 @@ class ShadowPass extends BasePass {
|
|
|
3504
3522
|
console.warn("ShadowPass: No pipeline or meshes", { pipeline: !!this.pipeline, meshes: !!meshes });
|
|
3505
3523
|
return;
|
|
3506
3524
|
}
|
|
3525
|
+
this._meshBindGroups = /* @__PURE__ */ new WeakMap();
|
|
3526
|
+
if (this._skinBindGroups) {
|
|
3527
|
+
this._skinBindGroups = /* @__PURE__ */ new WeakMap();
|
|
3528
|
+
}
|
|
3507
3529
|
let shadowDrawCalls = 0;
|
|
3508
3530
|
let shadowTriangles = 0;
|
|
3509
3531
|
let shadowCulledInstances = 0;
|
|
@@ -3542,7 +3564,7 @@ class ShadowPass extends BasePass {
|
|
|
3542
3564
|
visibleMeshes[name] = mesh;
|
|
3543
3565
|
continue;
|
|
3544
3566
|
}
|
|
3545
|
-
const localBsphere = geometry.getBoundingSphere?.();
|
|
3567
|
+
const localBsphere = mesh.combinedBsphere || geometry.getBoundingSphere?.();
|
|
3546
3568
|
if (!localBsphere || localBsphere.radius <= 0) {
|
|
3547
3569
|
meshNoBsphere++;
|
|
3548
3570
|
visibleMeshes[name] = mesh;
|
|
@@ -3551,23 +3573,28 @@ class ShadowPass extends BasePass {
|
|
|
3551
3573
|
const matrix = geometry.instanceData?.subarray(0, 16);
|
|
3552
3574
|
const worldBsphere = matrix ? transformBoundingSphere(localBsphere, matrix) : localBsphere;
|
|
3553
3575
|
const shadowBsphere = mainLightEnabled ? calculateShadowBoundingSphere(worldBsphere, lightDir, groundLevel) : worldBsphere;
|
|
3554
|
-
const
|
|
3555
|
-
const
|
|
3556
|
-
|
|
3557
|
-
|
|
3576
|
+
const skinnedExpansion = this.engine?.settings?.shadow?.skinnedBsphereExpansion ?? 2;
|
|
3577
|
+
const cullBsphere = mesh.hasSkin ? {
|
|
3578
|
+
center: shadowBsphere.center,
|
|
3579
|
+
radius: shadowBsphere.radius * skinnedExpansion
|
|
3580
|
+
} : shadowBsphere;
|
|
3581
|
+
const dx = cullBsphere.center[0] - camera.position[0];
|
|
3582
|
+
const dy = cullBsphere.center[1] - camera.position[1];
|
|
3583
|
+
const dz = cullBsphere.center[2] - camera.position[2];
|
|
3584
|
+
const distance = Math.sqrt(dx * dx + dy * dy + dz * dz) - cullBsphere.radius;
|
|
3558
3585
|
if (distance > shadowMaxDistance) {
|
|
3559
3586
|
meshDistanceCulled++;
|
|
3560
3587
|
continue;
|
|
3561
3588
|
}
|
|
3562
3589
|
if (shadowFrustumCullingEnabled && cameraFrustum) {
|
|
3563
|
-
if (!cameraFrustum.testSpherePlanes(
|
|
3590
|
+
if (!cameraFrustum.testSpherePlanes(cullBsphere)) {
|
|
3564
3591
|
meshFrustumCulled++;
|
|
3565
3592
|
continue;
|
|
3566
3593
|
}
|
|
3567
3594
|
}
|
|
3568
3595
|
if (shadowHiZEnabled && this.hizPass) {
|
|
3569
3596
|
const occluded = this.hizPass.testSphereOcclusion(
|
|
3570
|
-
|
|
3597
|
+
cullBsphere,
|
|
3571
3598
|
camera.viewProj,
|
|
3572
3599
|
camera.near,
|
|
3573
3600
|
camera.far,
|
|
@@ -3629,7 +3656,9 @@ class ShadowPass extends BasePass {
|
|
|
3629
3656
|
geometry,
|
|
3630
3657
|
this.cascadeMatrices[cascade],
|
|
3631
3658
|
lightDir,
|
|
3632
|
-
groundLevel
|
|
3659
|
+
groundLevel,
|
|
3660
|
+
mesh.combinedBsphere
|
|
3661
|
+
// Use combined bsphere for skinned models
|
|
3633
3662
|
);
|
|
3634
3663
|
if (filtered.count === 0) {
|
|
3635
3664
|
cascadeCulledInstances += geometry.instanceCount;
|
|
@@ -3748,6 +3777,9 @@ class ShadowPass extends BasePass {
|
|
|
3748
3777
|
this.cascadeMatricesData.set(this.cascadeMatrices[i], i * 16);
|
|
3749
3778
|
}
|
|
3750
3779
|
device.queue.writeBuffer(this.cascadeMatricesBuffer, 0, this.cascadeMatricesData);
|
|
3780
|
+
if (mainLightEnabled) {
|
|
3781
|
+
this._updateCameraShadowDetection(camera);
|
|
3782
|
+
}
|
|
3751
3783
|
if (!mainLightEnabled) {
|
|
3752
3784
|
for (const name in meshes) {
|
|
3753
3785
|
const mesh = meshes[name];
|
|
@@ -3832,7 +3864,9 @@ class ShadowPass extends BasePass {
|
|
|
3832
3864
|
lightPos,
|
|
3833
3865
|
spotLightDir,
|
|
3834
3866
|
spotShadowMaxDist,
|
|
3835
|
-
coneAngle
|
|
3867
|
+
coneAngle,
|
|
3868
|
+
mesh.combinedBsphere
|
|
3869
|
+
// Use combined bsphere for skinned models
|
|
3836
3870
|
);
|
|
3837
3871
|
if (filtered.count === 0) {
|
|
3838
3872
|
spotCulledInstances += geometry.instanceCount;
|
|
@@ -4061,6 +4095,184 @@ class ShadowPass extends BasePass {
|
|
|
4061
4095
|
getLastSlotInfo() {
|
|
4062
4096
|
return this.lastSlotInfo;
|
|
4063
4097
|
}
|
|
4098
|
+
/**
|
|
4099
|
+
* Get cascade matrices as JavaScript arrays (for CPU-side calculations)
|
|
4100
|
+
*/
|
|
4101
|
+
getCascadeMatrices() {
|
|
4102
|
+
return this.cascadeMatrices;
|
|
4103
|
+
}
|
|
4104
|
+
/**
|
|
4105
|
+
* Create resources for camera shadow detection
|
|
4106
|
+
* Uses a compute shader to sample shadow at camera position
|
|
4107
|
+
*/
|
|
4108
|
+
async _createCameraShadowDetection() {
|
|
4109
|
+
const { device } = this.engine;
|
|
4110
|
+
this._cameraShadowUniformBuffer = device.createBuffer({
|
|
4111
|
+
label: "Camera Shadow Detection Uniforms",
|
|
4112
|
+
size: 256,
|
|
4113
|
+
// Aligned
|
|
4114
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
4115
|
+
});
|
|
4116
|
+
this._cameraShadowBuffer = device.createBuffer({
|
|
4117
|
+
label: "Camera Shadow Result",
|
|
4118
|
+
size: 4,
|
|
4119
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
|
|
4120
|
+
});
|
|
4121
|
+
this._cameraShadowReadBuffer = device.createBuffer({
|
|
4122
|
+
label: "Camera Shadow Readback",
|
|
4123
|
+
size: 4,
|
|
4124
|
+
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
|
|
4125
|
+
});
|
|
4126
|
+
const shaderModule = device.createShaderModule({
|
|
4127
|
+
label: "Camera Shadow Detection Shader",
|
|
4128
|
+
code: `
|
|
4129
|
+
struct Uniforms {
|
|
4130
|
+
cameraPosition: vec3f,
|
|
4131
|
+
_pad0: f32,
|
|
4132
|
+
cascadeMatrix0: mat4x4f,
|
|
4133
|
+
cascadeMatrix1: mat4x4f,
|
|
4134
|
+
cascadeMatrix2: mat4x4f,
|
|
4135
|
+
}
|
|
4136
|
+
|
|
4137
|
+
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
|
|
4138
|
+
@group(0) @binding(1) var shadowMap: texture_depth_2d_array;
|
|
4139
|
+
@group(0) @binding(2) var shadowSampler: sampler_comparison;
|
|
4140
|
+
@group(0) @binding(3) var<storage, read_write> result: f32;
|
|
4141
|
+
|
|
4142
|
+
fn sampleShadowCascade(worldPos: vec3f, cascadeMatrix: mat4x4f, cascadeIndex: i32) -> f32 {
|
|
4143
|
+
let lightSpacePos = cascadeMatrix * vec4f(worldPos, 1.0);
|
|
4144
|
+
let projCoords = lightSpacePos.xyz / lightSpacePos.w;
|
|
4145
|
+
|
|
4146
|
+
// Convert to UV space
|
|
4147
|
+
let uv = vec2f(projCoords.x * 0.5 + 0.5, 0.5 - projCoords.y * 0.5);
|
|
4148
|
+
|
|
4149
|
+
// Check bounds
|
|
4150
|
+
if (uv.x < 0.01 || uv.x > 0.99 || uv.y < 0.01 || uv.y > 0.99 ||
|
|
4151
|
+
projCoords.z < 0.0 || projCoords.z > 1.0) {
|
|
4152
|
+
return -1.0; // Out of bounds, try next cascade
|
|
4153
|
+
}
|
|
4154
|
+
|
|
4155
|
+
let bias = 0.005;
|
|
4156
|
+
let depth = projCoords.z - bias;
|
|
4157
|
+
return textureSampleCompareLevel(shadowMap, shadowSampler, uv, cascadeIndex, depth);
|
|
4158
|
+
}
|
|
4159
|
+
|
|
4160
|
+
@compute @workgroup_size(1)
|
|
4161
|
+
fn main() {
|
|
4162
|
+
let pos = uniforms.cameraPosition;
|
|
4163
|
+
|
|
4164
|
+
// Sample multiple points around camera (5m sphere)
|
|
4165
|
+
var totalShadow = 0.0;
|
|
4166
|
+
var sampleCount = 0.0;
|
|
4167
|
+
|
|
4168
|
+
let offsets = array<vec3f, 7>(
|
|
4169
|
+
vec3f(0.0, 0.0, 0.0), // Center
|
|
4170
|
+
vec3f(0.0, 3.0, 0.0), // Above
|
|
4171
|
+
vec3f(0.0, -2.0, 0.0), // Below
|
|
4172
|
+
vec3f(4.0, 0.0, 0.0), // Right
|
|
4173
|
+
vec3f(-4.0, 0.0, 0.0), // Left
|
|
4174
|
+
vec3f(0.0, 0.0, 4.0), // Front
|
|
4175
|
+
vec3f(0.0, 0.0, -4.0), // Back
|
|
4176
|
+
);
|
|
4177
|
+
|
|
4178
|
+
for (var i = 0; i < 7; i++) {
|
|
4179
|
+
let samplePos = pos + offsets[i];
|
|
4180
|
+
|
|
4181
|
+
// Try cascade 0 first (closest)
|
|
4182
|
+
var shadow = sampleShadowCascade(samplePos, uniforms.cascadeMatrix0, 0);
|
|
4183
|
+
if (shadow < 0.0) {
|
|
4184
|
+
// Try cascade 1
|
|
4185
|
+
shadow = sampleShadowCascade(samplePos, uniforms.cascadeMatrix1, 1);
|
|
4186
|
+
}
|
|
4187
|
+
if (shadow < 0.0) {
|
|
4188
|
+
// Try cascade 2
|
|
4189
|
+
shadow = sampleShadowCascade(samplePos, uniforms.cascadeMatrix2, 2);
|
|
4190
|
+
}
|
|
4191
|
+
|
|
4192
|
+
if (shadow >= 0.0) {
|
|
4193
|
+
totalShadow += shadow;
|
|
4194
|
+
sampleCount += 1.0;
|
|
4195
|
+
}
|
|
4196
|
+
}
|
|
4197
|
+
|
|
4198
|
+
// Average shadow (0 = all in shadow, 1 = all lit)
|
|
4199
|
+
// If no valid samples, assume lit
|
|
4200
|
+
if (sampleCount > 0.0) {
|
|
4201
|
+
result = totalShadow / sampleCount;
|
|
4202
|
+
} else {
|
|
4203
|
+
result = 1.0;
|
|
4204
|
+
}
|
|
4205
|
+
}
|
|
4206
|
+
`
|
|
4207
|
+
});
|
|
4208
|
+
this._cameraShadowBGL = device.createBindGroupLayout({
|
|
4209
|
+
label: "Camera Shadow Detection BGL",
|
|
4210
|
+
entries: [
|
|
4211
|
+
{ binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: "uniform" } },
|
|
4212
|
+
{ binding: 1, visibility: GPUShaderStage.COMPUTE, texture: { sampleType: "depth", viewDimension: "2d-array" } },
|
|
4213
|
+
{ binding: 2, visibility: GPUShaderStage.COMPUTE, sampler: { type: "comparison" } },
|
|
4214
|
+
{ binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: "storage" } }
|
|
4215
|
+
]
|
|
4216
|
+
});
|
|
4217
|
+
this._cameraShadowPipeline = await device.createComputePipelineAsync({
|
|
4218
|
+
label: "Camera Shadow Detection Pipeline",
|
|
4219
|
+
layout: device.createPipelineLayout({ bindGroupLayouts: [this._cameraShadowBGL] }),
|
|
4220
|
+
compute: { module: shaderModule, entryPoint: "main" }
|
|
4221
|
+
});
|
|
4222
|
+
}
|
|
4223
|
+
/**
|
|
4224
|
+
* Update camera shadow detection (called during execute)
|
|
4225
|
+
* Dispatches compute shader and starts async readback
|
|
4226
|
+
*/
|
|
4227
|
+
_updateCameraShadowDetection(camera) {
|
|
4228
|
+
if (!this._cameraShadowPipeline || !this.directionalShadowMap) return;
|
|
4229
|
+
if (this._cameraShadowPending) return;
|
|
4230
|
+
const { device } = this.engine;
|
|
4231
|
+
const cameraPos = camera.position || [0, 0, 0];
|
|
4232
|
+
const data = new Float32Array(64);
|
|
4233
|
+
data[0] = cameraPos[0];
|
|
4234
|
+
data[1] = cameraPos[1];
|
|
4235
|
+
data[2] = cameraPos[2];
|
|
4236
|
+
data[3] = 0;
|
|
4237
|
+
if (this.cascadeMatrices[0]) data.set(this.cascadeMatrices[0], 4);
|
|
4238
|
+
if (this.cascadeMatrices[1]) data.set(this.cascadeMatrices[1], 20);
|
|
4239
|
+
if (this.cascadeMatrices[2]) data.set(this.cascadeMatrices[2], 36);
|
|
4240
|
+
device.queue.writeBuffer(this._cameraShadowUniformBuffer, 0, data);
|
|
4241
|
+
const bindGroup = device.createBindGroup({
|
|
4242
|
+
layout: this._cameraShadowBGL,
|
|
4243
|
+
entries: [
|
|
4244
|
+
{ binding: 0, resource: { buffer: this._cameraShadowUniformBuffer } },
|
|
4245
|
+
{ binding: 1, resource: this.directionalShadowMapView },
|
|
4246
|
+
{ binding: 2, resource: this.shadowSampler },
|
|
4247
|
+
{ binding: 3, resource: { buffer: this._cameraShadowBuffer } }
|
|
4248
|
+
]
|
|
4249
|
+
});
|
|
4250
|
+
const encoder = device.createCommandEncoder({ label: "Camera Shadow Detection" });
|
|
4251
|
+
const pass = encoder.beginComputePass();
|
|
4252
|
+
pass.setPipeline(this._cameraShadowPipeline);
|
|
4253
|
+
pass.setBindGroup(0, bindGroup);
|
|
4254
|
+
pass.dispatchWorkgroups(1);
|
|
4255
|
+
pass.end();
|
|
4256
|
+
encoder.copyBufferToBuffer(this._cameraShadowBuffer, 0, this._cameraShadowReadBuffer, 0, 4);
|
|
4257
|
+
device.queue.submit([encoder.finish()]);
|
|
4258
|
+
this._cameraShadowPending = true;
|
|
4259
|
+
this._cameraShadowReadBuffer.mapAsync(GPUMapMode.READ).then(() => {
|
|
4260
|
+
const data2 = new Float32Array(this._cameraShadowReadBuffer.getMappedRange());
|
|
4261
|
+
const shadowValue = data2[0];
|
|
4262
|
+
this._cameraShadowReadBuffer.unmap();
|
|
4263
|
+
this._cameraShadowPending = false;
|
|
4264
|
+
this._cameraInShadow = shadowValue < 0.3;
|
|
4265
|
+
}).catch(() => {
|
|
4266
|
+
this._cameraShadowPending = false;
|
|
4267
|
+
});
|
|
4268
|
+
}
|
|
4269
|
+
/**
|
|
4270
|
+
* Check if camera is in shadow (uses async readback result from previous frames)
|
|
4271
|
+
* @returns {boolean} True if camera is mostly in shadow
|
|
4272
|
+
*/
|
|
4273
|
+
isCameraInShadow() {
|
|
4274
|
+
return this._cameraInShadow;
|
|
4275
|
+
}
|
|
4064
4276
|
}
|
|
4065
4277
|
class ProbeCapture {
|
|
4066
4278
|
constructor(engine) {
|
|
@@ -5718,8 +5930,10 @@ class Pipeline {
|
|
|
5718
5930
|
// Optional tile light indices buffer for tiled lighting
|
|
5719
5931
|
lightBuffer = null,
|
|
5720
5932
|
// Optional light storage buffer for tiled lighting
|
|
5721
|
-
noiseTexture = null
|
|
5933
|
+
noiseTexture = null,
|
|
5722
5934
|
// Optional noise texture for alpha hashing
|
|
5935
|
+
doubleSided = false
|
|
5936
|
+
// Optional: disable backface culling for double-sided materials
|
|
5723
5937
|
}) {
|
|
5724
5938
|
let texture = textures[0];
|
|
5725
5939
|
const { canvas, device, canvasFormat, options } = engine;
|
|
@@ -5781,7 +5995,7 @@ class Pipeline {
|
|
|
5781
5995
|
geometry.vertexBufferLayout,
|
|
5782
5996
|
geometry.instanceBufferLayout
|
|
5783
5997
|
];
|
|
5784
|
-
pipelineDescriptor.primitive.cullMode = "back";
|
|
5998
|
+
pipelineDescriptor.primitive.cullMode = doubleSided ? "none" : "back";
|
|
5785
5999
|
pipelineDescriptor.depthStencil = {
|
|
5786
6000
|
depthWriteEnabled: true,
|
|
5787
6001
|
depthCompare: "less",
|
|
@@ -6009,6 +6223,7 @@ class Pipeline {
|
|
|
6009
6223
|
p.tileLightBuffer = tileLightBuffer;
|
|
6010
6224
|
p.lightBuffer = lightBuffer;
|
|
6011
6225
|
p.noiseTexture = noiseTexture;
|
|
6226
|
+
p.doubleSided = doubleSided;
|
|
6012
6227
|
return p;
|
|
6013
6228
|
}
|
|
6014
6229
|
static async pipelineFromTextures(engine, pipelineDescriptor, label, textures, uniformBuffer, skin = null, noiseTexture = null) {
|
|
@@ -6183,7 +6398,7 @@ class Pipeline {
|
|
|
6183
6398
|
}
|
|
6184
6399
|
}
|
|
6185
6400
|
}
|
|
6186
|
-
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}";
|
|
6401
|
+
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}";
|
|
6187
6402
|
class GBuffer {
|
|
6188
6403
|
constructor() {
|
|
6189
6404
|
this.isGBuffer = true;
|
|
@@ -6404,7 +6619,8 @@ class GBufferPass extends BasePass {
|
|
|
6404
6619
|
const isSkinned = mesh.hasSkin && mesh.skin;
|
|
6405
6620
|
const meshId = mesh.uid || mesh.geometry?.uid || "default";
|
|
6406
6621
|
const forceEmissive = mesh.material?.forceEmissive ? "_emissive" : "";
|
|
6407
|
-
|
|
6622
|
+
const doubleSided = mesh.material?.doubleSided ? "_dbl" : "";
|
|
6623
|
+
return `${mesh.material.uid}_${meshId}${isSkinned ? "_skinned" : ""}${forceEmissive}${doubleSided}`;
|
|
6408
6624
|
}
|
|
6409
6625
|
/**
|
|
6410
6626
|
* Check if pipeline is ready for a mesh (non-blocking)
|
|
@@ -6444,7 +6660,8 @@ class GBufferPass extends BasePass {
|
|
|
6444
6660
|
textures: mesh.material.textures,
|
|
6445
6661
|
renderTarget: this.gbuffer,
|
|
6446
6662
|
skin: isSkinned ? mesh.skin : null,
|
|
6447
|
-
noiseTexture: this.noiseTexture
|
|
6663
|
+
noiseTexture: this.noiseTexture,
|
|
6664
|
+
doubleSided: mesh.material?.doubleSided ?? false
|
|
6448
6665
|
}).then((pipeline) => {
|
|
6449
6666
|
this.pendingPipelines.delete(key);
|
|
6450
6667
|
pipeline._warmupFrames = 2;
|
|
@@ -6479,7 +6696,8 @@ class GBufferPass extends BasePass {
|
|
|
6479
6696
|
textures: mesh.material.textures,
|
|
6480
6697
|
renderTarget: this.gbuffer,
|
|
6481
6698
|
skin: isSkinned ? mesh.skin : null,
|
|
6482
|
-
noiseTexture: this.noiseTexture
|
|
6699
|
+
noiseTexture: this.noiseTexture,
|
|
6700
|
+
doubleSided: mesh.material?.doubleSided ?? false
|
|
6483
6701
|
});
|
|
6484
6702
|
pipeline._warmupFrames = 2;
|
|
6485
6703
|
pipelinesMap.set(key, pipeline);
|
|
@@ -6502,6 +6720,8 @@ class GBufferPass extends BasePass {
|
|
|
6502
6720
|
const prevViewProjMatrix = prevData?.hasValidHistory ? prevData.viewProj : camera.viewProj;
|
|
6503
6721
|
const emissionFactor = this.settings?.environment?.emissionFactor ?? [1, 1, 1, 4];
|
|
6504
6722
|
const mipBias = this.settings?.rendering?.mipBias ?? options.mipBias ?? 0;
|
|
6723
|
+
const animationSpeed = this.settings?.animation?.speed ?? 1;
|
|
6724
|
+
const globalAnimTime = performance.now() / 1e3 * animationSpeed;
|
|
6505
6725
|
stats.drawCalls = 0;
|
|
6506
6726
|
stats.triangles = 0;
|
|
6507
6727
|
camera.aspect = canvas.width / canvas.height;
|
|
@@ -6510,12 +6730,18 @@ class GBufferPass extends BasePass {
|
|
|
6510
6730
|
this._extractCameraVectors(camera.view);
|
|
6511
6731
|
let commandEncoder = null;
|
|
6512
6732
|
let passEncoder = null;
|
|
6733
|
+
const updatedSkins = /* @__PURE__ */ new Set();
|
|
6513
6734
|
if (batches && batches.size > 0) {
|
|
6514
6735
|
for (const [modelId, batch] of batches) {
|
|
6515
6736
|
const mesh = batch.mesh;
|
|
6516
6737
|
if (!mesh) continue;
|
|
6517
|
-
if (batch.hasSkin && batch.skin && !batch.skin.externallyManaged) {
|
|
6518
|
-
batch.skin.
|
|
6738
|
+
if (batch.hasSkin && batch.skin && !batch.skin.externallyManaged && !updatedSkins.has(batch.skin)) {
|
|
6739
|
+
if (batch.skin._animStartTime === void 0) {
|
|
6740
|
+
batch.skin._animStartTime = globalAnimTime;
|
|
6741
|
+
}
|
|
6742
|
+
const skinAnimTime = globalAnimTime - batch.skin._animStartTime;
|
|
6743
|
+
batch.skin.updateAtTime(skinAnimTime);
|
|
6744
|
+
updatedSkins.add(batch.skin);
|
|
6519
6745
|
}
|
|
6520
6746
|
const pipeline = await this._getOrCreatePipeline(mesh);
|
|
6521
6747
|
if (pipeline._warmupFrames > 0) {
|
|
@@ -6634,8 +6860,13 @@ class GBufferPass extends BasePass {
|
|
|
6634
6860
|
pipeline._warmupFrames--;
|
|
6635
6861
|
}
|
|
6636
6862
|
this.legacyCullingStats.rendered++;
|
|
6637
|
-
if (mesh.skin && mesh.hasSkin && !mesh.skin.externallyManaged) {
|
|
6638
|
-
mesh.skin.
|
|
6863
|
+
if (mesh.skin && mesh.hasSkin && !mesh.skin.externallyManaged && !updatedSkins.has(mesh.skin)) {
|
|
6864
|
+
if (mesh.skin._animStartTime === void 0) {
|
|
6865
|
+
mesh.skin._animStartTime = globalAnimTime;
|
|
6866
|
+
}
|
|
6867
|
+
const skinAnimTime = globalAnimTime - mesh.skin._animStartTime;
|
|
6868
|
+
mesh.skin.updateAtTime(skinAnimTime);
|
|
6869
|
+
updatedSkins.add(mesh.skin);
|
|
6639
6870
|
}
|
|
6640
6871
|
if (pipeline.geometry !== mesh.geometry) {
|
|
6641
6872
|
pipeline.geometry = mesh.geometry;
|
|
@@ -7258,9 +7489,21 @@ class LightingPass extends BasePass {
|
|
|
7258
7489
|
getOutputTexture() {
|
|
7259
7490
|
return this.outputTexture;
|
|
7260
7491
|
}
|
|
7492
|
+
/**
|
|
7493
|
+
* Get the light buffer for volumetric fog
|
|
7494
|
+
*/
|
|
7495
|
+
getLightBuffer() {
|
|
7496
|
+
return this.lightBuffer;
|
|
7497
|
+
}
|
|
7498
|
+
/**
|
|
7499
|
+
* Get the current light count
|
|
7500
|
+
*/
|
|
7501
|
+
getLightCount() {
|
|
7502
|
+
return this.lights?.length ?? 0;
|
|
7503
|
+
}
|
|
7261
7504
|
}
|
|
7262
7505
|
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}";
|
|
7263
|
-
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}";
|
|
7506
|
+
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}";
|
|
7264
7507
|
class ParticlePass extends BasePass {
|
|
7265
7508
|
constructor(engine = null) {
|
|
7266
7509
|
super("Particles", engine);
|
|
@@ -7798,7 +8041,7 @@ class ParticlePass extends BasePass {
|
|
|
7798
8041
|
uniformData[87] = 0;
|
|
7799
8042
|
uniformData[88] = fogHeightFade[0];
|
|
7800
8043
|
uniformData[89] = fogHeightFade[1];
|
|
7801
|
-
uniformData[90] = 0;
|
|
8044
|
+
uniformData[90] = fogSettings.debug ?? 0;
|
|
7802
8045
|
uniformData[91] = 0;
|
|
7803
8046
|
const emitterRenderData = new Float32Array(16 * 4);
|
|
7804
8047
|
for (let i = 0; i < Math.min(emitters.length, 16); i++) {
|
|
@@ -7841,7 +8084,8 @@ class ParticlePass extends BasePass {
|
|
|
7841
8084
|
if (hasAlpha) {
|
|
7842
8085
|
uniformData[48] = 0;
|
|
7843
8086
|
device.queue.writeBuffer(this.renderUniformBuffer, 0, uniformData);
|
|
7844
|
-
const
|
|
8087
|
+
const alphaEncoder = device.createCommandEncoder({ label: "Particle Alpha Pass" });
|
|
8088
|
+
const renderPass = alphaEncoder.beginRenderPass({
|
|
7845
8089
|
colorAttachments: [{
|
|
7846
8090
|
view: this.outputTexture.view,
|
|
7847
8091
|
loadOp: "load",
|
|
@@ -7857,11 +8101,13 @@ class ParticlePass extends BasePass {
|
|
|
7857
8101
|
renderPass.setBindGroup(0, renderBindGroup);
|
|
7858
8102
|
renderPass.draw(6, maxParticles, 0, 0);
|
|
7859
8103
|
renderPass.end();
|
|
8104
|
+
device.queue.submit([alphaEncoder.finish()]);
|
|
7860
8105
|
}
|
|
7861
8106
|
if (hasAdditive) {
|
|
7862
8107
|
uniformData[48] = 1;
|
|
7863
8108
|
device.queue.writeBuffer(this.renderUniformBuffer, 0, uniformData);
|
|
7864
|
-
const
|
|
8109
|
+
const additiveEncoder = device.createCommandEncoder({ label: "Particle Additive Pass" });
|
|
8110
|
+
const renderPass = additiveEncoder.beginRenderPass({
|
|
7865
8111
|
colorAttachments: [{
|
|
7866
8112
|
view: this.outputTexture.view,
|
|
7867
8113
|
loadOp: "load",
|
|
@@ -7877,6 +8123,7 @@ class ParticlePass extends BasePass {
|
|
|
7877
8123
|
renderPass.setBindGroup(0, renderBindGroup);
|
|
7878
8124
|
renderPass.draw(6, maxParticles, 0, 0);
|
|
7879
8125
|
renderPass.end();
|
|
8126
|
+
device.queue.submit([additiveEncoder.finish()]);
|
|
7880
8127
|
}
|
|
7881
8128
|
}
|
|
7882
8129
|
async _resize(width, height) {
|
|
@@ -7918,6 +8165,10 @@ class FogPass extends BasePass {
|
|
|
7918
8165
|
get fogBrightResist() {
|
|
7919
8166
|
return this.settings?.environment?.fog?.brightResist ?? 0.8;
|
|
7920
8167
|
}
|
|
8168
|
+
get fogDebug() {
|
|
8169
|
+
return this.settings?.environment?.fog?.debug ?? 0;
|
|
8170
|
+
}
|
|
8171
|
+
// 0=off, 1=show fogAlpha, 2=show distance, 3=show heightFactor
|
|
7921
8172
|
/**
|
|
7922
8173
|
* Set the input texture (HDR lighting output)
|
|
7923
8174
|
*/
|
|
@@ -7994,6 +8245,8 @@ class FogPass extends BasePass {
|
|
|
7994
8245
|
brightResist: f32, // float 47
|
|
7995
8246
|
heightFade: vec2f, // floats 48-49
|
|
7996
8247
|
screenSize: vec2f, // floats 50-51
|
|
8248
|
+
debug: f32, // float 52: 0=off, 1=fogAlpha, 2=distance, 3=heightFactor
|
|
8249
|
+
_pad: vec3f, // floats 53-55
|
|
7997
8250
|
}
|
|
7998
8251
|
|
|
7999
8252
|
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
|
|
@@ -8122,6 +8375,26 @@ class FogPass extends BasePass {
|
|
|
8122
8375
|
let brightnessResist = clamp((luminance - 1.0) / 2.0, 0.0, 1.0);
|
|
8123
8376
|
fogAlpha *= (1.0 - brightnessResist * uniforms.brightResist);
|
|
8124
8377
|
|
|
8378
|
+
// Debug output
|
|
8379
|
+
let debugMode = i32(uniforms.debug);
|
|
8380
|
+
if (debugMode == 1) {
|
|
8381
|
+
// Show fog alpha as grayscale
|
|
8382
|
+
return vec4f(vec3f(fogAlpha), 1.0);
|
|
8383
|
+
} else if (debugMode == 2) {
|
|
8384
|
+
// Show distance (normalized to 0-100m range)
|
|
8385
|
+
let normDist = clamp(cameraDistance / 100.0, 0.0, 1.0);
|
|
8386
|
+
return vec4f(vec3f(normDist), 1.0);
|
|
8387
|
+
} else if (debugMode == 3) {
|
|
8388
|
+
// Show height factor
|
|
8389
|
+
return vec4f(vec3f(heightFactor), 1.0);
|
|
8390
|
+
} else if (debugMode == 4) {
|
|
8391
|
+
// Show distance fog (before height fade)
|
|
8392
|
+
return vec4f(vec3f(distanceFog), 1.0);
|
|
8393
|
+
} else if (debugMode == 5) {
|
|
8394
|
+
// Show the actual fog color being used (to verify it matches particles)
|
|
8395
|
+
return vec4f(uniforms.fogColor, 1.0);
|
|
8396
|
+
}
|
|
8397
|
+
|
|
8125
8398
|
// Apply fog
|
|
8126
8399
|
let foggedColor = mix(color.rgb, uniforms.fogColor, fogAlpha);
|
|
8127
8400
|
|
|
@@ -8196,6 +8469,7 @@ class FogPass extends BasePass {
|
|
|
8196
8469
|
uniformData[49] = heightFade[1];
|
|
8197
8470
|
uniformData[50] = this.width;
|
|
8198
8471
|
uniformData[51] = this.height;
|
|
8472
|
+
uniformData[52] = this.fogDebug;
|
|
8199
8473
|
device.queue.writeBuffer(this.uniformBuffer, 0, uniformData);
|
|
8200
8474
|
const commandEncoder = device.createCommandEncoder({ label: "Fog Pass" });
|
|
8201
8475
|
const renderPass = commandEncoder.beginRenderPass({
|
|
@@ -8594,6 +8868,7 @@ class HiZPass extends BasePass {
|
|
|
8594
8868
|
this.screenWidth = 0;
|
|
8595
8869
|
this.screenHeight = 0;
|
|
8596
8870
|
this._destroyed = false;
|
|
8871
|
+
this._warmupFramesRemaining = 5;
|
|
8597
8872
|
}
|
|
8598
8873
|
/**
|
|
8599
8874
|
* Set the depth texture to read from (from GBuffer)
|
|
@@ -8602,6 +8877,18 @@ class HiZPass extends BasePass {
|
|
|
8602
8877
|
setDepthTexture(depth) {
|
|
8603
8878
|
this.depthTexture = depth;
|
|
8604
8879
|
}
|
|
8880
|
+
/**
|
|
8881
|
+
* Invalidate occlusion culling data and reset warmup period.
|
|
8882
|
+
* Call this after engine creation, scene loading, or major camera changes
|
|
8883
|
+
* to prevent incorrect occlusion culling with stale data.
|
|
8884
|
+
*/
|
|
8885
|
+
invalidate() {
|
|
8886
|
+
this.hasValidHistory = false;
|
|
8887
|
+
this.hizDataReady = false;
|
|
8888
|
+
this._warmupFramesRemaining = 5;
|
|
8889
|
+
vec3$1.set(this.lastCameraPosition, 0, 0, 0);
|
|
8890
|
+
vec3$1.set(this.lastCameraDirection, 0, 0, 0);
|
|
8891
|
+
}
|
|
8605
8892
|
async _init() {
|
|
8606
8893
|
const { device, canvas } = this.engine;
|
|
8607
8894
|
await this._createResources(canvas.width, canvas.height);
|
|
@@ -8685,6 +8972,7 @@ class HiZPass extends BasePass {
|
|
|
8685
8972
|
this._destroyed = false;
|
|
8686
8973
|
this.hizDataReady = false;
|
|
8687
8974
|
this.pendingReadback = null;
|
|
8975
|
+
this._warmupFramesRemaining = 5;
|
|
8688
8976
|
}
|
|
8689
8977
|
/**
|
|
8690
8978
|
* Check if camera has moved significantly, requiring HiZ invalidation
|
|
@@ -8718,6 +9006,9 @@ class HiZPass extends BasePass {
|
|
|
8718
9006
|
const { device } = this.engine;
|
|
8719
9007
|
const { camera } = context;
|
|
8720
9008
|
this._frameCounter++;
|
|
9009
|
+
if (this._warmupFramesRemaining > 0) {
|
|
9010
|
+
this._warmupFramesRemaining--;
|
|
9011
|
+
}
|
|
8721
9012
|
if (!this.settings?.occlusionCulling?.enabled) {
|
|
8722
9013
|
return;
|
|
8723
9014
|
}
|
|
@@ -8868,6 +9159,9 @@ class HiZPass extends BasePass {
|
|
|
8868
9159
|
*/
|
|
8869
9160
|
testSphereOcclusion(bsphere, viewProj, near, far, cameraPos) {
|
|
8870
9161
|
this.debugStats.tested++;
|
|
9162
|
+
if (this._warmupFramesRemaining > 0) {
|
|
9163
|
+
return false;
|
|
9164
|
+
}
|
|
8871
9165
|
if (!this.hizDataReady || !this.hasValidHistory) {
|
|
8872
9166
|
return false;
|
|
8873
9167
|
}
|
|
@@ -9281,6 +9575,8 @@ class SSGITilePass extends BasePass {
|
|
|
9281
9575
|
this.tilePropagateBuffer = null;
|
|
9282
9576
|
this.tileCountX = 0;
|
|
9283
9577
|
this.tileCountY = 0;
|
|
9578
|
+
this.renderWidth = 0;
|
|
9579
|
+
this.renderHeight = 0;
|
|
9284
9580
|
this.prevHDRTexture = null;
|
|
9285
9581
|
this.emissiveTexture = null;
|
|
9286
9582
|
this.uniformBuffer = null;
|
|
@@ -9309,6 +9605,8 @@ class SSGITilePass extends BasePass {
|
|
|
9309
9605
|
}
|
|
9310
9606
|
async _createResources(width, height) {
|
|
9311
9607
|
const { device } = this.engine;
|
|
9608
|
+
this.renderWidth = width;
|
|
9609
|
+
this.renderHeight = height;
|
|
9312
9610
|
this.tileCountX = Math.ceil(width / TILE_SIZE$1);
|
|
9313
9611
|
this.tileCountY = Math.ceil(height / TILE_SIZE$1);
|
|
9314
9612
|
const totalTiles = this.tileCountX * this.tileCountY;
|
|
@@ -9376,13 +9674,13 @@ class SSGITilePass extends BasePass {
|
|
|
9376
9674
|
this._needsRebuild = false;
|
|
9377
9675
|
}
|
|
9378
9676
|
async _execute(context) {
|
|
9379
|
-
const { device
|
|
9677
|
+
const { device } = this.engine;
|
|
9380
9678
|
const ssgiSettings = this.settings?.ssgi;
|
|
9381
9679
|
if (!ssgiSettings?.enabled) {
|
|
9382
9680
|
return;
|
|
9383
9681
|
}
|
|
9384
9682
|
if (this._needsRebuild) {
|
|
9385
|
-
await this._createResources(
|
|
9683
|
+
await this._createResources(this.renderWidth, this.renderHeight);
|
|
9386
9684
|
}
|
|
9387
9685
|
if (!this.prevHDRTexture || !this.emissiveTexture) {
|
|
9388
9686
|
return;
|
|
@@ -9390,8 +9688,8 @@ class SSGITilePass extends BasePass {
|
|
|
9390
9688
|
if (!this.accumulatePipeline || !this.propagatePipeline) {
|
|
9391
9689
|
return;
|
|
9392
9690
|
}
|
|
9393
|
-
const width =
|
|
9394
|
-
const height =
|
|
9691
|
+
const width = this.renderWidth;
|
|
9692
|
+
const height = this.renderHeight;
|
|
9395
9693
|
const emissiveBoost = ssgiSettings.emissiveBoost ?? 2;
|
|
9396
9694
|
const maxBrightness = ssgiSettings.maxBrightness ?? 4;
|
|
9397
9695
|
device.queue.writeBuffer(this.uniformBuffer, 0, new Float32Array([
|
|
@@ -9587,7 +9885,7 @@ class SSGIPass extends BasePass {
|
|
|
9587
9885
|
if (!this.pipeline) {
|
|
9588
9886
|
return;
|
|
9589
9887
|
}
|
|
9590
|
-
this._updateUniforms(ssgiSettings,
|
|
9888
|
+
this._updateUniforms(ssgiSettings, this.width * 2, this.height * 2);
|
|
9591
9889
|
const bindGroup = device.createBindGroup({
|
|
9592
9890
|
label: "ssgiBindGroup",
|
|
9593
9891
|
layout: this.bindGroupLayout,
|
|
@@ -10059,9 +10357,11 @@ class BloomPass extends BasePass {
|
|
|
10059
10357
|
const scale = this.bloomScale;
|
|
10060
10358
|
const bloomWidth = Math.max(1, Math.floor(width * scale));
|
|
10061
10359
|
const bloomHeight = Math.max(1, Math.floor(height * scale));
|
|
10360
|
+
if (this.bloomWidth !== bloomWidth || this.bloomHeight !== bloomHeight) {
|
|
10361
|
+
console.log(`Bloom: ${width}x${height} -> ${bloomWidth}x${bloomHeight} (scale: ${scale})`);
|
|
10362
|
+
}
|
|
10062
10363
|
this.bloomWidth = bloomWidth;
|
|
10063
10364
|
this.bloomHeight = bloomHeight;
|
|
10064
|
-
console.log(`Bloom: ${width}x${height} -> ${bloomWidth}x${bloomHeight} (scale: ${scale})`);
|
|
10065
10365
|
const createBloomTexture = (label) => {
|
|
10066
10366
|
const texture = device.createTexture({
|
|
10067
10367
|
label,
|
|
@@ -11030,150 +11330,842 @@ fn fragmentMain(input: VertexOutput) -> @location(0) vec4f {
|
|
|
11030
11330
|
this.pipelineCache.clear();
|
|
11031
11331
|
}
|
|
11032
11332
|
}
|
|
11033
|
-
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}";
|
|
11034
|
-
|
|
11333
|
+
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}";
|
|
11334
|
+
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}";
|
|
11335
|
+
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}";
|
|
11336
|
+
class VolumetricFogPass extends BasePass {
|
|
11035
11337
|
constructor(engine = null) {
|
|
11036
|
-
super("
|
|
11037
|
-
this.
|
|
11338
|
+
super("VolumetricFog", engine);
|
|
11339
|
+
this.raymarchPipeline = null;
|
|
11340
|
+
this.blurHPipeline = null;
|
|
11341
|
+
this.blurVPipeline = null;
|
|
11342
|
+
this.compositePipeline = null;
|
|
11343
|
+
this.raymarchBGL = null;
|
|
11344
|
+
this.blurBGL = null;
|
|
11345
|
+
this.compositeBGL = null;
|
|
11346
|
+
this.raymarchTexture = null;
|
|
11347
|
+
this.blurTempTexture = null;
|
|
11348
|
+
this.blurredTexture = null;
|
|
11349
|
+
this.outputTexture = null;
|
|
11350
|
+
this.raymarchUniformBuffer = null;
|
|
11351
|
+
this.blurHUniformBuffer = null;
|
|
11352
|
+
this.blurVUniformBuffer = null;
|
|
11353
|
+
this.compositeUniformBuffer = null;
|
|
11354
|
+
this.linearSampler = null;
|
|
11038
11355
|
this.inputTexture = null;
|
|
11039
|
-
this.
|
|
11040
|
-
this.
|
|
11041
|
-
this.
|
|
11042
|
-
this.
|
|
11043
|
-
this.
|
|
11044
|
-
this.
|
|
11045
|
-
this.
|
|
11046
|
-
this.
|
|
11356
|
+
this.gbuffer = null;
|
|
11357
|
+
this.shadowPass = null;
|
|
11358
|
+
this.lightingPass = null;
|
|
11359
|
+
this.canvasWidth = 0;
|
|
11360
|
+
this.canvasHeight = 0;
|
|
11361
|
+
this.renderWidth = 0;
|
|
11362
|
+
this.renderHeight = 0;
|
|
11363
|
+
this._currentMainLightScatter = null;
|
|
11364
|
+
this._lastUpdateTime = 0;
|
|
11365
|
+
this._cameraInShadowSmooth = 0;
|
|
11366
|
+
this._skyVisible = true;
|
|
11367
|
+
this._skyCheckPending = false;
|
|
11368
|
+
this._lastSkyCheckTime = 0;
|
|
11047
11369
|
}
|
|
11048
|
-
//
|
|
11049
|
-
get
|
|
11050
|
-
return this.settings?.
|
|
11370
|
+
// Settings getters
|
|
11371
|
+
get volumetricSettings() {
|
|
11372
|
+
return this.settings?.volumetricFog ?? {};
|
|
11051
11373
|
}
|
|
11052
|
-
|
|
11053
|
-
|
|
11054
|
-
return this.settings?.rendering?.fxaa ?? true;
|
|
11374
|
+
get fogSettings() {
|
|
11375
|
+
return this.settings?.fog ?? {};
|
|
11055
11376
|
}
|
|
11056
|
-
|
|
11057
|
-
|
|
11058
|
-
return this.settings?.dithering?.enabled ?? true;
|
|
11377
|
+
get isVolumetricEnabled() {
|
|
11378
|
+
return this.volumetricSettings.enabled ?? false;
|
|
11059
11379
|
}
|
|
11060
|
-
get
|
|
11061
|
-
return this.
|
|
11380
|
+
get resolution() {
|
|
11381
|
+
return this.volumetricSettings.resolution ?? 0.25;
|
|
11062
11382
|
}
|
|
11063
|
-
|
|
11064
|
-
|
|
11065
|
-
return this.settings?.bloom?.enabled ?? true;
|
|
11383
|
+
get maxSamples() {
|
|
11384
|
+
return this.volumetricSettings.maxSamples ?? 32;
|
|
11066
11385
|
}
|
|
11067
|
-
get
|
|
11068
|
-
return this.
|
|
11386
|
+
get blurRadius() {
|
|
11387
|
+
return this.volumetricSettings.blurRadius ?? 4;
|
|
11069
11388
|
}
|
|
11070
|
-
get
|
|
11071
|
-
return this.
|
|
11389
|
+
get fogDensity() {
|
|
11390
|
+
return this.volumetricSettings.density ?? this.volumetricSettings.densityMultiplier ?? 0.5;
|
|
11391
|
+
}
|
|
11392
|
+
get scatterStrength() {
|
|
11393
|
+
return this.volumetricSettings.scatterStrength ?? 1;
|
|
11394
|
+
}
|
|
11395
|
+
get maxDistance() {
|
|
11396
|
+
return this.volumetricSettings.maxDistance ?? 20;
|
|
11397
|
+
}
|
|
11398
|
+
get heightRange() {
|
|
11399
|
+
return this.volumetricSettings.heightRange ?? [-5, 20];
|
|
11400
|
+
}
|
|
11401
|
+
get shadowsEnabled() {
|
|
11402
|
+
return this.volumetricSettings.shadowsEnabled ?? true;
|
|
11403
|
+
}
|
|
11404
|
+
get noiseStrength() {
|
|
11405
|
+
return this.volumetricSettings.noiseStrength ?? 1;
|
|
11406
|
+
}
|
|
11407
|
+
// 0 = uniform fog, 1 = full noise
|
|
11408
|
+
get noiseAnimated() {
|
|
11409
|
+
return this.volumetricSettings.noiseAnimated ?? true;
|
|
11410
|
+
}
|
|
11411
|
+
get noiseScale() {
|
|
11412
|
+
return this.volumetricSettings.noiseScale ?? 0.25;
|
|
11413
|
+
}
|
|
11414
|
+
// Noise frequency (higher = finer detail)
|
|
11415
|
+
get mainLightScatter() {
|
|
11416
|
+
return this.volumetricSettings.mainLightScatter ?? 1;
|
|
11417
|
+
}
|
|
11418
|
+
// Scatter when camera in light
|
|
11419
|
+
get mainLightScatterDark() {
|
|
11420
|
+
return this.volumetricSettings.mainLightScatterDark ?? 3;
|
|
11421
|
+
}
|
|
11422
|
+
// Scatter when camera in shadow
|
|
11423
|
+
get mainLightSaturation() {
|
|
11424
|
+
return this.volumetricSettings.mainLightSaturation ?? 1;
|
|
11425
|
+
}
|
|
11426
|
+
// Max brightness cap
|
|
11427
|
+
// Brightness-based attenuation (fog less visible over bright surfaces)
|
|
11428
|
+
get brightnessThreshold() {
|
|
11429
|
+
return this.volumetricSettings.brightnessThreshold ?? 1;
|
|
11430
|
+
}
|
|
11431
|
+
// Scene luminance where fog starts fading
|
|
11432
|
+
get minVisibility() {
|
|
11433
|
+
return this.volumetricSettings.minVisibility ?? 0.15;
|
|
11434
|
+
}
|
|
11435
|
+
// Minimum fog visibility over very bright surfaces
|
|
11436
|
+
get skyBrightness() {
|
|
11437
|
+
return this.volumetricSettings.skyBrightness ?? 5;
|
|
11438
|
+
}
|
|
11439
|
+
// Virtual brightness for sky (far depth)
|
|
11440
|
+
// Debug mode: 0=normal, 1=depth, 2=ray dir, 3=noise, 4=viewDir.z, 5=worldPos, 6=accum, 7=light dist, 8=light pos
|
|
11441
|
+
get debugMode() {
|
|
11442
|
+
return this.volumetricSettings.debug ?? 0;
|
|
11072
11443
|
}
|
|
11073
|
-
/**
|
|
11074
|
-
* Set the input texture (HDR image from LightingPass)
|
|
11075
|
-
* @param {Texture} texture - Input HDR texture
|
|
11076
|
-
*/
|
|
11077
11444
|
setInputTexture(texture) {
|
|
11078
|
-
|
|
11079
|
-
this.inputTexture = texture;
|
|
11080
|
-
this._needsRebuild = true;
|
|
11081
|
-
}
|
|
11445
|
+
this.inputTexture = texture;
|
|
11082
11446
|
}
|
|
11083
|
-
|
|
11084
|
-
|
|
11085
|
-
* @param {Object} bloomTexture - Bloom texture with mip levels
|
|
11086
|
-
*/
|
|
11087
|
-
setBloomTexture(bloomTexture) {
|
|
11088
|
-
if (this.bloomTexture !== bloomTexture) {
|
|
11089
|
-
this.bloomTexture = bloomTexture;
|
|
11090
|
-
this._needsRebuild = true;
|
|
11091
|
-
}
|
|
11447
|
+
setGBuffer(gbuffer) {
|
|
11448
|
+
this.gbuffer = gbuffer;
|
|
11092
11449
|
}
|
|
11093
|
-
|
|
11094
|
-
|
|
11095
|
-
* @param {Texture} texture - Noise texture (blue noise or bayer dither)
|
|
11096
|
-
* @param {number} size - Texture size
|
|
11097
|
-
* @param {boolean} animated - Whether to animate noise offset each frame
|
|
11098
|
-
*/
|
|
11099
|
-
setNoise(texture, size = 64, animated = true) {
|
|
11100
|
-
this.noiseTexture = texture;
|
|
11101
|
-
this.noiseSize = size;
|
|
11102
|
-
this.noiseAnimated = animated;
|
|
11103
|
-
this._needsRebuild = true;
|
|
11450
|
+
setShadowPass(shadowPass) {
|
|
11451
|
+
this.shadowPass = shadowPass;
|
|
11104
11452
|
}
|
|
11105
|
-
|
|
11106
|
-
|
|
11107
|
-
|
|
11108
|
-
|
|
11109
|
-
|
|
11110
|
-
|
|
11453
|
+
setLightingPass(lightingPass) {
|
|
11454
|
+
this.lightingPass = lightingPass;
|
|
11455
|
+
}
|
|
11456
|
+
getOutputTexture() {
|
|
11457
|
+
return this.outputTexture;
|
|
11458
|
+
}
|
|
11459
|
+
// Unused setters (kept for API compatibility)
|
|
11460
|
+
setHiZPass() {
|
|
11111
11461
|
}
|
|
11112
11462
|
async _init() {
|
|
11113
11463
|
const { device } = this.engine;
|
|
11114
|
-
|
|
11115
|
-
label: "
|
|
11116
|
-
size: [1, 1, 1],
|
|
11117
|
-
format: "rgba16float",
|
|
11118
|
-
mipLevelCount: 1,
|
|
11119
|
-
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST
|
|
11120
|
-
});
|
|
11121
|
-
device.queue.writeTexture(
|
|
11122
|
-
{ texture: dummyTexture },
|
|
11123
|
-
new Float32Array([0, 0, 0, 0]).buffer,
|
|
11124
|
-
{ bytesPerRow: 8 },
|
|
11125
|
-
{ width: 1, height: 1 }
|
|
11126
|
-
);
|
|
11127
|
-
const dummySampler = device.createSampler({
|
|
11128
|
-
label: "Dummy Bloom Sampler",
|
|
11464
|
+
this.linearSampler = device.createSampler({
|
|
11465
|
+
label: "Volumetric Linear Sampler",
|
|
11129
11466
|
minFilter: "linear",
|
|
11130
|
-
magFilter: "linear"
|
|
11467
|
+
magFilter: "linear",
|
|
11468
|
+
addressModeU: "clamp-to-edge",
|
|
11469
|
+
addressModeV: "clamp-to-edge"
|
|
11131
11470
|
});
|
|
11132
|
-
this.
|
|
11133
|
-
|
|
11134
|
-
|
|
11135
|
-
sampler: dummySampler,
|
|
11136
|
-
mipCount: 1
|
|
11137
|
-
};
|
|
11138
|
-
this.guiSampler = device.createSampler({
|
|
11139
|
-
label: "GUI Sampler",
|
|
11140
|
-
minFilter: "linear",
|
|
11141
|
-
magFilter: "linear"
|
|
11471
|
+
this.fallbackShadowSampler = device.createSampler({
|
|
11472
|
+
label: "Volumetric Fallback Shadow Sampler",
|
|
11473
|
+
compare: "less"
|
|
11142
11474
|
});
|
|
11143
|
-
|
|
11144
|
-
label: "
|
|
11475
|
+
this.fallbackCascadeShadowMap = device.createTexture({
|
|
11476
|
+
label: "Volumetric Fallback Cascade Shadow",
|
|
11477
|
+
size: [1, 1, 3],
|
|
11478
|
+
format: "depth32float",
|
|
11479
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT
|
|
11480
|
+
});
|
|
11481
|
+
const identityMatrix = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
|
|
11482
|
+
const matrixData = new Float32Array(16 * 3);
|
|
11483
|
+
for (let i = 0; i < 3; i++) matrixData.set(identityMatrix, i * 16);
|
|
11484
|
+
this.fallbackCascadeMatrices = device.createBuffer({
|
|
11485
|
+
label: "Volumetric Fallback Cascade Matrices",
|
|
11486
|
+
size: 16 * 4 * 3,
|
|
11487
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
11488
|
+
});
|
|
11489
|
+
device.queue.writeBuffer(this.fallbackCascadeMatrices, 0, matrixData);
|
|
11490
|
+
this.fallbackLightsBuffer = device.createBuffer({
|
|
11491
|
+
label: "Volumetric Fallback Lights",
|
|
11492
|
+
size: 768 * 96,
|
|
11493
|
+
// MAX_LIGHTS * 96 bytes per light
|
|
11494
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
11495
|
+
});
|
|
11496
|
+
this.fallbackSpotShadowAtlas = device.createTexture({
|
|
11497
|
+
label: "Volumetric Fallback Spot Shadow Atlas",
|
|
11145
11498
|
size: [1, 1, 1],
|
|
11146
|
-
format: "
|
|
11147
|
-
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.
|
|
11499
|
+
format: "depth32float",
|
|
11500
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT
|
|
11148
11501
|
});
|
|
11149
|
-
|
|
11150
|
-
|
|
11151
|
-
|
|
11152
|
-
|
|
11153
|
-
|
|
11154
|
-
|
|
11155
|
-
|
|
11156
|
-
|
|
11157
|
-
|
|
11158
|
-
sampler: this.guiSampler
|
|
11159
|
-
};
|
|
11502
|
+
const spotMatrixData = new Float32Array(16 * 16);
|
|
11503
|
+
for (let i = 0; i < 16; i++) spotMatrixData.set(identityMatrix, i * 16);
|
|
11504
|
+
this.fallbackSpotMatrices = device.createBuffer({
|
|
11505
|
+
label: "Volumetric Fallback Spot Matrices",
|
|
11506
|
+
size: 16 * 4 * 16,
|
|
11507
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
11508
|
+
});
|
|
11509
|
+
device.queue.writeBuffer(this.fallbackSpotMatrices, 0, spotMatrixData);
|
|
11510
|
+
await this._createResources(this.engine.canvas.width, this.engine.canvas.height);
|
|
11160
11511
|
}
|
|
11161
|
-
|
|
11162
|
-
|
|
11163
|
-
|
|
11164
|
-
|
|
11165
|
-
|
|
11166
|
-
|
|
11167
|
-
|
|
11168
|
-
|
|
11169
|
-
|
|
11170
|
-
|
|
11171
|
-
|
|
11172
|
-
|
|
11173
|
-
|
|
11174
|
-
|
|
11175
|
-
|
|
11176
|
-
|
|
11512
|
+
async _createResources(width, height) {
|
|
11513
|
+
const { device } = this.engine;
|
|
11514
|
+
this.canvasWidth = width;
|
|
11515
|
+
this.canvasHeight = height;
|
|
11516
|
+
this.renderWidth = Math.max(1, Math.floor(width * this.resolution));
|
|
11517
|
+
this.renderHeight = Math.max(1, Math.floor(height * this.resolution));
|
|
11518
|
+
this._destroyTextures();
|
|
11519
|
+
this.raymarchTexture = this._create2DTexture("Raymarch Output", this.renderWidth, this.renderHeight);
|
|
11520
|
+
this.blurTempTexture = this._create2DTexture("Blur Temp", this.renderWidth, this.renderHeight);
|
|
11521
|
+
this.blurredTexture = this._create2DTexture("Blurred Fog", this.renderWidth, this.renderHeight);
|
|
11522
|
+
this.outputTexture = await Texture.renderTarget(this.engine, "rgba16float", width, height);
|
|
11523
|
+
this.raymarchUniformBuffer = this._createUniformBuffer("Raymarch Uniforms", 256);
|
|
11524
|
+
this.blurHUniformBuffer = this._createUniformBuffer("Blur H Uniforms", 32);
|
|
11525
|
+
this.blurVUniformBuffer = this._createUniformBuffer("Blur V Uniforms", 32);
|
|
11526
|
+
this.compositeUniformBuffer = this._createUniformBuffer("Composite Uniforms", 48);
|
|
11527
|
+
await this._createPipelines();
|
|
11528
|
+
}
|
|
11529
|
+
_create2DTexture(label, width, height) {
|
|
11530
|
+
const texture = this.engine.device.createTexture({
|
|
11531
|
+
label,
|
|
11532
|
+
size: [width, height, 1],
|
|
11533
|
+
format: "rgba16float",
|
|
11534
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING
|
|
11535
|
+
});
|
|
11536
|
+
return { texture, view: texture.createView(), width, height };
|
|
11537
|
+
}
|
|
11538
|
+
_createUniformBuffer(label, size) {
|
|
11539
|
+
return this.engine.device.createBuffer({
|
|
11540
|
+
label,
|
|
11541
|
+
size,
|
|
11542
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
11543
|
+
});
|
|
11544
|
+
}
|
|
11545
|
+
async _createPipelines() {
|
|
11546
|
+
const { device } = this.engine;
|
|
11547
|
+
const raymarchModule = device.createShaderModule({
|
|
11548
|
+
label: "Volumetric Raymarch Shader",
|
|
11549
|
+
code: volumetric_raymarch_default
|
|
11550
|
+
});
|
|
11551
|
+
this.raymarchBGL = device.createBindGroupLayout({
|
|
11552
|
+
label: "Volumetric Raymarch BGL",
|
|
11553
|
+
entries: [
|
|
11554
|
+
{ binding: 0, visibility: GPUShaderStage.FRAGMENT | GPUShaderStage.VERTEX, buffer: { type: "uniform" } },
|
|
11555
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "depth" } },
|
|
11556
|
+
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "depth", viewDimension: "2d-array" } },
|
|
11557
|
+
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "comparison" } },
|
|
11558
|
+
{ binding: 4, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "read-only-storage" } },
|
|
11559
|
+
// cascade matrices
|
|
11560
|
+
{ binding: 5, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "read-only-storage" } },
|
|
11561
|
+
// lights
|
|
11562
|
+
{ binding: 6, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "depth" } },
|
|
11563
|
+
// spot shadow atlas
|
|
11564
|
+
{ binding: 7, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "read-only-storage" } }
|
|
11565
|
+
// spot matrices
|
|
11566
|
+
]
|
|
11567
|
+
});
|
|
11568
|
+
this.raymarchPipeline = await device.createRenderPipelineAsync({
|
|
11569
|
+
label: "Volumetric Raymarch Pipeline",
|
|
11570
|
+
layout: device.createPipelineLayout({ bindGroupLayouts: [this.raymarchBGL] }),
|
|
11571
|
+
vertex: { module: raymarchModule, entryPoint: "vertexMain" },
|
|
11572
|
+
fragment: {
|
|
11573
|
+
module: raymarchModule,
|
|
11574
|
+
entryPoint: "fragmentMain",
|
|
11575
|
+
targets: [{ format: "rgba16float" }]
|
|
11576
|
+
},
|
|
11577
|
+
primitive: { topology: "triangle-list" }
|
|
11578
|
+
});
|
|
11579
|
+
const blurModule = device.createShaderModule({
|
|
11580
|
+
label: "Volumetric Blur Shader",
|
|
11581
|
+
code: volumetric_blur_default
|
|
11582
|
+
});
|
|
11583
|
+
this.blurBGL = device.createBindGroupLayout({
|
|
11584
|
+
label: "Volumetric Blur BGL",
|
|
11585
|
+
entries: [
|
|
11586
|
+
{ binding: 0, visibility: GPUShaderStage.FRAGMENT | GPUShaderStage.VERTEX, buffer: { type: "uniform" } },
|
|
11587
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
11588
|
+
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } }
|
|
11589
|
+
]
|
|
11590
|
+
});
|
|
11591
|
+
const blurPipelineDesc = {
|
|
11592
|
+
layout: device.createPipelineLayout({ bindGroupLayouts: [this.blurBGL] }),
|
|
11593
|
+
vertex: { module: blurModule, entryPoint: "vertexMain" },
|
|
11594
|
+
fragment: {
|
|
11595
|
+
module: blurModule,
|
|
11596
|
+
entryPoint: "fragmentMain",
|
|
11597
|
+
targets: [{ format: "rgba16float" }]
|
|
11598
|
+
},
|
|
11599
|
+
primitive: { topology: "triangle-list" }
|
|
11600
|
+
};
|
|
11601
|
+
this.blurHPipeline = await device.createRenderPipelineAsync({ ...blurPipelineDesc, label: "Volumetric Blur H" });
|
|
11602
|
+
this.blurVPipeline = await device.createRenderPipelineAsync({ ...blurPipelineDesc, label: "Volumetric Blur V" });
|
|
11603
|
+
const compositeModule = device.createShaderModule({
|
|
11604
|
+
label: "Volumetric Composite Shader",
|
|
11605
|
+
code: volumetric_composite_default
|
|
11606
|
+
});
|
|
11607
|
+
this.compositeBGL = device.createBindGroupLayout({
|
|
11608
|
+
label: "Volumetric Composite BGL",
|
|
11609
|
+
entries: [
|
|
11610
|
+
{ binding: 0, visibility: GPUShaderStage.FRAGMENT | GPUShaderStage.VERTEX, buffer: { type: "uniform" } },
|
|
11611
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
11612
|
+
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
11613
|
+
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } },
|
|
11614
|
+
{ binding: 4, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "depth" } }
|
|
11615
|
+
]
|
|
11616
|
+
});
|
|
11617
|
+
this.compositePipeline = await device.createRenderPipelineAsync({
|
|
11618
|
+
label: "Volumetric Composite Pipeline",
|
|
11619
|
+
layout: device.createPipelineLayout({ bindGroupLayouts: [this.compositeBGL] }),
|
|
11620
|
+
vertex: { module: compositeModule, entryPoint: "vertexMain" },
|
|
11621
|
+
fragment: {
|
|
11622
|
+
module: compositeModule,
|
|
11623
|
+
entryPoint: "fragmentMain",
|
|
11624
|
+
targets: [{ format: "rgba16float" }]
|
|
11625
|
+
},
|
|
11626
|
+
primitive: { topology: "triangle-list" }
|
|
11627
|
+
});
|
|
11628
|
+
}
|
|
11629
|
+
async _execute(context) {
|
|
11630
|
+
if (!this.isVolumetricEnabled) return;
|
|
11631
|
+
if (!this.inputTexture || !this.gbuffer) return;
|
|
11632
|
+
const { device } = this.engine;
|
|
11633
|
+
const { camera, mainLight, lights } = context;
|
|
11634
|
+
const time = performance.now() / 1e3;
|
|
11635
|
+
this._updateAdaptiveScatter(camera, mainLight, time);
|
|
11636
|
+
const lightCount = lights?.length ?? this.lightingPass?.lightCount ?? 0;
|
|
11637
|
+
const commandEncoder = device.createCommandEncoder({ label: "Volumetric Fog Pass" });
|
|
11638
|
+
this._updateRaymarchUniforms(camera, mainLight, time, lightCount);
|
|
11639
|
+
const raymarchBindGroup = this._createRaymarchBindGroup();
|
|
11640
|
+
if (raymarchBindGroup) {
|
|
11641
|
+
const raymarchPass = commandEncoder.beginRenderPass({
|
|
11642
|
+
label: "Volumetric Raymarch",
|
|
11643
|
+
colorAttachments: [{
|
|
11644
|
+
view: this.raymarchTexture.view,
|
|
11645
|
+
loadOp: "clear",
|
|
11646
|
+
storeOp: "store",
|
|
11647
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 }
|
|
11648
|
+
}]
|
|
11649
|
+
});
|
|
11650
|
+
raymarchPass.setPipeline(this.raymarchPipeline);
|
|
11651
|
+
raymarchPass.setBindGroup(0, raymarchBindGroup);
|
|
11652
|
+
raymarchPass.draw(3);
|
|
11653
|
+
raymarchPass.end();
|
|
11654
|
+
}
|
|
11655
|
+
this._updateBlurUniforms(this.blurHUniformBuffer, 1, 0);
|
|
11656
|
+
this._updateBlurUniforms(this.blurVUniformBuffer, 0, 1);
|
|
11657
|
+
const blurHBindGroup = this._createBlurBindGroup(this.raymarchTexture, this.blurHUniformBuffer);
|
|
11658
|
+
const blurHPass = commandEncoder.beginRenderPass({
|
|
11659
|
+
label: "Volumetric Blur H",
|
|
11660
|
+
colorAttachments: [{
|
|
11661
|
+
view: this.blurTempTexture.view,
|
|
11662
|
+
loadOp: "clear",
|
|
11663
|
+
storeOp: "store",
|
|
11664
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 }
|
|
11665
|
+
}]
|
|
11666
|
+
});
|
|
11667
|
+
blurHPass.setPipeline(this.blurHPipeline);
|
|
11668
|
+
blurHPass.setBindGroup(0, blurHBindGroup);
|
|
11669
|
+
blurHPass.draw(3);
|
|
11670
|
+
blurHPass.end();
|
|
11671
|
+
const blurVBindGroup = this._createBlurBindGroup(this.blurTempTexture, this.blurVUniformBuffer);
|
|
11672
|
+
const blurVPass = commandEncoder.beginRenderPass({
|
|
11673
|
+
label: "Volumetric Blur V",
|
|
11674
|
+
colorAttachments: [{
|
|
11675
|
+
view: this.blurredTexture.view,
|
|
11676
|
+
loadOp: "clear",
|
|
11677
|
+
storeOp: "store",
|
|
11678
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 }
|
|
11679
|
+
}]
|
|
11680
|
+
});
|
|
11681
|
+
blurVPass.setPipeline(this.blurVPipeline);
|
|
11682
|
+
blurVPass.setBindGroup(0, blurVBindGroup);
|
|
11683
|
+
blurVPass.draw(3);
|
|
11684
|
+
blurVPass.end();
|
|
11685
|
+
this._updateCompositeUniforms();
|
|
11686
|
+
const compositeBindGroup = this._createCompositeBindGroup();
|
|
11687
|
+
if (compositeBindGroup) {
|
|
11688
|
+
const compositePass = commandEncoder.beginRenderPass({
|
|
11689
|
+
label: "Volumetric Composite",
|
|
11690
|
+
colorAttachments: [{
|
|
11691
|
+
view: this.outputTexture.view,
|
|
11692
|
+
loadOp: "clear",
|
|
11693
|
+
storeOp: "store",
|
|
11694
|
+
clearValue: { r: 0, g: 0, b: 0, a: 1 }
|
|
11695
|
+
}]
|
|
11696
|
+
});
|
|
11697
|
+
compositePass.setPipeline(this.compositePipeline);
|
|
11698
|
+
compositePass.setBindGroup(0, compositeBindGroup);
|
|
11699
|
+
compositePass.draw(3);
|
|
11700
|
+
compositePass.end();
|
|
11701
|
+
}
|
|
11702
|
+
device.queue.submit([commandEncoder.finish()]);
|
|
11703
|
+
}
|
|
11704
|
+
/**
|
|
11705
|
+
* Update adaptive main light scatter based on camera shadow state and sky visibility
|
|
11706
|
+
* Smoothly transitions between light/dark scatter values
|
|
11707
|
+
* Only uses dark scatter if camera is in shadow AND there's something overhead (no sky)
|
|
11708
|
+
*/
|
|
11709
|
+
_updateAdaptiveScatter(camera, mainLight, currentTime) {
|
|
11710
|
+
if (this._currentMainLightScatter === null) {
|
|
11711
|
+
this._currentMainLightScatter = this.mainLightScatter;
|
|
11712
|
+
}
|
|
11713
|
+
const deltaTime = this._lastUpdateTime > 0 ? currentTime - this._lastUpdateTime : 0.016;
|
|
11714
|
+
this._lastUpdateTime = currentTime;
|
|
11715
|
+
let cameraInShadow = false;
|
|
11716
|
+
if (this.shadowPass && this.shadowsEnabled && mainLight?.enabled !== false) {
|
|
11717
|
+
const cameraPos = camera.position || [0, 0, 0];
|
|
11718
|
+
cameraInShadow = this._isCameraInShadow(cameraPos);
|
|
11719
|
+
if (!this._skyCheckPending && currentTime - this._lastSkyCheckTime > 0.5) {
|
|
11720
|
+
this._checkSkyVisibility(cameraPos);
|
|
11721
|
+
}
|
|
11722
|
+
}
|
|
11723
|
+
const inDarkArea = cameraInShadow && !this._skyVisible;
|
|
11724
|
+
const targetShadowState = inDarkArea ? 1 : 0;
|
|
11725
|
+
const transitionSpeed = inDarkArea ? 1 / 5 : 1 / 1;
|
|
11726
|
+
const t = 1 - Math.exp(-transitionSpeed * deltaTime * 3);
|
|
11727
|
+
this._cameraInShadowSmooth += (targetShadowState - this._cameraInShadowSmooth) * t;
|
|
11728
|
+
this._cameraInShadowSmooth = Math.max(0, Math.min(1, this._cameraInShadowSmooth));
|
|
11729
|
+
const lightScatter = this.mainLightScatter;
|
|
11730
|
+
const darkScatter = this.mainLightScatterDark;
|
|
11731
|
+
this._currentMainLightScatter = lightScatter + (darkScatter - lightScatter) * this._cameraInShadowSmooth;
|
|
11732
|
+
}
|
|
11733
|
+
/**
|
|
11734
|
+
* Check if sky is visible above the camera using raycaster
|
|
11735
|
+
* This is async and updates _skyVisible when complete
|
|
11736
|
+
*/
|
|
11737
|
+
_checkSkyVisibility(cameraPos) {
|
|
11738
|
+
const raycaster = this.engine?.raycaster;
|
|
11739
|
+
if (!raycaster) return;
|
|
11740
|
+
this._skyCheckPending = true;
|
|
11741
|
+
this._lastSkyCheckTime = this._lastUpdateTime;
|
|
11742
|
+
const skyCheckDistance = this.volumetricSettings.skyCheckDistance ?? 100;
|
|
11743
|
+
const debugSkyCheck = this.volumetricSettings.debugSkyCheck ?? false;
|
|
11744
|
+
raycaster.cast(
|
|
11745
|
+
cameraPos,
|
|
11746
|
+
[0, 1, 0],
|
|
11747
|
+
// Straight up
|
|
11748
|
+
skyCheckDistance,
|
|
11749
|
+
(result) => {
|
|
11750
|
+
this._skyVisible;
|
|
11751
|
+
this._skyVisible = !result.hit;
|
|
11752
|
+
this._skyCheckPending = false;
|
|
11753
|
+
if (debugSkyCheck) {
|
|
11754
|
+
const pos = cameraPos.map((v) => v.toFixed(1)).join(", ");
|
|
11755
|
+
if (result.hit) {
|
|
11756
|
+
console.log(`Sky check from [${pos}]: HIT ${result.meshName || result.candidateId} at dist=${result.distance?.toFixed(1)}`);
|
|
11757
|
+
} else {
|
|
11758
|
+
console.log(`Sky check from [${pos}]: NO HIT (sky visible)`);
|
|
11759
|
+
}
|
|
11760
|
+
}
|
|
11761
|
+
},
|
|
11762
|
+
{ backfaces: true, debug: debugSkyCheck }
|
|
11763
|
+
// Need backfaces to hit ceilings from below
|
|
11764
|
+
);
|
|
11765
|
+
}
|
|
11766
|
+
/**
|
|
11767
|
+
* Check if camera position is in shadow
|
|
11768
|
+
* Uses the shadow pass's isCameraInShadow method if available,
|
|
11769
|
+
* otherwise falls back to checking fog height bounds
|
|
11770
|
+
*/
|
|
11771
|
+
_isCameraInShadow(cameraPos) {
|
|
11772
|
+
if (typeof this.shadowPass?.isCameraInShadow === "function") {
|
|
11773
|
+
return this.shadowPass.isCameraInShadow(cameraPos);
|
|
11774
|
+
}
|
|
11775
|
+
const heightRange = this.heightRange;
|
|
11776
|
+
const fogBottom = heightRange[0];
|
|
11777
|
+
const fogTop = heightRange[1];
|
|
11778
|
+
const lowerThird = fogBottom + (fogTop - fogBottom) * 0.33;
|
|
11779
|
+
if (cameraPos[1] < lowerThird) {
|
|
11780
|
+
return true;
|
|
11781
|
+
}
|
|
11782
|
+
const mainLightDir = this.engine?.settings?.mainLight?.direction;
|
|
11783
|
+
if (mainLightDir) {
|
|
11784
|
+
const sunAngle = mainLightDir[1];
|
|
11785
|
+
if (sunAngle > 0.7) {
|
|
11786
|
+
return true;
|
|
11787
|
+
}
|
|
11788
|
+
}
|
|
11789
|
+
return false;
|
|
11790
|
+
}
|
|
11791
|
+
_updateRaymarchUniforms(camera, mainLight, time, lightCount) {
|
|
11792
|
+
const { device } = this.engine;
|
|
11793
|
+
if (!camera.iProj || !camera.iView) {
|
|
11794
|
+
console.warn("VolumetricFogPass: Camera missing iProj or iView matrices");
|
|
11795
|
+
return;
|
|
11796
|
+
}
|
|
11797
|
+
const invProj = camera.iProj;
|
|
11798
|
+
const invView = camera.iView;
|
|
11799
|
+
const cameraPos = camera.position || [0, 0, 0];
|
|
11800
|
+
const mainLightEnabled = mainLight?.enabled !== false;
|
|
11801
|
+
const mainLightDir = mainLight?.direction ?? [-1, 1, -0.5];
|
|
11802
|
+
const mainLightColor = mainLight?.color ?? [1, 0.95, 0.9];
|
|
11803
|
+
const mainLightIntensity = mainLightEnabled ? mainLight?.intensity ?? 1 : 0;
|
|
11804
|
+
const fogColor = this.fogSettings.color ?? [0.8, 0.85, 0.9];
|
|
11805
|
+
const heightFade = this.heightRange;
|
|
11806
|
+
const shadowsEnabled = this.shadowsEnabled && this.shadowPass != null;
|
|
11807
|
+
const data = new Float32Array(64);
|
|
11808
|
+
let offset = 0;
|
|
11809
|
+
data.set(invProj, offset);
|
|
11810
|
+
offset += 16;
|
|
11811
|
+
data.set(invView, offset);
|
|
11812
|
+
offset += 16;
|
|
11813
|
+
data[offset++] = cameraPos[0];
|
|
11814
|
+
data[offset++] = cameraPos[1];
|
|
11815
|
+
data[offset++] = cameraPos[2];
|
|
11816
|
+
data[offset++] = camera.near ?? 0.1;
|
|
11817
|
+
data[offset++] = camera.far ?? 1e3;
|
|
11818
|
+
data[offset++] = this.maxSamples;
|
|
11819
|
+
data[offset++] = time;
|
|
11820
|
+
data[offset++] = this.fogDensity;
|
|
11821
|
+
data[offset++] = fogColor[0];
|
|
11822
|
+
data[offset++] = fogColor[1];
|
|
11823
|
+
data[offset++] = fogColor[2];
|
|
11824
|
+
data[offset++] = shadowsEnabled ? 1 : 0;
|
|
11825
|
+
data[offset++] = mainLightDir[0];
|
|
11826
|
+
data[offset++] = mainLightDir[1];
|
|
11827
|
+
data[offset++] = mainLightDir[2];
|
|
11828
|
+
data[offset++] = mainLightIntensity;
|
|
11829
|
+
data[offset++] = mainLightColor[0];
|
|
11830
|
+
data[offset++] = mainLightColor[1];
|
|
11831
|
+
data[offset++] = mainLightColor[2];
|
|
11832
|
+
data[offset++] = this.scatterStrength;
|
|
11833
|
+
data[offset++] = heightFade[0];
|
|
11834
|
+
data[offset++] = heightFade[1];
|
|
11835
|
+
data[offset++] = this.maxDistance;
|
|
11836
|
+
data[offset++] = lightCount;
|
|
11837
|
+
data[offset++] = this.debugMode;
|
|
11838
|
+
data[offset++] = this.noiseStrength;
|
|
11839
|
+
data[offset++] = this.noiseAnimated ? 1 : 0;
|
|
11840
|
+
data[offset++] = this._currentMainLightScatter;
|
|
11841
|
+
data[offset++] = this.noiseScale;
|
|
11842
|
+
data[offset++] = this.mainLightSaturation;
|
|
11843
|
+
device.queue.writeBuffer(this.raymarchUniformBuffer, 0, data);
|
|
11844
|
+
}
|
|
11845
|
+
_updateBlurUniforms(buffer, dirX, dirY) {
|
|
11846
|
+
const data = new Float32Array([
|
|
11847
|
+
dirX,
|
|
11848
|
+
dirY,
|
|
11849
|
+
1 / this.renderWidth,
|
|
11850
|
+
1 / this.renderHeight,
|
|
11851
|
+
this.blurRadius,
|
|
11852
|
+
0,
|
|
11853
|
+
0,
|
|
11854
|
+
0
|
|
11855
|
+
]);
|
|
11856
|
+
this.engine.device.queue.writeBuffer(buffer, 0, data);
|
|
11857
|
+
}
|
|
11858
|
+
_updateCompositeUniforms() {
|
|
11859
|
+
const data = new Float32Array([
|
|
11860
|
+
this.canvasWidth,
|
|
11861
|
+
this.canvasHeight,
|
|
11862
|
+
this.renderWidth,
|
|
11863
|
+
this.renderHeight,
|
|
11864
|
+
1 / this.canvasWidth,
|
|
11865
|
+
1 / this.canvasHeight,
|
|
11866
|
+
this.brightnessThreshold,
|
|
11867
|
+
this.minVisibility,
|
|
11868
|
+
this.skyBrightness,
|
|
11869
|
+
0,
|
|
11870
|
+
// skyBrightness + padding
|
|
11871
|
+
0,
|
|
11872
|
+
0
|
|
11873
|
+
// extra padding to 48 bytes
|
|
11874
|
+
]);
|
|
11875
|
+
this.engine.device.queue.writeBuffer(this.compositeUniformBuffer, 0, data);
|
|
11876
|
+
}
|
|
11877
|
+
_createRaymarchBindGroup() {
|
|
11878
|
+
const { device } = this.engine;
|
|
11879
|
+
const depthTexture = this.gbuffer?.depth;
|
|
11880
|
+
if (!depthTexture) return null;
|
|
11881
|
+
const cascadeShadows = this.shadowPass?.getShadowMap?.() ?? this.fallbackCascadeShadowMap;
|
|
11882
|
+
const cascadeMatrices = this.shadowPass?.getCascadeMatricesBuffer?.() ?? this.fallbackCascadeMatrices;
|
|
11883
|
+
const shadowSampler = this.shadowPass?.getShadowSampler?.() ?? this.fallbackShadowSampler;
|
|
11884
|
+
const lightsBuffer = this.lightingPass?.getLightBuffer?.() ?? this.fallbackLightsBuffer;
|
|
11885
|
+
const spotShadowAtlas = this.shadowPass?.getSpotShadowAtlasView?.() ?? this.fallbackSpotShadowAtlas.createView();
|
|
11886
|
+
const spotMatrices = this.shadowPass?.getSpotMatricesBuffer?.() ?? this.fallbackSpotMatrices;
|
|
11887
|
+
return device.createBindGroup({
|
|
11888
|
+
label: "Volumetric Raymarch Bind Group",
|
|
11889
|
+
layout: this.raymarchBGL,
|
|
11890
|
+
entries: [
|
|
11891
|
+
{ binding: 0, resource: { buffer: this.raymarchUniformBuffer } },
|
|
11892
|
+
{ binding: 1, resource: depthTexture.texture.createView({ aspect: "depth-only" }) },
|
|
11893
|
+
{ binding: 2, resource: cascadeShadows.createView({ dimension: "2d-array", aspect: "depth-only" }) },
|
|
11894
|
+
{ binding: 3, resource: shadowSampler },
|
|
11895
|
+
{ binding: 4, resource: { buffer: cascadeMatrices } },
|
|
11896
|
+
{ binding: 5, resource: { buffer: lightsBuffer } },
|
|
11897
|
+
{ binding: 6, resource: spotShadowAtlas },
|
|
11898
|
+
{ binding: 7, resource: { buffer: spotMatrices } }
|
|
11899
|
+
]
|
|
11900
|
+
});
|
|
11901
|
+
}
|
|
11902
|
+
_createBlurBindGroup(inputTexture, uniformBuffer) {
|
|
11903
|
+
return this.engine.device.createBindGroup({
|
|
11904
|
+
label: "Volumetric Blur Bind Group",
|
|
11905
|
+
layout: this.blurBGL,
|
|
11906
|
+
entries: [
|
|
11907
|
+
{ binding: 0, resource: { buffer: uniformBuffer } },
|
|
11908
|
+
{ binding: 1, resource: inputTexture.view },
|
|
11909
|
+
{ binding: 2, resource: this.linearSampler }
|
|
11910
|
+
]
|
|
11911
|
+
});
|
|
11912
|
+
}
|
|
11913
|
+
_createCompositeBindGroup() {
|
|
11914
|
+
if (!this.inputTexture) return null;
|
|
11915
|
+
const depthTexture = this.gbuffer?.depth;
|
|
11916
|
+
if (!depthTexture) return null;
|
|
11917
|
+
return this.engine.device.createBindGroup({
|
|
11918
|
+
label: "Volumetric Composite Bind Group",
|
|
11919
|
+
layout: this.compositeBGL,
|
|
11920
|
+
entries: [
|
|
11921
|
+
{ binding: 0, resource: { buffer: this.compositeUniformBuffer } },
|
|
11922
|
+
{ binding: 1, resource: this.inputTexture.view },
|
|
11923
|
+
{ binding: 2, resource: this.blurredTexture.view },
|
|
11924
|
+
{ binding: 3, resource: this.linearSampler },
|
|
11925
|
+
{ binding: 4, resource: depthTexture.texture.createView({ aspect: "depth-only" }) }
|
|
11926
|
+
]
|
|
11927
|
+
});
|
|
11928
|
+
}
|
|
11929
|
+
_destroyTextures() {
|
|
11930
|
+
const textures = [this.raymarchTexture, this.blurTempTexture, this.blurredTexture, this.outputTexture];
|
|
11931
|
+
for (const tex of textures) {
|
|
11932
|
+
if (tex?.texture) tex.texture.destroy();
|
|
11933
|
+
}
|
|
11934
|
+
this.raymarchTexture = null;
|
|
11935
|
+
this.blurTempTexture = null;
|
|
11936
|
+
this.blurredTexture = null;
|
|
11937
|
+
this.outputTexture = null;
|
|
11938
|
+
}
|
|
11939
|
+
async _resize(width, height) {
|
|
11940
|
+
await this._createResources(width, height);
|
|
11941
|
+
}
|
|
11942
|
+
_destroy() {
|
|
11943
|
+
this._destroyTextures();
|
|
11944
|
+
const buffers = [this.raymarchUniformBuffer, this.blurHUniformBuffer, this.blurVUniformBuffer, this.compositeUniformBuffer];
|
|
11945
|
+
for (const buf of buffers) {
|
|
11946
|
+
if (buf) buf.destroy();
|
|
11947
|
+
}
|
|
11948
|
+
this.raymarchPipeline = null;
|
|
11949
|
+
this.blurHPipeline = null;
|
|
11950
|
+
this.blurVPipeline = null;
|
|
11951
|
+
this.compositePipeline = null;
|
|
11952
|
+
}
|
|
11953
|
+
}
|
|
11954
|
+
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}";
|
|
11955
|
+
class PostProcessPass extends BasePass {
|
|
11956
|
+
constructor(engine = null) {
|
|
11957
|
+
super("PostProcess", engine);
|
|
11958
|
+
this.pipeline = null;
|
|
11959
|
+
this.inputTexture = null;
|
|
11960
|
+
this.bloomTexture = null;
|
|
11961
|
+
this.dummyBloomTexture = null;
|
|
11962
|
+
this.noiseTexture = null;
|
|
11963
|
+
this.noiseSize = 64;
|
|
11964
|
+
this.noiseAnimated = true;
|
|
11965
|
+
this.guiCanvas = null;
|
|
11966
|
+
this.guiTexture = null;
|
|
11967
|
+
this.guiSampler = null;
|
|
11968
|
+
this.intermediateTexture = null;
|
|
11969
|
+
this._outputWidth = 0;
|
|
11970
|
+
this._outputHeight = 0;
|
|
11971
|
+
this._lastOutputToTexture = false;
|
|
11972
|
+
this._resizeWidth = 0;
|
|
11973
|
+
this._resizeHeight = 0;
|
|
11974
|
+
}
|
|
11975
|
+
// Convenience getter for exposure setting
|
|
11976
|
+
get exposure() {
|
|
11977
|
+
return this.settings?.environment?.exposure ?? 1.6;
|
|
11978
|
+
}
|
|
11979
|
+
// Convenience getter for fxaa setting
|
|
11980
|
+
get fxaa() {
|
|
11981
|
+
return this.settings?.rendering?.fxaa ?? true;
|
|
11982
|
+
}
|
|
11983
|
+
// Convenience getter for dithering settings
|
|
11984
|
+
get ditheringEnabled() {
|
|
11985
|
+
return this.settings?.dithering?.enabled ?? true;
|
|
11986
|
+
}
|
|
11987
|
+
get colorLevels() {
|
|
11988
|
+
return this.settings?.dithering?.colorLevels ?? 32;
|
|
11989
|
+
}
|
|
11990
|
+
// Tonemap mode: 0=ACES, 1=Reinhard, 2=None/Linear
|
|
11991
|
+
get tonemapMode() {
|
|
11992
|
+
return this.settings?.rendering?.tonemapMode ?? 0;
|
|
11993
|
+
}
|
|
11994
|
+
// Convenience getters for bloom settings
|
|
11995
|
+
get bloomEnabled() {
|
|
11996
|
+
return this.settings?.bloom?.enabled ?? true;
|
|
11997
|
+
}
|
|
11998
|
+
get bloomIntensity() {
|
|
11999
|
+
return this.settings?.bloom?.intensity ?? 1;
|
|
12000
|
+
}
|
|
12001
|
+
get bloomRadius() {
|
|
12002
|
+
return this.settings?.bloom?.radius ?? 5;
|
|
12003
|
+
}
|
|
12004
|
+
// CRT settings (determines if we output to intermediate texture)
|
|
12005
|
+
get crtEnabled() {
|
|
12006
|
+
return this.settings?.crt?.enabled ?? false;
|
|
12007
|
+
}
|
|
12008
|
+
get crtUpscaleEnabled() {
|
|
12009
|
+
return this.settings?.crt?.upscaleEnabled ?? false;
|
|
12010
|
+
}
|
|
12011
|
+
get shouldOutputToTexture() {
|
|
12012
|
+
return this.crtEnabled || this.crtUpscaleEnabled;
|
|
12013
|
+
}
|
|
12014
|
+
/**
|
|
12015
|
+
* Set the input texture (HDR image from LightingPass)
|
|
12016
|
+
* @param {Texture} texture - Input HDR texture
|
|
12017
|
+
*/
|
|
12018
|
+
setInputTexture(texture) {
|
|
12019
|
+
if (this.inputTexture !== texture) {
|
|
12020
|
+
this.inputTexture = texture;
|
|
12021
|
+
this._needsRebuild = true;
|
|
12022
|
+
}
|
|
12023
|
+
}
|
|
12024
|
+
/**
|
|
12025
|
+
* Set the bloom texture (from BloomPass)
|
|
12026
|
+
* @param {Object} bloomTexture - Bloom texture with mip levels
|
|
12027
|
+
*/
|
|
12028
|
+
setBloomTexture(bloomTexture) {
|
|
12029
|
+
if (this.bloomTexture !== bloomTexture) {
|
|
12030
|
+
this.bloomTexture = bloomTexture;
|
|
12031
|
+
this._needsRebuild = true;
|
|
12032
|
+
}
|
|
12033
|
+
}
|
|
12034
|
+
/**
|
|
12035
|
+
* Set the noise texture for dithering
|
|
12036
|
+
* @param {Texture} texture - Noise texture (blue noise or bayer dither)
|
|
12037
|
+
* @param {number} size - Texture size
|
|
12038
|
+
* @param {boolean} animated - Whether to animate noise offset each frame
|
|
12039
|
+
*/
|
|
12040
|
+
setNoise(texture, size = 64, animated = true) {
|
|
12041
|
+
this.noiseTexture = texture;
|
|
12042
|
+
this.noiseSize = size;
|
|
12043
|
+
this.noiseAnimated = animated;
|
|
12044
|
+
this._needsRebuild = true;
|
|
12045
|
+
}
|
|
12046
|
+
/**
|
|
12047
|
+
* Set the GUI canvas for overlay rendering
|
|
12048
|
+
* @param {HTMLCanvasElement} canvas - 2D canvas with GUI content
|
|
12049
|
+
*/
|
|
12050
|
+
setGuiCanvas(canvas) {
|
|
12051
|
+
this.guiCanvas = canvas;
|
|
12052
|
+
}
|
|
12053
|
+
/**
|
|
12054
|
+
* Get the output texture (for CRT pass to use)
|
|
12055
|
+
* Returns null if outputting directly to canvas
|
|
12056
|
+
*/
|
|
12057
|
+
getOutputTexture() {
|
|
12058
|
+
return this.shouldOutputToTexture ? this.intermediateTexture : null;
|
|
12059
|
+
}
|
|
12060
|
+
/**
|
|
12061
|
+
* Create or resize the intermediate texture for CRT
|
|
12062
|
+
*/
|
|
12063
|
+
async _createIntermediateTexture(width, height) {
|
|
12064
|
+
if (!this.shouldOutputToTexture) {
|
|
12065
|
+
if (this.intermediateTexture?.texture) {
|
|
12066
|
+
this.intermediateTexture.texture.destroy();
|
|
12067
|
+
this.intermediateTexture = null;
|
|
12068
|
+
}
|
|
12069
|
+
return;
|
|
12070
|
+
}
|
|
12071
|
+
if (this._outputWidth === width && this._outputHeight === height && this.intermediateTexture) {
|
|
12072
|
+
return;
|
|
12073
|
+
}
|
|
12074
|
+
const { device } = this.engine;
|
|
12075
|
+
if (this.intermediateTexture?.texture) {
|
|
12076
|
+
this.intermediateTexture.texture.destroy();
|
|
12077
|
+
}
|
|
12078
|
+
const texture = device.createTexture({
|
|
12079
|
+
label: "PostProcess Intermediate",
|
|
12080
|
+
size: [width, height, 1],
|
|
12081
|
+
format: "rgba8unorm",
|
|
12082
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC
|
|
12083
|
+
});
|
|
12084
|
+
const sampler = device.createSampler({
|
|
12085
|
+
label: "PostProcess Intermediate Sampler",
|
|
12086
|
+
minFilter: "nearest",
|
|
12087
|
+
magFilter: "nearest"
|
|
12088
|
+
});
|
|
12089
|
+
this.intermediateTexture = {
|
|
12090
|
+
texture,
|
|
12091
|
+
view: texture.createView(),
|
|
12092
|
+
sampler,
|
|
12093
|
+
width,
|
|
12094
|
+
height,
|
|
12095
|
+
format: "rgba8unorm"
|
|
12096
|
+
// Required by Pipeline.create()
|
|
12097
|
+
};
|
|
12098
|
+
this._outputWidth = width;
|
|
12099
|
+
this._outputHeight = height;
|
|
12100
|
+
this._needsRebuild = true;
|
|
12101
|
+
console.log(`PostProcessPass: Created intermediate texture ${width}x${height} for CRT`);
|
|
12102
|
+
}
|
|
12103
|
+
async _init() {
|
|
12104
|
+
const { device } = this.engine;
|
|
12105
|
+
const dummyTexture = device.createTexture({
|
|
12106
|
+
label: "Dummy Bloom Texture",
|
|
12107
|
+
size: [1, 1, 1],
|
|
12108
|
+
format: "rgba16float",
|
|
12109
|
+
mipLevelCount: 1,
|
|
12110
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST
|
|
12111
|
+
});
|
|
12112
|
+
device.queue.writeTexture(
|
|
12113
|
+
{ texture: dummyTexture },
|
|
12114
|
+
new Float32Array([0, 0, 0, 0]).buffer,
|
|
12115
|
+
{ bytesPerRow: 8 },
|
|
12116
|
+
{ width: 1, height: 1 }
|
|
12117
|
+
);
|
|
12118
|
+
const dummySampler = device.createSampler({
|
|
12119
|
+
label: "Dummy Bloom Sampler",
|
|
12120
|
+
minFilter: "linear",
|
|
12121
|
+
magFilter: "linear"
|
|
12122
|
+
});
|
|
12123
|
+
this.dummyBloomTexture = {
|
|
12124
|
+
texture: dummyTexture,
|
|
12125
|
+
view: dummyTexture.createView(),
|
|
12126
|
+
sampler: dummySampler,
|
|
12127
|
+
mipCount: 1
|
|
12128
|
+
};
|
|
12129
|
+
this.guiSampler = device.createSampler({
|
|
12130
|
+
label: "GUI Sampler",
|
|
12131
|
+
minFilter: "linear",
|
|
12132
|
+
magFilter: "linear"
|
|
12133
|
+
});
|
|
12134
|
+
const dummyGuiTexture = device.createTexture({
|
|
12135
|
+
label: "Dummy GUI Texture",
|
|
12136
|
+
size: [1, 1, 1],
|
|
12137
|
+
format: "rgba8unorm",
|
|
12138
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST
|
|
12139
|
+
});
|
|
12140
|
+
device.queue.writeTexture(
|
|
12141
|
+
{ texture: dummyGuiTexture },
|
|
12142
|
+
new Uint8Array([0, 0, 0, 0]),
|
|
12143
|
+
{ bytesPerRow: 4 },
|
|
12144
|
+
{ width: 1, height: 1 }
|
|
12145
|
+
);
|
|
12146
|
+
this.dummyGuiTexture = {
|
|
12147
|
+
texture: dummyGuiTexture,
|
|
12148
|
+
view: dummyGuiTexture.createView(),
|
|
12149
|
+
sampler: this.guiSampler
|
|
12150
|
+
};
|
|
12151
|
+
}
|
|
12152
|
+
/**
|
|
12153
|
+
* Build or rebuild the pipeline
|
|
12154
|
+
*/
|
|
12155
|
+
async _buildPipeline() {
|
|
12156
|
+
if (!this.inputTexture) {
|
|
12157
|
+
return;
|
|
12158
|
+
}
|
|
12159
|
+
const textures = [this.inputTexture];
|
|
12160
|
+
if (this.noiseTexture) {
|
|
12161
|
+
textures.push(this.noiseTexture);
|
|
12162
|
+
}
|
|
12163
|
+
const effectiveBloomTexture = this.bloomTexture || this.dummyBloomTexture;
|
|
12164
|
+
textures.push(effectiveBloomTexture);
|
|
12165
|
+
const effectiveGuiTexture = this.guiTexture || this.dummyGuiTexture;
|
|
12166
|
+
textures.push(effectiveGuiTexture);
|
|
12167
|
+
const hasBloom = this.bloomTexture && this.bloomEnabled;
|
|
12168
|
+
const renderTarget = this.shouldOutputToTexture && this.intermediateTexture ? this.intermediateTexture : null;
|
|
11177
12169
|
this.pipeline = await Pipeline.create(this.engine, {
|
|
11178
12170
|
label: "postProcess",
|
|
11179
12171
|
wgslSource: postproc_default,
|
|
@@ -11181,75 +12173,662 @@ class PostProcessPass extends BasePass {
|
|
|
11181
12173
|
textures,
|
|
11182
12174
|
uniforms: () => ({
|
|
11183
12175
|
noiseParams: [this.noiseSize, this.noiseAnimated ? Math.random() : 0, this.noiseAnimated ? Math.random() : 0, this.fxaa ? 1 : 0],
|
|
11184
|
-
ditherParams: [this.ditheringEnabled ? 1 : 0, this.colorLevels,
|
|
12176
|
+
ditherParams: [this.ditheringEnabled ? 1 : 0, this.colorLevels, this.tonemapMode, 0],
|
|
11185
12177
|
bloomParams: [hasBloom ? 1 : 0, this.bloomIntensity, this.bloomRadius, effectiveBloomTexture?.mipCount ?? 1]
|
|
11186
|
-
})
|
|
11187
|
-
|
|
12178
|
+
}),
|
|
12179
|
+
renderTarget
|
|
12180
|
+
});
|
|
12181
|
+
this._needsRebuild = false;
|
|
12182
|
+
}
|
|
12183
|
+
async _execute(context) {
|
|
12184
|
+
const { device, canvas } = this.engine;
|
|
12185
|
+
const needsOutputToTexture = this.shouldOutputToTexture;
|
|
12186
|
+
const hasIntermediateTexture = !!this.intermediateTexture;
|
|
12187
|
+
if (needsOutputToTexture !== this._lastOutputToTexture) {
|
|
12188
|
+
this._lastOutputToTexture = needsOutputToTexture;
|
|
12189
|
+
this._needsRebuild = true;
|
|
12190
|
+
if (needsOutputToTexture && !hasIntermediateTexture) {
|
|
12191
|
+
const w = this._resizeWidth || canvas.width;
|
|
12192
|
+
const h = this._resizeHeight || canvas.height;
|
|
12193
|
+
await this._createIntermediateTexture(w, h);
|
|
12194
|
+
} else if (!needsOutputToTexture && hasIntermediateTexture) {
|
|
12195
|
+
if (this.intermediateTexture?.texture) {
|
|
12196
|
+
this.intermediateTexture.texture.destroy();
|
|
12197
|
+
this.intermediateTexture = null;
|
|
12198
|
+
}
|
|
12199
|
+
}
|
|
12200
|
+
}
|
|
12201
|
+
if (this.guiCanvas && this.guiCanvas.width > 0 && this.guiCanvas.height > 0) {
|
|
12202
|
+
const needsNewTexture = !this.guiTexture || this.guiTexture.width !== this.guiCanvas.width || this.guiTexture.height !== this.guiCanvas.height;
|
|
12203
|
+
if (needsNewTexture) {
|
|
12204
|
+
if (this.guiTexture?.texture) {
|
|
12205
|
+
this.guiTexture.texture.destroy();
|
|
12206
|
+
}
|
|
12207
|
+
const texture = device.createTexture({
|
|
12208
|
+
label: "GUI Texture",
|
|
12209
|
+
size: [this.guiCanvas.width, this.guiCanvas.height, 1],
|
|
12210
|
+
format: "rgba8unorm",
|
|
12211
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
|
|
12212
|
+
});
|
|
12213
|
+
this.guiTexture = {
|
|
12214
|
+
texture,
|
|
12215
|
+
view: texture.createView(),
|
|
12216
|
+
sampler: this.guiSampler,
|
|
12217
|
+
width: this.guiCanvas.width,
|
|
12218
|
+
height: this.guiCanvas.height
|
|
12219
|
+
};
|
|
12220
|
+
this._needsRebuild = true;
|
|
12221
|
+
}
|
|
12222
|
+
device.queue.copyExternalImageToTexture(
|
|
12223
|
+
{ source: this.guiCanvas },
|
|
12224
|
+
{ texture: this.guiTexture.texture },
|
|
12225
|
+
[this.guiCanvas.width, this.guiCanvas.height]
|
|
12226
|
+
);
|
|
12227
|
+
}
|
|
12228
|
+
if (this._needsRebuild) {
|
|
12229
|
+
await this._buildPipeline();
|
|
12230
|
+
}
|
|
12231
|
+
if (!this.pipeline || this._needsRebuild) {
|
|
12232
|
+
console.warn("PostProcessPass: Pipeline not ready");
|
|
12233
|
+
return;
|
|
12234
|
+
}
|
|
12235
|
+
const hasBloom = this.bloomTexture && this.bloomEnabled;
|
|
12236
|
+
const effectiveBloomTexture = this.bloomTexture || this.dummyBloomTexture;
|
|
12237
|
+
this.pipeline.uniformValues.set({
|
|
12238
|
+
noiseParams: [this.noiseSize, this.noiseAnimated ? Math.random() : 0, this.noiseAnimated ? Math.random() : 0, this.fxaa ? 1 : 0],
|
|
12239
|
+
ditherParams: [this.ditheringEnabled ? 1 : 0, this.colorLevels, this.tonemapMode, 0],
|
|
12240
|
+
bloomParams: [hasBloom ? 1 : 0, this.bloomIntensity, this.bloomRadius, effectiveBloomTexture?.mipCount ?? 1]
|
|
12241
|
+
});
|
|
12242
|
+
this.pipeline.render();
|
|
12243
|
+
}
|
|
12244
|
+
async _resize(width, height) {
|
|
12245
|
+
this._resizeWidth = width;
|
|
12246
|
+
this._resizeHeight = height;
|
|
12247
|
+
await this._createIntermediateTexture(width, height);
|
|
12248
|
+
this._needsRebuild = true;
|
|
12249
|
+
}
|
|
12250
|
+
_destroy() {
|
|
12251
|
+
this.pipeline = null;
|
|
12252
|
+
if (this.dummyBloomTexture?.texture) {
|
|
12253
|
+
this.dummyBloomTexture.texture.destroy();
|
|
12254
|
+
this.dummyBloomTexture = null;
|
|
12255
|
+
}
|
|
12256
|
+
if (this.guiTexture?.texture) {
|
|
12257
|
+
this.guiTexture.texture.destroy();
|
|
12258
|
+
this.guiTexture = null;
|
|
12259
|
+
}
|
|
12260
|
+
if (this.dummyGuiTexture?.texture) {
|
|
12261
|
+
this.dummyGuiTexture.texture.destroy();
|
|
12262
|
+
this.dummyGuiTexture = null;
|
|
12263
|
+
}
|
|
12264
|
+
if (this.intermediateTexture?.texture) {
|
|
12265
|
+
this.intermediateTexture.texture.destroy();
|
|
12266
|
+
this.intermediateTexture = null;
|
|
12267
|
+
}
|
|
12268
|
+
}
|
|
12269
|
+
}
|
|
12270
|
+
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}";
|
|
12271
|
+
class CRTPass extends BasePass {
|
|
12272
|
+
constructor(engine = null) {
|
|
12273
|
+
super("CRT", engine);
|
|
12274
|
+
this.pipeline = null;
|
|
12275
|
+
this.inputTexture = null;
|
|
12276
|
+
this.upscaledTexture = null;
|
|
12277
|
+
this.upscaledWidth = 0;
|
|
12278
|
+
this.upscaledHeight = 0;
|
|
12279
|
+
this.linearSampler = null;
|
|
12280
|
+
this.nearestSampler = null;
|
|
12281
|
+
this.phosphorMaskTexture = null;
|
|
12282
|
+
this.canvasWidth = 0;
|
|
12283
|
+
this.canvasHeight = 0;
|
|
12284
|
+
this.renderWidth = 0;
|
|
12285
|
+
this.renderHeight = 0;
|
|
12286
|
+
}
|
|
12287
|
+
// Settings getters
|
|
12288
|
+
get crtSettings() {
|
|
12289
|
+
return this.engine?.settings?.crt ?? {};
|
|
12290
|
+
}
|
|
12291
|
+
get crtEnabled() {
|
|
12292
|
+
return this.crtSettings.enabled ?? false;
|
|
12293
|
+
}
|
|
12294
|
+
get upscaleEnabled() {
|
|
12295
|
+
return this.crtSettings.upscaleEnabled ?? false;
|
|
12296
|
+
}
|
|
12297
|
+
get upscaleTarget() {
|
|
12298
|
+
return this.crtSettings.upscaleTarget ?? 4;
|
|
12299
|
+
}
|
|
12300
|
+
get maxTextureSize() {
|
|
12301
|
+
return this.crtSettings.maxTextureSize ?? 4096;
|
|
12302
|
+
}
|
|
12303
|
+
// Geometry
|
|
12304
|
+
get curvature() {
|
|
12305
|
+
return this.crtSettings.curvature ?? 0.03;
|
|
12306
|
+
}
|
|
12307
|
+
get cornerRadius() {
|
|
12308
|
+
return this.crtSettings.cornerRadius ?? 0.03;
|
|
12309
|
+
}
|
|
12310
|
+
get zoom() {
|
|
12311
|
+
return this.crtSettings.zoom ?? 1;
|
|
12312
|
+
}
|
|
12313
|
+
// Scanlines
|
|
12314
|
+
get scanlineIntensity() {
|
|
12315
|
+
return this.crtSettings.scanlineIntensity ?? 0.25;
|
|
12316
|
+
}
|
|
12317
|
+
get scanlineWidth() {
|
|
12318
|
+
return this.crtSettings.scanlineWidth ?? 0.5;
|
|
12319
|
+
}
|
|
12320
|
+
get scanlineBrightBoost() {
|
|
12321
|
+
return this.crtSettings.scanlineBrightBoost ?? 1;
|
|
12322
|
+
}
|
|
12323
|
+
get scanlineHeight() {
|
|
12324
|
+
return this.crtSettings.scanlineHeight ?? 3;
|
|
12325
|
+
}
|
|
12326
|
+
// pixels per scanline
|
|
12327
|
+
// Convergence
|
|
12328
|
+
get convergence() {
|
|
12329
|
+
return this.crtSettings.convergence ?? [0.5, 0, -0.5];
|
|
12330
|
+
}
|
|
12331
|
+
// Phosphor mask
|
|
12332
|
+
get maskType() {
|
|
12333
|
+
const type = this.crtSettings.maskType ?? "aperture";
|
|
12334
|
+
switch (type) {
|
|
12335
|
+
case "none":
|
|
12336
|
+
return 0;
|
|
12337
|
+
case "aperture":
|
|
12338
|
+
return 1;
|
|
12339
|
+
case "slot":
|
|
12340
|
+
return 2;
|
|
12341
|
+
case "shadow":
|
|
12342
|
+
return 3;
|
|
12343
|
+
default:
|
|
12344
|
+
return 1;
|
|
12345
|
+
}
|
|
12346
|
+
}
|
|
12347
|
+
get maskIntensity() {
|
|
12348
|
+
return this.crtSettings.maskIntensity ?? 0.15;
|
|
12349
|
+
}
|
|
12350
|
+
get maskScale() {
|
|
12351
|
+
return this.crtSettings.maskScale ?? 1;
|
|
12352
|
+
}
|
|
12353
|
+
// Vignette
|
|
12354
|
+
get vignetteIntensity() {
|
|
12355
|
+
return this.crtSettings.vignetteIntensity ?? 0.15;
|
|
12356
|
+
}
|
|
12357
|
+
get vignetteSize() {
|
|
12358
|
+
return this.crtSettings.vignetteSize ?? 0.4;
|
|
12359
|
+
}
|
|
12360
|
+
// Blur
|
|
12361
|
+
get blurSize() {
|
|
12362
|
+
return this.crtSettings.blurSize ?? 0.5;
|
|
12363
|
+
}
|
|
12364
|
+
/**
|
|
12365
|
+
* Calculate brightness compensation for phosphor mask
|
|
12366
|
+
* Pre-computed on CPU to avoid per-pixel calculation in shader
|
|
12367
|
+
* @param {number} maskType - 0=none, 1=aperture, 2=slot, 3=shadow
|
|
12368
|
+
* @param {number} intensity - mask intensity 0-1
|
|
12369
|
+
* @returns {number} compensation multiplier
|
|
12370
|
+
*/
|
|
12371
|
+
_calculateMaskCompensation(maskType, intensity) {
|
|
12372
|
+
if (maskType < 0.5 || intensity <= 0) {
|
|
12373
|
+
return 1;
|
|
12374
|
+
}
|
|
12375
|
+
let darkening;
|
|
12376
|
+
let useLinearOnly = false;
|
|
12377
|
+
if (maskType < 1.5) {
|
|
12378
|
+
darkening = 0.25;
|
|
12379
|
+
} else if (maskType < 2.5) {
|
|
12380
|
+
darkening = 0.27;
|
|
12381
|
+
} else {
|
|
12382
|
+
darkening = 0.82;
|
|
12383
|
+
useLinearOnly = true;
|
|
12384
|
+
}
|
|
12385
|
+
const linearComp = 1 / Math.max(1 - intensity * darkening, 0.1);
|
|
12386
|
+
if (useLinearOnly) {
|
|
12387
|
+
return linearComp;
|
|
12388
|
+
}
|
|
12389
|
+
const expComp = Math.exp(intensity * darkening);
|
|
12390
|
+
const t = Math.max(0, Math.min(1, (intensity - 0.4) / 0.2));
|
|
12391
|
+
const blendFactor = t * t * (3 - 2 * t);
|
|
12392
|
+
return linearComp * (1 - blendFactor) + expComp * blendFactor;
|
|
12393
|
+
}
|
|
12394
|
+
/**
|
|
12395
|
+
* Set the input texture (from PostProcessPass intermediate output)
|
|
12396
|
+
* @param {Object} texture - Input texture object
|
|
12397
|
+
*/
|
|
12398
|
+
setInputTexture(texture) {
|
|
12399
|
+
if (this.inputTexture !== texture) {
|
|
12400
|
+
this.inputTexture = texture;
|
|
12401
|
+
this._needsRebuild = true;
|
|
12402
|
+
this._blitPipeline = null;
|
|
12403
|
+
}
|
|
12404
|
+
}
|
|
12405
|
+
/**
|
|
12406
|
+
* Set the render size (before upscaling)
|
|
12407
|
+
* @param {number} width - Render width
|
|
12408
|
+
* @param {number} height - Render height
|
|
12409
|
+
*/
|
|
12410
|
+
setRenderSize(width, height) {
|
|
12411
|
+
if (this.renderWidth !== width || this.renderHeight !== height) {
|
|
12412
|
+
this.renderWidth = width;
|
|
12413
|
+
this.renderHeight = height;
|
|
12414
|
+
this._needsUpscaleRebuild = true;
|
|
12415
|
+
}
|
|
12416
|
+
}
|
|
12417
|
+
/**
|
|
12418
|
+
* Calculate the upscaled texture size
|
|
12419
|
+
* @returns {{width: number, height: number, scale: number}}
|
|
12420
|
+
*/
|
|
12421
|
+
_calculateUpscaledSize() {
|
|
12422
|
+
const renderW = this.renderWidth || this.canvasWidth;
|
|
12423
|
+
const renderH = this.renderHeight || this.canvasHeight;
|
|
12424
|
+
const maxSize = this.maxTextureSize;
|
|
12425
|
+
let scale = this.upscaleTarget;
|
|
12426
|
+
while (scale > 1 && (renderW * scale > maxSize || renderH * scale > maxSize)) {
|
|
12427
|
+
scale--;
|
|
12428
|
+
}
|
|
12429
|
+
const maxCanvasScale = 2;
|
|
12430
|
+
while (scale > 1 && (renderW * scale > this.canvasWidth * maxCanvasScale || renderH * scale > this.canvasHeight * maxCanvasScale)) {
|
|
12431
|
+
scale--;
|
|
12432
|
+
}
|
|
12433
|
+
scale = Math.max(scale, 1);
|
|
12434
|
+
const targetW = renderW * scale;
|
|
12435
|
+
const targetH = renderH * scale;
|
|
12436
|
+
return { width: targetW, height: targetH, scale };
|
|
12437
|
+
}
|
|
12438
|
+
/**
|
|
12439
|
+
* Check if actual upscaling is needed
|
|
12440
|
+
* Returns true only if the upscaled texture would be larger than the input
|
|
12441
|
+
*/
|
|
12442
|
+
_needsUpscaling() {
|
|
12443
|
+
if (!this.upscaleEnabled) return false;
|
|
12444
|
+
const { scale } = this._calculateUpscaledSize();
|
|
12445
|
+
return scale > 1;
|
|
12446
|
+
}
|
|
12447
|
+
async _init() {
|
|
12448
|
+
const { device } = this.engine;
|
|
12449
|
+
this.linearSampler = device.createSampler({
|
|
12450
|
+
label: "CRT Linear Sampler",
|
|
12451
|
+
minFilter: "linear",
|
|
12452
|
+
magFilter: "linear",
|
|
12453
|
+
addressModeU: "mirror-repeat",
|
|
12454
|
+
addressModeV: "mirror-repeat"
|
|
11188
12455
|
});
|
|
11189
|
-
this.
|
|
12456
|
+
this.nearestSampler = device.createSampler({
|
|
12457
|
+
label: "CRT Nearest Sampler",
|
|
12458
|
+
minFilter: "nearest",
|
|
12459
|
+
magFilter: "nearest",
|
|
12460
|
+
addressModeU: "mirror-repeat",
|
|
12461
|
+
addressModeV: "mirror-repeat"
|
|
12462
|
+
});
|
|
12463
|
+
await this._createPhosphorMaskTexture();
|
|
11190
12464
|
}
|
|
11191
|
-
|
|
12465
|
+
/**
|
|
12466
|
+
* Create phosphor mask texture
|
|
12467
|
+
* This is a simple procedural texture for the aperture grille pattern
|
|
12468
|
+
*/
|
|
12469
|
+
async _createPhosphorMaskTexture() {
|
|
11192
12470
|
const { device } = this.engine;
|
|
11193
|
-
|
|
11194
|
-
|
|
11195
|
-
|
|
11196
|
-
|
|
11197
|
-
|
|
12471
|
+
const size = 6;
|
|
12472
|
+
const data = new Uint8Array(size * 2 * 4);
|
|
12473
|
+
for (let y = 0; y < 2; y++) {
|
|
12474
|
+
for (let x = 0; x < size; x++) {
|
|
12475
|
+
const idx = (y * size + x) * 4;
|
|
12476
|
+
const phase = x % 3;
|
|
12477
|
+
if (phase === 0) {
|
|
12478
|
+
data[idx] = 255;
|
|
12479
|
+
data[idx + 1] = 50;
|
|
12480
|
+
data[idx + 2] = 50;
|
|
12481
|
+
} else if (phase === 1) {
|
|
12482
|
+
data[idx] = 50;
|
|
12483
|
+
data[idx + 1] = 255;
|
|
12484
|
+
data[idx + 2] = 50;
|
|
12485
|
+
} else {
|
|
12486
|
+
data[idx] = 50;
|
|
12487
|
+
data[idx + 1] = 50;
|
|
12488
|
+
data[idx + 2] = 255;
|
|
11198
12489
|
}
|
|
11199
|
-
|
|
11200
|
-
label: "GUI Texture",
|
|
11201
|
-
size: [this.guiCanvas.width, this.guiCanvas.height, 1],
|
|
11202
|
-
format: "rgba8unorm",
|
|
11203
|
-
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
|
|
11204
|
-
});
|
|
11205
|
-
this.guiTexture = {
|
|
11206
|
-
texture,
|
|
11207
|
-
view: texture.createView(),
|
|
11208
|
-
sampler: this.guiSampler,
|
|
11209
|
-
width: this.guiCanvas.width,
|
|
11210
|
-
height: this.guiCanvas.height
|
|
11211
|
-
};
|
|
11212
|
-
this._needsRebuild = true;
|
|
12490
|
+
data[idx + 3] = 255;
|
|
11213
12491
|
}
|
|
11214
|
-
device.queue.copyExternalImageToTexture(
|
|
11215
|
-
{ source: this.guiCanvas },
|
|
11216
|
-
{ texture: this.guiTexture.texture },
|
|
11217
|
-
[this.guiCanvas.width, this.guiCanvas.height]
|
|
11218
|
-
);
|
|
11219
12492
|
}
|
|
11220
|
-
|
|
11221
|
-
|
|
12493
|
+
const texture = device.createTexture({
|
|
12494
|
+
label: "Phosphor Mask",
|
|
12495
|
+
size: [size, 2, 1],
|
|
12496
|
+
format: "rgba8unorm",
|
|
12497
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST
|
|
12498
|
+
});
|
|
12499
|
+
device.queue.writeTexture(
|
|
12500
|
+
{ texture },
|
|
12501
|
+
data,
|
|
12502
|
+
{ bytesPerRow: size * 4 },
|
|
12503
|
+
{ width: size, height: 2 }
|
|
12504
|
+
);
|
|
12505
|
+
const sampler = device.createSampler({
|
|
12506
|
+
label: "Phosphor Mask Sampler",
|
|
12507
|
+
minFilter: "nearest",
|
|
12508
|
+
magFilter: "nearest",
|
|
12509
|
+
addressModeU: "repeat",
|
|
12510
|
+
addressModeV: "repeat"
|
|
12511
|
+
});
|
|
12512
|
+
this.phosphorMaskTexture = {
|
|
12513
|
+
texture,
|
|
12514
|
+
view: texture.createView(),
|
|
12515
|
+
sampler
|
|
12516
|
+
};
|
|
12517
|
+
}
|
|
12518
|
+
/**
|
|
12519
|
+
* Create or resize the upscaled texture
|
|
12520
|
+
*/
|
|
12521
|
+
async _createUpscaledTexture() {
|
|
12522
|
+
const { device } = this.engine;
|
|
12523
|
+
const { width, height, scale } = this._calculateUpscaledSize();
|
|
12524
|
+
if (this.upscaledWidth === width && this.upscaledHeight === height) {
|
|
12525
|
+
return;
|
|
11222
12526
|
}
|
|
11223
|
-
if (
|
|
11224
|
-
|
|
12527
|
+
if (this.upscaledTexture?.texture) {
|
|
12528
|
+
this.upscaledTexture.texture.destroy();
|
|
12529
|
+
}
|
|
12530
|
+
const texture = device.createTexture({
|
|
12531
|
+
label: "CRT Upscaled Texture",
|
|
12532
|
+
size: [width, height, 1],
|
|
12533
|
+
format: "rgba8unorm",
|
|
12534
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_DST
|
|
12535
|
+
});
|
|
12536
|
+
this.upscaledTexture = {
|
|
12537
|
+
texture,
|
|
12538
|
+
view: texture.createView(),
|
|
12539
|
+
width,
|
|
12540
|
+
height,
|
|
12541
|
+
scale,
|
|
12542
|
+
format: "rgba8unorm"
|
|
12543
|
+
};
|
|
12544
|
+
this.upscaledWidth = width;
|
|
12545
|
+
this.upscaledHeight = height;
|
|
12546
|
+
console.log(`CRTPass: Created upscaled texture ${width}x${height} (${scale.toFixed(1)}x)`);
|
|
12547
|
+
this._needsRebuild = true;
|
|
12548
|
+
this._blitPipeline = null;
|
|
12549
|
+
}
|
|
12550
|
+
/**
|
|
12551
|
+
* Build or rebuild the CRT pipeline
|
|
12552
|
+
*/
|
|
12553
|
+
async _buildPipeline() {
|
|
12554
|
+
if (!this.inputTexture && !this.upscaledTexture) {
|
|
11225
12555
|
return;
|
|
11226
12556
|
}
|
|
11227
|
-
const
|
|
11228
|
-
const
|
|
11229
|
-
this.
|
|
11230
|
-
|
|
11231
|
-
|
|
11232
|
-
|
|
12557
|
+
const { device } = this.engine;
|
|
12558
|
+
const needsUpscaling = this._needsUpscaling();
|
|
12559
|
+
const effectiveInput = (this.crtEnabled || this.upscaleEnabled) && needsUpscaling && this.upscaledTexture ? this.upscaledTexture : this.inputTexture;
|
|
12560
|
+
if (!effectiveInput) return;
|
|
12561
|
+
const bindGroupLayout = device.createBindGroupLayout({
|
|
12562
|
+
label: "CRT Bind Group Layout",
|
|
12563
|
+
entries: [
|
|
12564
|
+
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
12565
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
12566
|
+
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } },
|
|
12567
|
+
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } },
|
|
12568
|
+
{ binding: 4, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
12569
|
+
{ binding: 5, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } }
|
|
12570
|
+
]
|
|
11233
12571
|
});
|
|
11234
|
-
|
|
12572
|
+
const uniformBuffer = device.createBuffer({
|
|
12573
|
+
label: "CRT Uniforms",
|
|
12574
|
+
size: 128,
|
|
12575
|
+
// Padded for alignment
|
|
12576
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
12577
|
+
});
|
|
12578
|
+
const bindGroup = device.createBindGroup({
|
|
12579
|
+
label: "CRT Bind Group",
|
|
12580
|
+
layout: bindGroupLayout,
|
|
12581
|
+
entries: [
|
|
12582
|
+
{ binding: 0, resource: { buffer: uniformBuffer } },
|
|
12583
|
+
{ binding: 1, resource: effectiveInput.view },
|
|
12584
|
+
{ binding: 2, resource: this.linearSampler },
|
|
12585
|
+
{ binding: 3, resource: this.nearestSampler },
|
|
12586
|
+
{ binding: 4, resource: this.phosphorMaskTexture.view },
|
|
12587
|
+
{ binding: 5, resource: this.phosphorMaskTexture.sampler }
|
|
12588
|
+
]
|
|
12589
|
+
});
|
|
12590
|
+
const pipelineLayout = device.createPipelineLayout({
|
|
12591
|
+
label: "CRT Pipeline Layout",
|
|
12592
|
+
bindGroupLayouts: [bindGroupLayout]
|
|
12593
|
+
});
|
|
12594
|
+
const shaderModule = device.createShaderModule({
|
|
12595
|
+
label: "CRT Shader",
|
|
12596
|
+
code: crt_default
|
|
12597
|
+
});
|
|
12598
|
+
const pipeline = device.createRenderPipeline({
|
|
12599
|
+
label: "CRT Pipeline",
|
|
12600
|
+
layout: pipelineLayout,
|
|
12601
|
+
vertex: {
|
|
12602
|
+
module: shaderModule,
|
|
12603
|
+
entryPoint: "vertexMain"
|
|
12604
|
+
},
|
|
12605
|
+
fragment: {
|
|
12606
|
+
module: shaderModule,
|
|
12607
|
+
entryPoint: "fragmentMain",
|
|
12608
|
+
targets: [{
|
|
12609
|
+
format: navigator.gpu.getPreferredCanvasFormat()
|
|
12610
|
+
}]
|
|
12611
|
+
},
|
|
12612
|
+
primitive: {
|
|
12613
|
+
topology: "triangle-list"
|
|
12614
|
+
}
|
|
12615
|
+
});
|
|
12616
|
+
this.pipeline = {
|
|
12617
|
+
pipeline,
|
|
12618
|
+
bindGroup,
|
|
12619
|
+
uniformBuffer
|
|
12620
|
+
};
|
|
12621
|
+
this._pipelineInputTexture = this.inputTexture;
|
|
12622
|
+
this._pipelineUpscaledTexture = needsUpscaling ? this.upscaledTexture : null;
|
|
12623
|
+
this._needsRebuild = false;
|
|
12624
|
+
}
|
|
12625
|
+
/**
|
|
12626
|
+
* Upscale the input texture using nearest-neighbor filtering
|
|
12627
|
+
*/
|
|
12628
|
+
_upscaleInput() {
|
|
12629
|
+
if (!this.inputTexture || !this.upscaledTexture) return;
|
|
12630
|
+
const { device } = this.engine;
|
|
12631
|
+
const commandEncoder = device.createCommandEncoder({ label: "CRT Upscale" });
|
|
12632
|
+
if (this.inputTexture.width === this.upscaledTexture.width && this.inputTexture.height === this.upscaledTexture.height) {
|
|
12633
|
+
commandEncoder.copyTextureToTexture(
|
|
12634
|
+
{ texture: this.inputTexture.texture },
|
|
12635
|
+
{ texture: this.upscaledTexture.texture },
|
|
12636
|
+
[this.inputTexture.width, this.inputTexture.height]
|
|
12637
|
+
);
|
|
12638
|
+
} else {
|
|
12639
|
+
if (!this._blitPipeline || this._blitInputTexture !== this.inputTexture) {
|
|
12640
|
+
this._createBlitPipeline();
|
|
12641
|
+
}
|
|
12642
|
+
if (this._blitPipeline) {
|
|
12643
|
+
const renderPass = commandEncoder.beginRenderPass({
|
|
12644
|
+
colorAttachments: [{
|
|
12645
|
+
view: this.upscaledTexture.view,
|
|
12646
|
+
loadOp: "clear",
|
|
12647
|
+
storeOp: "store",
|
|
12648
|
+
clearValue: { r: 0, g: 0, b: 0, a: 1 }
|
|
12649
|
+
}]
|
|
12650
|
+
});
|
|
12651
|
+
renderPass.setPipeline(this._blitPipeline.pipeline);
|
|
12652
|
+
renderPass.setBindGroup(0, this._blitPipeline.bindGroup);
|
|
12653
|
+
renderPass.draw(3, 1, 0, 0);
|
|
12654
|
+
renderPass.end();
|
|
12655
|
+
}
|
|
12656
|
+
}
|
|
12657
|
+
device.queue.submit([commandEncoder.finish()]);
|
|
12658
|
+
}
|
|
12659
|
+
/**
|
|
12660
|
+
* Create a simple nearest-neighbor blit pipeline
|
|
12661
|
+
*/
|
|
12662
|
+
_createBlitPipeline() {
|
|
12663
|
+
if (!this.inputTexture) return;
|
|
12664
|
+
const { device } = this.engine;
|
|
12665
|
+
this._blitInputTexture = this.inputTexture;
|
|
12666
|
+
const blitShader = `
|
|
12667
|
+
struct VertexOutput {
|
|
12668
|
+
@builtin(position) position: vec4f,
|
|
12669
|
+
@location(0) uv: vec2f,
|
|
12670
|
+
}
|
|
12671
|
+
|
|
12672
|
+
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
12673
|
+
@group(0) @binding(1) var inputSampler: sampler;
|
|
12674
|
+
|
|
12675
|
+
@vertex
|
|
12676
|
+
fn vertexMain(@builtin(vertex_index) idx: u32) -> VertexOutput {
|
|
12677
|
+
var output: VertexOutput;
|
|
12678
|
+
let x = f32(idx & 1u) * 4.0 - 1.0;
|
|
12679
|
+
let y = f32(idx >> 1u) * 4.0 - 1.0;
|
|
12680
|
+
output.position = vec4f(x, y, 0.0, 1.0);
|
|
12681
|
+
output.uv = vec2f((x + 1.0) * 0.5, (1.0 - y) * 0.5);
|
|
12682
|
+
return output;
|
|
12683
|
+
}
|
|
12684
|
+
|
|
12685
|
+
@fragment
|
|
12686
|
+
fn fragmentMain(input: VertexOutput) -> @location(0) vec4f {
|
|
12687
|
+
return textureSample(inputTexture, inputSampler, input.uv);
|
|
12688
|
+
}
|
|
12689
|
+
`;
|
|
12690
|
+
const shaderModule = device.createShaderModule({
|
|
12691
|
+
label: "Blit Shader",
|
|
12692
|
+
code: blitShader
|
|
12693
|
+
});
|
|
12694
|
+
const bindGroupLayout = device.createBindGroupLayout({
|
|
12695
|
+
label: "Blit Bind Group Layout",
|
|
12696
|
+
entries: [
|
|
12697
|
+
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
12698
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } }
|
|
12699
|
+
]
|
|
12700
|
+
});
|
|
12701
|
+
const bindGroup = device.createBindGroup({
|
|
12702
|
+
label: "Blit Bind Group",
|
|
12703
|
+
layout: bindGroupLayout,
|
|
12704
|
+
entries: [
|
|
12705
|
+
{ binding: 0, resource: this.inputTexture.view },
|
|
12706
|
+
{ binding: 1, resource: this.nearestSampler }
|
|
12707
|
+
]
|
|
12708
|
+
});
|
|
12709
|
+
const pipeline = device.createRenderPipeline({
|
|
12710
|
+
label: "Blit Pipeline",
|
|
12711
|
+
layout: device.createPipelineLayout({ bindGroupLayouts: [bindGroupLayout] }),
|
|
12712
|
+
vertex: {
|
|
12713
|
+
module: shaderModule,
|
|
12714
|
+
entryPoint: "vertexMain"
|
|
12715
|
+
},
|
|
12716
|
+
fragment: {
|
|
12717
|
+
module: shaderModule,
|
|
12718
|
+
entryPoint: "fragmentMain",
|
|
12719
|
+
targets: [{ format: "rgba8unorm" }]
|
|
12720
|
+
},
|
|
12721
|
+
primitive: { topology: "triangle-list" }
|
|
12722
|
+
});
|
|
12723
|
+
this._blitPipeline = { pipeline, bindGroup };
|
|
12724
|
+
}
|
|
12725
|
+
async _execute(context) {
|
|
12726
|
+
const { device, canvas } = this.engine;
|
|
12727
|
+
const needsUpscaling = this._needsUpscaling();
|
|
12728
|
+
const effectiveUpscaledTexture = needsUpscaling ? this.upscaledTexture : null;
|
|
12729
|
+
if (this.pipeline && (this._pipelineInputTexture !== this.inputTexture || this._pipelineUpscaledTexture !== effectiveUpscaledTexture)) {
|
|
12730
|
+
this._needsRebuild = true;
|
|
12731
|
+
this._blitPipeline = null;
|
|
12732
|
+
}
|
|
12733
|
+
if (!this.crtEnabled && !this.upscaleEnabled) {
|
|
12734
|
+
if (!this.inputTexture) return;
|
|
12735
|
+
if (this._needsRebuild || !this.pipeline) {
|
|
12736
|
+
await this._buildPipeline();
|
|
12737
|
+
}
|
|
12738
|
+
} else {
|
|
12739
|
+
if (needsUpscaling) {
|
|
12740
|
+
if (this._needsUpscaleRebuild) {
|
|
12741
|
+
await this._createUpscaledTexture();
|
|
12742
|
+
this._needsUpscaleRebuild = false;
|
|
12743
|
+
}
|
|
12744
|
+
if (this.inputTexture && this.upscaledTexture) {
|
|
12745
|
+
this._upscaleInput();
|
|
12746
|
+
}
|
|
12747
|
+
}
|
|
12748
|
+
if (this._needsRebuild || !this.pipeline) {
|
|
12749
|
+
await this._buildPipeline();
|
|
12750
|
+
}
|
|
12751
|
+
}
|
|
12752
|
+
if (!this.pipeline) return;
|
|
12753
|
+
const uniformData = new Float32Array(32);
|
|
12754
|
+
uniformData[0] = this.canvasWidth;
|
|
12755
|
+
uniformData[1] = this.canvasHeight;
|
|
12756
|
+
const inputW = needsUpscaling && this.upscaledTexture ? this.upscaledTexture.width : this.inputTexture?.width || this.canvasWidth;
|
|
12757
|
+
const inputH = needsUpscaling && this.upscaledTexture ? this.upscaledTexture.height : this.inputTexture?.height || this.canvasHeight;
|
|
12758
|
+
uniformData[2] = inputW;
|
|
12759
|
+
uniformData[3] = inputH;
|
|
12760
|
+
const renderW = this.renderWidth || this.canvasWidth;
|
|
12761
|
+
const renderH = this.renderHeight || this.canvasHeight;
|
|
12762
|
+
uniformData[4] = renderW;
|
|
12763
|
+
uniformData[5] = renderH;
|
|
12764
|
+
if (!this._loggedDimensions) {
|
|
12765
|
+
const renderScale = renderW > 0 ? (renderW / this.canvasWidth).toFixed(2) : "?";
|
|
12766
|
+
const upscaleInfo = needsUpscaling ? `upscale=${this.upscaledTexture?.scale || "?"}x` : "no-upscale (direct)";
|
|
12767
|
+
console.log(`CRT: canvas=${this.canvasWidth}x${this.canvasHeight}, render=${renderW}x${renderH} (scale ${renderScale}), input=${inputW}x${inputH}, ${upscaleInfo}`);
|
|
12768
|
+
console.log(`CRT: Scanlines use fragment coords (${this.canvasHeight}px), should repeat every ${this.scanlineHeight}px = ${Math.floor(this.canvasHeight / this.scanlineHeight)} scanlines`);
|
|
12769
|
+
this._loggedDimensions = true;
|
|
12770
|
+
}
|
|
12771
|
+
uniformData[6] = this.curvature;
|
|
12772
|
+
uniformData[7] = this.cornerRadius;
|
|
12773
|
+
uniformData[8] = this.zoom;
|
|
12774
|
+
uniformData[9] = 0;
|
|
12775
|
+
uniformData[10] = this.scanlineIntensity;
|
|
12776
|
+
uniformData[11] = this.scanlineWidth;
|
|
12777
|
+
uniformData[12] = this.scanlineBrightBoost;
|
|
12778
|
+
uniformData[13] = this.scanlineHeight;
|
|
12779
|
+
uniformData[14] = 0;
|
|
12780
|
+
uniformData[15] = 0;
|
|
12781
|
+
const conv = this.convergence;
|
|
12782
|
+
uniformData[16] = conv[0];
|
|
12783
|
+
uniformData[17] = conv[1];
|
|
12784
|
+
uniformData[18] = conv[2];
|
|
12785
|
+
uniformData[19] = 0;
|
|
12786
|
+
uniformData[20] = this.maskType;
|
|
12787
|
+
uniformData[21] = this.maskIntensity;
|
|
12788
|
+
uniformData[22] = this.maskScale;
|
|
12789
|
+
const maskCompensation = this._calculateMaskCompensation(this.maskType, this.maskIntensity);
|
|
12790
|
+
uniformData[23] = maskCompensation;
|
|
12791
|
+
uniformData[24] = this.vignetteIntensity;
|
|
12792
|
+
uniformData[25] = this.vignetteSize;
|
|
12793
|
+
uniformData[26] = this.blurSize;
|
|
12794
|
+
uniformData[27] = this.crtEnabled ? 1 : 0;
|
|
12795
|
+
uniformData[28] = this.upscaleEnabled ? 1 : 0;
|
|
12796
|
+
device.queue.writeBuffer(this.pipeline.uniformBuffer, 0, uniformData);
|
|
12797
|
+
const commandEncoder = device.createCommandEncoder({ label: "CRT Render" });
|
|
12798
|
+
const canvasTexture = this.engine.context.getCurrentTexture();
|
|
12799
|
+
const renderPass = commandEncoder.beginRenderPass({
|
|
12800
|
+
colorAttachments: [{
|
|
12801
|
+
view: canvasTexture.createView(),
|
|
12802
|
+
loadOp: "clear",
|
|
12803
|
+
storeOp: "store",
|
|
12804
|
+
clearValue: { r: 0, g: 0, b: 0, a: 1 }
|
|
12805
|
+
}]
|
|
12806
|
+
});
|
|
12807
|
+
renderPass.setPipeline(this.pipeline.pipeline);
|
|
12808
|
+
renderPass.setBindGroup(0, this.pipeline.bindGroup);
|
|
12809
|
+
renderPass.draw(3, 1, 0, 0);
|
|
12810
|
+
renderPass.end();
|
|
12811
|
+
device.queue.submit([commandEncoder.finish()]);
|
|
11235
12812
|
}
|
|
11236
12813
|
async _resize(width, height) {
|
|
12814
|
+
this.canvasWidth = width;
|
|
12815
|
+
this.canvasHeight = height;
|
|
12816
|
+
this._needsUpscaleRebuild = true;
|
|
11237
12817
|
this._needsRebuild = true;
|
|
12818
|
+
this._loggedDimensions = false;
|
|
11238
12819
|
}
|
|
11239
12820
|
_destroy() {
|
|
11240
|
-
this.pipeline
|
|
11241
|
-
|
|
11242
|
-
this.dummyBloomTexture.texture.destroy();
|
|
11243
|
-
this.dummyBloomTexture = null;
|
|
12821
|
+
if (this.pipeline?.uniformBuffer) {
|
|
12822
|
+
this.pipeline.uniformBuffer.destroy();
|
|
11244
12823
|
}
|
|
11245
|
-
if (this.
|
|
11246
|
-
this.
|
|
11247
|
-
this.guiTexture = null;
|
|
12824
|
+
if (this.upscaledTexture?.texture) {
|
|
12825
|
+
this.upscaledTexture.texture.destroy();
|
|
11248
12826
|
}
|
|
11249
|
-
if (this.
|
|
11250
|
-
this.
|
|
11251
|
-
this.dummyGuiTexture = null;
|
|
12827
|
+
if (this.phosphorMaskTexture?.texture) {
|
|
12828
|
+
this.phosphorMaskTexture.texture.destroy();
|
|
11252
12829
|
}
|
|
12830
|
+
this.pipeline = null;
|
|
12831
|
+
this._blitPipeline = null;
|
|
11253
12832
|
}
|
|
11254
12833
|
}
|
|
11255
12834
|
class AmbientCapturePass extends BasePass {
|
|
@@ -13413,8 +14992,10 @@ class RenderGraph {
|
|
|
13413
14992
|
// Pass 11: Forward transparent objects
|
|
13414
14993
|
particles: null,
|
|
13415
14994
|
// Pass 12: GPU particle rendering
|
|
13416
|
-
postProcess: null
|
|
14995
|
+
postProcess: null,
|
|
13417
14996
|
// Pass 13: Tone mapping + bloom composite
|
|
14997
|
+
crt: null
|
|
14998
|
+
// Pass 14: CRT effect (optional)
|
|
13418
14999
|
};
|
|
13419
15000
|
this.historyManager = null;
|
|
13420
15001
|
this.cullingSystem = new CullingSystem(engine);
|
|
@@ -13487,7 +15068,9 @@ class RenderGraph {
|
|
|
13487
15068
|
this.passes.transparent = new TransparentPass(this.engine);
|
|
13488
15069
|
this.passes.particles = new ParticlePass(this.engine);
|
|
13489
15070
|
this.passes.fog = new FogPass(this.engine);
|
|
15071
|
+
this.passes.volumetricFog = new VolumetricFogPass(this.engine);
|
|
13490
15072
|
this.passes.postProcess = new PostProcessPass(this.engine);
|
|
15073
|
+
this.passes.crt = new CRTPass(this.engine);
|
|
13491
15074
|
const { canvas } = this.engine;
|
|
13492
15075
|
this.historyManager = new HistoryBufferManager(this.engine);
|
|
13493
15076
|
start = performance.now();
|
|
@@ -13539,9 +15122,15 @@ class RenderGraph {
|
|
|
13539
15122
|
await this.passes.fog.initialize();
|
|
13540
15123
|
timings.push({ name: "init:fog", time: performance.now() - start });
|
|
13541
15124
|
start = performance.now();
|
|
15125
|
+
await this.passes.volumetricFog.initialize();
|
|
15126
|
+
timings.push({ name: "init:volumetricFog", time: performance.now() - start });
|
|
15127
|
+
start = performance.now();
|
|
13542
15128
|
await this.passes.postProcess.initialize();
|
|
13543
15129
|
timings.push({ name: "init:postProcess", time: performance.now() - start });
|
|
13544
15130
|
start = performance.now();
|
|
15131
|
+
await this.passes.crt.initialize();
|
|
15132
|
+
timings.push({ name: "init:crt", time: performance.now() - start });
|
|
15133
|
+
start = performance.now();
|
|
13545
15134
|
this.passes.reflection.setFallbackEnvironment(environmentMap, this.environmentEncoding);
|
|
13546
15135
|
this.passes.lighting.setEnvironmentMap(environmentMap, this.environmentEncoding);
|
|
13547
15136
|
await this.passes.lighting.setGBuffer(this.passes.gbuffer.getGBuffer());
|
|
@@ -13605,11 +15194,16 @@ class RenderGraph {
|
|
|
13605
15194
|
this.passes.particles.setShadowPass(this.passes.shadow);
|
|
13606
15195
|
this.passes.particles.setEnvironmentMap(environmentMap, this.environmentEncoding);
|
|
13607
15196
|
this.passes.particles.setLightingPass(this.passes.lighting);
|
|
15197
|
+
this.passes.volumetricFog.setGBuffer(this.passes.gbuffer.getGBuffer());
|
|
15198
|
+
this.passes.volumetricFog.setShadowPass(this.passes.shadow);
|
|
15199
|
+
this.passes.volumetricFog.setLightingPass(this.passes.lighting);
|
|
15200
|
+
this.passes.volumetricFog.setHiZPass(this.passes.hiz);
|
|
13608
15201
|
this.passes.postProcess.setInputTexture(this.passes.lighting.getOutputTexture());
|
|
13609
15202
|
this.passes.postProcess.setNoise(this.noiseTexture, this.noiseSize, this.noiseAnimated);
|
|
13610
15203
|
if (this.engine?.guiCanvas) {
|
|
13611
15204
|
this.passes.postProcess.setGuiCanvas(this.engine.guiCanvas);
|
|
13612
15205
|
}
|
|
15206
|
+
this.invalidateOcclusionCulling();
|
|
13613
15207
|
}
|
|
13614
15208
|
/**
|
|
13615
15209
|
* Render a frame using the new entity/asset system
|
|
@@ -13862,10 +15456,6 @@ class RenderGraph {
|
|
|
13862
15456
|
this.passes.transparent.distanceFadeStart = transparentMaxDist * transparentFadeStart;
|
|
13863
15457
|
await this.passes.transparent.execute(passContext);
|
|
13864
15458
|
}
|
|
13865
|
-
if (this.passes.particles && this.particleSystem.getActiveEmitters().length > 0) {
|
|
13866
|
-
this.passes.particles.setOutputTexture(hdrSource);
|
|
13867
|
-
await this.passes.particles.execute(passContext);
|
|
13868
|
-
}
|
|
13869
15459
|
const fogEnabled = this.engine?.settings?.environment?.fog?.enabled;
|
|
13870
15460
|
if (this.passes.fog && fogEnabled) {
|
|
13871
15461
|
this.passes.fog.setInputTexture(hdrSource);
|
|
@@ -13876,6 +15466,20 @@ class RenderGraph {
|
|
|
13876
15466
|
hdrSource = fogOutput;
|
|
13877
15467
|
}
|
|
13878
15468
|
}
|
|
15469
|
+
if (this.passes.particles && this.particleSystem.getActiveEmitters().length > 0) {
|
|
15470
|
+
this.passes.particles.setOutputTexture(hdrSource);
|
|
15471
|
+
await this.passes.particles.execute(passContext);
|
|
15472
|
+
}
|
|
15473
|
+
const volumetricFogEnabled = this.engine?.settings?.volumetricFog?.enabled;
|
|
15474
|
+
if (this.passes.volumetricFog && volumetricFogEnabled) {
|
|
15475
|
+
this.passes.volumetricFog.setInputTexture(hdrSource);
|
|
15476
|
+
this.passes.volumetricFog.setGBuffer(gbuffer);
|
|
15477
|
+
await this.passes.volumetricFog.execute(passContext);
|
|
15478
|
+
const volFogOutput = this.passes.volumetricFog.getOutputTexture();
|
|
15479
|
+
if (volFogOutput && volFogOutput !== hdrSource) {
|
|
15480
|
+
hdrSource = volFogOutput;
|
|
15481
|
+
}
|
|
15482
|
+
}
|
|
13879
15483
|
const bloomEnabled = this.engine?.settings?.bloom?.enabled;
|
|
13880
15484
|
if (this.passes.bloom && bloomEnabled) {
|
|
13881
15485
|
this.passes.bloom.setInputTexture(hdrSource);
|
|
@@ -13886,6 +15490,19 @@ class RenderGraph {
|
|
|
13886
15490
|
}
|
|
13887
15491
|
this.passes.postProcess.setInputTexture(hdrSource);
|
|
13888
15492
|
await this.passes.postProcess.execute(passContext);
|
|
15493
|
+
const crtEnabled = this.engine?.settings?.crt?.enabled;
|
|
15494
|
+
const crtUpscaleEnabled = this.engine?.settings?.crt?.upscaleEnabled;
|
|
15495
|
+
if (crtEnabled || crtUpscaleEnabled) {
|
|
15496
|
+
const postProcessOutput = this.passes.postProcess.getOutputTexture();
|
|
15497
|
+
if (postProcessOutput) {
|
|
15498
|
+
this.passes.crt.setInputTexture(postProcessOutput);
|
|
15499
|
+
this.passes.crt.setRenderSize(
|
|
15500
|
+
this.passes.gbuffer.getGBuffer()?.depth?.width || canvas.width,
|
|
15501
|
+
this.passes.gbuffer.getGBuffer()?.depth?.height || canvas.height
|
|
15502
|
+
);
|
|
15503
|
+
await this.passes.crt.execute(passContext);
|
|
15504
|
+
}
|
|
15505
|
+
}
|
|
13889
15506
|
this.historyManager.swap(camera);
|
|
13890
15507
|
this._lastRenderContext = {
|
|
13891
15508
|
meshes,
|
|
@@ -13950,6 +15567,40 @@ class RenderGraph {
|
|
|
13950
15567
|
}
|
|
13951
15568
|
}
|
|
13952
15569
|
const asset = assetManager.get(modelId);
|
|
15570
|
+
if (asset?.meshNames && !asset.mesh) {
|
|
15571
|
+
for (const meshName of asset.meshNames) {
|
|
15572
|
+
const submeshId = assetManager.createModelId(modelId, meshName);
|
|
15573
|
+
const submeshAsset = assetManager.get(submeshId);
|
|
15574
|
+
if (!submeshAsset?.mesh) continue;
|
|
15575
|
+
if (submeshAsset.hasSkin && submeshAsset.skin) {
|
|
15576
|
+
for (const item of entities) {
|
|
15577
|
+
const entity = item.entity;
|
|
15578
|
+
const animation = entity.animation || "default";
|
|
15579
|
+
const phase = entity.phase || 0;
|
|
15580
|
+
const quantizedPhase = Math.floor(phase / 0.05) * 0.05;
|
|
15581
|
+
const key = `${submeshId}|${animation}|${quantizedPhase.toFixed(2)}`;
|
|
15582
|
+
if (!skinnedInstancedGroups.has(key)) {
|
|
15583
|
+
skinnedInstancedGroups.set(key, {
|
|
15584
|
+
modelId: submeshId,
|
|
15585
|
+
animation,
|
|
15586
|
+
phase: quantizedPhase,
|
|
15587
|
+
asset: submeshAsset,
|
|
15588
|
+
entities: []
|
|
15589
|
+
});
|
|
15590
|
+
}
|
|
15591
|
+
skinnedInstancedGroups.get(key).entities.push(item);
|
|
15592
|
+
}
|
|
15593
|
+
} else {
|
|
15594
|
+
if (!nonSkinnedGroups.has(submeshId)) {
|
|
15595
|
+
nonSkinnedGroups.set(submeshId, { asset: submeshAsset, entities: [] });
|
|
15596
|
+
}
|
|
15597
|
+
for (const item of entities) {
|
|
15598
|
+
nonSkinnedGroups.get(submeshId).entities.push(item);
|
|
15599
|
+
}
|
|
15600
|
+
}
|
|
15601
|
+
}
|
|
15602
|
+
continue;
|
|
15603
|
+
}
|
|
13953
15604
|
if (!asset?.mesh) continue;
|
|
13954
15605
|
if (asset.hasSkin && asset.skin) {
|
|
13955
15606
|
for (const item of entities) {
|
|
@@ -14047,7 +15698,8 @@ class RenderGraph {
|
|
|
14047
15698
|
geometry._instanceDataDirty = true;
|
|
14048
15699
|
updatedMeshes.add(meshName);
|
|
14049
15700
|
}
|
|
14050
|
-
const
|
|
15701
|
+
const animationSpeed = this.engine?.settings?.animation?.speed ?? 1;
|
|
15702
|
+
const globalTime = performance.now() / 1e3 * animationSpeed;
|
|
14051
15703
|
for (const { id: entityId, entity, asset, modelId } of skinnedIndividualEntities) {
|
|
14052
15704
|
const entityAnimation = entity.animation || "default";
|
|
14053
15705
|
const entityPhase = entity.phase || 0;
|
|
@@ -14090,7 +15742,9 @@ class RenderGraph {
|
|
|
14090
15742
|
material: asset.mesh.material,
|
|
14091
15743
|
skin: individualSkin2,
|
|
14092
15744
|
hasSkin: true,
|
|
14093
|
-
uid: `individual_${entityId}
|
|
15745
|
+
uid: `individual_${entityId}`,
|
|
15746
|
+
// Use asset's combined bsphere for culling (all skinned submeshes share one sphere)
|
|
15747
|
+
combinedBsphere: asset.bsphere || null
|
|
14094
15748
|
};
|
|
14095
15749
|
cached = {
|
|
14096
15750
|
skin: individualSkin2,
|
|
@@ -14219,7 +15873,9 @@ class RenderGraph {
|
|
|
14219
15873
|
material: asset.mesh.material,
|
|
14220
15874
|
skin: clonedSkin2,
|
|
14221
15875
|
hasSkin: true,
|
|
14222
|
-
uid: asset.mesh.uid + "_phase_" + key.replace(/[^a-zA-Z0-9]/g, "_")
|
|
15876
|
+
uid: asset.mesh.uid + "_phase_" + key.replace(/[^a-zA-Z0-9]/g, "_"),
|
|
15877
|
+
// Use asset's combined bsphere for culling (all skinned submeshes share one sphere)
|
|
15878
|
+
combinedBsphere: asset.bsphere || null
|
|
14223
15879
|
};
|
|
14224
15880
|
cached = { skin: clonedSkin2, mesh: phaseMesh2, geometry: phaseGeometry2 };
|
|
14225
15881
|
this._skinnedPhaseCache.set(key, cached);
|
|
@@ -14354,45 +16010,62 @@ class RenderGraph {
|
|
|
14354
16010
|
}
|
|
14355
16011
|
/**
|
|
14356
16012
|
* Handle window resize
|
|
14357
|
-
* @param {number} width -
|
|
14358
|
-
* @param {number} height -
|
|
16013
|
+
* @param {number} width - Canvas width (full device pixels)
|
|
16014
|
+
* @param {number} height - Canvas height (full device pixels)
|
|
16015
|
+
* @param {number} renderScale - Scale for internal rendering (1.0 = full resolution)
|
|
14359
16016
|
*/
|
|
14360
|
-
async resize(width, height) {
|
|
16017
|
+
async resize(width, height, renderScale = 1) {
|
|
14361
16018
|
const timings = [];
|
|
14362
16019
|
performance.now();
|
|
16020
|
+
this.canvasWidth = width;
|
|
16021
|
+
this.canvasHeight = height;
|
|
16022
|
+
const renderWidth = Math.max(1, Math.round(width * renderScale));
|
|
16023
|
+
const renderHeight = Math.max(1, Math.round(height * renderScale));
|
|
16024
|
+
this.renderWidth = renderWidth;
|
|
16025
|
+
this.renderHeight = renderHeight;
|
|
16026
|
+
this.renderScale = renderScale;
|
|
14363
16027
|
const autoScale = this.engine?.settings?.rendering?.autoScale;
|
|
14364
16028
|
let effectScale = 1;
|
|
14365
16029
|
if (autoScale && !autoScale.enabled && autoScale.enabledForEffects) {
|
|
14366
|
-
if (
|
|
16030
|
+
if (renderHeight > (autoScale.maxHeight ?? 1536)) {
|
|
14367
16031
|
effectScale = autoScale.scaleFactor ?? 0.5;
|
|
14368
16032
|
if (!this._effectScaleWarned) {
|
|
14369
|
-
console.log(`Effect auto-scale: Reducing effect resolution by ${effectScale} (height: ${
|
|
16033
|
+
console.log(`Effect auto-scale: Reducing effect resolution by ${effectScale} (height: ${renderHeight}px > ${autoScale.maxHeight}px)`);
|
|
14370
16034
|
this._effectScaleWarned = true;
|
|
14371
16035
|
}
|
|
14372
16036
|
} else if (this._effectScaleWarned) {
|
|
14373
|
-
console.log(`Effect auto-scale: Restoring full effect resolution (height: ${
|
|
16037
|
+
console.log(`Effect auto-scale: Restoring full effect resolution (height: ${renderHeight}px <= ${autoScale.maxHeight}px)`);
|
|
14374
16038
|
this._effectScaleWarned = false;
|
|
14375
16039
|
}
|
|
14376
16040
|
}
|
|
14377
|
-
const
|
|
14378
|
-
const
|
|
14379
|
-
const
|
|
16041
|
+
const fullResPasses = /* @__PURE__ */ new Set(["crt"]);
|
|
16042
|
+
const effectScaledPasses = /* @__PURE__ */ new Set(["bloom", "ao", "planarReflection"]);
|
|
16043
|
+
const effectWidth = Math.max(1, Math.floor(renderWidth * effectScale));
|
|
16044
|
+
const effectHeight = Math.max(1, Math.floor(renderHeight * effectScale));
|
|
14380
16045
|
this.effectWidth = effectWidth;
|
|
14381
16046
|
this.effectHeight = effectHeight;
|
|
14382
16047
|
this.effectScale = effectScale;
|
|
14383
16048
|
for (const passName in this.passes) {
|
|
14384
16049
|
if (this.passes[passName]) {
|
|
14385
16050
|
const start2 = performance.now();
|
|
14386
|
-
|
|
14387
|
-
|
|
14388
|
-
|
|
16051
|
+
let w, h;
|
|
16052
|
+
if (fullResPasses.has(passName)) {
|
|
16053
|
+
w = width;
|
|
16054
|
+
h = height;
|
|
16055
|
+
} else if (effectScaledPasses.has(passName) && effectScale < 1) {
|
|
16056
|
+
w = effectWidth;
|
|
16057
|
+
h = effectHeight;
|
|
16058
|
+
} else {
|
|
16059
|
+
w = renderWidth;
|
|
16060
|
+
h = renderHeight;
|
|
16061
|
+
}
|
|
14389
16062
|
await this.passes[passName].resize(w, h);
|
|
14390
16063
|
timings.push({ name: `pass:${passName}`, time: performance.now() - start2 });
|
|
14391
16064
|
}
|
|
14392
16065
|
}
|
|
14393
16066
|
if (this.historyManager) {
|
|
14394
16067
|
const start2 = performance.now();
|
|
14395
|
-
await this.historyManager.resize(
|
|
16068
|
+
await this.historyManager.resize(renderWidth, renderHeight);
|
|
14396
16069
|
timings.push({ name: "historyManager", time: performance.now() - start2 });
|
|
14397
16070
|
}
|
|
14398
16071
|
let start = performance.now();
|
|
@@ -14411,8 +16084,18 @@ class RenderGraph {
|
|
|
14411
16084
|
this.passes.lighting.setHiZPass(this.passes.hiz);
|
|
14412
16085
|
this.passes.transparent.setHiZPass(this.passes.hiz);
|
|
14413
16086
|
this.passes.shadow.setHiZPass(this.passes.hiz);
|
|
16087
|
+
if (this.passes.volumetricFog) {
|
|
16088
|
+
this.passes.volumetricFog.setHiZPass(this.passes.hiz);
|
|
16089
|
+
}
|
|
14414
16090
|
}
|
|
14415
16091
|
timings.push({ name: "rewire:hiz", time: performance.now() - start });
|
|
16092
|
+
if (this.passes.volumetricFog) {
|
|
16093
|
+
start = performance.now();
|
|
16094
|
+
this.passes.volumetricFog.setGBuffer(this.passes.gbuffer.getGBuffer());
|
|
16095
|
+
this.passes.volumetricFog.setShadowPass(this.passes.shadow);
|
|
16096
|
+
this.passes.volumetricFog.setLightingPass(this.passes.lighting);
|
|
16097
|
+
timings.push({ name: "rewire:volumetricFog", time: performance.now() - start });
|
|
16098
|
+
}
|
|
14416
16099
|
start = performance.now();
|
|
14417
16100
|
this.passes.shadow.setNoise(this.noiseTexture, this.noiseSize, this.noiseAnimated);
|
|
14418
16101
|
timings.push({ name: "rewire:shadow.setNoise", time: performance.now() - start });
|
|
@@ -14790,8 +16473,18 @@ class RenderGraph {
|
|
|
14790
16473
|
* @param {string} passName - Name of pass
|
|
14791
16474
|
* @returns {BasePass} The pass instance
|
|
14792
16475
|
*/
|
|
14793
|
-
getPass(passName) {
|
|
14794
|
-
return this.passes[passName];
|
|
16476
|
+
getPass(passName) {
|
|
16477
|
+
return this.passes[passName];
|
|
16478
|
+
}
|
|
16479
|
+
/**
|
|
16480
|
+
* Invalidate occlusion culling data and reset warmup period.
|
|
16481
|
+
* Call this after scene loading or major camera changes to prevent
|
|
16482
|
+
* incorrect occlusion culling with stale depth buffer data.
|
|
16483
|
+
*/
|
|
16484
|
+
invalidateOcclusionCulling() {
|
|
16485
|
+
if (this.passes.hiz) {
|
|
16486
|
+
this.passes.hiz.invalidate();
|
|
16487
|
+
}
|
|
14795
16488
|
}
|
|
14796
16489
|
/**
|
|
14797
16490
|
* Load noise texture based on settings
|
|
@@ -14929,6 +16622,38 @@ class RenderGraph {
|
|
|
14929
16622
|
height: 8
|
|
14930
16623
|
};
|
|
14931
16624
|
}
|
|
16625
|
+
/**
|
|
16626
|
+
* Reload noise texture and update all passes that use it
|
|
16627
|
+
* Called when noise settings change at runtime
|
|
16628
|
+
*/
|
|
16629
|
+
async reloadNoiseTexture() {
|
|
16630
|
+
if (this.noiseTexture?.texture) {
|
|
16631
|
+
this.noiseTexture.texture.destroy();
|
|
16632
|
+
}
|
|
16633
|
+
await this._loadNoiseTexture();
|
|
16634
|
+
if (this.passes.gbuffer) {
|
|
16635
|
+
this.passes.gbuffer.setNoise(this.noiseTexture, this.noiseSize, this.noiseAnimated);
|
|
16636
|
+
}
|
|
16637
|
+
if (this.passes.shadow) {
|
|
16638
|
+
this.passes.shadow.setNoise(this.noiseTexture, this.noiseSize, this.noiseAnimated);
|
|
16639
|
+
}
|
|
16640
|
+
if (this.passes.lighting) {
|
|
16641
|
+
this.passes.lighting.setNoise(this.noiseTexture, this.noiseSize, this.noiseAnimated);
|
|
16642
|
+
}
|
|
16643
|
+
if (this.passes.ao) {
|
|
16644
|
+
this.passes.ao.setNoise(this.noiseTexture, this.noiseSize, this.noiseAnimated);
|
|
16645
|
+
}
|
|
16646
|
+
if (this.passes.transparent) {
|
|
16647
|
+
this.passes.transparent.setNoise(this.noiseTexture, this.noiseSize, this.noiseAnimated);
|
|
16648
|
+
}
|
|
16649
|
+
if (this.passes.postProcess) {
|
|
16650
|
+
this.passes.postProcess.setNoise(this.noiseTexture, this.noiseSize, this.noiseAnimated);
|
|
16651
|
+
}
|
|
16652
|
+
if (this.passes.renderPost) {
|
|
16653
|
+
this.passes.renderPost.setNoise(this.noiseTexture, this.noiseSize, this.noiseAnimated);
|
|
16654
|
+
}
|
|
16655
|
+
console.log(`RenderGraph: Reloaded noise texture (${this.engine?.settings?.noise?.type || "bluenoise"})`);
|
|
16656
|
+
}
|
|
14932
16657
|
/**
|
|
14933
16658
|
* Destroy all resources
|
|
14934
16659
|
*/
|
|
@@ -15556,6 +17281,7 @@ class Joint {
|
|
|
15556
17281
|
function expandTriangles(attributes, expansion) {
|
|
15557
17282
|
const positions = attributes.position;
|
|
15558
17283
|
const normals = attributes.normal;
|
|
17284
|
+
const tangents = attributes.tangent;
|
|
15559
17285
|
const uvs = attributes.uv;
|
|
15560
17286
|
const indices = attributes.indices;
|
|
15561
17287
|
const weights = attributes.weights;
|
|
@@ -15564,6 +17290,7 @@ function expandTriangles(attributes, expansion) {
|
|
|
15564
17290
|
const triCount = indices.length / 3;
|
|
15565
17291
|
const newPositions = new Float32Array(triCount * 3 * 3);
|
|
15566
17292
|
const newNormals = normals ? new Float32Array(triCount * 3 * 3) : null;
|
|
17293
|
+
const newTangents = tangents ? new Float32Array(triCount * 3 * 4) : null;
|
|
15567
17294
|
const newUvs = uvs ? new Float32Array(triCount * 3 * 2) : null;
|
|
15568
17295
|
const newWeights = weights ? new Float32Array(triCount * 3 * 4) : null;
|
|
15569
17296
|
const newJoints = joints ? new Uint16Array(triCount * 3 * 4) : null;
|
|
@@ -15599,6 +17326,12 @@ function expandTriangles(attributes, expansion) {
|
|
|
15599
17326
|
newNormals[newIdx * 3 + 1] = normals[origIdx * 3 + 1];
|
|
15600
17327
|
newNormals[newIdx * 3 + 2] = normals[origIdx * 3 + 2];
|
|
15601
17328
|
}
|
|
17329
|
+
if (newTangents) {
|
|
17330
|
+
newTangents[newIdx * 4 + 0] = tangents[origIdx * 4 + 0];
|
|
17331
|
+
newTangents[newIdx * 4 + 1] = tangents[origIdx * 4 + 1];
|
|
17332
|
+
newTangents[newIdx * 4 + 2] = tangents[origIdx * 4 + 2];
|
|
17333
|
+
newTangents[newIdx * 4 + 3] = tangents[origIdx * 4 + 3];
|
|
17334
|
+
}
|
|
15602
17335
|
if (newUvs) {
|
|
15603
17336
|
newUvs[newIdx * 2 + 0] = uvs[origIdx * 2 + 0];
|
|
15604
17337
|
newUvs[newIdx * 2 + 1] = uvs[origIdx * 2 + 1];
|
|
@@ -15621,6 +17354,7 @@ function expandTriangles(attributes, expansion) {
|
|
|
15621
17354
|
return {
|
|
15622
17355
|
position: newPositions,
|
|
15623
17356
|
normal: newNormals,
|
|
17357
|
+
tangent: newTangents,
|
|
15624
17358
|
uv: newUvs,
|
|
15625
17359
|
indices: newIndices,
|
|
15626
17360
|
weights: newWeights,
|
|
@@ -15836,6 +17570,7 @@ async function loadGltfData(engine, url, options = {}) {
|
|
|
15836
17570
|
let attrs = {
|
|
15837
17571
|
position: getAccessor(attributes.POSITION),
|
|
15838
17572
|
normal: getAccessor(attributes.NORMAL),
|
|
17573
|
+
tangent: getAccessor(attributes.TANGENT),
|
|
15839
17574
|
uv: getAccessor(attributes.TEXCOORD_0),
|
|
15840
17575
|
indices: getAccessor(primitive.indices),
|
|
15841
17576
|
weights: getAccessor(attributes.WEIGHTS_0),
|
|
@@ -15954,6 +17689,9 @@ async function loadGltf(engine, url, options = {}) {
|
|
|
15954
17689
|
const transmission = mesh.material.extensions.KHR_materials_transmission;
|
|
15955
17690
|
material.opacity = 1 - (transmission.transmissionFactor ?? 0);
|
|
15956
17691
|
}
|
|
17692
|
+
if (mesh.material.doubleSided) {
|
|
17693
|
+
material.doubleSided = true;
|
|
17694
|
+
}
|
|
15957
17695
|
const nmesh = new Mesh(geometry, material, name);
|
|
15958
17696
|
if (mesh.skinIndex !== void 0 && mesh.skinIndex !== null && skins[mesh.skinIndex]) {
|
|
15959
17697
|
nmesh.skin = skins[mesh.skinIndex];
|
|
@@ -16463,6 +18201,22 @@ class AssetManager {
|
|
|
16463
18201
|
try {
|
|
16464
18202
|
const result = await loadGltf(this.engine, path, options);
|
|
16465
18203
|
const meshNames = Object.keys(result.meshes);
|
|
18204
|
+
let combinedBsphere = null;
|
|
18205
|
+
const hasAnySkin = Object.values(result.meshes).some((m) => m.hasSkin);
|
|
18206
|
+
if (hasAnySkin) {
|
|
18207
|
+
const allPositions = [];
|
|
18208
|
+
for (const mesh of Object.values(result.meshes)) {
|
|
18209
|
+
if (mesh.geometry?.attributes?.position) {
|
|
18210
|
+
const positions = mesh.geometry.attributes.position;
|
|
18211
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
18212
|
+
allPositions.push(positions[i], positions[i + 1], positions[i + 2]);
|
|
18213
|
+
}
|
|
18214
|
+
}
|
|
18215
|
+
}
|
|
18216
|
+
if (allPositions.length > 0) {
|
|
18217
|
+
combinedBsphere = calculateBoundingSphere(new Float32Array(allPositions));
|
|
18218
|
+
}
|
|
18219
|
+
}
|
|
16466
18220
|
this.assets[path] = {
|
|
16467
18221
|
gltf: result,
|
|
16468
18222
|
meshes: result.meshes,
|
|
@@ -16470,12 +18224,17 @@ class AssetManager {
|
|
|
16470
18224
|
animations: result.animations,
|
|
16471
18225
|
nodes: result.nodes,
|
|
16472
18226
|
meshNames,
|
|
18227
|
+
bsphere: combinedBsphere,
|
|
18228
|
+
// Combined bsphere for parent path entities
|
|
18229
|
+
hasSkin: hasAnySkin,
|
|
18230
|
+
// Flag for skinned model detection
|
|
16473
18231
|
ready: true,
|
|
16474
18232
|
loading: false
|
|
16475
18233
|
};
|
|
16476
18234
|
for (const meshName of meshNames) {
|
|
16477
|
-
const
|
|
16478
|
-
|
|
18235
|
+
const mesh = result.meshes[meshName];
|
|
18236
|
+
const bsphere = hasAnySkin && combinedBsphere ? combinedBsphere : null;
|
|
18237
|
+
await this._registerMesh(path, meshName, mesh, bsphere);
|
|
16479
18238
|
}
|
|
16480
18239
|
this._triggerReady(path);
|
|
16481
18240
|
return this.assets[path];
|
|
@@ -16491,10 +18250,14 @@ class AssetManager {
|
|
|
16491
18250
|
}
|
|
16492
18251
|
/**
|
|
16493
18252
|
* Register a mesh asset (internal)
|
|
18253
|
+
* @param {string} path - GLTF file path
|
|
18254
|
+
* @param {string} meshName - Mesh name
|
|
18255
|
+
* @param {Object} mesh - Mesh object
|
|
18256
|
+
* @param {Object|null} overrideBsphere - Optional bounding sphere (for skinned mesh combined sphere)
|
|
16494
18257
|
*/
|
|
16495
|
-
async _registerMesh(path, meshName, mesh) {
|
|
18258
|
+
async _registerMesh(path, meshName, mesh, overrideBsphere = null) {
|
|
16496
18259
|
const modelId = this.createModelId(path, meshName);
|
|
16497
|
-
const bsphere = calculateBoundingSphere(mesh.geometry.attributes.position);
|
|
18260
|
+
const bsphere = overrideBsphere || calculateBoundingSphere(mesh.geometry.attributes.position);
|
|
16498
18261
|
this.assets[modelId] = {
|
|
16499
18262
|
mesh,
|
|
16500
18263
|
geometry: mesh.geometry,
|
|
@@ -16691,19 +18454,20 @@ class DebugUI {
|
|
|
16691
18454
|
this._addStyles();
|
|
16692
18455
|
this._createStatsFolder();
|
|
16693
18456
|
this._createRenderingFolder();
|
|
16694
|
-
this.
|
|
16695
|
-
this._createShadowFolder();
|
|
16696
|
-
this._createMainLightFolder();
|
|
18457
|
+
this._createCameraFolder();
|
|
16697
18458
|
this._createEnvironmentFolder();
|
|
16698
18459
|
this._createLightingFolder();
|
|
16699
|
-
this.
|
|
16700
|
-
this.
|
|
16701
|
-
this._createBloomFolder();
|
|
18460
|
+
this._createMainLightFolder();
|
|
18461
|
+
this._createShadowFolder();
|
|
16702
18462
|
this._createPlanarReflectionFolder();
|
|
18463
|
+
this._createAOFolder();
|
|
16703
18464
|
this._createAmbientCaptureFolder();
|
|
16704
|
-
this.
|
|
18465
|
+
this._createSSGIFolder();
|
|
18466
|
+
this._createVolumetricFogFolder();
|
|
18467
|
+
this._createBloomFolder();
|
|
18468
|
+
this._createTonemapFolder();
|
|
16705
18469
|
this._createDitheringFolder();
|
|
16706
|
-
this.
|
|
18470
|
+
this._createCRTFolder();
|
|
16707
18471
|
this._createDebugFolder();
|
|
16708
18472
|
for (const folder of Object.values(this.folders)) {
|
|
16709
18473
|
folder.close();
|
|
@@ -16847,6 +18611,33 @@ Lights: ${fmt(visibleLights)} (occ:${fmt(lightOccCulled)})`;
|
|
|
16847
18611
|
this.gui.domElement.style.display = "none";
|
|
16848
18612
|
}
|
|
16849
18613
|
});
|
|
18614
|
+
if (s.culling) {
|
|
18615
|
+
const cullFolder = folder.addFolder("Culling");
|
|
18616
|
+
cullFolder.add(s.culling, "frustumEnabled").name("Frustum Culling");
|
|
18617
|
+
if (s.occlusionCulling) {
|
|
18618
|
+
cullFolder.add(s.occlusionCulling, "enabled").name("Occlusion Culling");
|
|
18619
|
+
cullFolder.add(s.occlusionCulling, "threshold", 0.1, 2, 0.1).name("Occlusion Threshold");
|
|
18620
|
+
}
|
|
18621
|
+
if (s.culling.planarReflection) {
|
|
18622
|
+
const prFolder = cullFolder.addFolder("Planar Reflection");
|
|
18623
|
+
prFolder.add(s.culling.planarReflection, "frustum").name("Frustum Culling");
|
|
18624
|
+
prFolder.add(s.culling.planarReflection, "maxDistance", 10, 200, 10).name("Max Distance");
|
|
18625
|
+
prFolder.add(s.culling.planarReflection, "maxSkinned", 0, 100, 1).name("Max Skinned");
|
|
18626
|
+
prFolder.add(s.culling.planarReflection, "minPixelSize", 0, 16, 1).name("Min Pixel Size");
|
|
18627
|
+
prFolder.close();
|
|
18628
|
+
}
|
|
18629
|
+
cullFolder.close();
|
|
18630
|
+
}
|
|
18631
|
+
if (s.noise) {
|
|
18632
|
+
const noiseFolder = folder.addFolder("Noise");
|
|
18633
|
+
noiseFolder.add(s.noise, "type", ["bluenoise", "bayer8"]).name("Type").onChange(() => {
|
|
18634
|
+
if (this.engine.renderer?.renderGraph) {
|
|
18635
|
+
this.engine.renderer.renderGraph.reloadNoiseTexture();
|
|
18636
|
+
}
|
|
18637
|
+
});
|
|
18638
|
+
noiseFolder.add(s.noise, "animated").name("Animated");
|
|
18639
|
+
noiseFolder.close();
|
|
18640
|
+
}
|
|
16850
18641
|
}
|
|
16851
18642
|
_createAOFolder() {
|
|
16852
18643
|
const s = this.engine.settings;
|
|
@@ -16925,6 +18716,8 @@ Lights: ${fmt(visibleLights)} (occ:${fmt(lightOccCulled)})`;
|
|
|
16925
18716
|
fogFolder.add(s.environment.fog.heightFade, "0", -100, 100, 1).name("Bottom Y");
|
|
16926
18717
|
fogFolder.add(s.environment.fog.heightFade, "1", -50, 200, 5).name("Top Y");
|
|
16927
18718
|
fogFolder.add(s.environment.fog, "brightResist", 0, 1, 0.05).name("Bright Resist");
|
|
18719
|
+
if (s.environment.fog.debug === void 0) s.environment.fog.debug = 0;
|
|
18720
|
+
fogFolder.add(s.environment.fog, "debug", 0, 10, 1).name("Debug Mode");
|
|
16928
18721
|
}
|
|
16929
18722
|
}
|
|
16930
18723
|
_createLightingFolder() {
|
|
@@ -16941,25 +18734,6 @@ Lights: ${fmt(visibleLights)} (occ:${fmt(lightOccCulled)})`;
|
|
|
16941
18734
|
folder.add(s.lighting, "specularBoostRoughnessCutoff", 0.1, 1, 0.05).name("Boost Roughness Cutoff");
|
|
16942
18735
|
}
|
|
16943
18736
|
}
|
|
16944
|
-
_createCullingFolder() {
|
|
16945
|
-
const s = this.engine.settings;
|
|
16946
|
-
if (!s.culling) return;
|
|
16947
|
-
const folder = this.gui.addFolder("Culling");
|
|
16948
|
-
this.folders.culling = folder;
|
|
16949
|
-
folder.add(s.culling, "frustumEnabled").name("Frustum Culling");
|
|
16950
|
-
if (s.occlusionCulling) {
|
|
16951
|
-
folder.add(s.occlusionCulling, "enabled").name("Occlusion Culling");
|
|
16952
|
-
folder.add(s.occlusionCulling, "threshold", 0.1, 2, 0.1).name("Occlusion Threshold");
|
|
16953
|
-
}
|
|
16954
|
-
if (s.culling.planarReflection) {
|
|
16955
|
-
const prFolder = folder.addFolder("Planar Reflection");
|
|
16956
|
-
prFolder.add(s.culling.planarReflection, "frustum").name("Frustum Culling");
|
|
16957
|
-
prFolder.add(s.culling.planarReflection, "maxDistance", 10, 200, 10).name("Max Distance");
|
|
16958
|
-
prFolder.add(s.culling.planarReflection, "maxSkinned", 0, 100, 1).name("Max Skinned");
|
|
16959
|
-
prFolder.add(s.culling.planarReflection, "minPixelSize", 0, 16, 1).name("Min Pixel Size");
|
|
16960
|
-
prFolder.close();
|
|
16961
|
-
}
|
|
16962
|
-
}
|
|
16963
18737
|
_createSSGIFolder() {
|
|
16964
18738
|
const s = this.engine.settings;
|
|
16965
18739
|
if (!s.ssgi) return;
|
|
@@ -16972,6 +18746,57 @@ Lights: ${fmt(visibleLights)} (occ:${fmt(lightOccCulled)})`;
|
|
|
16972
18746
|
folder.add(s.ssgi, "sampleRadius", 0.5, 4, 0.5).name("Sample Radius");
|
|
16973
18747
|
folder.add(s.ssgi, "saturateLevel", 0.1, 2, 0.1).name("Saturate Level");
|
|
16974
18748
|
}
|
|
18749
|
+
_createVolumetricFogFolder() {
|
|
18750
|
+
const s = this.engine.settings;
|
|
18751
|
+
if (!s.volumetricFog) return;
|
|
18752
|
+
const vf = s.volumetricFog;
|
|
18753
|
+
if (vf.density === void 0 && vf.densityMultiplier === void 0) vf.density = 0.5;
|
|
18754
|
+
if (vf.scatterStrength === void 0) vf.scatterStrength = 1;
|
|
18755
|
+
if (!vf.heightRange) vf.heightRange = [-5, 20];
|
|
18756
|
+
if (vf.resolution === void 0) vf.resolution = 0.25;
|
|
18757
|
+
if (vf.maxSamples === void 0) vf.maxSamples = 32;
|
|
18758
|
+
if (vf.blurRadius === void 0) vf.blurRadius = 4;
|
|
18759
|
+
if (vf.noiseStrength === void 0) vf.noiseStrength = 1;
|
|
18760
|
+
if (vf.noiseScale === void 0) vf.noiseScale = 0.25;
|
|
18761
|
+
if (vf.noiseAnimated === void 0) vf.noiseAnimated = true;
|
|
18762
|
+
if (vf.shadowsEnabled === void 0) vf.shadowsEnabled = true;
|
|
18763
|
+
if (vf.mainLightScatter === void 0) vf.mainLightScatter = 1;
|
|
18764
|
+
if (vf.mainLightScatterDark === void 0) vf.mainLightScatterDark = 3;
|
|
18765
|
+
if (vf.mainLightSaturation === void 0) vf.mainLightSaturation = 1;
|
|
18766
|
+
if (vf.brightnessThreshold === void 0) vf.brightnessThreshold = 1;
|
|
18767
|
+
if (vf.minVisibility === void 0) vf.minVisibility = 0.15;
|
|
18768
|
+
if (vf.skyBrightness === void 0) vf.skyBrightness = 5;
|
|
18769
|
+
if (vf.debug === void 0) vf.debug = 0;
|
|
18770
|
+
const folder = this.gui.addFolder("Volumetric Fog");
|
|
18771
|
+
this.folders.volumetricFog = folder;
|
|
18772
|
+
folder.add(vf, "enabled").name("Enabled");
|
|
18773
|
+
if (vf.density !== void 0) {
|
|
18774
|
+
folder.add(vf, "density", 0, 2, 0.05).name("Density");
|
|
18775
|
+
} else if (vf.densityMultiplier !== void 0) {
|
|
18776
|
+
folder.add(vf, "densityMultiplier", 0, 2, 0.05).name("Density");
|
|
18777
|
+
}
|
|
18778
|
+
folder.add(vf, "scatterStrength", 0, 10, 0.1).name("Scatter (Lights)");
|
|
18779
|
+
folder.add(vf, "mainLightScatter", 0, 5, 0.1).name("Sun Scatter (Light)");
|
|
18780
|
+
folder.add(vf, "mainLightScatterDark", 0, 10, 0.1).name("Sun Scatter (Dark)");
|
|
18781
|
+
folder.add(vf, "mainLightSaturation", 0, 1, 0.01).name("Sun Saturation");
|
|
18782
|
+
folder.add(vf, "brightnessThreshold", 0.1, 5, 0.1).name("Bright Threshold");
|
|
18783
|
+
folder.add(vf, "minVisibility", 0, 1, 0.05).name("Min Visibility");
|
|
18784
|
+
folder.add(vf, "skyBrightness", 0, 10, 0.5).name("Sky Brightness");
|
|
18785
|
+
folder.add(vf.heightRange, "0", -50, 50, 1).name("Height Bottom");
|
|
18786
|
+
folder.add(vf.heightRange, "1", -10, 100, 1).name("Height Top");
|
|
18787
|
+
const qualityFolder = folder.addFolder("Quality");
|
|
18788
|
+
qualityFolder.add(vf, "resolution", 0.125, 0.5, 0.125).name("Resolution");
|
|
18789
|
+
qualityFolder.add(vf, "maxSamples", 16, 128, 8).name("Max Samples");
|
|
18790
|
+
qualityFolder.add(vf, "blurRadius", 0, 8, 1).name("Blur Radius");
|
|
18791
|
+
qualityFolder.close();
|
|
18792
|
+
const noiseFolder = folder.addFolder("Noise");
|
|
18793
|
+
noiseFolder.add(vf, "noiseStrength", 0, 1, 0.1).name("Strength");
|
|
18794
|
+
noiseFolder.add(vf, "noiseScale", 0.05, 1, 0.05).name("Scale (Detail)");
|
|
18795
|
+
noiseFolder.add(vf, "noiseAnimated").name("Animated");
|
|
18796
|
+
noiseFolder.close();
|
|
18797
|
+
folder.add(vf, "shadowsEnabled").name("Shadows");
|
|
18798
|
+
folder.add(vf, "debug", 0, 12, 1).name("Debug Mode");
|
|
18799
|
+
}
|
|
16975
18800
|
_createBloomFolder() {
|
|
16976
18801
|
const s = this.engine.settings;
|
|
16977
18802
|
if (!s.bloom) return;
|
|
@@ -16988,6 +18813,14 @@ Lights: ${fmt(visibleLights)} (occ:${fmt(lightOccCulled)})`;
|
|
|
16988
18813
|
folder.add(s.bloom, "scale", 0.25, 1, 0.25).name("Resolution Scale");
|
|
16989
18814
|
}
|
|
16990
18815
|
}
|
|
18816
|
+
_createTonemapFolder() {
|
|
18817
|
+
const s = this.engine.settings;
|
|
18818
|
+
if (!s.rendering) return;
|
|
18819
|
+
if (s.rendering.tonemapMode === void 0) s.rendering.tonemapMode = 0;
|
|
18820
|
+
const folder = this.gui.addFolder("Tone Mapping");
|
|
18821
|
+
this.folders.tonemap = folder;
|
|
18822
|
+
folder.add(s.rendering, "tonemapMode", { "ACES": 0, "Reinhard": 1, "None (Linear)": 2 }).name("Mode");
|
|
18823
|
+
}
|
|
16991
18824
|
_createPlanarReflectionFolder() {
|
|
16992
18825
|
const s = this.engine.settings;
|
|
16993
18826
|
if (!s.planarReflection) return;
|
|
@@ -17003,7 +18836,7 @@ Lights: ${fmt(visibleLights)} (occ:${fmt(lightOccCulled)})`;
|
|
|
17003
18836
|
_createAmbientCaptureFolder() {
|
|
17004
18837
|
const s = this.engine.settings;
|
|
17005
18838
|
if (!s.ambientCapture) return;
|
|
17006
|
-
const folder = this.gui.addFolder("Ambient Capture");
|
|
18839
|
+
const folder = this.gui.addFolder("Probe GI (Ambient Capture)");
|
|
17007
18840
|
this.folders.ambientCapture = folder;
|
|
17008
18841
|
folder.add(s.ambientCapture, "enabled").name("Enabled");
|
|
17009
18842
|
folder.add(s.ambientCapture, "intensity", 0, 2, 0.05).name("Intensity");
|
|
@@ -17011,14 +18844,6 @@ Lights: ${fmt(visibleLights)} (occ:${fmt(lightOccCulled)})`;
|
|
|
17011
18844
|
folder.add(s.ambientCapture, "emissiveBoost", 0, 10, 0.1).name("Emissive Boost");
|
|
17012
18845
|
folder.add(s.ambientCapture, "saturateLevel", 0, 2, 0.05).name("Saturate Level");
|
|
17013
18846
|
}
|
|
17014
|
-
_createNoiseFolder() {
|
|
17015
|
-
const s = this.engine.settings;
|
|
17016
|
-
if (!s.noise) return;
|
|
17017
|
-
const folder = this.gui.addFolder("Noise");
|
|
17018
|
-
this.folders.noise = folder;
|
|
17019
|
-
folder.add(s.noise, "type", ["bluenoise", "bayer8"]).name("Type");
|
|
17020
|
-
folder.add(s.noise, "animated").name("Animated");
|
|
17021
|
-
}
|
|
17022
18847
|
_createDitheringFolder() {
|
|
17023
18848
|
const s = this.engine.settings;
|
|
17024
18849
|
if (!s.dithering) return;
|
|
@@ -17027,6 +18852,40 @@ Lights: ${fmt(visibleLights)} (occ:${fmt(lightOccCulled)})`;
|
|
|
17027
18852
|
folder.add(s.dithering, "enabled").name("Enabled");
|
|
17028
18853
|
folder.add(s.dithering, "colorLevels", 4, 256, 1).name("Color Levels");
|
|
17029
18854
|
}
|
|
18855
|
+
_createCRTFolder() {
|
|
18856
|
+
const s = this.engine.settings;
|
|
18857
|
+
if (!s.crt) return;
|
|
18858
|
+
const folder = this.gui.addFolder("CRT Effect");
|
|
18859
|
+
this.folders.crt = folder;
|
|
18860
|
+
folder.add(s.crt, "enabled").name("CRT Enabled");
|
|
18861
|
+
folder.add(s.crt, "upscaleEnabled").name("Upscale Only");
|
|
18862
|
+
folder.add(s.crt, "upscaleTarget", 1, 8, 1).name("Upscale Target");
|
|
18863
|
+
const geomFolder = folder.addFolder("Geometry");
|
|
18864
|
+
geomFolder.add(s.crt, "curvature", 0, 0.25, 5e-3).name("Curvature");
|
|
18865
|
+
geomFolder.add(s.crt, "cornerRadius", 0, 0.2, 5e-3).name("Corner Radius");
|
|
18866
|
+
geomFolder.add(s.crt, "zoom", 1, 1.25, 5e-3).name("Zoom");
|
|
18867
|
+
geomFolder.close();
|
|
18868
|
+
const scanFolder = folder.addFolder("Scanlines");
|
|
18869
|
+
scanFolder.add(s.crt, "scanlineIntensity", 0, 1, 0.05).name("Intensity");
|
|
18870
|
+
scanFolder.add(s.crt, "scanlineWidth", 0, 1, 0.05).name("Width");
|
|
18871
|
+
scanFolder.add(s.crt, "scanlineBrightBoost", 0, 2, 0.05).name("Bright Boost");
|
|
18872
|
+
scanFolder.add(s.crt, "scanlineHeight", 1, 10, 1).name("Height (px)");
|
|
18873
|
+
scanFolder.close();
|
|
18874
|
+
const convFolder = folder.addFolder("RGB Convergence");
|
|
18875
|
+
convFolder.add(s.crt.convergence, "0", -3, 3, 0.1).name("Red X Offset");
|
|
18876
|
+
convFolder.add(s.crt.convergence, "1", -3, 3, 0.1).name("Green X Offset");
|
|
18877
|
+
convFolder.add(s.crt.convergence, "2", -3, 3, 0.1).name("Blue X Offset");
|
|
18878
|
+
convFolder.close();
|
|
18879
|
+
const maskFolder = folder.addFolder("Phosphor Mask");
|
|
18880
|
+
maskFolder.add(s.crt, "maskType", ["none", "aperture", "slot", "shadow"]).name("Type");
|
|
18881
|
+
maskFolder.add(s.crt, "maskIntensity", 0, 1, 0.05).name("Intensity");
|
|
18882
|
+
maskFolder.close();
|
|
18883
|
+
const vigFolder = folder.addFolder("Vignette");
|
|
18884
|
+
vigFolder.add(s.crt, "vignetteIntensity", 0, 1, 0.05).name("Intensity");
|
|
18885
|
+
vigFolder.add(s.crt, "vignetteSize", 0.1, 1, 0.05).name("Size");
|
|
18886
|
+
vigFolder.close();
|
|
18887
|
+
folder.add(s.crt, "blurSize", 0, 8, 0.1).name("H-Blur (px)");
|
|
18888
|
+
}
|
|
17030
18889
|
_createCameraFolder() {
|
|
17031
18890
|
const s = this.engine.settings;
|
|
17032
18891
|
if (!s.camera) return;
|
|
@@ -17106,6 +18965,619 @@ Lights: ${fmt(visibleLights)} (occ:${fmt(lightOccCulled)})`;
|
|
|
17106
18965
|
} : { r: 1, g: 1, b: 1 };
|
|
17107
18966
|
}
|
|
17108
18967
|
}
|
|
18968
|
+
class Raycaster {
|
|
18969
|
+
constructor(engine) {
|
|
18970
|
+
this.engine = engine;
|
|
18971
|
+
this.worker = null;
|
|
18972
|
+
this._pendingCallbacks = /* @__PURE__ */ new Map();
|
|
18973
|
+
this._nextRequestId = 0;
|
|
18974
|
+
this._initialized = false;
|
|
18975
|
+
}
|
|
18976
|
+
async initialize() {
|
|
18977
|
+
const workerCode = this._getWorkerCode();
|
|
18978
|
+
const blob = new Blob([workerCode], { type: "application/javascript" });
|
|
18979
|
+
const workerUrl = URL.createObjectURL(blob);
|
|
18980
|
+
this.worker = new Worker(workerUrl);
|
|
18981
|
+
this.worker.onmessage = this._handleWorkerMessage.bind(this);
|
|
18982
|
+
this.worker.onerror = (e) => console.error("Raycaster worker error:", e);
|
|
18983
|
+
this._initialized = true;
|
|
18984
|
+
URL.revokeObjectURL(workerUrl);
|
|
18985
|
+
}
|
|
18986
|
+
/**
|
|
18987
|
+
* Cast a ray and get the closest intersection
|
|
18988
|
+
* @param {Array|vec3} origin - Ray start point [x, y, z]
|
|
18989
|
+
* @param {Array|vec3} direction - Ray direction (will be normalized)
|
|
18990
|
+
* @param {number} maxDistance - Maximum ray length
|
|
18991
|
+
* @param {Function} callback - Called with result: { hit, distance, point, normal, entity, mesh, triangleIndex }
|
|
18992
|
+
* @param {Object} options - Optional settings
|
|
18993
|
+
* @param {Array} options.entities - Specific entities to test (default: all scene entities)
|
|
18994
|
+
* @param {Array} options.meshes - Specific meshes to test (default: all scene meshes)
|
|
18995
|
+
* @param {boolean} options.backfaces - Test backfaces (default: false)
|
|
18996
|
+
* @param {Array} options.exclude - Entities/meshes to exclude
|
|
18997
|
+
*/
|
|
18998
|
+
cast(origin, direction, maxDistance, callback, options = {}) {
|
|
18999
|
+
if (!this._initialized) {
|
|
19000
|
+
console.warn("Raycaster not initialized");
|
|
19001
|
+
callback({ hit: false, error: "not initialized" });
|
|
19002
|
+
return;
|
|
19003
|
+
}
|
|
19004
|
+
const ray = {
|
|
19005
|
+
origin: Array.from(origin),
|
|
19006
|
+
direction: this._normalize(Array.from(direction)),
|
|
19007
|
+
maxDistance
|
|
19008
|
+
};
|
|
19009
|
+
const candidates = this._collectCandidates(ray, options);
|
|
19010
|
+
if (candidates.length === 0) {
|
|
19011
|
+
callback({ hit: false });
|
|
19012
|
+
return;
|
|
19013
|
+
}
|
|
19014
|
+
const requestId = this._nextRequestId++;
|
|
19015
|
+
this._pendingCallbacks.set(requestId, { callback, candidates });
|
|
19016
|
+
this.worker.postMessage({
|
|
19017
|
+
type: "raycast",
|
|
19018
|
+
requestId,
|
|
19019
|
+
ray,
|
|
19020
|
+
debug: options.debug ?? false,
|
|
19021
|
+
candidates: candidates.map((c) => ({
|
|
19022
|
+
id: c.id,
|
|
19023
|
+
vertices: c.vertices,
|
|
19024
|
+
indices: c.indices,
|
|
19025
|
+
matrix: c.matrix,
|
|
19026
|
+
backfaces: options.backfaces ?? false
|
|
19027
|
+
}))
|
|
19028
|
+
});
|
|
19029
|
+
}
|
|
19030
|
+
/**
|
|
19031
|
+
* Cast a ray upward from a position to check for sky visibility
|
|
19032
|
+
* Useful for determining if camera is under cover
|
|
19033
|
+
* @param {Array|vec3} position - Position to test from
|
|
19034
|
+
* @param {number} maxDistance - How far to check (default: 100)
|
|
19035
|
+
* @param {Function} callback - Called with { hitSky: boolean, distance?: number, entity?: object }
|
|
19036
|
+
*/
|
|
19037
|
+
castToSky(position, maxDistance, callback) {
|
|
19038
|
+
this.cast(
|
|
19039
|
+
position,
|
|
19040
|
+
[0, 1, 0],
|
|
19041
|
+
// Straight up
|
|
19042
|
+
maxDistance ?? 100,
|
|
19043
|
+
(result) => {
|
|
19044
|
+
callback({
|
|
19045
|
+
hitSky: !result.hit,
|
|
19046
|
+
distance: result.distance,
|
|
19047
|
+
entity: result.entity,
|
|
19048
|
+
mesh: result.mesh
|
|
19049
|
+
});
|
|
19050
|
+
}
|
|
19051
|
+
);
|
|
19052
|
+
}
|
|
19053
|
+
/**
|
|
19054
|
+
* Cast a ray from screen coordinates (mouse picking)
|
|
19055
|
+
* @param {number} screenX - Screen X coordinate
|
|
19056
|
+
* @param {number} screenY - Screen Y coordinate
|
|
19057
|
+
* @param {Object} camera - Camera with projection/view matrices
|
|
19058
|
+
* @param {Function} callback - Called with intersection result
|
|
19059
|
+
* @param {Object} options - Cast options
|
|
19060
|
+
*/
|
|
19061
|
+
castFromScreen(screenX, screenY, camera, callback, options = {}) {
|
|
19062
|
+
const { width, height } = this.engine.canvas;
|
|
19063
|
+
const ndcX = screenX / width * 2 - 1;
|
|
19064
|
+
const ndcY = 1 - screenY / height * 2;
|
|
19065
|
+
const invViewProj = mat4$1.create();
|
|
19066
|
+
mat4$1.multiply(invViewProj, camera.proj, camera.view);
|
|
19067
|
+
mat4$1.invert(invViewProj, invViewProj);
|
|
19068
|
+
const nearPoint = this._unproject([ndcX, ndcY, 0], invViewProj);
|
|
19069
|
+
const farPoint = this._unproject([ndcX, ndcY, 1], invViewProj);
|
|
19070
|
+
const direction = [
|
|
19071
|
+
farPoint[0] - nearPoint[0],
|
|
19072
|
+
farPoint[1] - nearPoint[1],
|
|
19073
|
+
farPoint[2] - nearPoint[2]
|
|
19074
|
+
];
|
|
19075
|
+
const maxDistance = options.maxDistance ?? camera.far ?? 1e3;
|
|
19076
|
+
this.cast(nearPoint, direction, maxDistance, callback, options);
|
|
19077
|
+
}
|
|
19078
|
+
/**
|
|
19079
|
+
* Collect candidate geometries that pass bounding sphere test
|
|
19080
|
+
*/
|
|
19081
|
+
_collectCandidates(ray, options) {
|
|
19082
|
+
const candidates = [];
|
|
19083
|
+
const exclude = new Set(options.exclude ?? []);
|
|
19084
|
+
const debug = options.debug;
|
|
19085
|
+
const entities = options.entities ?? this._getAllEntities();
|
|
19086
|
+
const assetManager = this.engine.assetManager;
|
|
19087
|
+
for (const entity of entities) {
|
|
19088
|
+
if (exclude.has(entity)) continue;
|
|
19089
|
+
if (!entity.model) continue;
|
|
19090
|
+
const asset = assetManager?.get(entity.model);
|
|
19091
|
+
if (!asset?.geometry) continue;
|
|
19092
|
+
const bsphere = this._getEntityBoundingSphere(entity);
|
|
19093
|
+
if (!bsphere) continue;
|
|
19094
|
+
if (this._raySphereIntersect(ray, bsphere)) {
|
|
19095
|
+
const geometryData = this._extractGeometry(asset.geometry);
|
|
19096
|
+
if (geometryData) {
|
|
19097
|
+
const matrix = entity._matrix ?? mat4$1.create();
|
|
19098
|
+
candidates.push({
|
|
19099
|
+
id: entity.id ?? entity.name ?? `entity_${candidates.length}`,
|
|
19100
|
+
type: "entity",
|
|
19101
|
+
entity,
|
|
19102
|
+
asset,
|
|
19103
|
+
vertices: geometryData.vertices,
|
|
19104
|
+
indices: geometryData.indices,
|
|
19105
|
+
matrix: Array.from(matrix),
|
|
19106
|
+
bsphereDistance: this._raySphereDistance(ray, bsphere)
|
|
19107
|
+
});
|
|
19108
|
+
}
|
|
19109
|
+
}
|
|
19110
|
+
}
|
|
19111
|
+
const meshes = options.meshes ?? this._getAllMeshes();
|
|
19112
|
+
let debugStats = debug ? { total: 0, noGeom: 0, noBsphere: 0, noData: 0, sphereMiss: 0, candidates: 0 } : null;
|
|
19113
|
+
for (const [name, mesh] of Object.entries(meshes)) {
|
|
19114
|
+
if (exclude.has(mesh)) continue;
|
|
19115
|
+
if (!mesh.geometry) {
|
|
19116
|
+
if (debug) debugStats.noGeom++;
|
|
19117
|
+
continue;
|
|
19118
|
+
}
|
|
19119
|
+
const bsphere = this._getMeshBoundingSphere(mesh);
|
|
19120
|
+
if (!bsphere) {
|
|
19121
|
+
if (debug) debugStats.noBsphere++;
|
|
19122
|
+
continue;
|
|
19123
|
+
}
|
|
19124
|
+
const geometryData = this._extractGeometry(mesh.geometry);
|
|
19125
|
+
if (!geometryData) {
|
|
19126
|
+
if (debug) debugStats.noData++;
|
|
19127
|
+
continue;
|
|
19128
|
+
}
|
|
19129
|
+
if (debug) debugStats.total++;
|
|
19130
|
+
let instanceCount = mesh.geometry.instanceCount ?? 0;
|
|
19131
|
+
if (instanceCount === 0) {
|
|
19132
|
+
if (mesh.geometry.instanceData) {
|
|
19133
|
+
instanceCount = mesh.static ? mesh.geometry.maxInstances ?? 1 : 1;
|
|
19134
|
+
} else {
|
|
19135
|
+
instanceCount = 1;
|
|
19136
|
+
}
|
|
19137
|
+
}
|
|
19138
|
+
for (let i = 0; i < instanceCount; i++) {
|
|
19139
|
+
const matrix = this._getInstanceMatrix(mesh.geometry, i);
|
|
19140
|
+
const instanceBsphere = this._transformBoundingSphere(bsphere, matrix);
|
|
19141
|
+
if (this._raySphereIntersect(ray, instanceBsphere)) {
|
|
19142
|
+
if (debug) debugStats.candidates++;
|
|
19143
|
+
candidates.push({
|
|
19144
|
+
id: `${name}_${i}`,
|
|
19145
|
+
type: "mesh",
|
|
19146
|
+
mesh,
|
|
19147
|
+
meshName: name,
|
|
19148
|
+
instanceIndex: i,
|
|
19149
|
+
vertices: geometryData.vertices,
|
|
19150
|
+
indices: geometryData.indices,
|
|
19151
|
+
matrix: Array.from(matrix),
|
|
19152
|
+
bsphereDistance: this._raySphereDistance(ray, instanceBsphere)
|
|
19153
|
+
});
|
|
19154
|
+
} else {
|
|
19155
|
+
if (debug) debugStats.sphereMiss++;
|
|
19156
|
+
}
|
|
19157
|
+
}
|
|
19158
|
+
}
|
|
19159
|
+
if (debug && debugStats) {
|
|
19160
|
+
console.log(`Raycaster: meshes=${debugStats.total}, sphereHit=${debugStats.candidates}, sphereMiss=${debugStats.sphereMiss}`);
|
|
19161
|
+
if (candidates.length > 0 && candidates.length < 50) {
|
|
19162
|
+
const candInfo = candidates.map((c) => {
|
|
19163
|
+
const m = c.matrix;
|
|
19164
|
+
const pos = [m[12], m[13], m[14]];
|
|
19165
|
+
return `${c.id}@[${pos.map((v) => v.toFixed(1)).join(",")}]`;
|
|
19166
|
+
}).join(", ");
|
|
19167
|
+
console.log(`Candidates: ${candInfo}`);
|
|
19168
|
+
}
|
|
19169
|
+
}
|
|
19170
|
+
candidates.sort((a, b) => a.bsphereDistance - b.bsphereDistance);
|
|
19171
|
+
return candidates;
|
|
19172
|
+
}
|
|
19173
|
+
_getAllEntities() {
|
|
19174
|
+
const entities = this.engine.entities;
|
|
19175
|
+
if (!entities) return [];
|
|
19176
|
+
return Object.values(entities);
|
|
19177
|
+
}
|
|
19178
|
+
_getAllMeshes() {
|
|
19179
|
+
return this.engine.meshes ?? {};
|
|
19180
|
+
}
|
|
19181
|
+
_getEntityBoundingSphere(entity) {
|
|
19182
|
+
if (entity._bsphere && entity._bsphere.radius > 0) {
|
|
19183
|
+
return {
|
|
19184
|
+
center: Array.from(entity._bsphere.center),
|
|
19185
|
+
radius: entity._bsphere.radius
|
|
19186
|
+
};
|
|
19187
|
+
}
|
|
19188
|
+
const geometry = entity.mesh?.geometry;
|
|
19189
|
+
if (!geometry) return null;
|
|
19190
|
+
const localBsphere = geometry.getBoundingSphere?.();
|
|
19191
|
+
if (!localBsphere || localBsphere.radius <= 0) return null;
|
|
19192
|
+
const matrix = entity._matrix ?? entity.matrix ?? mat4$1.create();
|
|
19193
|
+
return this._transformBoundingSphere(localBsphere, matrix);
|
|
19194
|
+
}
|
|
19195
|
+
_getMeshBoundingSphere(mesh) {
|
|
19196
|
+
const geometry = mesh.geometry;
|
|
19197
|
+
if (!geometry) return null;
|
|
19198
|
+
return geometry.getBoundingSphere?.() ?? null;
|
|
19199
|
+
}
|
|
19200
|
+
_transformBoundingSphere(bsphere, matrix) {
|
|
19201
|
+
const center = vec3$1.create();
|
|
19202
|
+
vec3$1.transformMat4(center, bsphere.center, matrix);
|
|
19203
|
+
const scaleX = Math.sqrt(matrix[0] * matrix[0] + matrix[1] * matrix[1] + matrix[2] * matrix[2]);
|
|
19204
|
+
const scaleY = Math.sqrt(matrix[4] * matrix[4] + matrix[5] * matrix[5] + matrix[6] * matrix[6]);
|
|
19205
|
+
const scaleZ = Math.sqrt(matrix[8] * matrix[8] + matrix[9] * matrix[9] + matrix[10] * matrix[10]);
|
|
19206
|
+
const maxScale = Math.max(scaleX, scaleY, scaleZ);
|
|
19207
|
+
return {
|
|
19208
|
+
center: Array.from(center),
|
|
19209
|
+
radius: bsphere.radius * maxScale
|
|
19210
|
+
};
|
|
19211
|
+
}
|
|
19212
|
+
_getInstanceMatrix(geometry, instanceIndex) {
|
|
19213
|
+
if (!geometry.instanceData) {
|
|
19214
|
+
return mat4$1.create();
|
|
19215
|
+
}
|
|
19216
|
+
const stride = 28;
|
|
19217
|
+
const offset = instanceIndex * stride;
|
|
19218
|
+
if (offset + 16 > geometry.instanceData.length) {
|
|
19219
|
+
return mat4$1.create();
|
|
19220
|
+
}
|
|
19221
|
+
const matrix = mat4$1.create();
|
|
19222
|
+
for (let i = 0; i < 16; i++) {
|
|
19223
|
+
matrix[i] = geometry.instanceData[offset + i];
|
|
19224
|
+
}
|
|
19225
|
+
return matrix;
|
|
19226
|
+
}
|
|
19227
|
+
_extractGeometry(geometry) {
|
|
19228
|
+
if (!geometry.vertexArray || !geometry.indexArray) {
|
|
19229
|
+
return null;
|
|
19230
|
+
}
|
|
19231
|
+
const stride = 20;
|
|
19232
|
+
const vertexCount = geometry.vertexArray.length / stride;
|
|
19233
|
+
const vertices = new Float32Array(vertexCount * 3);
|
|
19234
|
+
for (let i = 0; i < vertexCount; i++) {
|
|
19235
|
+
vertices[i * 3] = geometry.vertexArray[i * stride];
|
|
19236
|
+
vertices[i * 3 + 1] = geometry.vertexArray[i * stride + 1];
|
|
19237
|
+
vertices[i * 3 + 2] = geometry.vertexArray[i * stride + 2];
|
|
19238
|
+
}
|
|
19239
|
+
return {
|
|
19240
|
+
vertices,
|
|
19241
|
+
indices: geometry.indexArray
|
|
19242
|
+
};
|
|
19243
|
+
}
|
|
19244
|
+
/**
|
|
19245
|
+
* Ray-sphere intersection test
|
|
19246
|
+
* Returns true if ray intersects sphere within maxDistance
|
|
19247
|
+
* Handles case where ray origin is inside the sphere
|
|
19248
|
+
*/
|
|
19249
|
+
_raySphereIntersect(ray, sphere) {
|
|
19250
|
+
const oc = [
|
|
19251
|
+
ray.origin[0] - sphere.center[0],
|
|
19252
|
+
ray.origin[1] - sphere.center[1],
|
|
19253
|
+
ray.origin[2] - sphere.center[2]
|
|
19254
|
+
];
|
|
19255
|
+
const distToCenter = Math.sqrt(oc[0] * oc[0] + oc[1] * oc[1] + oc[2] * oc[2]);
|
|
19256
|
+
if (distToCenter < sphere.radius) {
|
|
19257
|
+
return true;
|
|
19258
|
+
}
|
|
19259
|
+
const a = this._dot(ray.direction, ray.direction);
|
|
19260
|
+
const b = 2 * this._dot(oc, ray.direction);
|
|
19261
|
+
const c = this._dot(oc, oc) - sphere.radius * sphere.radius;
|
|
19262
|
+
const discriminant = b * b - 4 * a * c;
|
|
19263
|
+
if (discriminant < 0) return false;
|
|
19264
|
+
const sqrtDisc = Math.sqrt(discriminant);
|
|
19265
|
+
const t1 = (-b - sqrtDisc) / (2 * a);
|
|
19266
|
+
const t2 = (-b + sqrtDisc) / (2 * a);
|
|
19267
|
+
if (t1 >= 0 && t1 <= ray.maxDistance) return true;
|
|
19268
|
+
if (t2 >= 0 && t2 <= ray.maxDistance) return true;
|
|
19269
|
+
return false;
|
|
19270
|
+
}
|
|
19271
|
+
/**
|
|
19272
|
+
* Get distance to sphere along ray (for sorting)
|
|
19273
|
+
*/
|
|
19274
|
+
_raySphereDistance(ray, sphere) {
|
|
19275
|
+
const oc = [
|
|
19276
|
+
ray.origin[0] - sphere.center[0],
|
|
19277
|
+
ray.origin[1] - sphere.center[1],
|
|
19278
|
+
ray.origin[2] - sphere.center[2]
|
|
19279
|
+
];
|
|
19280
|
+
const a = this._dot(ray.direction, ray.direction);
|
|
19281
|
+
const b = 2 * this._dot(oc, ray.direction);
|
|
19282
|
+
const c = this._dot(oc, oc) - sphere.radius * sphere.radius;
|
|
19283
|
+
const discriminant = b * b - 4 * a * c;
|
|
19284
|
+
if (discriminant < 0) return Infinity;
|
|
19285
|
+
const t = (-b - Math.sqrt(discriminant)) / (2 * a);
|
|
19286
|
+
return Math.max(0, t);
|
|
19287
|
+
}
|
|
19288
|
+
_handleWorkerMessage(event) {
|
|
19289
|
+
const { type, requestId, result } = event.data;
|
|
19290
|
+
if (type === "raycastResult") {
|
|
19291
|
+
const pending = this._pendingCallbacks.get(requestId);
|
|
19292
|
+
if (pending) {
|
|
19293
|
+
this._pendingCallbacks.delete(requestId);
|
|
19294
|
+
if (result.hit && pending.candidates) {
|
|
19295
|
+
const candidate = pending.candidates.find((c) => c.id === result.candidateId);
|
|
19296
|
+
if (candidate) {
|
|
19297
|
+
result.entity = candidate.entity;
|
|
19298
|
+
result.mesh = candidate.mesh;
|
|
19299
|
+
result.meshName = candidate.meshName;
|
|
19300
|
+
result.instanceIndex = candidate.instanceIndex;
|
|
19301
|
+
}
|
|
19302
|
+
}
|
|
19303
|
+
pending.callback(result);
|
|
19304
|
+
}
|
|
19305
|
+
}
|
|
19306
|
+
}
|
|
19307
|
+
_normalize(v) {
|
|
19308
|
+
const len = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
|
|
19309
|
+
if (len === 0) return [0, 0, 1];
|
|
19310
|
+
return [v[0] / len, v[1] / len, v[2] / len];
|
|
19311
|
+
}
|
|
19312
|
+
_dot(a, b) {
|
|
19313
|
+
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
19314
|
+
}
|
|
19315
|
+
/**
|
|
19316
|
+
* Get perpendicular distance from a point to a ray
|
|
19317
|
+
*/
|
|
19318
|
+
_pointToRayDistance(point, ray) {
|
|
19319
|
+
const op = [
|
|
19320
|
+
point[0] - ray.origin[0],
|
|
19321
|
+
point[1] - ray.origin[1],
|
|
19322
|
+
point[2] - ray.origin[2]
|
|
19323
|
+
];
|
|
19324
|
+
const t = this._dot(op, ray.direction);
|
|
19325
|
+
const closest = [
|
|
19326
|
+
ray.origin[0] + ray.direction[0] * t,
|
|
19327
|
+
ray.origin[1] + ray.direction[1] * t,
|
|
19328
|
+
ray.origin[2] + ray.direction[2] * t
|
|
19329
|
+
];
|
|
19330
|
+
const dx = point[0] - closest[0];
|
|
19331
|
+
const dy = point[1] - closest[1];
|
|
19332
|
+
const dz = point[2] - closest[2];
|
|
19333
|
+
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
19334
|
+
}
|
|
19335
|
+
_unproject(ndc, invViewProj) {
|
|
19336
|
+
const x = ndc[0];
|
|
19337
|
+
const y = ndc[1];
|
|
19338
|
+
const z = ndc[2];
|
|
19339
|
+
const w = invViewProj[3] * x + invViewProj[7] * y + invViewProj[11] * z + invViewProj[15];
|
|
19340
|
+
return [
|
|
19341
|
+
(invViewProj[0] * x + invViewProj[4] * y + invViewProj[8] * z + invViewProj[12]) / w,
|
|
19342
|
+
(invViewProj[1] * x + invViewProj[5] * y + invViewProj[9] * z + invViewProj[13]) / w,
|
|
19343
|
+
(invViewProj[2] * x + invViewProj[6] * y + invViewProj[10] * z + invViewProj[14]) / w
|
|
19344
|
+
];
|
|
19345
|
+
}
|
|
19346
|
+
/**
|
|
19347
|
+
* Generate Web Worker code as string
|
|
19348
|
+
*/
|
|
19349
|
+
_getWorkerCode() {
|
|
19350
|
+
return `
|
|
19351
|
+
// Raycaster Web Worker
|
|
19352
|
+
// Performs triangle intersection tests off the main thread
|
|
19353
|
+
|
|
19354
|
+
self.onmessage = function(event) {
|
|
19355
|
+
const { type, requestId, ray, candidates, debug } = event.data
|
|
19356
|
+
|
|
19357
|
+
if (type === 'raycast') {
|
|
19358
|
+
const result = raycastTriangles(ray, candidates, debug)
|
|
19359
|
+
self.postMessage({ type: 'raycastResult', requestId, result })
|
|
19360
|
+
}
|
|
19361
|
+
}
|
|
19362
|
+
|
|
19363
|
+
function raycastTriangles(ray, candidates, debug) {
|
|
19364
|
+
let closestHit = null
|
|
19365
|
+
let closestDistance = ray.maxDistance
|
|
19366
|
+
let debugInfo = debug ? { totalTris: 0, testedCandidates: 0, scales: [] } : null
|
|
19367
|
+
|
|
19368
|
+
for (const candidate of candidates) {
|
|
19369
|
+
if (debug) debugInfo.testedCandidates++
|
|
19370
|
+
const result = testCandidate(ray, candidate, closestDistance, debug ? debugInfo : null)
|
|
19371
|
+
|
|
19372
|
+
if (result && result.distance < closestDistance) {
|
|
19373
|
+
closestDistance = result.distance
|
|
19374
|
+
closestHit = {
|
|
19375
|
+
hit: true,
|
|
19376
|
+
distance: result.distance,
|
|
19377
|
+
point: result.point,
|
|
19378
|
+
normal: result.normal,
|
|
19379
|
+
triangleIndex: result.triangleIndex,
|
|
19380
|
+
candidateId: candidate.id,
|
|
19381
|
+
localT: result.localT,
|
|
19382
|
+
scale: result.scale
|
|
19383
|
+
}
|
|
19384
|
+
}
|
|
19385
|
+
}
|
|
19386
|
+
|
|
19387
|
+
if (debug) {
|
|
19388
|
+
let msg = 'Worker: candidates=' + debugInfo.testedCandidates + ', triangles=' + debugInfo.totalTris
|
|
19389
|
+
if (closestHit) {
|
|
19390
|
+
msg += ', hit=' + closestHit.distance.toFixed(2) + ' (localT=' + closestHit.localT.toFixed(2) + ', scale=' + closestHit.scale.toFixed(2) + ')'
|
|
19391
|
+
} else {
|
|
19392
|
+
msg += ', hit=none'
|
|
19393
|
+
}
|
|
19394
|
+
console.log(msg)
|
|
19395
|
+
}
|
|
19396
|
+
|
|
19397
|
+
return closestHit ?? { hit: false }
|
|
19398
|
+
}
|
|
19399
|
+
|
|
19400
|
+
function testCandidate(ray, candidate, maxDistance, debugInfo) {
|
|
19401
|
+
const { vertices, indices, matrix, backfaces } = candidate
|
|
19402
|
+
|
|
19403
|
+
// Compute inverse matrix for transforming ray to local space
|
|
19404
|
+
const invMatrix = invertMatrix4(matrix)
|
|
19405
|
+
|
|
19406
|
+
// Transform ray to local space
|
|
19407
|
+
const localOrigin = transformPoint(ray.origin, invMatrix)
|
|
19408
|
+
const localDir = transformDirection(ray.direction, invMatrix)
|
|
19409
|
+
|
|
19410
|
+
// Calculate the scale factor of the transformation (for correct distance)
|
|
19411
|
+
const dirScale = Math.sqrt(localDir[0]*localDir[0] + localDir[1]*localDir[1] + localDir[2]*localDir[2])
|
|
19412
|
+
const localDirNorm = [localDir[0]/dirScale, localDir[1]/dirScale, localDir[2]/dirScale]
|
|
19413
|
+
|
|
19414
|
+
let closestHit = null
|
|
19415
|
+
let closestT = maxDistance
|
|
19416
|
+
|
|
19417
|
+
// Test each triangle
|
|
19418
|
+
const triangleCount = indices.length / 3
|
|
19419
|
+
if (debugInfo) debugInfo.totalTris += triangleCount
|
|
19420
|
+
for (let i = 0; i < triangleCount; i++) {
|
|
19421
|
+
const i0 = indices[i * 3]
|
|
19422
|
+
const i1 = indices[i * 3 + 1]
|
|
19423
|
+
const i2 = indices[i * 3 + 2]
|
|
19424
|
+
|
|
19425
|
+
const v0 = [vertices[i0 * 3], vertices[i0 * 3 + 1], vertices[i0 * 3 + 2]]
|
|
19426
|
+
const v1 = [vertices[i1 * 3], vertices[i1 * 3 + 1], vertices[i1 * 3 + 2]]
|
|
19427
|
+
const v2 = [vertices[i2 * 3], vertices[i2 * 3 + 1], vertices[i2 * 3 + 2]]
|
|
19428
|
+
|
|
19429
|
+
const hit = rayTriangleIntersect(localOrigin, localDirNorm, v0, v1, v2, backfaces)
|
|
19430
|
+
|
|
19431
|
+
if (hit && hit.t > 0) {
|
|
19432
|
+
// Transform hit point and normal back to world space
|
|
19433
|
+
const worldPoint = transformPoint(hit.point, matrix)
|
|
19434
|
+
|
|
19435
|
+
// Calculate world-space distance (local t may be wrong due to matrix scale)
|
|
19436
|
+
const worldDist = Math.sqrt(
|
|
19437
|
+
(worldPoint[0] - ray.origin[0]) ** 2 +
|
|
19438
|
+
(worldPoint[1] - ray.origin[1]) ** 2 +
|
|
19439
|
+
(worldPoint[2] - ray.origin[2]) ** 2
|
|
19440
|
+
)
|
|
19441
|
+
|
|
19442
|
+
if (worldDist < closestT) {
|
|
19443
|
+
closestT = worldDist
|
|
19444
|
+
const worldNormal = transformDirection(hit.normal, matrix)
|
|
19445
|
+
|
|
19446
|
+
closestHit = {
|
|
19447
|
+
distance: worldDist,
|
|
19448
|
+
point: worldPoint,
|
|
19449
|
+
normal: normalize(worldNormal),
|
|
19450
|
+
triangleIndex: i,
|
|
19451
|
+
localT: hit.t,
|
|
19452
|
+
scale: dirScale
|
|
19453
|
+
}
|
|
19454
|
+
}
|
|
19455
|
+
}
|
|
19456
|
+
}
|
|
19457
|
+
|
|
19458
|
+
return closestHit
|
|
19459
|
+
}
|
|
19460
|
+
|
|
19461
|
+
// Möller–Trumbore intersection algorithm
|
|
19462
|
+
function rayTriangleIntersect(origin, dir, v0, v1, v2, backfaces) {
|
|
19463
|
+
const EPSILON = 0.0000001
|
|
19464
|
+
|
|
19465
|
+
const edge1 = sub(v1, v0)
|
|
19466
|
+
const edge2 = sub(v2, v0)
|
|
19467
|
+
const h = cross(dir, edge2)
|
|
19468
|
+
const a = dot(edge1, h)
|
|
19469
|
+
|
|
19470
|
+
// Check if ray is parallel to triangle
|
|
19471
|
+
if (a > -EPSILON && a < EPSILON) return null
|
|
19472
|
+
|
|
19473
|
+
// Check backface
|
|
19474
|
+
if (!backfaces && a < 0) return null
|
|
19475
|
+
|
|
19476
|
+
const f = 1.0 / a
|
|
19477
|
+
const s = sub(origin, v0)
|
|
19478
|
+
const u = f * dot(s, h)
|
|
19479
|
+
|
|
19480
|
+
if (u < 0.0 || u > 1.0) return null
|
|
19481
|
+
|
|
19482
|
+
const q = cross(s, edge1)
|
|
19483
|
+
const v = f * dot(dir, q)
|
|
19484
|
+
|
|
19485
|
+
if (v < 0.0 || u + v > 1.0) return null
|
|
19486
|
+
|
|
19487
|
+
const t = f * dot(edge2, q)
|
|
19488
|
+
|
|
19489
|
+
if (t > EPSILON) {
|
|
19490
|
+
const point = [
|
|
19491
|
+
origin[0] + dir[0] * t,
|
|
19492
|
+
origin[1] + dir[1] * t,
|
|
19493
|
+
origin[2] + dir[2] * t
|
|
19494
|
+
]
|
|
19495
|
+
const normal = normalize(cross(edge1, edge2))
|
|
19496
|
+
return { t, point, normal, u, v }
|
|
19497
|
+
}
|
|
19498
|
+
|
|
19499
|
+
return null
|
|
19500
|
+
}
|
|
19501
|
+
|
|
19502
|
+
// Matrix and vector utilities
|
|
19503
|
+
function invertMatrix4(m) {
|
|
19504
|
+
const inv = new Array(16)
|
|
19505
|
+
|
|
19506
|
+
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]
|
|
19507
|
+
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]
|
|
19508
|
+
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]
|
|
19509
|
+
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]
|
|
19510
|
+
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]
|
|
19511
|
+
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]
|
|
19512
|
+
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]
|
|
19513
|
+
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]
|
|
19514
|
+
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]
|
|
19515
|
+
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]
|
|
19516
|
+
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]
|
|
19517
|
+
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]
|
|
19518
|
+
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]
|
|
19519
|
+
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]
|
|
19520
|
+
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]
|
|
19521
|
+
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]
|
|
19522
|
+
|
|
19523
|
+
let det = m[0]*inv[0] + m[1]*inv[4] + m[2]*inv[8] + m[3]*inv[12]
|
|
19524
|
+
if (det === 0) return m // Return original if singular
|
|
19525
|
+
|
|
19526
|
+
det = 1.0 / det
|
|
19527
|
+
for (let i = 0; i < 16; i++) inv[i] *= det
|
|
19528
|
+
|
|
19529
|
+
return inv
|
|
19530
|
+
}
|
|
19531
|
+
|
|
19532
|
+
function transformPoint(p, m) {
|
|
19533
|
+
const w = m[3]*p[0] + m[7]*p[1] + m[11]*p[2] + m[15]
|
|
19534
|
+
return [
|
|
19535
|
+
(m[0]*p[0] + m[4]*p[1] + m[8]*p[2] + m[12]) / w,
|
|
19536
|
+
(m[1]*p[0] + m[5]*p[1] + m[9]*p[2] + m[13]) / w,
|
|
19537
|
+
(m[2]*p[0] + m[6]*p[1] + m[10]*p[2] + m[14]) / w
|
|
19538
|
+
]
|
|
19539
|
+
}
|
|
19540
|
+
|
|
19541
|
+
function transformDirection(d, m) {
|
|
19542
|
+
return [
|
|
19543
|
+
m[0]*d[0] + m[4]*d[1] + m[8]*d[2],
|
|
19544
|
+
m[1]*d[0] + m[5]*d[1] + m[9]*d[2],
|
|
19545
|
+
m[2]*d[0] + m[6]*d[1] + m[10]*d[2]
|
|
19546
|
+
]
|
|
19547
|
+
}
|
|
19548
|
+
|
|
19549
|
+
function normalize(v) {
|
|
19550
|
+
const len = Math.sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2])
|
|
19551
|
+
if (len === 0) return [0, 0, 1]
|
|
19552
|
+
return [v[0]/len, v[1]/len, v[2]/len]
|
|
19553
|
+
}
|
|
19554
|
+
|
|
19555
|
+
function dot(a, b) {
|
|
19556
|
+
return a[0]*b[0] + a[1]*b[1] + a[2]*b[2]
|
|
19557
|
+
}
|
|
19558
|
+
|
|
19559
|
+
function cross(a, b) {
|
|
19560
|
+
return [
|
|
19561
|
+
a[1]*b[2] - a[2]*b[1],
|
|
19562
|
+
a[2]*b[0] - a[0]*b[2],
|
|
19563
|
+
a[0]*b[1] - a[1]*b[0]
|
|
19564
|
+
]
|
|
19565
|
+
}
|
|
19566
|
+
|
|
19567
|
+
function sub(a, b) {
|
|
19568
|
+
return [a[0]-b[0], a[1]-b[1], a[2]-b[2]]
|
|
19569
|
+
}
|
|
19570
|
+
`;
|
|
19571
|
+
}
|
|
19572
|
+
destroy() {
|
|
19573
|
+
if (this.worker) {
|
|
19574
|
+
this.worker.terminate();
|
|
19575
|
+
this.worker = null;
|
|
19576
|
+
}
|
|
19577
|
+
this._pendingCallbacks.clear();
|
|
19578
|
+
this._initialized = false;
|
|
19579
|
+
}
|
|
19580
|
+
}
|
|
17109
19581
|
function fail(engine, msg, data) {
|
|
17110
19582
|
if (engine?.canvas) {
|
|
17111
19583
|
engine.canvas.style.display = "none";
|
|
@@ -17171,7 +19643,7 @@ const DEFAULT_SETTINGS = {
|
|
|
17171
19643
|
renderScale: 1,
|
|
17172
19644
|
// Render resolution multiplier (1.5-2.0 for supersampling AA)
|
|
17173
19645
|
autoScale: {
|
|
17174
|
-
enabled:
|
|
19646
|
+
enabled: true,
|
|
17175
19647
|
// Auto-reduce renderScale for high resolutions
|
|
17176
19648
|
enabledForEffects: true,
|
|
17177
19649
|
// Auto scale effects at high resolutions (when main autoScale disabled)
|
|
@@ -17196,8 +19668,10 @@ const DEFAULT_SETTINGS = {
|
|
|
17196
19668
|
// Enable alpha hashing/dithering for cutout transparency (global default)
|
|
17197
19669
|
alphaHashScale: 1,
|
|
17198
19670
|
// Scale factor for alpha hash threshold (higher = more opaque)
|
|
17199
|
-
luminanceToAlpha: false
|
|
19671
|
+
luminanceToAlpha: false,
|
|
17200
19672
|
// Derive alpha from color luminance (for old game assets where black=transparent)
|
|
19673
|
+
tonemapMode: 0
|
|
19674
|
+
// 0=ACES, 1=Reinhard, 2=None (linear clamp)
|
|
17201
19675
|
},
|
|
17202
19676
|
// Noise settings for dithering, jittering, etc.
|
|
17203
19677
|
noise: {
|
|
@@ -17244,13 +19718,14 @@ const DEFAULT_SETTINGS = {
|
|
|
17244
19718
|
exposure: 1.6,
|
|
17245
19719
|
fog: {
|
|
17246
19720
|
enabled: true,
|
|
17247
|
-
color: [
|
|
17248
|
-
distances: [
|
|
19721
|
+
color: [100 / 255, 135 / 255, 170 / 255],
|
|
19722
|
+
distances: [0, 15, 50],
|
|
17249
19723
|
alpha: [0, 0.5, 0.9],
|
|
17250
19724
|
heightFade: [-2, 185],
|
|
17251
19725
|
// [bottomY, topY] - full fog at bottomY, zero at topY
|
|
17252
|
-
brightResist: 0
|
|
19726
|
+
brightResist: 0,
|
|
17253
19727
|
// How much bright/emissive colors resist fog (0-1)
|
|
19728
|
+
debug: 0
|
|
17254
19729
|
}
|
|
17255
19730
|
},
|
|
17256
19731
|
// Main directional light
|
|
@@ -17280,6 +19755,8 @@ const DEFAULT_SETTINGS = {
|
|
|
17280
19755
|
surfaceBias: 0,
|
|
17281
19756
|
// Scale shadow projection larger (0.01 = 1% larger)
|
|
17282
19757
|
strength: 1
|
|
19758
|
+
//frustum: false,
|
|
19759
|
+
//hiZ: false,
|
|
17283
19760
|
},
|
|
17284
19761
|
// Ambient Occlusion settings
|
|
17285
19762
|
ao: {
|
|
@@ -17387,6 +19864,54 @@ const DEFAULT_SETTINGS = {
|
|
|
17387
19864
|
saturateLevel: 0.5
|
|
17388
19865
|
// Logarithmic saturation level for indirect light
|
|
17389
19866
|
},
|
|
19867
|
+
// Volumetric Fog (light scattering through particles)
|
|
19868
|
+
volumetricFog: {
|
|
19869
|
+
enabled: false,
|
|
19870
|
+
// Disabled by default (performance impact)
|
|
19871
|
+
resolution: 0.125,
|
|
19872
|
+
// 1/4 render resolution for ray marching
|
|
19873
|
+
maxSamples: 32,
|
|
19874
|
+
// Ray march samples (8-32)
|
|
19875
|
+
blurRadius: 8,
|
|
19876
|
+
// Gaussian blur radius
|
|
19877
|
+
densityMultiplier: 1,
|
|
19878
|
+
// Multiplies base fog density
|
|
19879
|
+
scatterStrength: 0.35,
|
|
19880
|
+
// Light scattering intensity
|
|
19881
|
+
mainLightScatter: 1.4,
|
|
19882
|
+
// Main directional light scattering boost
|
|
19883
|
+
mainLightScatterDark: 5,
|
|
19884
|
+
// Main directional light scattering boost
|
|
19885
|
+
mainLightSaturation: 0.15,
|
|
19886
|
+
// Main light color saturation in fog
|
|
19887
|
+
maxFogOpacity: 0.3,
|
|
19888
|
+
// Maximum fog opacity (0-1)
|
|
19889
|
+
heightRange: [-2, 8],
|
|
19890
|
+
// [bottom, top] Y bounds for fog (low ground fog)
|
|
19891
|
+
windDirection: [1, 0, 0.2],
|
|
19892
|
+
// Wind direction for fog animation
|
|
19893
|
+
windSpeed: 0.5,
|
|
19894
|
+
// Wind speed multiplier
|
|
19895
|
+
noiseScale: 0.9,
|
|
19896
|
+
// 3D noise frequency (higher = finer detail)
|
|
19897
|
+
noiseStrength: 0.8,
|
|
19898
|
+
// Noise intensity (0 = uniform, 1 = full variation)
|
|
19899
|
+
noiseOctaves: 6,
|
|
19900
|
+
// Noise detail layers
|
|
19901
|
+
noiseEnabled: true,
|
|
19902
|
+
// Enable 3D noise (disable for debug)
|
|
19903
|
+
lightingEnabled: true,
|
|
19904
|
+
// Light fog from scene lights
|
|
19905
|
+
shadowsEnabled: true,
|
|
19906
|
+
// Apply shadows to fog
|
|
19907
|
+
brightnessThreshold: 0.8,
|
|
19908
|
+
// Scene luminance where fog starts fading (like bloom)
|
|
19909
|
+
minVisibility: 0.15,
|
|
19910
|
+
// Minimum fog visibility over bright surfaces (0-1)
|
|
19911
|
+
skyBrightness: 1.2
|
|
19912
|
+
// Virtual brightness for sky pixels (depth at far plane)
|
|
19913
|
+
//debugSkyCheck: true
|
|
19914
|
+
},
|
|
17390
19915
|
// Planar Reflections (alternative to SSR for water/floor)
|
|
17391
19916
|
planarReflection: {
|
|
17392
19917
|
enabled: true,
|
|
@@ -17442,15 +19967,68 @@ const DEFAULT_SETTINGS = {
|
|
|
17442
19967
|
// FPS threshold for auto-disable
|
|
17443
19968
|
disableDelay: 3
|
|
17444
19969
|
// Seconds below threshold before disabling
|
|
19970
|
+
},
|
|
19971
|
+
// CRT effect (retro monitor simulation)
|
|
19972
|
+
crt: {
|
|
19973
|
+
enabled: false,
|
|
19974
|
+
// Enable CRT effect (geometry, scanlines, etc.)
|
|
19975
|
+
upscaleEnabled: false,
|
|
19976
|
+
// Enable upscaling (pixelated look) even when CRT disabled
|
|
19977
|
+
upscaleTarget: 4,
|
|
19978
|
+
// Target upscale multiplier (4x render resolution)
|
|
19979
|
+
maxTextureSize: 4096,
|
|
19980
|
+
// Max upscaled texture dimension
|
|
19981
|
+
// Geometry distortion
|
|
19982
|
+
curvature: 0.14,
|
|
19983
|
+
// Screen curvature amount (0-0.15)
|
|
19984
|
+
cornerRadius: 0.055,
|
|
19985
|
+
// Rounded corner radius (0-0.1)
|
|
19986
|
+
zoom: 1.06,
|
|
19987
|
+
// Zoom to compensate for curvature shrinkage
|
|
19988
|
+
// Scanlines (electron beam simulation - Gaussian profile)
|
|
19989
|
+
scanlineIntensity: 0.4,
|
|
19990
|
+
// Scanline effect strength (0-1)
|
|
19991
|
+
scanlineWidth: 0,
|
|
19992
|
+
// Beam width (0=thin/center only, 1=no gap)
|
|
19993
|
+
scanlineBrightBoost: 0.8,
|
|
19994
|
+
// Bright pixels widen beam to fill gaps (0-1)
|
|
19995
|
+
scanlineHeight: 5,
|
|
19996
|
+
// Scanline height in canvas pixels
|
|
19997
|
+
// RGB convergence error (color channel misalignment)
|
|
19998
|
+
convergence: [0.79, 0, -0.77],
|
|
19999
|
+
// RGB X offset in source pixels
|
|
20000
|
+
// Phosphor mask
|
|
20001
|
+
maskType: "aperture",
|
|
20002
|
+
// 'aperture', 'slot', 'shadow', 'none'
|
|
20003
|
+
maskIntensity: 0.25,
|
|
20004
|
+
// Mask strength (0-1)
|
|
20005
|
+
maskScale: 1,
|
|
20006
|
+
// Mask size multiplier
|
|
20007
|
+
// Vignette (edge darkening)
|
|
20008
|
+
vignetteIntensity: 0.54,
|
|
20009
|
+
// Edge darkening strength (0-1)
|
|
20010
|
+
vignetteSize: 0.85,
|
|
20011
|
+
// Vignette size (larger = more visible)
|
|
20012
|
+
// Horizontal blur (beam softness)
|
|
20013
|
+
blurSize: 0.79
|
|
20014
|
+
// Horizontal blur in pixels (0-2)
|
|
17445
20015
|
}
|
|
17446
20016
|
};
|
|
17447
20017
|
async function createWebGPUContext(engine, canvasId) {
|
|
17448
20018
|
try {
|
|
17449
20019
|
let configureContext = function() {
|
|
17450
|
-
|
|
17451
|
-
|
|
17452
|
-
|
|
17453
|
-
|
|
20020
|
+
let pixelWidth, pixelHeight;
|
|
20021
|
+
if (engine._devicePixelSize) {
|
|
20022
|
+
pixelWidth = engine._devicePixelSize.width;
|
|
20023
|
+
pixelHeight = engine._devicePixelSize.height;
|
|
20024
|
+
} else {
|
|
20025
|
+
const devicePixelRatio = window.devicePixelRatio || 1;
|
|
20026
|
+
pixelWidth = Math.round(canvas.clientWidth * devicePixelRatio);
|
|
20027
|
+
pixelHeight = Math.round(canvas.clientHeight * devicePixelRatio);
|
|
20028
|
+
}
|
|
20029
|
+
canvas.width = pixelWidth;
|
|
20030
|
+
canvas.height = pixelHeight;
|
|
20031
|
+
engine._canvasPixelSize = { width: pixelWidth, height: pixelHeight };
|
|
17454
20032
|
context.configure({
|
|
17455
20033
|
device,
|
|
17456
20034
|
format: canvasFormat,
|
|
@@ -17623,9 +20201,34 @@ class Engine {
|
|
|
17623
20201
|
this.stats.avg_fps = 60;
|
|
17624
20202
|
this.stats.avg_dt_render = 0.1;
|
|
17625
20203
|
requestAnimationFrame(() => this._frame());
|
|
17626
|
-
|
|
17627
|
-
|
|
17628
|
-
|
|
20204
|
+
this._devicePixelSize = null;
|
|
20205
|
+
try {
|
|
20206
|
+
const resizeObserver = new ResizeObserver((entries) => {
|
|
20207
|
+
for (const entry of entries) {
|
|
20208
|
+
if (entry.devicePixelContentBoxSize) {
|
|
20209
|
+
const size = entry.devicePixelContentBoxSize[0];
|
|
20210
|
+
this._devicePixelSize = {
|
|
20211
|
+
width: size.inlineSize,
|
|
20212
|
+
height: size.blockSize
|
|
20213
|
+
};
|
|
20214
|
+
} else if (entry.contentBoxSize) {
|
|
20215
|
+
const size = entry.contentBoxSize[0];
|
|
20216
|
+
const dpr = window.devicePixelRatio || 1;
|
|
20217
|
+
this._devicePixelSize = {
|
|
20218
|
+
width: Math.round(size.inlineSize * dpr),
|
|
20219
|
+
height: Math.round(size.blockSize * dpr)
|
|
20220
|
+
};
|
|
20221
|
+
}
|
|
20222
|
+
this.needsResize = true;
|
|
20223
|
+
}
|
|
20224
|
+
});
|
|
20225
|
+
resizeObserver.observe(this.canvas, { box: "device-pixel-content-box" });
|
|
20226
|
+
} catch (e) {
|
|
20227
|
+
console.log("ResizeObserver device-pixel-content-box not supported, falling back to window resize");
|
|
20228
|
+
window.addEventListener("resize", () => {
|
|
20229
|
+
this.needsResize = true;
|
|
20230
|
+
});
|
|
20231
|
+
}
|
|
17629
20232
|
setInterval(() => {
|
|
17630
20233
|
if (this.needsResize && !this._resizing) {
|
|
17631
20234
|
this.needsResize = false;
|
|
@@ -17745,11 +20348,19 @@ class Engine {
|
|
|
17745
20348
|
* @param {Array} options.position - Optional position offset [x, y, z]
|
|
17746
20349
|
* @param {Array} options.rotation - Optional rotation offset [x, y, z] in radians
|
|
17747
20350
|
* @param {number} options.scale - Optional uniform scale multiplier
|
|
20351
|
+
* @param {boolean} options.doubleSided - Optional: force all materials to be double-sided
|
|
17748
20352
|
* @returns {Promise<Object>} Object containing { meshes, nodes, skins, animations }
|
|
17749
20353
|
*/
|
|
17750
20354
|
async loadScene(url, options = {}) {
|
|
17751
20355
|
const result = await loadGltf(this, url, options);
|
|
17752
20356
|
const { meshes, nodes } = result;
|
|
20357
|
+
if (options.doubleSided) {
|
|
20358
|
+
for (const mesh of Object.values(meshes)) {
|
|
20359
|
+
if (mesh.material) {
|
|
20360
|
+
mesh.material.doubleSided = true;
|
|
20361
|
+
}
|
|
20362
|
+
}
|
|
20363
|
+
}
|
|
17753
20364
|
for (const node of nodes) {
|
|
17754
20365
|
if (!node.parent) {
|
|
17755
20366
|
node.updateMatrix(null);
|
|
@@ -17769,6 +20380,23 @@ class Engine {
|
|
|
17769
20380
|
[scl, scl, scl]
|
|
17770
20381
|
);
|
|
17771
20382
|
}
|
|
20383
|
+
let combinedBsphere = null;
|
|
20384
|
+
const hasAnySkin = Object.values(meshes).some((m) => m.hasSkin);
|
|
20385
|
+
if (hasAnySkin) {
|
|
20386
|
+
const allPositions = [];
|
|
20387
|
+
for (const mesh of Object.values(meshes)) {
|
|
20388
|
+
const positions = mesh.geometry?.attributes?.position;
|
|
20389
|
+
if (positions) {
|
|
20390
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
20391
|
+
allPositions.push(positions[i], positions[i + 1], positions[i + 2]);
|
|
20392
|
+
}
|
|
20393
|
+
}
|
|
20394
|
+
}
|
|
20395
|
+
if (allPositions.length > 0) {
|
|
20396
|
+
const { calculateBoundingSphere: calculateBoundingSphere2 } = await Promise.resolve().then(() => BoundingSphere);
|
|
20397
|
+
combinedBsphere = calculateBoundingSphere2(new Float32Array(allPositions));
|
|
20398
|
+
}
|
|
20399
|
+
}
|
|
17772
20400
|
for (const [name, mesh] of Object.entries(meshes)) {
|
|
17773
20401
|
let meshNode = null;
|
|
17774
20402
|
if (mesh.nodeIndex !== null && mesh.nodeIndex !== void 0) {
|
|
@@ -17781,7 +20409,7 @@ class Engine {
|
|
|
17781
20409
|
if (options.position || options.rotation || options.scale) {
|
|
17782
20410
|
mat4.multiply(worldMatrix, rootTransform, worldMatrix);
|
|
17783
20411
|
}
|
|
17784
|
-
const localBsphere = mesh.geometry.getBoundingSphere?.();
|
|
20412
|
+
const localBsphere = hasAnySkin && combinedBsphere ? combinedBsphere : mesh.geometry.getBoundingSphere?.();
|
|
17785
20413
|
let worldCenter = [0, 0, 0];
|
|
17786
20414
|
let worldRadius = 1;
|
|
17787
20415
|
if (localBsphere && localBsphere.radius > 0) {
|
|
@@ -17796,6 +20424,9 @@ class Engine {
|
|
|
17796
20424
|
const scaleZ = Math.sqrt(worldMatrix[8] ** 2 + worldMatrix[9] ** 2 + worldMatrix[10] ** 2);
|
|
17797
20425
|
worldRadius = localBsphere.radius * Math.max(scaleX, scaleY, scaleZ);
|
|
17798
20426
|
}
|
|
20427
|
+
if (hasAnySkin && combinedBsphere) {
|
|
20428
|
+
mesh.combinedBsphere = combinedBsphere;
|
|
20429
|
+
}
|
|
17799
20430
|
mesh.addInstance(worldCenter, worldRadius);
|
|
17800
20431
|
mesh.updateInstance(0, worldMatrix);
|
|
17801
20432
|
mesh.static = true;
|
|
@@ -17864,6 +20495,16 @@ class Engine {
|
|
|
17864
20495
|
getEntity(id) {
|
|
17865
20496
|
return this.entityManager.get(id);
|
|
17866
20497
|
}
|
|
20498
|
+
/**
|
|
20499
|
+
* Invalidate occlusion culling data and reset warmup period.
|
|
20500
|
+
* Call this after scene loading or major camera teleportation to prevent
|
|
20501
|
+
* incorrect occlusion culling with stale depth buffer data.
|
|
20502
|
+
*/
|
|
20503
|
+
invalidateOcclusionCulling() {
|
|
20504
|
+
if (this.renderer) {
|
|
20505
|
+
this.renderer.invalidateOcclusionCulling();
|
|
20506
|
+
}
|
|
20507
|
+
}
|
|
17867
20508
|
async _create() {
|
|
17868
20509
|
let camera = new Camera(this);
|
|
17869
20510
|
camera.updateMatrix();
|
|
@@ -17886,6 +20527,8 @@ class Engine {
|
|
|
17886
20527
|
}
|
|
17887
20528
|
async _after_create() {
|
|
17888
20529
|
this.renderer = await RenderGraph.create(this, this.environment, this.environmentEncoding);
|
|
20530
|
+
this.raycaster = new Raycaster(this);
|
|
20531
|
+
await this.raycaster.initialize();
|
|
17889
20532
|
}
|
|
17890
20533
|
_update(dt) {
|
|
17891
20534
|
this._updateInput();
|
|
@@ -17941,7 +20584,7 @@ class Engine {
|
|
|
17941
20584
|
this.guiCanvas.height = canvas.height;
|
|
17942
20585
|
this.guiCtx.clearRect(0, 0, canvas.width, canvas.height);
|
|
17943
20586
|
}
|
|
17944
|
-
await this.renderer.resize(canvas.width, canvas.height);
|
|
20587
|
+
await this.renderer.resize(canvas.width, canvas.height, this.renderScale);
|
|
17945
20588
|
this.resize();
|
|
17946
20589
|
await new Promise((resolve) => setTimeout(resolve, 16));
|
|
17947
20590
|
this._resizing = false;
|
|
@@ -18194,6 +20837,7 @@ exports.Material = Material;
|
|
|
18194
20837
|
exports.Mesh = Mesh;
|
|
18195
20838
|
exports.ParticleEmitter = ParticleEmitter;
|
|
18196
20839
|
exports.ParticleSystem = ParticleSystem;
|
|
20840
|
+
exports.Raycaster = Raycaster;
|
|
18197
20841
|
exports.RenderGraph = RenderGraph;
|
|
18198
20842
|
exports.Texture = Texture;
|
|
18199
20843
|
exports.fail = fail;
|