planettech
Version:
Toolkit for creating real 3D planets that can be transtioned from ground to sky.
426 lines (351 loc) • 17.2 kB
JavaScript
import * as THREE from 'three'
import {SMAAEffect,BlendFunction, Effect, EffectComposer, RenderPass,EffectPass,EffectAttribute, WebGLExtension} from "postprocessing";
import { Uniform, HalfFloatType } from "three";
const structAtmospheresBlock = `
struct Atmospheres {
vec3 PLANET_CENTER;
vec3 lightDir;
float PLANET_RADIUS;
float ATMOSPHERE_RADIUS;
float G;
int PRIMARY_STEPS;
int LIGHT_STEPS;
vec3 ulight_intensity;
vec3 uray_light_color;
vec3 umie_light_color;
vec3 RAY_BETA;
vec3 MIE_BETA;
vec3 AMBIENT_BETA;
vec3 ABSORPTION_BETA;
float HEIGHT_RAY;
float HEIGHT_MIE;
float HEIGHT_ABSORPTION;
float ABSORPTION_FALLOFF;
float textureIntensity;
float AmbientLightIntensity;
};
`
const uniformBlock = `
uniform mat4 inverseProjection;
uniform mat4 inverseView;
uniform vec3 uCameraPosition;
uniform vec3 uCameraDir;
uniform Atmospheres atmospheres[1];
`
const calculateScatteringBlock = `
vec3 calculate_scattering(
vec3 start, // the start of the ray (the camera position)
vec3 dir, // the direction of the ray (the camera vector)
float max_dist, // the maximum distance the ray can travel (because something is in the way, like an object)
vec3 scene_color, // the color of the scene
vec3 light_dir, // the direction of the light
vec3 light_intensity, // how bright the light is, affects the brightness of the atmosphere
vec3 ray_light_color, //mod
vec3 mie_light_color, //mod
vec3 planet_position, // the position of the planet
float planet_radius, // the radius of the planet
float atmo_radius, // the radius of the atmosphere
vec3 beta_ray, // the amount rayleigh scattering scatters the colors (for earth: causes the blue atmosphere)
vec3 beta_mie, // the amount mie scattering scatters colors
vec3 beta_absorption, // how much air is absorbed
vec3 beta_ambient, // the amount of scattering that always occurs, cna help make the back side of the atmosphere a bit brighter
float g, // the direction mie scatters the light in (like a cone). closer to -1 means more towards a single direction
float height_ray, // how high do you have to go before there is no rayleigh scattering?
float height_mie, // the same, but for mie
float height_absorption, // the height at which the most absorption happens
float absorption_falloff, // how fast the absorption falls off from the absorption height
int steps_i, // the amount of steps along the 'primary' ray, more looks better but slower
int steps_l // the amount of steps along the light ray, more looks better but slower
) {
// add an offset to the camera position, so that the atmosphere is in the correct position
start -= planet_position;
// calculate the start and end position of the ray, as a distance along the ray
// we do this with a ray sphere intersect
float a = dot(dir, dir);
float b = 2.0 * dot(dir, start);
float c = dot(start, start) - (atmo_radius * atmo_radius);
float d = (b * b) - 4.0 * a * c;
// stop early if there is no intersect
if (d < 0.0) return scene_color;
// calculate the ray length
vec2 ray_length = vec2(
max((-b - sqrt(d)) / (2.0 * a), 0.0),
min((-b + sqrt(d)) / (2.0 * a), max_dist)
);
// if the ray did not hit the atmosphere, return a black color
if (ray_length.x > ray_length.y) return scene_color;
// prevent the mie glow from appearing if there's an object in front of the camera
bool allow_mie = max_dist > ray_length.y;
// make sure the ray is no longer than allowed
ray_length.y = min(ray_length.y, max_dist);
ray_length.x = max(ray_length.x, 0.0);
// get the step size of the ray
float step_size_i = (ray_length.y - ray_length.x) / float(steps_i);
// next, set how far we are along the ray, so we can calculate the position of the sample
// if the camera is outside the atmosphere, the ray should start at the edge of the atmosphere
// if it's inside, it should start at the position of the camera
// the min statement makes sure of that
float ray_pos_i = ray_length.x + step_size_i * 0.5;
// these are the values we use to gather all the scattered light
vec3 total_ray = vec3(0.0); // for rayleigh
vec3 total_mie = vec3(0.0); // for mie
// initialize the optical depth. This is used to calculate how much air was in the ray
vec3 opt_i = vec3(0.0);
// also init the scale height, avoids some vec2's later on
vec2 scale_height = vec2(height_ray, height_mie);
// Calculate the Rayleigh and Mie phases.
// This is the color that will be scattered for this ray
// mu, mumu and gg are used quite a lot in the calculation, so to speed it up, precalculate them
float mu = dot(dir, light_dir);
float mumu = mu * mu;
float gg = g * g;
float phase_ray = 3.0 / (50.2654824574 /* (16 * pi) */) * (1.0 + mumu);
float phase_mie = 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;
// now we need to sample the 'primary' ray. this ray gathers the light that gets scattered onto it
for (int i = 0; i < steps_i; ++i) {
// calculate where we are along this ray
vec3 pos_i = start + dir * ray_pos_i;
// and how high we are above the surface
float height_i = length(pos_i) - planet_radius;
// now calculate the density of the particles (both for rayleigh and mie)
vec3 density = vec3(exp(-height_i / scale_height), 0.0);
// and the absorption density. this is for ozone, which scales together with the rayleigh,
// but absorbs the most at a specific height, so use the sech function for a nice curve falloff for this height
// clamp it to avoid it going out of bounds. This prevents weird black spheres on the night side
float denom = (height_absorption - height_i) / absorption_falloff;
density.z = (1.0 / (denom * denom + 1.0)) * density.x;
// multiply it by the step size here
// we are going to use the density later on as well
density *= step_size_i;
// Add these densities to the optical depth, so that we know how many particles are on this ray.
opt_i += density;
// Calculate the step size of the light ray.
// again with a ray sphere intersect
// a, b, c and d are already defined
a = dot(light_dir, light_dir);
b = 2.0 * dot(light_dir, pos_i);
c = dot(pos_i, pos_i) - (atmo_radius * atmo_radius);
d = (b * b) - 4.0 * a * c;
// no early stopping, this one should always be inside the atmosphere
// calculate the ray length
float step_size_l = (-b + sqrt(d)) / (2.0 * a * float(steps_l));
// and the position along this ray
// this time we are sure the ray is in the atmosphere, so set it to 0
float ray_pos_l = step_size_l * 0.5;
// and the optical depth of this ray
vec3 opt_l = vec3(0.0);
// now sample the light ray
// this is similar to what we did before
for (int l = 0; l < steps_l; ++l) {
// calculate where we are along this ray
vec3 pos_l = pos_i + light_dir * ray_pos_l;
// the heigth of the position
float height_l = length(pos_l) - planet_radius;
// calculate the particle density, and add it
// this is a bit verbose
// first, set the density for ray and mie
vec3 density_l = vec3(exp(-height_l / scale_height), 0.0);
// then, the absorption
float denom = (height_absorption - height_l) / absorption_falloff;
density_l.z = (1.0 / (denom * denom + 1.0)) * density_l.x;
// multiply the density by the step size
density_l *= step_size_l;
// and add it to the total optical depth
opt_l += density_l;
// and increment where we are along the light ray.
ray_pos_l += step_size_l;
}
// Now we need to calculate the attenuation
// this is essentially how much light reaches the current sample point due to scattering
vec3 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));
// accumulate the scattered light (how much will be scattered towards the camera)
total_ray += density.x * attn;
total_mie += density.y * attn;
// and increment the position on this ray
ray_pos_i += step_size_i;
}
// calculate how much light can pass through the atmosphere
vec3 opacity = exp(-(beta_mie * opt_i.y + beta_ray * opt_i.x + beta_absorption * opt_i.z));
// calculate and return the final color
return (
phase_ray * beta_ray * total_ray * ray_light_color// rayleigh color
+ phase_mie * beta_mie * total_mie * mie_light_color// mie
+ opt_i.x * beta_ambient // and ambient
) * light_intensity + scene_color * opacity; // now make sure the background is rendered correctly
}
`
const screenToWorldBlock = `
vec3 _ScreenToWorld(vec3 posS) {
vec2 uv = posS.xy;
float z = posS.z;
float nearZ = 0.01;
float farZ = cameraFar;
float depth = pow(2.0, z * log2(farZ + 1.0)) - 1.0;
vec3 direction = (inverseProjection * vec4(vUv * 2.0 - 1.0, 0.0, 1.0)).xyz; //vUv bug
direction = (inverseView * vec4(direction, 0.0)).xyz;
direction = normalize(direction);
direction /= dot(direction, uCameraDir);
return uCameraPosition + direction * depth;
}
`
const postFragmentShader =
`
${structAtmospheresBlock}
${uniformBlock}
${calculateScatteringBlock}
${screenToWorldBlock}
vec2 ray_sphere_intersect(
vec3 start, // starting position of the ray
vec3 dir, // the direction of the ray
float radius // and the sphere radius
) {
// ray-sphere intersection that assumes
// the sphere is centered at the origin.
// No intersection when result.x > result.y
float a = dot(dir, dir);
float b = 2.0 * dot(dir, start);
float c = dot(start, start) - (radius * radius);
float d = (b*b) - 4.0*a*c;
if (d < 0.0) return vec2(1e5,-1e5);
return vec2(
(-b - sqrt(d))/(2.0*a),
(-b + sqrt(d))/(2.0*a)
);
}
vec3 skylight(vec3 sample_pos, vec3 surface_normal, vec3 light_dir, vec3 background_col) {
surface_normal = normalize(mix(surface_normal, light_dir, 0.6));
Atmospheres currentAtmospheres = atmospheres[0];
return calculate_scattering(
sample_pos, // the position of the camera
surface_normal, // the camera vector (ray direction of this pixel)
3.0 * currentAtmospheres.ATMOSPHERE_RADIUS, // max dist, since nothing will stop the ray here, just use some arbitrary value
background_col, // scene color, just the background color here
light_dir, // light direction
currentAtmospheres.ulight_intensity, // light intensity, 40 looks nice
currentAtmospheres.uray_light_color,
currentAtmospheres.umie_light_color,
currentAtmospheres.PLANET_CENTER, // position of the planet
currentAtmospheres.PLANET_RADIUS, // radius of the planet in meters
currentAtmospheres.ATMOSPHERE_RADIUS, // radius of the atmosphere in meters
currentAtmospheres.RAY_BETA, // Rayleigh scattering coefficient
currentAtmospheres.MIE_BETA, // Mie scattering coefficient
currentAtmospheres.ABSORPTION_BETA, // Absorbtion coefficient
currentAtmospheres.AMBIENT_BETA, // ambient scattering, turned off for now. This causes the air to glow a bit when no light reaches it
currentAtmospheres.G,
currentAtmospheres.HEIGHT_RAY,
currentAtmospheres.HEIGHT_MIE,
currentAtmospheres.HEIGHT_ABSORPTION,
currentAtmospheres.ABSORPTION_FALLOFF,
currentAtmospheres.PRIMARY_STEPS,
currentAtmospheres.LIGHT_STEPS
);
}
vec4 render_scene(vec3 pos, vec3 dir, vec3 light_dir, vec3 addColor, vec3 PLANET_POS, float PLANET_RADIUS, float AmbientLightIntensity) {
// the color to use, w is the scene depth
vec4 color = vec4(addColor, 1e25);
// add a sun, if the angle between the ray direction and the light direction is small enough, color the pixels white
//color.xyz = vec3(dot(dir, light_dir) > 0.9998 ? 3.0 : 0.0);
// get where the ray intersects the planet
vec2 planet_intersect = ray_sphere_intersect(pos - PLANET_POS, dir, PLANET_RADIUS);
// if the ray hit the planet, set the max distance to that ray
if (0.0 < planet_intersect.y) {
//color.w = max(planet_intersect.x, 0.0);
// sample position, where the pixel is
vec3 sample_pos = pos + (dir * planet_intersect.x) - PLANET_POS;
// and the surface normal
vec3 surface_normal = normalize(sample_pos);
// get the color of the sphere
color.xyz = addColor;
// get wether this point is shadowed, + how much light scatters towards the camera according to the lommel-seelinger law
vec3 N = surface_normal;
vec3 V = -dir;
vec3 L = light_dir;
float dotNV = max(1e-6, dot(N, V));
float dotNL = max(1e-6, dot(N, L));
float shadow = dotNL / (dotNL + dotNV);
// apply the shadow
color.xyz *= shadow + AmbientLightIntensity;
// apply skylight
//color.xyz += clamp(skylight(sample_pos, surface_normal, light_dir, vec3(0.0)) * addColor, 0.0, 1.0);
}
return color;
}
void mainImage(const in vec4 inputColor, const in vec2 uv, const in float depth, out vec4 outputColor) {
float d = texture2D(depthBuffer, uv).x;
vec3 posWS = _ScreenToWorld(vec3(uv, d));
vec3 rayOrigin = uCameraPosition;
vec3 rayDirection = normalize(posWS - uCameraPosition);
float sceneDepth = length(posWS.xyz - uCameraPosition);
vec3 addColor = inputColor.xyz;
vec3 col = vec3(0.0);
Atmospheres currentAtmospheres = atmospheres[0];
vec3 lightDirection = normalize(currentAtmospheres.lightDir);
addColor *= currentAtmospheres.textureIntensity;
/*addColor = render_scene(
rayOrigin,
rayDirection,
lightDirection,
addColor,
currentAtmospheres.PLANET_CENTER,
currentAtmospheres.PLANET_RADIUS,
currentAtmospheres.AmbientLightIntensity
).rgb;*/
col += calculate_scattering(
rayOrigin,
rayDirection,
sceneDepth,
addColor,
lightDirection,
currentAtmospheres.ulight_intensity,
currentAtmospheres.uray_light_color,
currentAtmospheres.umie_light_color,
currentAtmospheres.PLANET_CENTER,
currentAtmospheres.PLANET_RADIUS,
currentAtmospheres.ATMOSPHERE_RADIUS,
currentAtmospheres.RAY_BETA,
currentAtmospheres.MIE_BETA,
currentAtmospheres.ABSORPTION_BETA,
currentAtmospheres.AMBIENT_BETA,
currentAtmospheres.G,
currentAtmospheres.HEIGHT_RAY,
currentAtmospheres.HEIGHT_MIE,
currentAtmospheres.HEIGHT_ABSORPTION,
currentAtmospheres.ABSORPTION_FALLOFF,
currentAtmospheres.PRIMARY_STEPS,
currentAtmospheres.LIGHT_STEPS
);
col = 1.0 - exp(-col);
outputColor = vec4(col, 1.0);
}
`;
const cameraDir = new THREE.Vector3();
export class Atmosphere{
constructor() {
}
createcomposer(params,camera_){
class CustomEffect extends Effect {
constructor() {
camera_.getWorldDirection(cameraDir);
super("CustomEffect", postFragmentShader, {
uniforms: new Map([
["atmospheres", new Uniform(params)],
["uCameraPosition", new Uniform(camera_.position)],
["inverseProjection", new Uniform(camera_.projectionMatrixInverse)],
["inverseView", new Uniform(camera_.matrixWorld)],
["uCameraDir", new Uniform(cameraDir)],
]),
attributes: EffectAttribute.DEPTH,
extensions: new Set([WebGLExtension.DERIVATIVES]),
});
}
}
this.depthPass = new CustomEffect();
}
run(camera_) {
camera_.getWorldDirection(cameraDir);
this.depthPass.uniforms.get('uCameraPosition') .value = camera_.position
this.depthPass.uniforms.get('inverseProjection').value = camera_.projectionMatrixInverse
this.depthPass.uniforms.get('inverseView') .value = camera_.matrixWorld
this.depthPass.uniforms.get('uCameraDir') .value = cameraDir
};
}