Skip to content

Commit fb8f8a3

Browse files
committed
Client: Add atmosphere scattering
1 parent 1120e99 commit fb8f8a3

10 files changed

Lines changed: 896 additions & 3 deletions

assets/3d.passlist

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,20 @@ passlist "Forward Passlist"
3434
flag "LightShadowing"
3535
}
3636

37+
attachment "AtmosphereOutput"
38+
{
39+
format "RGBA16F"
40+
}
41+
42+
pass "AtmospherePass"
43+
{
44+
impl "TSOM_AtmosphereScattering"
45+
46+
input "Color" "ForwardOutput"
47+
input "Depth" "DepthBuffer"
48+
output "Output" "AtmosphereOutput"
49+
}
50+
3751
attachment "Gamma corrected"
3852
{
3953
format "RGBA8"
@@ -46,7 +60,7 @@ passlist "Forward Passlist"
4660
Shader "PostProcess.GammaCorrection"
4761
}
4862

49-
input "Input" "ForwardOutput"
63+
input "Input" "AtmosphereOutput"
5064
output "Output" "Gamma corrected"
5165
}
5266

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,20 @@ passlist "Forward Passlist"
3434
flag "LightShadowing"
3535
}
3636

37+
attachment "AtmosphereOutput"
38+
{
39+
format "RGBA16F"
40+
}
41+
42+
pass "AtmospherePass"
43+
{
44+
impl "TSOM_AtmosphereScattering"
45+
46+
input "Color" "ForwardOutput"
47+
input "Depth" "DepthBuffer"
48+
output "Output" "AtmosphereOutput"
49+
}
50+
3751
attachment "Gamma corrected"
3852
{
3953
format "RGBA8"
@@ -46,7 +60,7 @@ passlist "Forward Passlist"
4660
Shader "PostProcess.GammaCorrection"
4761
}
4862

