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