diff --git a/assets/shaders/gl_textured.frag b/assets/shaders/gl_textured.frag index b264f77..3362fc3 100644 --- a/assets/shaders/gl_textured.frag +++ b/assets/shaders/gl_textured.frag @@ -1,34 +1,157 @@ #version 330 core -in vec3 v_normal; +in vec3 v_world_normal; in vec2 v_tex; -in vec3 v_position; +in vec3 v_world_pos; out vec4 frag_color; -uniform vec3 u_light; uniform sampler2D tex; -uniform vec3 color; // base colour factor (acts as solid colour when no texture) +uniform vec3 color; // base colour factor +uniform vec3 camera_pos; // camera world position +uniform vec3 light_dir; // directional light direction (from light towards scene) +uniform vec3 light_color; // directional light color + +// ------------------------------------- +// Constants and helpers (ported from atmosphere.cginc) +const float PI = 3.14159265359; +const float PLANET_RADIUS = 6371000.0; +const float ATMOSPHERE_HEIGHT = 100000.0; +const float RAYLEIGH_HEIGHT = (ATMOSPHERE_HEIGHT * 0.08); +const float MIE_HEIGHT = (ATMOSPHERE_HEIGHT * 0.012); +const float EXPOSURE = 20.0; + +// Coefficients (1e-6 scaling kept) +const vec3 C_RAYLEIGH = vec3(5.802, 13.558, 33.100) * 1e-6; +const vec3 C_MIE = vec3(3.996, 3.996, 3.996) * 1e-6; +const vec3 C_OZONE = vec3(0.650, 1.881, 0.085) * 1e-6; + +float saturate(float x) { return clamp(x, 0.0, 1.0); } +vec2 saturate(vec2 x) { return clamp(x, vec2(0.0), vec2(1.0)); } +vec3 saturate(vec3 x) { return clamp(x, vec3(0.0), vec3(1.0)); } + +float AtmosphereHeight(vec3 positionWS) { + return length(positionWS - vec3(0.0, -PLANET_RADIUS, 0.0)) - PLANET_RADIUS; +} +float DensityRayleigh(float h) { return exp(-max(0.0, h / RAYLEIGH_HEIGHT)); } +float DensityMie (float h) { return exp(-max(0.0, h / MIE_HEIGHT)); } +float DensityOzone (float h) { return max(0.0, 1.0 - abs(h - 25000.0) / 15000.0); } +vec3 AtmosphereDensity(float h) { return vec3(DensityRayleigh(h), DensityMie(h), DensityOzone(h)); } + +// Sphere intersection with atmosphere shell +vec2 SphereIntersection(vec3 rayStart, vec3 rayDir, vec3 sphereCenter, float sphereRadius) { + vec3 o = rayStart - sphereCenter; + float a = dot(rayDir, rayDir); + float b = 2.0 * dot(o, rayDir); + float c = dot(o, o) - (sphereRadius * sphereRadius); + float d = b*b - 4.0*a*c; + if (d < 0.0) return vec2(-1.0); + d = sqrt(d); + return vec2(-b - d, -b + d) / (2.0 * a); +} +vec2 AtmosphereIntersection(vec3 rayStart, vec3 rayDir) { + return SphereIntersection(rayStart, rayDir, vec3(0.0, -PLANET_RADIUS, 0.0), PLANET_RADIUS + ATMOSPHERE_HEIGHT); +} + +float PhaseRayleigh(float costh) { + return 3.0 * (1.0 + costh*costh) / (16.0 * PI); +} +float PhaseMie(float costh, float g) { + g = min(g, 0.9381); + float k = 1.55*g - 0.55*g*g*g; + float kcosth = k*costh; + return (1.0 - k*k) / ((4.0*PI) * (1.0 - kcosth) * (1.0 - kcosth)); +} + +vec3 IntegrateOpticalDepth(vec3 rayStart, vec3 rayDir) { + vec2 intersection = AtmosphereIntersection(rayStart, rayDir); + float rayLength = intersection.y; + int sampleCount = 8; + float stepSize = rayLength / float(sampleCount); + vec3 opticalDepth = vec3(0.0); + for (int i = 0; i < sampleCount; ++i) { + vec3 localPosition = rayStart + rayDir * (float(i) + 0.5) * stepSize; + float localHeight = AtmosphereHeight(localPosition); + vec3 localDensity = AtmosphereDensity(localHeight); + opticalDepth += localDensity * stepSize; + } + return opticalDepth; +} + +vec3 Absorb(vec3 opticalDepth) { + // Mie absorbs slightly more than it scatters (~10%) + return exp(-(opticalDepth.x * C_RAYLEIGH + opticalDepth.y * C_MIE * 1.1 + opticalDepth.z * C_OZONE)); +} + +vec3 IntegrateScattering(vec3 rayStart, vec3 rayDir, float rayLength, vec3 lightDir, vec3 lightColor, out vec3 transmittance) { + float rayHeight = AtmosphereHeight(rayStart); + float sampleDistributionExponent = 1.0 + saturate(1.0 - rayHeight / ATMOSPHERE_HEIGHT) * 8.0; + + vec2 intersection = AtmosphereIntersection(rayStart, rayDir); + rayLength = min(rayLength, intersection.y); + if (intersection.x > 0.0) { + rayStart += rayDir * intersection.x; + rayLength -= intersection.x; + } + + float costh = dot(rayDir, lightDir); + float phaseR = PhaseRayleigh(costh); + float phaseM = PhaseMie(costh, 0.85); + + int sampleCount = 64; + + vec3 opticalDepth = vec3(0.0); + vec3 rayleigh = vec3(0.0); + vec3 mie = vec3(0.0); + float prevRayTime = 0.0; + + for (int i = 0; i < sampleCount; ++i) { + float t = pow(float(i) / float(sampleCount), sampleDistributionExponent) * rayLength; + float stepSize = (t - prevRayTime); + vec3 localPosition = rayStart + rayDir * t; + float localHeight = AtmosphereHeight(localPosition); + vec3 localDensity = AtmosphereDensity(localHeight); + opticalDepth += localDensity * stepSize; + + vec3 viewTransmittance = Absorb(opticalDepth); + vec3 opticalDepthLight = IntegrateOpticalDepth(localPosition, lightDir); + vec3 lightTransmittance = Absorb(opticalDepthLight); + + rayleigh += viewTransmittance * lightTransmittance * phaseR * localDensity.x * stepSize; + mie += viewTransmittance * lightTransmittance * phaseM * localDensity.y * stepSize; + prevRayTime = t; + } + + transmittance = Absorb(opticalDepth); + return (rayleigh * C_RAYLEIGH + mie * C_MIE) * lightColor * EXPOSURE; +} void main() { - // Combine base texture (or constant white) with colour factor supplied by CPU. + // Base albedo vec3 base_col = texture(tex, v_tex).rgb * color; - vec3 ambient_color = base_col * 0.2; - vec3 diffuse_color = base_col * 0.6; - vec3 specular_color = vec3(1.0); + // Lighting setup + vec3 L = normalize(light_dir); + vec3 N = normalize(v_world_normal); - // u_light is the direction **from the light towards the fragment**. - float diffuse = max(dot(normalize(v_normal), normalize(u_light)), 0.0); + // Directional light transmittance (planet shadow) + vec3 lightTransmittance = Absorb(IntegrateOpticalDepth(v_world_pos, L)); - vec3 camera_dir = normalize(-v_position); - vec3 half_dir = normalize(normalize(u_light) + camera_dir); - float specular = pow(max(dot(half_dir, normalize(v_normal)), 0.0), 16.0); + // Rough ambient by sampling sky upwards + vec3 tmp; + vec3 ambient = IntegrateScattering(v_world_pos, vec3(0.0, 1.0, 0.0), 1.0/0.0, L, light_color, tmp); - vec3 result = ambient_color + diffuse * diffuse_color + specular * specular_color; + // Lambert + atmospheric directional lighting + float NdotL = max(0.0, dot(N, L)); + vec3 lit = base_col * NdotL * (ambient + light_color * lightTransmittance); - // Convert from linear to sRGB for display (approximate γ-correction) - result = pow(result, vec3(1.0 / 2.2)); + // View-ray scattering and transmittance between surface and camera + vec3 V = camera_pos - v_world_pos; + float rayLength = length(V); + V = V / max(rayLength, 1e-6); + vec3 transmittance; + vec3 scattering = IntegrateScattering(camera_pos, -V, rayLength, L, light_color, transmittance); + vec3 result = lit * transmittance + scattering; frag_color = vec4(result, 1.0); } diff --git a/assets/shaders/gl_textured.vert b/assets/shaders/gl_textured.vert index df2c27d..1d7bb32 100644 --- a/assets/shaders/gl_textured.vert +++ b/assets/shaders/gl_textured.vert @@ -10,14 +10,14 @@ uniform mat4 projection; uniform vec2 uv_offset; uniform vec2 uv_scale; -out vec3 v_normal; +out vec3 v_world_normal; out vec2 v_tex; -out vec3 v_position; +out vec3 v_world_pos; void main() { - mat4 modelview = view * model; - v_normal = transpose(inverse(mat3(modelview))) * normal; - v_tex = tex_coords * uv_scale + uv_offset; - v_position = (modelview * vec4(position, 1.0)).xyz; - gl_Position = projection * modelview * vec4(position, 1.0); + mat3 model3 = mat3(model); + v_world_normal = normalize(transpose(inverse(model3)) * normal); + v_tex = tex_coords * uv_scale + uv_offset; + v_world_pos = (model * vec4(position, 1.0)).xyz; + gl_Position = projection * view * vec4(v_world_pos, 1.0); } diff --git a/assets/shaders/sky_atmosphere.frag b/assets/shaders/sky_atmosphere.frag new file mode 100644 index 0000000..df9f9f1 --- /dev/null +++ b/assets/shaders/sky_atmosphere.frag @@ -0,0 +1,136 @@ +#version 330 core + +in vec2 v_uv; +out vec4 frag_color; + +uniform mat4 inv_view; +uniform mat4 inv_projection; +uniform vec3 camera_pos; +uniform vec3 light_dir; +uniform vec3 light_color; +uniform float draw_planet; + +const float PI = 3.14159265359; +const float PLANET_RADIUS = 6371000.0; +const float ATMOSPHERE_HEIGHT = 100000.0; +const float RAYLEIGH_HEIGHT = (ATMOSPHERE_HEIGHT * 0.08); +const float MIE_HEIGHT = (ATMOSPHERE_HEIGHT * 0.012); +const float EXPOSURE = 20.0; +const vec3 C_RAYLEIGH = vec3(5.802, 13.558, 33.100) * 1e-6; +const vec3 C_MIE = vec3(3.996, 3.996, 3.996) * 1e-6; +const vec3 C_OZONE = vec3(0.650, 1.881, 0.085) * 1e-6; + +float AtmosphereHeight(vec3 positionWS) { + return length(positionWS - vec3(0.0, -PLANET_RADIUS, 0.0)) - PLANET_RADIUS; +} +float DensityRayleigh(float h) { return exp(-max(0.0, h / RAYLEIGH_HEIGHT)); } +float DensityMie (float h) { return exp(-max(0.0, h / MIE_HEIGHT)); } +float DensityOzone (float h) { return max(0.0, 1.0 - abs(h - 25000.0) / 15000.0); } +vec3 AtmosphereDensity(float h) { return vec3(DensityRayleigh(h), DensityMie(h), DensityOzone(h)); } + +vec2 SphereIntersection(vec3 rayStart, vec3 rayDir, vec3 sphereCenter, float sphereRadius) { + vec3 o = rayStart - sphereCenter; + float a = dot(rayDir, rayDir); + float b = 2.0 * dot(o, rayDir); + float c = dot(o, o) - (sphereRadius * sphereRadius); + float d = b*b - 4.0*a*c; + if (d < 0.0) return vec2(-1.0); + d = sqrt(d); + return vec2(-b - d, -b + d) / (2.0 * a); +} +vec2 PlanetIntersection(vec3 rayStart, vec3 rayDir) { + return SphereIntersection(rayStart, rayDir, vec3(0.0, -PLANET_RADIUS, 0.0), PLANET_RADIUS); +} +vec2 AtmosphereIntersection(vec3 rayStart, vec3 rayDir) { + return SphereIntersection(rayStart, rayDir, vec3(0.0, -PLANET_RADIUS, 0.0), PLANET_RADIUS + ATMOSPHERE_HEIGHT); +} + +float PhaseRayleigh(float costh) { + return 3.0 * (1.0 + costh*costh) / (16.0 * PI); +} +float PhaseMie(float costh, float g) { + g = min(g, 0.9381); + float k = 1.55*g - 0.55*g*g*g; + float kcosth = k*costh; + return (1.0 - k*k) / ((4.0*PI) * (1.0 - kcosth) * (1.0 - kcosth)); +} + +vec3 IntegrateOpticalDepth(vec3 rayStart, vec3 rayDir) { + vec2 intersection = AtmosphereIntersection(rayStart, rayDir); + float rayLength = intersection.y; + int sampleCount = 8; + float stepSize = rayLength / float(sampleCount); + vec3 opticalDepth = vec3(0.0); + for (int i = 0; i < sampleCount; ++i) { + vec3 localPosition = rayStart + rayDir * (float(i) + 0.5) * stepSize; + float localHeight = AtmosphereHeight(localPosition); + vec3 localDensity = AtmosphereDensity(localHeight); + opticalDepth += localDensity * stepSize; + } + return opticalDepth; +} + +vec3 Absorb(vec3 opticalDepth) { + return exp(-(opticalDepth.x * C_RAYLEIGH + opticalDepth.y * C_MIE * 1.1 + opticalDepth.z * C_OZONE)); +} + +vec3 IntegrateScattering(vec3 rayStart, vec3 rayDir, float rayLength, vec3 lightDir, vec3 lightColor, out vec3 transmittance) { + float rayHeight = AtmosphereHeight(rayStart); + float sampleDistributionExponent = 1.0 + clamp(1.0 - rayHeight / ATMOSPHERE_HEIGHT, 0.0, 1.0) * 8.0; + + vec2 intersection = AtmosphereIntersection(rayStart, rayDir); + rayLength = min(rayLength, intersection.y); + if (intersection.x > 0.0) { + rayStart += rayDir * intersection.x; + rayLength -= intersection.x; + } + + float costh = dot(rayDir, lightDir); + float phaseR = PhaseRayleigh(costh); + float phaseM = PhaseMie(costh, 0.85); + + int sampleCount = 64; + vec3 opticalDepth = vec3(0.0); + vec3 rayleigh = vec3(0.0); + vec3 mie = vec3(0.0); + float prevRayTime = 0.0; + for (int i = 0; i < sampleCount; ++i) { + float t = pow(float(i) / float(sampleCount), sampleDistributionExponent) * rayLength; + float stepSize = (t - prevRayTime); + vec3 localPosition = rayStart + rayDir * t; + float localHeight = AtmosphereHeight(localPosition); + vec3 localDensity = AtmosphereDensity(localHeight); + opticalDepth += localDensity * stepSize; + vec3 viewTransmittance = Absorb(opticalDepth); + vec3 opticalDepthLight = IntegrateOpticalDepth(localPosition, lightDir); + vec3 lightTransmittance = Absorb(opticalDepthLight); + rayleigh += viewTransmittance * lightTransmittance * phaseR * localDensity.x * stepSize; + mie += viewTransmittance * lightTransmittance * phaseM * localDensity.y * stepSize; + prevRayTime = t; + } + transmittance = Absorb(opticalDepth); + return (rayleigh * C_RAYLEIGH + mie * C_MIE) * lightColor * EXPOSURE; +} + +void main() { + // Reconstruct view ray from NDC + vec2 ndc = v_uv * 2.0 - 1.0; + vec4 clip = vec4(ndc, 1.0, 1.0); + vec4 view = inv_projection * clip; + view = vec4(view.xy, -1.0, 0.0); + vec3 worldDir = normalize((inv_view * view).xyz); + + float rayLength = 1.0/0.0; // infinity + if (draw_planet == 1.0) { + vec2 isect = PlanetIntersection(camera_pos, worldDir); + if (isect.x > 0.0) { + rayLength = min(rayLength, isect.x); + } + } + + vec3 transmittance; + vec3 color = IntegrateScattering(camera_pos, worldDir, rayLength, normalize(light_dir), light_color, transmittance); + frag_color = vec4(color, 1.0); +} + + diff --git a/assets/shaders/sky_atmosphere.vert b/assets/shaders/sky_atmosphere.vert new file mode 100644 index 0000000..a82eca4 --- /dev/null +++ b/assets/shaders/sky_atmosphere.vert @@ -0,0 +1,13 @@ +#version 330 core + +// Fullscreen triangle (no vbo) +out vec2 v_uv; + +void main() { + // gl_VertexID in {0,1,2} + vec2 pos = vec2((gl_VertexID << 1) & 2, gl_VertexID & 2); + v_uv = pos; + gl_Position = vec4(pos * 2.0 - 1.0, 0.0, 1.0); +} + + diff --git a/core/src/context.rs b/core/src/context.rs index f1cdffe..befd356 100644 --- a/core/src/context.rs +++ b/core/src/context.rs @@ -1,7 +1,7 @@ use std::cell::RefCell; use std::rc::Rc; use std::sync::Arc; -use winit::event::Event; +use winit::{event::Event, window::Window}; use raidillon_assets::{ModelManagerRef, ModelManager}; #[derive(Clone)] @@ -10,4 +10,5 @@ pub struct PlatformContext { pub asset_manager: ModelManagerRef, pub frame_width: f32, pub frame_height: f32, + // pub window: &'a Window, } diff --git a/glium_platform/src/platform.rs b/glium_platform/src/platform.rs index 4848c7c..310e56a 100644 --- a/glium_platform/src/platform.rs +++ b/glium_platform/src/platform.rs @@ -12,7 +12,7 @@ use winit::event::{Event, WindowEvent}; use raidillon_assets::ModelManagerRef; use raidillon_core::engine::EngineTrait; use crate::render::debug_ui::ImguiBridge; -use crate::render::BasicMeshRenderingSystem; +use crate::render::{BasicMeshRenderingSystem, SkyAtmosphereRenderingSystem}; use crate::GliumAssetManager; pub struct GliumPlatform { @@ -39,6 +39,8 @@ impl Platform for GliumPlatform { let mut rendering_system_manager = RenderingSystemManager::new(); // Install rendering systems + // Draw sky first, then meshes, then debug UI + rendering_system_manager.add::(&display, &window); rendering_system_manager.add::(&display, &window); rendering_system_manager.add::(&display, &window); diff --git a/glium_platform/src/render/basic.rs b/glium_platform/src/render/basic.rs index 0f1db93..6c37722 100644 --- a/glium_platform/src/render/basic.rs +++ b/glium_platform/src/render/basic.rs @@ -55,8 +55,9 @@ impl RenderingSystem for BasicMeshRenderingSystem { } }; - // Direction from the light source (0,+Y) towards the scene. - let light_dir: Vec3 = Vec3::new(0.0, -1.0, 0.0).normalize(); + // Match sky light direction: from light towards the scene (upwards +Y) + let light_dir: Vec3 = Vec3::new(0.0, 1.0, 0.0).normalize(); + let light_color: [f32; 3] = [1.0, 1.0, 1.0]; // let asset_manager = ctx.asset_manager.borrow(); // let any_ref: &dyn Any = &**asset_manager; @@ -93,7 +94,9 @@ impl RenderingSystem for BasicMeshRenderingSystem { model: tr.matrix().to_cols_array_2d(), view: cam.view().to_cols_array_2d(), projection: cam.projection().to_cols_array_2d(), - u_light: [light_dir.x, light_dir.y, light_dir.z], + camera_pos: [cam.eye.x, cam.eye.y, cam.eye.z], + light_dir: [light_dir.x, light_dir.y, light_dir.z], + light_color: light_color, tex: sampler, color: [c[0], c[1], c[2]], uv_offset: [mat.uv_offset.x, mat.uv_offset.y], diff --git a/glium_platform/src/render/mod.rs b/glium_platform/src/render/mod.rs index e98554f..fa14f71 100644 --- a/glium_platform/src/render/mod.rs +++ b/glium_platform/src/render/mod.rs @@ -1,4 +1,6 @@ mod basic; +mod sky; pub mod debug_ui; -pub use basic::BasicMeshRenderingSystem; \ No newline at end of file +pub use basic::BasicMeshRenderingSystem; +pub use sky::SkyAtmosphereRenderingSystem; \ No newline at end of file diff --git a/glium_platform/src/render/sky.rs b/glium_platform/src/render/sky.rs new file mode 100644 index 0000000..1d758d0 --- /dev/null +++ b/glium_platform/src/render/sky.rs @@ -0,0 +1,64 @@ +use glium::{uniform, Display, Program, Surface}; +use glium::glutin::surface::WindowSurface; +use glium::index::NoIndices; +use glium::index::PrimitiveType; +use glium::vertex::EmptyVertexAttributes; +use glam::Mat4; +use crate::system::RenderingContext; +use crate::RenderingSystem; +use raidillon_assets::include_shader; +use raidillon_platform::Camera; + +pub struct SkyAtmosphereRenderingSystem { + program: Program, + params: glium::DrawParameters<'static>, +} + +impl RenderingSystem for SkyAtmosphereRenderingSystem { + fn initialize(display: &Display, _window: &glium::winit::window::Window) -> Self { + const VERT_SRC: &str = include_shader!("sky_atmosphere.vert"); + const FRAG_SRC: &str = include_shader!("sky_atmosphere.frag"); + + let program = Program::from_source(display, VERT_SRC, FRAG_SRC, None).unwrap(); + + let params = glium::DrawParameters { + depth: glium::Depth { + test: glium::draw_parameters::DepthTest::Overwrite, + write: false, + .. Default::default() + }, + backface_culling: glium::draw_parameters::BackfaceCullingMode::CullingDisabled, + .. Default::default() + }; + + Self { program, params } + } + + fn render(&mut self, ctx: &mut RenderingContext) { + let cam = match ctx.scene.world.query::<&Camera>().iter().next() { + Some((_, cam)) => *cam, + None => return, + }; + + let inv_view: Mat4 = cam.view().inverse(); + let inv_proj: Mat4 = cam.projection().inverse(); + + let light_dir = glam::Vec3::new(0.0, 2.0, 0.0).normalize(); + let light_color: [f32; 3] = [1.0, 1.0, 1.0]; + + let uniforms = uniform! { + inv_view: inv_view.to_cols_array_2d(), + inv_projection: inv_proj.to_cols_array_2d(), + camera_pos: [cam.eye.x, cam.eye.y, cam.eye.z], + light_dir: [light_dir.x, light_dir.y, light_dir.z], + light_color: light_color, + draw_planet: 1.0f32, + }; + + let vb = EmptyVertexAttributes { len: 3 }; + let ib = NoIndices(PrimitiveType::TrianglesList); + ctx.target.draw(vb, &ib, &self.program, &uniforms, &self.params).ok(); + } +} + +