49-
input "Input" "ForwardOutput"
63+
input "Input" "AtmosphereOutput"
5064
output "Output" "Gamma corrected"
5165
}
5266

Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
1+
[nzsl_version("1.0")]
2+
module TSOM.PlanetAtmosphere;
3+
4+
import VertexShader from Engine.FullscreenVertex;
5+
6+
option MaxAtmosphereCount: u32;
7+
8+
[layout(std140)]
9+
struct AtmosphereScatteringData
10+
{
11+
sunDir: vec3[f32],
12+
sunIntensity: vec3[f32],
13+
14+
planetPosition: vec3[f32],
15+
atmosphereRadius: f32,
16+
planetRadius: f32,
17+
18+
// scattering coeffs
19+
rayleighBeta: vec3[f32], // rayleigh, affects the color of the sky
20+
mieBeta: vec3[f32], // mie, affects the color of the blob around the sun
21+
ambientBeta: vec3[f32], // ambient, affects the scattering color when there is no lighting from the sun
22+
absorptionBeta: vec3[f32], // what color gets absorbed by the atmosphere (Due to things like ozone)
23+
mieScattering: f32, // mie scattering direction, or how big the blob around the sun is
24+
25+
// and the heights (how far to go up before the scattering has no effect)
26+
rayleighHeight: f32, // rayleigh height
27+
mieHeight: f32, // and mie
28+
heightAbsorption: f32, // at what height the absorption is at it's maximum
29+
absorptionFalloff: f32, // how much the absorption decreases the further away it gets from the maximum height
30+
31+
// and the steps (more looks better, but is slower)
32+
// the primary step has the most effect on looks
33+
primarySteps: i32,
34+
lightSteps: i32,
35+
}
36+
37+
[layout(std140)]
38+
struct PassData
39+
{
40+
invProjectionMatrix: mat4[f32],
41+
invViewMatrix: mat4[f32],
42+
viewerPosition: vec3[f32],
43+
zNear: f32,
44+
45+
atmosphereCount: u32,
46+
atmospheres: array[AtmosphereScatteringData, MaxAtmosphereCount]
47+
}
48+
49+
external
50+
{
51+
[binding(0)] passData: uniform[PassData],
52+
[binding(1)] colorTexture: sampler2D[f32],
53+
[binding(2)] depthTexture: sampler2D[f32],
54+
}
55+
56+
// Fragment stage
57+
struct VertOut
58+
{
59+
[builtin(position)] position: vec4[f32],
60+
[builtin(frag_coord)] frag_coord: vec4[f32],
61+
[location(0)] uv: vec2[f32]
62+
}
63+
64+
struct FragOut
65+
{
66+
[location(0)] color: vec4[f32]
67+
}
68+
69+
[entry(frag), depth_write(less)]
70+
fn main(input: VertOut) -> FragOut
71+
{
72+
let viewVector = passData.invProjectionMatrix * vec4[f32](input.uv * 2.0 - (1.0).xx, 0.0, 1.0);
73+
viewVector = passData.invViewMatrix * vec4[f32](viewVector.xyz, 0.0);
74+
75+
let viewVector = normalize(viewVector.xyz);
76+
77+
let cameraPos = passData.viewerPosition;
78+
79+
let planetDims = (80.0).xxx;
80+
let planetCornerRadius = 16.0;
81+
82+
let color = colorTexture.Sample(input.uv).rgb;
83+
let depth = depthTexture.Sample(input.uv).x;
84+
85+
let linearDepth = passData.zNear / depth;
86+
87+
// get the atmosphere color
88+
for atmosphereIndex in u32(0) -> passData.atmosphereCount
89+
{
90+
let atmosphere = passData.atmospheres[atmosphereIndex];
91+
92+
color = calculate_scattering(
93+
cameraPos, // the position of the camera
94+
viewVector, // the camera vector (ray direction of this pixel)
95+
linearDepth, // max dist, essentially the scene depth
96+
color, // scene color, the color of the current pixel being rendered
97+
atmosphere.sunDir, // light direction
98+
atmosphere.sunIntensity, // light intensity, 40 looks nice
99+
atmosphere.planetPosition, // position of the planet
100+
atmosphere.planetRadius, // radius of the planet in meters
101+
atmosphere.atmosphereRadius, // radius of the atmosphere in meters
102+
atmosphere.rayleighBeta, // Rayleigh scattering coefficient
103+
atmosphere.mieBeta, // Mie scattering coefficient
104+
atmosphere.absorptionBeta, // Absorbtion coefficient
105+
atmosphere.ambientBeta, // ambient scattering, turned off for now. This causes the air to glow a bit when no light reaches it
106+
atmosphere.mieScattering, // Mie preferred scattering direction
107+
atmosphere.rayleighHeight, // Rayleigh scale height
108+
atmosphere.mieHeight, // Mie scale height
109+
atmosphere.heightAbsorption, // the height at which the most absorption happens
110+
atmosphere.absorptionFalloff,// how fast the absorption falls off from the absorption height
111+
atmosphere.primarySteps, // steps in the ray direction
112+
atmosphere.lightSteps // steps in the light direction
113+
);
114+
}
115+
116+
let output: FragOut;
117+
output.color = vec4[f32](color, 1.0);
118+
119+
return output;
120+
}
121+
122+
// From https://www.shadertoy.com/view/wlBXWK
123+
/*
124+
Next we'll define the main scattering function.
125+
This traces a ray from start to end and takes a certain amount of samples along this ray, in order to calculate the color.
126+
For every sample, we'll also trace a ray in the direction of the light,
127+
because the color that reaches the sample also changes due to scattering
128+
*/
129+
fn calculate_scattering(
130+
start: vec3[f32], // the start of the ray (the camera position)
131+
dir: vec3[f32], // the direction of the ray (the camera vector)
132+
max_dist: f32, // the maximum distance the ray can travel (because something is in the way, like an object)
133+
scene_color: vec3[f32], // the color of the scene
134+
light_dir: vec3[f32], // the direction of the light
135+
light_intensity: vec3[f32], // how bright the light is, affects the brightness of the atmosphere
136+
planet_position: vec3[f32], // the position of the planet
137+
planetRadius: f32, // the ground dimensions of the planet
138+
atmo_radius: f32, // the radius of the atmosphere
139+
beta_ray: vec3[f32], // the amount rayleigh scattering scatters the colors (for earth: causes the blue atmosphere)
140+
beta_mie: vec3[f32], // the amount mie scattering scatters colors
141+
beta_absorption: vec3[f32], // how much air is absorbed
142+
beta_ambient: vec3[f32], // the amount of scattering that always occurs, cna help make the back side of the atmosphere a bit brighter
143+
g: f32, // the direction mie scatters the light in (like a cone). closer to -1 means more towards a single direction
144+
height_ray: f32, // how high do you have to go before there is no rayleigh scattering?
145+
height_mie: f32, // the same, but for mie
146+
height_absorption: f32, // the height at which the most absorption happens
147+
absorption_falloff: f32, // how fast the absorption falls off from the absorption height
148+
steps_i: i32, // the amount of steps along the 'primary' ray, more looks better but slower
149+
steps_l: i32 // the amount of steps along the light ray, more looks better but slower
150+
) -> vec3[f32] {
151+
// add an offset to the camera position, so that the atmosphere is in the correct position
152+
start -= planet_position;
153+
// calculate the start and end position of the ray, as a distance along the ray
154+
// we do this with a ray sphere intersect
155+
let a = dot(dir, dir);
156+
let b = 2.0 * dot(dir, start);
157+
let c = dot(start, start) - (atmo_radius * atmo_radius);
158+
let d = (b * b) - 4.0 * a * c;
159+
160+
// stop early if there is no intersect
161+
if (d < 0.0)
162+
return scene_color;
163+
164+
// calculate the ray length
165+
let sqrt_d = sqrt(d);
166+
let ray_length = vec2[f32](
167+
max((-b - sqrt_d) / (2.0 * a), 0.0),
168+
min((-b + sqrt_d) / (2.0 * a), max_dist)
169+
);
170+
171+
// if the ray did not hit the atmosphere, return a black color
172+
if (ray_length.x > ray_length.y)
173+
return scene_color;
174+
175+
// prevent the mie glow from appearing if there's an object in front of the camera
176+
let allow_mie = max_dist > ray_length.y;
177+
// make sure the ray is no longer than allowed
178+
ray_length.y = min(ray_length.y, max_dist);
179+
ray_length.x = max(ray_length.x, 0.0);
180+
// get the step size of the ray
181+
let step_size_i = (ray_length.y - ray_length.x) / f32(steps_i);
182+
183+
// next, set how far we are along the ray, so we can calculate the position of the sample
184+
// if the camera is outside the atmosphere, the ray should start at the edge of the atmosphere
185+
// if it's inside, it should start at the position of the camera
186+
// the min statement makes sure of that
187+
let ray_pos_i = ray_length.x + step_size_i * 0.5;
188+
189+
// these are the values we use to gather all the scattered light
190+
let total_ray = (0.0).xxx; // for rayleigh
191+
let total_mie = (0.0).xxx; // for mie
192+
193+
// initialize the optical depth. This is used to calculate how much air was in the ray
194+
let opt_i = (0.0).xxx;
195+
196+
// also init the scale height, avoids some vec2's later on
197+
let scale_height = vec2[f32](height_ray, height_mie);
198+
199+
// Calculate the Rayleigh and Mie phases.
200+
// This is the color that will be scattered for this ray
201+
// mu, mumu and gg are used quite a lot in the calculation, so to speed it up, precalculate them
202+
let mu = dot(dir, light_dir);
203+
let mumu = mu * mu;
204+
let gg = g * g;
205+
let phase_ray = 3.0 / (50.2654824574 /* (16 * pi) */) * (1.0 + mumu);
206+
let phase_mie = select(allow_mie, 3.0 / (25.1327412287 /* (8 * pi) */) * ((1.0 - gg) * (mumu + 1.0)) / (pow(1.0 + gg - 2.0 * mu * g, 1.5) * (2.0 + gg)), 0.0);
207+
208+
// now we need to sample the 'primary' ray. this ray gathers the light that gets scattered onto it
209+
for i in 0 -> steps_i
210+
{
211+
// calculate where we are along this ray
212+
let pos_i = start + dir * ray_pos_i;
213+
214+
// and how high we are above the surface
215+
//let height_i = sdRoundBox(pos_i, planet_dims, planet_corner_radius);
216+
let height_i = length(pos_i) - planetRadius;
217+
218+
// now calculate the density of the particles (both for rayleigh and mie)
219+
let density = vec3[f32](exp(-height_i / scale_height), 0.0);
220+
221+
// and the absorption density. this is for ozone, which scales together with the rayleigh,
222+
// but absorbs the most at a specific height, so use the sech function for a nice curve falloff for this height
223+
// clamp it to avoid it going out of bounds. This prevents weird black spheres on the night side
224+
let denom = (height_absorption - height_i) / absorption_falloff;
225+
density.z = (1.0 / (denom * denom + 1.0)) * density.x;
226+
227+
// multiply it by the step size here
228+
// we are going to use the density later on as well
229+
density *= step_size_i;
230+
231+
// Add these densities to the optical depth, so that we know how many particles are on this ray.
232+
opt_i += density;
233+
234+
// Calculate the step size of the light ray.
235+
// again with a ray sphere intersect
236+
// a, b, c and d are already defined
237+
a = dot(light_dir, light_dir);
238+
b = 2.0 * dot(light_dir, pos_i);
239+
c = dot(pos_i, pos_i) - (atmo_radius * atmo_radius);
240+
d = (b * b) - 4.0 * a * c;
241+
242+
// no early stopping, this one should always be inside the atmosphere
243+
// calculate the ray length
244+
let step_size_l = (-b + sqrt(d)) / (2.0 * a * f32(steps_l));
245+
246+
// and the position along this ray
247+
// this time we are sure the ray is in the atmosphere, so set it to 0
248+
let ray_pos_l = step_size_l * 0.5;
249+
250+
// and the optical depth of this ray
251+
let opt_l = (0.0).xxx;
252+
253+
// now sample the light ray
254+
// this is similar to what we did before
255+
for l in 0 -> steps_l
256+
{
257+
// calculate where we are along this ray
258+
let pos_l = pos_i + light_dir * ray_pos_l;
259+
260+
// the heigth of the position
261+
//let height_l = sdRoundBox(pos_l, planet_dims, planet_corner_radius);
262+
let height_l = length(pos_l) - planetRadius;
263+
264+
// calculate the particle density, and add it
265+
// this is a bit verbose
266+
// first, set the density for ray and mie
267+
let density_l = vec3[f32](exp(-height_l / scale_height), 0.0);
268+
269+
// then, the absorption
270+
let denom = (height_absorption - height_l) / absorption_falloff;
271+
density_l.z = (1.0 / (denom * denom + 1.0)) * density_l.x;
272+
273+
// multiply the density by the step size
274+
density_l *= step_size_l;
275+
276+
// and add it to the total optical depth
277+
opt_l += density_l;
278+
279+
// and increment where we are along the light ray.
280+
ray_pos_l += step_size_l;
281+
}
282+
283+
// Now we need to calculate the attenuation
284+
// this is essentially how much light reaches the current sample point due to scattering
285+
let attn = exp(-beta_ray * (opt_i.x + opt_l.x) - beta_mie * (opt_i.y + opt_l.y) - beta_absorption * (opt_i.z + opt_l.z));
286+
287+
// accumulate the scattered light (how much will be scattered towards the camera)
288+
total_ray += density.x * attn;
289+
total_mie += density.y * attn;
290+
291+
// and increment the position on this ray
292+
ray_pos_i += step_size_i;
293+
}
294+
295+
// calculate how much light can pass through the atmosphere
296+
let opacity = exp(-(beta_mie * opt_i.y + beta_ray * opt_i.x + beta_absorption * opt_i.z));
297+
298+
// calculate and return the final color
299+
return vec3[f32]((
300+
phase_ray * beta_ray * total_ray // rayleigh color
301+
+ phase_mie * beta_mie * total_mie // mie
302+
+ opt_i.x * beta_ambient // and ambient
303+
) * light_intensity + scene_color * opacity); // now make sure the background is rendered correctly
304+
}
305+
306+
/*
307+
A ray-sphere intersect
308+
This was previously used in the atmosphere as well, but it's only used for the planet intersect now, since the atmosphere has this
309+
ray sphere intersect built in
310+
*/
311+
312+
fn ray_sphere_intersect(
313+
start: vec3[f32], // starting position of the ray
314+
dir: vec3[f32], // the direction of the ray
315+
radius: f32 // and the sphere radius
316+
) -> vec2[f32] {
317+
// ray-sphere intersection that assumes
318+
// the sphere is centered at the origin.
319+
// No intersection when result.x > result.y
320+
let a = dot(dir, dir);
321+
let b = 2.0 * dot(dir, start);
322+
let c = dot(start, start) - (radius * radius);
323+
let d = (b*b) - 4.0*a*c;
324+
if (d < 0.0)
325+
return vec2[f32](100000.0, -100000.0);
326+
327+
let sqrt_d = sqrt(d);
328+
return vec2[f32](
329+
(-b - sqrt_d)/(2.0*a),
330+
(-b + sqrt_d)/(2.0*a)
331+
);
332+
}
333+
334+
fn sdRoundBox(pos: vec3[f32], dims: vec3[f32], cornerRadius: f32) -> f32
335+
{
336+
let q = abs(pos) - dims + cornerRadius.xxx;
337+
return length(max(q, (0.0).xxx)) + min(max(q.x, max(q.y, q.z)), 0.0) - cornerRadius;
338+
}

0 commit comments

Comments
 (0)