topazcube 0.1.30 → 0.1.33
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/LICENSE.txt +0 -0
- package/README.md +0 -0
- package/dist/Renderer.cjs +18200 -0
- package/dist/Renderer.cjs.map +1 -0
- package/dist/Renderer.js +18183 -0
- package/dist/Renderer.js.map +1 -0
- package/dist/client.cjs +94 -260
- package/dist/client.cjs.map +1 -1
- package/dist/client.js +71 -215
- package/dist/client.js.map +1 -1
- package/dist/server.cjs +165 -432
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +117 -370
- package/dist/server.js.map +1 -1
- package/dist/terminal.cjs +113 -200
- package/dist/terminal.cjs.map +1 -1
- package/dist/terminal.js +50 -51
- package/dist/terminal.js.map +1 -1
- package/dist/utils-CRhi1BDa.cjs +259 -0
- package/dist/utils-CRhi1BDa.cjs.map +1 -0
- package/dist/utils-D7tXt6-2.js +260 -0
- package/dist/utils-D7tXt6-2.js.map +1 -0
- package/package.json +19 -15
- package/src/{client.ts → network/client.js} +173 -403
- package/src/{compress-browser.ts → network/compress-browser.js} +2 -4
- package/src/{compress-node.ts → network/compress-node.js} +8 -14
- package/src/{server.ts → network/server.js} +229 -317
- package/src/{terminal.js → network/terminal.js} +0 -0
- package/src/{topazcube.ts → network/topazcube.js} +2 -2
- package/src/network/utils.js +375 -0
- package/src/renderer/Camera.js +191 -0
- package/src/renderer/DebugUI.js +572 -0
- package/src/renderer/Geometry.js +1049 -0
- package/src/renderer/Material.js +61 -0
- package/src/renderer/Mesh.js +211 -0
- package/src/renderer/Node.js +112 -0
- package/src/renderer/Pipeline.js +643 -0
- package/src/renderer/Renderer.js +1324 -0
- package/src/renderer/Skin.js +792 -0
- package/src/renderer/Texture.js +584 -0
- package/src/renderer/core/AssetManager.js +359 -0
- package/src/renderer/core/CullingSystem.js +307 -0
- package/src/renderer/core/EntityManager.js +541 -0
- package/src/renderer/core/InstanceManager.js +343 -0
- package/src/renderer/core/ParticleEmitter.js +358 -0
- package/src/renderer/core/ParticleSystem.js +564 -0
- package/src/renderer/core/SpriteSystem.js +349 -0
- package/src/renderer/gltf.js +546 -0
- package/src/renderer/math.js +161 -0
- package/src/renderer/rendering/HistoryBufferManager.js +333 -0
- package/src/renderer/rendering/ProbeCapture.js +1495 -0
- package/src/renderer/rendering/ReflectionProbeManager.js +352 -0
- package/src/renderer/rendering/RenderGraph.js +2064 -0
- package/src/renderer/rendering/passes/AOPass.js +308 -0
- package/src/renderer/rendering/passes/AmbientCapturePass.js +593 -0
- package/src/renderer/rendering/passes/BasePass.js +101 -0
- package/src/renderer/rendering/passes/BloomPass.js +417 -0
- package/src/renderer/rendering/passes/FogPass.js +419 -0
- package/src/renderer/rendering/passes/GBufferPass.js +706 -0
- package/src/renderer/rendering/passes/HiZPass.js +714 -0
- package/src/renderer/rendering/passes/LightingPass.js +739 -0
- package/src/renderer/rendering/passes/ParticlePass.js +835 -0
- package/src/renderer/rendering/passes/PlanarReflectionPass.js +456 -0
- package/src/renderer/rendering/passes/PostProcessPass.js +282 -0
- package/src/renderer/rendering/passes/ReflectionPass.js +157 -0
- package/src/renderer/rendering/passes/RenderPostPass.js +364 -0
- package/src/renderer/rendering/passes/SSGIPass.js +265 -0
- package/src/renderer/rendering/passes/SSGITilePass.js +296 -0
- package/src/renderer/rendering/passes/ShadowPass.js +1822 -0
- package/src/renderer/rendering/passes/TransparentPass.js +831 -0
- package/src/renderer/rendering/shaders/ao.wgsl +182 -0
- package/src/renderer/rendering/shaders/bloom.wgsl +97 -0
- package/src/renderer/rendering/shaders/bloom_blur.wgsl +80 -0
- package/src/renderer/rendering/shaders/depth_copy.wgsl +17 -0
- package/src/renderer/rendering/shaders/geometry.wgsl +550 -0
- package/src/renderer/rendering/shaders/hiz_reduce.wgsl +114 -0
- package/src/renderer/rendering/shaders/light_culling.wgsl +204 -0
- package/src/renderer/rendering/shaders/lighting.wgsl +932 -0
- package/src/renderer/rendering/shaders/lighting_common.wgsl +143 -0
- package/src/renderer/rendering/shaders/particle_render.wgsl +525 -0
- package/src/renderer/rendering/shaders/particle_simulate.wgsl +440 -0
- package/src/renderer/rendering/shaders/postproc.wgsl +272 -0
- package/src/renderer/rendering/shaders/render_post.wgsl +289 -0
- package/src/renderer/rendering/shaders/shadow.wgsl +76 -0
- package/src/renderer/rendering/shaders/ssgi.wgsl +266 -0
- package/src/renderer/rendering/shaders/ssgi_accumulate.wgsl +114 -0
- package/src/renderer/rendering/shaders/ssgi_propagate.wgsl +132 -0
- package/src/renderer/utils/BoundingSphere.js +439 -0
- package/src/renderer/utils/Frustum.js +281 -0
- package/dist/client.d.cts +0 -211
- package/dist/client.d.ts +0 -211
- package/dist/server.d.cts +0 -120
- package/dist/server.d.ts +0 -120
- package/dist/terminal.d.cts +0 -64
- package/dist/terminal.d.ts +0 -64
- package/src/utils.ts +0 -403
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Shared lighting functions for deferred and forward rendering
|
|
2
|
+
// This file is imported as a string and concatenated with other shaders
|
|
3
|
+
|
|
4
|
+
const PI = 3.14159;
|
|
5
|
+
const MAX_LIGHTS = 768;
|
|
6
|
+
const CASCADE_COUNT = 3;
|
|
7
|
+
const MAX_LIGHTS_PER_TILE = 256;
|
|
8
|
+
const MAX_SPOT_SHADOWS = 16;
|
|
9
|
+
|
|
10
|
+
// Hard-coded spot shadow parameters
|
|
11
|
+
const SPOT_ATLAS_WIDTH: f32 = 2048.0;
|
|
12
|
+
const SPOT_ATLAS_HEIGHT: f32 = 2048.0;
|
|
13
|
+
const SPOT_TILE_SIZE: f32 = 512.0;
|
|
14
|
+
const SPOT_TILES_PER_ROW: i32 = 4;
|
|
15
|
+
const SPOT_FADE_START: f32 = 25.0;
|
|
16
|
+
const SPOT_MAX_DISTANCE: f32 = 30.0;
|
|
17
|
+
const SPOT_MIN_SHADOW: f32 = 0.5;
|
|
18
|
+
|
|
19
|
+
struct Light {
|
|
20
|
+
enabled: u32,
|
|
21
|
+
position: vec3f,
|
|
22
|
+
color: vec4f,
|
|
23
|
+
direction: vec3f,
|
|
24
|
+
geom: vec4f,
|
|
25
|
+
shadowIndex: i32,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
struct SpotShadowMatrices {
|
|
29
|
+
matrices: array<mat4x4<f32>, MAX_SPOT_SHADOWS>,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
struct CascadeMatrices {
|
|
33
|
+
matrices: array<mat4x4<f32>, CASCADE_COUNT>,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ============================================
|
|
37
|
+
// Environment mapping functions
|
|
38
|
+
// ============================================
|
|
39
|
+
|
|
40
|
+
fn SphToUV(n: vec3<f32>) -> vec2<f32> {
|
|
41
|
+
var uv: vec2<f32>;
|
|
42
|
+
uv.x = atan2(-n.x, n.z);
|
|
43
|
+
uv.x = (uv.x + PI / 2.0) / (PI * 2.0) + PI * (28.670 / 360.0);
|
|
44
|
+
uv.y = acos(n.y) / PI;
|
|
45
|
+
return uv;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
fn octEncode(n: vec3<f32>) -> vec2<f32> {
|
|
49
|
+
var n2 = n / (abs(n.x) + abs(n.y) + abs(n.z));
|
|
50
|
+
if (n2.y < 0.0) {
|
|
51
|
+
let signX = select(-1.0, 1.0, n2.x >= 0.0);
|
|
52
|
+
let signZ = select(-1.0, 1.0, n2.z >= 0.0);
|
|
53
|
+
n2 = vec3f(
|
|
54
|
+
(1.0 - abs(n2.z)) * signX,
|
|
55
|
+
n2.y,
|
|
56
|
+
(1.0 - abs(n2.x)) * signZ
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return n2.xz * 0.5 + 0.5;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
fn octDecode(uv: vec2<f32>) -> vec3<f32> {
|
|
63
|
+
var uv2 = uv * 2.0 - 1.0;
|
|
64
|
+
var n = vec3f(uv2.x, 1.0 - abs(uv2.x) - abs(uv2.y), uv2.y);
|
|
65
|
+
if (n.y < 0.0) {
|
|
66
|
+
let signX = select(-1.0, 1.0, n.x >= 0.0);
|
|
67
|
+
let signZ = select(-1.0, 1.0, n.z >= 0.0);
|
|
68
|
+
n = vec3f(
|
|
69
|
+
(1.0 - abs(n.z)) * signX,
|
|
70
|
+
n.y,
|
|
71
|
+
(1.0 - abs(n.x)) * signZ
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
return normalize(n);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ============================================
|
|
78
|
+
// Noise sampling functions
|
|
79
|
+
// ============================================
|
|
80
|
+
|
|
81
|
+
fn vogelDiskSample(sampleIndex: i32, numSamples: i32, rotation: f32) -> vec2f {
|
|
82
|
+
let goldenAngle = 2.399963229728653;
|
|
83
|
+
let r = sqrt((f32(sampleIndex) + 0.5) / f32(numSamples));
|
|
84
|
+
let theta = f32(sampleIndex) * goldenAngle + rotation;
|
|
85
|
+
return vec2f(r * cos(theta), r * sin(theta));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
fn buildOrthonormalBasis(n: vec3f) -> mat3x3<f32> {
|
|
89
|
+
let up = select(vec3f(1.0, 0.0, 0.0), vec3f(0.0, 1.0, 0.0), abs(n.y) < 0.999);
|
|
90
|
+
let tangent = normalize(cross(up, n));
|
|
91
|
+
let bitangent = cross(n, tangent);
|
|
92
|
+
return mat3x3<f32>(tangent, bitangent, n);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ============================================
|
|
96
|
+
// PBR BRDF functions
|
|
97
|
+
// ============================================
|
|
98
|
+
|
|
99
|
+
fn clampedDot(x: vec3<f32>, y: vec3<f32>) -> f32 {
|
|
100
|
+
return clamp(dot(x, y), 0.0, 1.0);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
fn F_Schlick(f0: vec3<f32>, f90: vec3<f32>, VdotH: f32) -> vec3<f32> {
|
|
104
|
+
return f0 + (f90 - f0) * pow(clamp(1.0 - VdotH, 0.0, 1.0), 5.0);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
fn V_GGX(NdotL: f32, NdotV: f32, alphaRoughness: f32) -> f32 {
|
|
108
|
+
let alphaRoughnessSq = alphaRoughness * alphaRoughness;
|
|
109
|
+
let GGXV = NdotL * sqrt(NdotV * NdotV * (1.0 - alphaRoughnessSq) + alphaRoughnessSq);
|
|
110
|
+
let GGXL = NdotV * sqrt(NdotL * NdotL * (1.0 - alphaRoughnessSq) + alphaRoughnessSq);
|
|
111
|
+
let GGX = GGXV + GGXL;
|
|
112
|
+
if (GGX > 0.0) {
|
|
113
|
+
return 0.5 / GGX;
|
|
114
|
+
}
|
|
115
|
+
return 0.0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
fn D_GGX(NdotH: f32, alphaRoughness: f32) -> f32 {
|
|
119
|
+
let alphaRoughnessSq = alphaRoughness * alphaRoughness;
|
|
120
|
+
let f = (NdotH * NdotH) * (alphaRoughnessSq - 1.0) + 1.0;
|
|
121
|
+
return alphaRoughnessSq / (PI * f * f);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
fn BRDF_lambertian(f0: vec3<f32>, f90: vec3<f32>, diffuseColor: vec3<f32>, specularWeight: f32, VdotH: f32) -> vec3<f32> {
|
|
125
|
+
return (1.0 - specularWeight * F_Schlick(f0, f90, VdotH)) * (diffuseColor / PI);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
fn BRDF_specularGGX(f0: vec3<f32>, f90: vec3<f32>, alphaRoughness: f32, specularWeight: f32, VdotH: f32, NdotL: f32, NdotV: f32, NdotH: f32) -> vec3<f32> {
|
|
129
|
+
let F = F_Schlick(f0, f90, VdotH);
|
|
130
|
+
let Vis = V_GGX(NdotL, NdotV, alphaRoughness);
|
|
131
|
+
let D = D_GGX(NdotH, alphaRoughness);
|
|
132
|
+
return specularWeight * F * Vis * D;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ============================================
|
|
136
|
+
// Squircle distance for cascade blending
|
|
137
|
+
// ============================================
|
|
138
|
+
|
|
139
|
+
fn squircleDistanceXZ(offset: vec2f, size: f32) -> f32 {
|
|
140
|
+
let normalized = offset / size;
|
|
141
|
+
let absNorm = abs(normalized);
|
|
142
|
+
return pow(pow(absNorm.x, 4.0) + pow(absNorm.y, 4.0), 0.25);
|
|
143
|
+
}
|
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
// Particle rendering shader
|
|
2
|
+
// Renders billboarded quads for each alive particle with soft depth fade
|
|
3
|
+
// Supports lighting, shadows, point/spot lights, IBL, and emissive brightness
|
|
4
|
+
|
|
5
|
+
const PI = 3.14159265359;
|
|
6
|
+
const CASCADE_COUNT = 3;
|
|
7
|
+
const MAX_EMITTERS = 16u;
|
|
8
|
+
const MAX_LIGHTS = 64u;
|
|
9
|
+
const MAX_SPOT_SHADOWS = 8;
|
|
10
|
+
|
|
11
|
+
// Spot shadow atlas constants (must match LightingPass)
|
|
12
|
+
const SPOT_ATLAS_WIDTH: f32 = 2048.0;
|
|
13
|
+
const SPOT_ATLAS_HEIGHT: f32 = 2048.0;
|
|
14
|
+
const SPOT_TILE_SIZE: f32 = 512.0;
|
|
15
|
+
const SPOT_TILES_PER_ROW: i32 = 4;
|
|
16
|
+
|
|
17
|
+
// Particle data structure (must match particle_simulate.wgsl)
|
|
18
|
+
struct Particle {
|
|
19
|
+
position: vec3f,
|
|
20
|
+
lifetime: f32,
|
|
21
|
+
velocity: vec3f,
|
|
22
|
+
maxLifetime: f32,
|
|
23
|
+
color: vec4f,
|
|
24
|
+
size: vec2f,
|
|
25
|
+
rotation: f32, // Current rotation in radians
|
|
26
|
+
flags: u32,
|
|
27
|
+
lighting: vec3f, // Pre-computed lighting (smoothed in compute shader)
|
|
28
|
+
lightingPad: f32,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Light structure (must match LightingPass)
|
|
32
|
+
struct Light {
|
|
33
|
+
enabled: u32,
|
|
34
|
+
position: vec3f,
|
|
35
|
+
color: vec4f,
|
|
36
|
+
direction: vec3f,
|
|
37
|
+
geom: vec4f, // x = radius, y = inner cone, z = outer cone, w = distance fade
|
|
38
|
+
shadowIndex: i32, // -1 if no shadow, 0-7 for spot shadow slot
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Per-emitter settings (must match ParticlePass.js)
|
|
42
|
+
struct EmitterRenderSettings {
|
|
43
|
+
lit: f32, // 0 = unlit, 1 = lit
|
|
44
|
+
emissive: f32, // Brightness multiplier (1 = normal, >1 = glow)
|
|
45
|
+
softness: f32,
|
|
46
|
+
zOffset: f32,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Spot shadow matrices
|
|
50
|
+
struct SpotShadowMatrices {
|
|
51
|
+
matrices: array<mat4x4<f32>, MAX_SPOT_SHADOWS>,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
struct ParticleUniforms {
|
|
55
|
+
viewMatrix: mat4x4f,
|
|
56
|
+
projectionMatrix: mat4x4f,
|
|
57
|
+
cameraPosition: vec3f,
|
|
58
|
+
time: f32,
|
|
59
|
+
cameraRight: vec3f,
|
|
60
|
+
softness: f32,
|
|
61
|
+
cameraUp: vec3f,
|
|
62
|
+
zOffset: f32,
|
|
63
|
+
screenSize: vec2f,
|
|
64
|
+
near: f32,
|
|
65
|
+
far: f32,
|
|
66
|
+
blendMode: f32, // 0 = alpha, 1 = additive
|
|
67
|
+
lit: f32, // 0 = unlit, 1 = simple lighting (global fallback)
|
|
68
|
+
shadowBias: f32,
|
|
69
|
+
shadowStrength: f32,
|
|
70
|
+
// Lighting uniforms
|
|
71
|
+
lightDir: vec3f,
|
|
72
|
+
shadowMapSize: f32,
|
|
73
|
+
lightColor: vec4f,
|
|
74
|
+
ambientColor: vec4f,
|
|
75
|
+
cascadeSizes: vec4f, // x, y, z = cascade half-widths
|
|
76
|
+
// IBL uniforms
|
|
77
|
+
envParams: vec4f, // x = diffuse level, y = mip count, z = encoding (0=equirect, 1=octahedral), w = exposure
|
|
78
|
+
// Light count
|
|
79
|
+
lightParams: vec4u, // x = light count, y = unused, z = unused, w = unused
|
|
80
|
+
// Fog uniforms
|
|
81
|
+
fogColor: vec3f,
|
|
82
|
+
fogEnabled: f32,
|
|
83
|
+
fogDistances: vec3f, // [near, mid, far]
|
|
84
|
+
fogBrightResist: f32,
|
|
85
|
+
fogAlphas: vec3f, // [nearAlpha, midAlpha, farAlpha]
|
|
86
|
+
fogPad1: f32,
|
|
87
|
+
fogHeightFade: vec2f, // [bottomY, topY]
|
|
88
|
+
fogPad2: vec2f,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
struct CascadeMatrices {
|
|
92
|
+
matrices: array<mat4x4<f32>, CASCADE_COUNT>,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
struct VertexOutput {
|
|
96
|
+
@builtin(position) position: vec4f,
|
|
97
|
+
@location(0) uv: vec2f,
|
|
98
|
+
@location(1) color: vec4f,
|
|
99
|
+
@location(2) viewZ: f32,
|
|
100
|
+
@location(3) linearDepth: f32, // For frag_depth output
|
|
101
|
+
@location(4) lighting: vec3f, // Pre-computed lighting from particle
|
|
102
|
+
@location(5) @interpolate(flat) emitterIdx: u32, // For per-emitter settings
|
|
103
|
+
@location(6) worldPos: vec3f, // For fog height fade
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@group(0) @binding(0) var<uniform> uniforms: ParticleUniforms;
|
|
107
|
+
@group(0) @binding(1) var<storage, read> particles: array<Particle>;
|
|
108
|
+
@group(0) @binding(2) var particleTexture: texture_2d<f32>;
|
|
109
|
+
@group(0) @binding(3) var particleSampler: sampler;
|
|
110
|
+
@group(0) @binding(4) var depthTexture: texture_depth_2d;
|
|
111
|
+
@group(0) @binding(5) var shadowMapArray: texture_depth_2d_array;
|
|
112
|
+
@group(0) @binding(6) var shadowSampler: sampler_comparison;
|
|
113
|
+
@group(0) @binding(7) var<storage, read> cascadeMatrices: CascadeMatrices;
|
|
114
|
+
@group(0) @binding(8) var<storage, read> emitterSettings: array<EmitterRenderSettings, MAX_EMITTERS>;
|
|
115
|
+
@group(0) @binding(9) var envMap: texture_2d<f32>;
|
|
116
|
+
@group(0) @binding(10) var envSampler: sampler;
|
|
117
|
+
// Point/spot lights
|
|
118
|
+
@group(0) @binding(11) var<storage, read> lights: array<Light, MAX_LIGHTS>;
|
|
119
|
+
// Spot shadow atlas
|
|
120
|
+
@group(0) @binding(12) var spotShadowAtlas: texture_depth_2d;
|
|
121
|
+
@group(0) @binding(13) var spotShadowSampler: sampler_comparison;
|
|
122
|
+
@group(0) @binding(14) var<storage, read> spotMatrices: SpotShadowMatrices;
|
|
123
|
+
|
|
124
|
+
// Quad vertices: 0=bottom-left, 1=bottom-right, 2=top-left, 3=top-right
|
|
125
|
+
// Two triangles: 0,1,2 and 1,3,2
|
|
126
|
+
fn getQuadVertex(vertexId: u32) -> vec2f {
|
|
127
|
+
// Map 6 vertices to 4 quad corners
|
|
128
|
+
// Triangle 1: 0,1,2 -> BL, BR, TL
|
|
129
|
+
// Triangle 2: 3,4,5 -> BR, TR, TL
|
|
130
|
+
var corners = array<vec2f, 6>(
|
|
131
|
+
vec2f(-0.5, -0.5), // 0: BL
|
|
132
|
+
vec2f(0.5, -0.5), // 1: BR
|
|
133
|
+
vec2f(-0.5, 0.5), // 2: TL
|
|
134
|
+
vec2f(0.5, -0.5), // 3: BR
|
|
135
|
+
vec2f(0.5, 0.5), // 4: TR
|
|
136
|
+
vec2f(-0.5, 0.5) // 5: TL
|
|
137
|
+
);
|
|
138
|
+
return corners[vertexId];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
fn getQuadUV(vertexId: u32) -> vec2f {
|
|
142
|
+
var uvs = array<vec2f, 6>(
|
|
143
|
+
vec2f(0.0, 1.0), // 0: BL
|
|
144
|
+
vec2f(1.0, 1.0), // 1: BR
|
|
145
|
+
vec2f(0.0, 0.0), // 2: TL
|
|
146
|
+
vec2f(1.0, 1.0), // 3: BR
|
|
147
|
+
vec2f(1.0, 0.0), // 4: TR
|
|
148
|
+
vec2f(0.0, 0.0) // 5: TL
|
|
149
|
+
);
|
|
150
|
+
return uvs[vertexId];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@vertex
|
|
154
|
+
fn vertexMain(
|
|
155
|
+
@builtin(vertex_index) vertexIndex: u32,
|
|
156
|
+
@builtin(instance_index) instanceIndex: u32
|
|
157
|
+
) -> VertexOutput {
|
|
158
|
+
var output: VertexOutput;
|
|
159
|
+
|
|
160
|
+
// Get particle data
|
|
161
|
+
let particle = particles[instanceIndex];
|
|
162
|
+
|
|
163
|
+
// Get emitter index from flags (bits 8-15)
|
|
164
|
+
let emitterIdx = (particle.flags >> 8u) & 0xFFu;
|
|
165
|
+
output.emitterIdx = emitterIdx;
|
|
166
|
+
|
|
167
|
+
// Check if particle is alive
|
|
168
|
+
if ((particle.flags & 1u) == 0u || particle.lifetime <= 0.0) {
|
|
169
|
+
// Dead particle - render degenerate triangle
|
|
170
|
+
output.position = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
171
|
+
output.uv = vec2f(0.0, 0.0);
|
|
172
|
+
output.color = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
173
|
+
output.viewZ = 0.0;
|
|
174
|
+
output.linearDepth = 0.0;
|
|
175
|
+
output.lighting = vec3f(0.0);
|
|
176
|
+
output.worldPos = vec3f(0.0);
|
|
177
|
+
return output;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Check blend mode: bit 1 of flags = additive (1) or alpha (0)
|
|
181
|
+
// uniforms.blendMode: 1.0 = additive, 0.0 = alpha
|
|
182
|
+
let particleIsAdditive = (particle.flags & 2u) != 0u;
|
|
183
|
+
let renderingAdditive = uniforms.blendMode > 0.5;
|
|
184
|
+
if (particleIsAdditive != renderingAdditive) {
|
|
185
|
+
// Wrong blend mode for this pass - skip
|
|
186
|
+
output.position = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
187
|
+
output.uv = vec2f(0.0, 0.0);
|
|
188
|
+
output.color = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
189
|
+
output.viewZ = 0.0;
|
|
190
|
+
output.linearDepth = 0.0;
|
|
191
|
+
output.lighting = vec3f(0.0);
|
|
192
|
+
output.worldPos = vec3f(0.0);
|
|
193
|
+
return output;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Get quad vertex position and UV
|
|
197
|
+
let localVertexId = vertexIndex % 6u;
|
|
198
|
+
var quadPos = getQuadVertex(localVertexId);
|
|
199
|
+
output.uv = getQuadUV(localVertexId);
|
|
200
|
+
|
|
201
|
+
// Apply rotation to quad position
|
|
202
|
+
let cosR = cos(particle.rotation);
|
|
203
|
+
let sinR = sin(particle.rotation);
|
|
204
|
+
let rotatedPos = vec2f(
|
|
205
|
+
quadPos.x * cosR - quadPos.y * sinR,
|
|
206
|
+
quadPos.x * sinR + quadPos.y * cosR
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
// Billboard: create quad facing camera
|
|
210
|
+
let particleWorldPos = particle.position;
|
|
211
|
+
|
|
212
|
+
// Scale rotated quad by particle size
|
|
213
|
+
let scaledOffset = rotatedPos * particle.size;
|
|
214
|
+
|
|
215
|
+
// Create billboard position using camera vectors
|
|
216
|
+
let right = uniforms.cameraRight;
|
|
217
|
+
let up = uniforms.cameraUp;
|
|
218
|
+
let billboardPos = particleWorldPos + right * scaledOffset.x + up * scaledOffset.y;
|
|
219
|
+
|
|
220
|
+
// Apply z-offset along view direction to prevent z-fighting
|
|
221
|
+
let toCamera = normalize(uniforms.cameraPosition - particleWorldPos);
|
|
222
|
+
let offsetPos = billboardPos + toCamera * uniforms.zOffset;
|
|
223
|
+
|
|
224
|
+
// Transform to clip space
|
|
225
|
+
let viewPos = uniforms.viewMatrix * vec4f(offsetPos, 1.0);
|
|
226
|
+
output.position = uniforms.projectionMatrix * viewPos;
|
|
227
|
+
output.viewZ = -viewPos.z; // Positive depth
|
|
228
|
+
|
|
229
|
+
// Calculate linear depth matching GBuffer format: (z - near) / (far - near)
|
|
230
|
+
let z = -viewPos.z; // View space Z (positive into screen)
|
|
231
|
+
output.linearDepth = (z - uniforms.near) / (uniforms.far - uniforms.near);
|
|
232
|
+
|
|
233
|
+
// Pass pre-computed lighting from particle (calculated in compute shader)
|
|
234
|
+
output.lighting = particle.lighting;
|
|
235
|
+
|
|
236
|
+
// Pass through particle color
|
|
237
|
+
output.color = particle.color;
|
|
238
|
+
|
|
239
|
+
// Pass world position for fog
|
|
240
|
+
output.worldPos = particleWorldPos;
|
|
241
|
+
|
|
242
|
+
return output;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Equirectangular UV from direction
|
|
246
|
+
fn SphToUV(n: vec3f) -> vec2f {
|
|
247
|
+
var uv: vec2f;
|
|
248
|
+
uv.x = atan2(-n.x, n.z);
|
|
249
|
+
uv.x = (uv.x + PI / 2.0) / (PI * 2.0) + PI * (28.670 / 360.0);
|
|
250
|
+
uv.y = acos(n.y) / PI;
|
|
251
|
+
return uv;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Octahedral encoding: direction to UV
|
|
255
|
+
fn octEncode(n: vec3f) -> vec2f {
|
|
256
|
+
var n2 = n / (abs(n.x) + abs(n.y) + abs(n.z));
|
|
257
|
+
if (n2.y < 0.0) {
|
|
258
|
+
let signX = select(-1.0, 1.0, n2.x >= 0.0);
|
|
259
|
+
let signZ = select(-1.0, 1.0, n2.z >= 0.0);
|
|
260
|
+
n2 = vec3f(
|
|
261
|
+
(1.0 - abs(n2.z)) * signX,
|
|
262
|
+
n2.y,
|
|
263
|
+
(1.0 - abs(n2.x)) * signZ
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
return n2.xz * 0.5 + 0.5;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Get environment UV based on encoding type
|
|
270
|
+
fn getEnvUV(dir: vec3f) -> vec2f {
|
|
271
|
+
if (uniforms.envParams.z > 0.5) {
|
|
272
|
+
return octEncode(dir);
|
|
273
|
+
}
|
|
274
|
+
return SphToUV(dir);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Sample IBL at direction with LOD
|
|
278
|
+
fn getIBLSample(dir: vec3f, lod: f32) -> vec3f {
|
|
279
|
+
let envRGBE = textureSampleLevel(envMap, envSampler, getEnvUV(dir), lod);
|
|
280
|
+
// RGBE decode
|
|
281
|
+
let envColor = envRGBE.rgb * pow(2.0, envRGBE.a * 255.0 - 128.0);
|
|
282
|
+
return envColor;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Squircle distance - returns distance normalized to cascade size
|
|
286
|
+
fn squircleDistanceXZ(offset: vec2f, size: f32) -> f32 {
|
|
287
|
+
let normalized = offset / size;
|
|
288
|
+
let absNorm = abs(normalized);
|
|
289
|
+
return pow(pow(absNorm.x, 4.0) + pow(absNorm.y, 4.0), 0.25);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Sample shadow from a specific cascade (simplified for particles)
|
|
293
|
+
fn sampleCascadeShadow(worldPos: vec3f, normal: vec3f, cascadeIndex: i32) -> f32 {
|
|
294
|
+
let bias = uniforms.shadowBias;
|
|
295
|
+
let shadowMapSize = uniforms.shadowMapSize;
|
|
296
|
+
|
|
297
|
+
// Apply normal bias
|
|
298
|
+
let biasedPos = worldPos + normal * bias * 0.5;
|
|
299
|
+
|
|
300
|
+
// Get cascade matrix
|
|
301
|
+
let lightMatrix = cascadeMatrices.matrices[cascadeIndex];
|
|
302
|
+
|
|
303
|
+
// Transform to light space
|
|
304
|
+
let lightSpacePos = lightMatrix * vec4f(biasedPos, 1.0);
|
|
305
|
+
let projCoords = lightSpacePos.xyz / lightSpacePos.w;
|
|
306
|
+
|
|
307
|
+
// Transform to [0,1] UV space
|
|
308
|
+
let shadowUV = vec2f(projCoords.x * 0.5 + 0.5, 0.5 - projCoords.y * 0.5);
|
|
309
|
+
let currentDepth = projCoords.z - bias;
|
|
310
|
+
|
|
311
|
+
// Check bounds
|
|
312
|
+
let inBoundsX = shadowUV.x >= 0.0 && shadowUV.x <= 1.0;
|
|
313
|
+
let inBoundsY = shadowUV.y >= 0.0 && shadowUV.y <= 1.0;
|
|
314
|
+
let inBoundsZ = currentDepth >= 0.0 && currentDepth <= 1.0;
|
|
315
|
+
|
|
316
|
+
if (!inBoundsX || !inBoundsY || !inBoundsZ) {
|
|
317
|
+
return 1.0; // Out of bounds = lit
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
let clampedUV = clamp(shadowUV, vec2f(0.001), vec2f(0.999));
|
|
321
|
+
let clampedDepth = clamp(currentDepth, 0.001, 0.999);
|
|
322
|
+
|
|
323
|
+
// Simple 4-tap PCF for particles (fast)
|
|
324
|
+
let texelSize = 1.0 / shadowMapSize;
|
|
325
|
+
var shadow = 0.0;
|
|
326
|
+
shadow += textureSampleCompareLevel(shadowMapArray, shadowSampler, clampedUV + vec2f(-texelSize, 0.0), cascadeIndex, clampedDepth);
|
|
327
|
+
shadow += textureSampleCompareLevel(shadowMapArray, shadowSampler, clampedUV + vec2f(texelSize, 0.0), cascadeIndex, clampedDepth);
|
|
328
|
+
shadow += textureSampleCompareLevel(shadowMapArray, shadowSampler, clampedUV + vec2f(0.0, -texelSize), cascadeIndex, clampedDepth);
|
|
329
|
+
shadow += textureSampleCompareLevel(shadowMapArray, shadowSampler, clampedUV + vec2f(0.0, texelSize), cascadeIndex, clampedDepth);
|
|
330
|
+
shadow /= 4.0;
|
|
331
|
+
|
|
332
|
+
return shadow;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Calculate cascaded shadow for particles (simplified cascade selection)
|
|
336
|
+
fn calculateParticleShadow(worldPos: vec3f, normal: vec3f) -> f32 {
|
|
337
|
+
let shadowStrength = uniforms.shadowStrength;
|
|
338
|
+
|
|
339
|
+
// Calculate XZ offset from camera
|
|
340
|
+
let camXZ = vec2f(uniforms.cameraPosition.x, uniforms.cameraPosition.z);
|
|
341
|
+
let posXZ = vec2f(worldPos.x, worldPos.z);
|
|
342
|
+
let offsetXZ = posXZ - camXZ;
|
|
343
|
+
|
|
344
|
+
// Cascade sizes
|
|
345
|
+
let cascade0Size = uniforms.cascadeSizes.x;
|
|
346
|
+
let cascade1Size = uniforms.cascadeSizes.y;
|
|
347
|
+
let cascade2Size = uniforms.cascadeSizes.z;
|
|
348
|
+
|
|
349
|
+
let dist0 = squircleDistanceXZ(offsetXZ, cascade0Size);
|
|
350
|
+
let dist1 = squircleDistanceXZ(offsetXZ, cascade1Size);
|
|
351
|
+
let dist2 = squircleDistanceXZ(offsetXZ, cascade2Size);
|
|
352
|
+
|
|
353
|
+
var shadow = 1.0;
|
|
354
|
+
|
|
355
|
+
// Simple cascade selection (no blending for performance)
|
|
356
|
+
if (dist0 < 0.95) {
|
|
357
|
+
shadow = sampleCascadeShadow(worldPos, normal, 0);
|
|
358
|
+
} else if (dist1 < 0.95) {
|
|
359
|
+
shadow = sampleCascadeShadow(worldPos, normal, 1);
|
|
360
|
+
} else if (dist2 < 0.95) {
|
|
361
|
+
shadow = sampleCascadeShadow(worldPos, normal, 2);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Apply shadow strength
|
|
365
|
+
return mix(1.0 - shadowStrength, 1.0, shadow);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Calculate spot light shadow (simplified for particles)
|
|
369
|
+
fn calculateSpotShadow(worldPos: vec3f, normal: vec3f, slotIndex: i32) -> f32 {
|
|
370
|
+
if (slotIndex < 0 || slotIndex >= MAX_SPOT_SHADOWS) {
|
|
371
|
+
return 1.0; // No shadow
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
let bias = uniforms.shadowBias;
|
|
375
|
+
let normalBias = bias * 2.0; // Increased for spot lights
|
|
376
|
+
|
|
377
|
+
// Apply normal bias
|
|
378
|
+
let biasedPos = worldPos + normal * normalBias;
|
|
379
|
+
|
|
380
|
+
// Get the light matrix from storage buffer
|
|
381
|
+
let lightMatrix = spotMatrices.matrices[slotIndex];
|
|
382
|
+
|
|
383
|
+
// Transform to light space
|
|
384
|
+
let lightSpacePos = lightMatrix * vec4f(biasedPos, 1.0);
|
|
385
|
+
|
|
386
|
+
// Perspective divide
|
|
387
|
+
let w = max(abs(lightSpacePos.w), 0.0001) * sign(lightSpacePos.w + 0.0001);
|
|
388
|
+
let projCoords = lightSpacePos.xyz / w;
|
|
389
|
+
|
|
390
|
+
// Check if outside frustum
|
|
391
|
+
if (projCoords.z < 0.0 || projCoords.z > 1.0 ||
|
|
392
|
+
abs(projCoords.x) > 1.0 || abs(projCoords.y) > 1.0) {
|
|
393
|
+
return 1.0; // Outside shadow frustum
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Calculate tile position in atlas
|
|
397
|
+
let col = slotIndex % SPOT_TILES_PER_ROW;
|
|
398
|
+
let row = slotIndex / SPOT_TILES_PER_ROW;
|
|
399
|
+
|
|
400
|
+
// Transform to [0,1] UV within tile
|
|
401
|
+
let localUV = vec2f(projCoords.x * 0.5 + 0.5, 0.5 - projCoords.y * 0.5);
|
|
402
|
+
|
|
403
|
+
// Transform to atlas UV
|
|
404
|
+
let tileOffset = vec2f(f32(col), f32(row)) * SPOT_TILE_SIZE;
|
|
405
|
+
let atlasUV = (tileOffset + localUV * SPOT_TILE_SIZE) / vec2f(SPOT_ATLAS_WIDTH, SPOT_ATLAS_HEIGHT);
|
|
406
|
+
|
|
407
|
+
// Sample shadow with simple 4-tap PCF
|
|
408
|
+
let texelSize = 1.0 / SPOT_TILE_SIZE;
|
|
409
|
+
let currentDepth = clamp(projCoords.z - bias * 3.0, 0.001, 0.999);
|
|
410
|
+
|
|
411
|
+
var shadowSample = 0.0;
|
|
412
|
+
shadowSample += textureSampleCompareLevel(spotShadowAtlas, spotShadowSampler, atlasUV + vec2f(-texelSize, 0.0), currentDepth);
|
|
413
|
+
shadowSample += textureSampleCompareLevel(spotShadowAtlas, spotShadowSampler, atlasUV + vec2f(texelSize, 0.0), currentDepth);
|
|
414
|
+
shadowSample += textureSampleCompareLevel(spotShadowAtlas, spotShadowSampler, atlasUV + vec2f(0.0, -texelSize), currentDepth);
|
|
415
|
+
shadowSample += textureSampleCompareLevel(spotShadowAtlas, spotShadowSampler, atlasUV + vec2f(0.0, texelSize), currentDepth);
|
|
416
|
+
shadowSample /= 4.0;
|
|
417
|
+
|
|
418
|
+
return shadowSample;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Apply pre-computed lighting to particle color
|
|
422
|
+
// Lighting is calculated per-particle in the compute shader with temporal smoothing
|
|
423
|
+
fn applyLighting(baseColor: vec3f, lighting: vec3f) -> vec3f {
|
|
424
|
+
// Just multiply base color by pre-computed lighting
|
|
425
|
+
// Lighting already includes ambient, shadows, point/spot lights, and emissive
|
|
426
|
+
return baseColor * lighting;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
struct FragmentOutput {
|
|
430
|
+
@location(0) color: vec4f,
|
|
431
|
+
@builtin(frag_depth) depth: f32,
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Calculate soft particle fade based on depth difference
|
|
435
|
+
fn calcSoftFade(fragPos: vec4f, particleLinearDepth: f32) -> f32 {
|
|
436
|
+
if (uniforms.softness <= 0.0) {
|
|
437
|
+
return 1.0;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Get screen coordinates
|
|
441
|
+
let screenPos = vec2i(fragPos.xy);
|
|
442
|
+
let screenSize = vec2i(uniforms.screenSize);
|
|
443
|
+
|
|
444
|
+
// Bounds check
|
|
445
|
+
if (screenPos.x < 0 || screenPos.x >= screenSize.x ||
|
|
446
|
+
screenPos.y < 0 || screenPos.y >= screenSize.y) {
|
|
447
|
+
return 1.0;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Sample scene depth (linear depth in 0-1 range from GBuffer)
|
|
451
|
+
let sceneDepthNorm = textureLoad(depthTexture, screenPos, 0);
|
|
452
|
+
|
|
453
|
+
// If depth is 0 (no valid data), skip soft fade
|
|
454
|
+
if (sceneDepthNorm <= 0.0) {
|
|
455
|
+
return 1.0;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Convert both to world units for comparison
|
|
459
|
+
let sceneDepth = uniforms.near + sceneDepthNorm * (uniforms.far - uniforms.near);
|
|
460
|
+
let particleDepth = uniforms.near + particleLinearDepth * (uniforms.far - uniforms.near);
|
|
461
|
+
|
|
462
|
+
// Fade based on depth difference (in world units)
|
|
463
|
+
let depthDiff = sceneDepth - particleDepth;
|
|
464
|
+
return saturate(depthDiff / uniforms.softness);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Fragment for alpha blend mode
|
|
468
|
+
@fragment
|
|
469
|
+
fn fragmentMainAlpha(input: VertexOutput) -> FragmentOutput {
|
|
470
|
+
var output: FragmentOutput;
|
|
471
|
+
|
|
472
|
+
// Sample texture directly
|
|
473
|
+
let texColor = textureSample(particleTexture, particleSampler, input.uv);
|
|
474
|
+
|
|
475
|
+
// Combine texture with particle color
|
|
476
|
+
var alpha = texColor.a * input.color.a;
|
|
477
|
+
|
|
478
|
+
// Apply soft particle fade
|
|
479
|
+
alpha *= calcSoftFade(input.position, input.linearDepth);
|
|
480
|
+
|
|
481
|
+
// Discard nearly transparent pixels
|
|
482
|
+
if (alpha < 0.001) {
|
|
483
|
+
discard;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Apply pre-computed lighting from particle
|
|
487
|
+
let baseColor = texColor.rgb * input.color.rgb;
|
|
488
|
+
let litColor = applyLighting(baseColor, input.lighting);
|
|
489
|
+
|
|
490
|
+
// Fog is applied as post-process to the combined HDR buffer
|
|
491
|
+
output.color = vec4f(litColor, alpha);
|
|
492
|
+
output.depth = input.linearDepth;
|
|
493
|
+
return output;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Fragment for additive mode
|
|
497
|
+
@fragment
|
|
498
|
+
fn fragmentMainAdditive(input: VertexOutput) -> FragmentOutput {
|
|
499
|
+
var output: FragmentOutput;
|
|
500
|
+
|
|
501
|
+
// Sample texture directly
|
|
502
|
+
let texColor = textureSample(particleTexture, particleSampler, input.uv);
|
|
503
|
+
|
|
504
|
+
// Combine texture with particle color
|
|
505
|
+
var alpha = texColor.a * input.color.a;
|
|
506
|
+
|
|
507
|
+
// Apply soft particle fade
|
|
508
|
+
alpha *= calcSoftFade(input.position, input.linearDepth);
|
|
509
|
+
|
|
510
|
+
// Discard nearly transparent pixels
|
|
511
|
+
if (alpha < 0.001) {
|
|
512
|
+
discard;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Apply pre-computed lighting from particle
|
|
516
|
+
let baseColor = texColor.rgb * input.color.rgb;
|
|
517
|
+
let litColor = applyLighting(baseColor, input.lighting);
|
|
518
|
+
|
|
519
|
+
// Fog is applied as post-process to the combined HDR buffer
|
|
520
|
+
// Premultiply for additive blending
|
|
521
|
+
let rgb = litColor * alpha;
|
|
522
|
+
output.color = vec4f(rgb, alpha);
|
|
523
|
+
output.depth = input.linearDepth;
|
|
524
|
+
return output;
|
|
525
|
+
}
|