diff --git a/assets/shaders/terrain/calc_normals.wgsl b/assets/shaders/terrain/calc_normals.wgsl new file mode 100644 index 00000000..4d55949e --- /dev/null +++ b/assets/shaders/terrain/calc_normals.wgsl @@ -0,0 +1,53 @@ +#include "unpack.wgsl" + +struct VertexOutput { + @location(0) v_TexCoord: vec2, + @builtin(position) member: vec4, +} + +@vertex +fn vert(@builtin(vertex_index) vi: u32) -> VertexOutput { + var tc: vec2 = vec2(0.0, 0.0); + switch (vi) { + case 0u: {tc = vec2(1.0, 0.0);} + case 1u: {tc = vec2(1.0, 1.0);} + case 2u: {tc = vec2(0.0, 0.0);} + case 3u: {tc = vec2(0.0, 1.0);} + default: {} + } + let pos: vec2 = tc * 2.0 - 1.0; + let gl_Position = vec4(pos.x, -pos.y, 0.5, 1.0); + + return VertexOutput(tc, gl_Position); +} + + +struct FragmentOutput { + @location(0) o_Target: u32, +} + +@group(0) @binding(0) var t_terrain: texture_2d; + +fn pack_diffs(diffs: vec2, lod_pow2: f32) -> u32 { + let x = u32((clamp(diffs.x, -MAX_DIFF, MAX_DIFF) / (MAX_DIFF * lod_pow2)) * 127.0 + 128.0); + let y = u32((clamp(diffs.y, -MAX_DIFF, MAX_DIFF) / (MAX_DIFF * lod_pow2)) * 127.0 + 128.0); + return (x << 8u) | y; +} + +@fragment +fn calc_normals(@location(0) v_TexCoord: vec2) -> FragmentOutput { + let dim: vec2 = textureDimensions(t_terrain); + + let id = vec2(v_TexCoord * vec2(dim)); + + + + let hR: f32 = unpack_height(textureLoad(t_terrain, id + vec2(1u, 0u), 0).r); + let hL: f32 = unpack_height(textureLoad(t_terrain, id - vec2(1u, 0u), 0).r); + let hT: f32 = unpack_height(textureLoad(t_terrain, id + vec2(0u, 1u), 0).r); + let hB: f32 = unpack_height(textureLoad(t_terrain, id - vec2(0u, 1u), 0).r); + + let diffs = vec2((hL - hR), (hB - hT)); + + return FragmentOutput(pack_diffs(diffs, 1.0)); +} diff --git a/assets/shaders/terrain/resample.wgsl b/assets/shaders/terrain/resample.wgsl new file mode 100644 index 00000000..c57f7076 --- /dev/null +++ b/assets/shaders/terrain/resample.wgsl @@ -0,0 +1,90 @@ +#include "unpack.wgsl" + +struct VertexOutput { + @location(0) v_TexCoord: vec2, + @builtin(position) member: vec4, +} + +@vertex +fn vert(@builtin(vertex_index) vi: u32) -> VertexOutput { + var tc: vec2 = vec2(0.0, 0.0); + switch (vi) { + case 0u: {tc = vec2(1.0, 0.0);} + case 1u: {tc = vec2(1.0, 1.0);} + case 2u: {tc = vec2(0.0, 0.0);} + case 3u: {tc = vec2(0.0, 1.0);} + default: {} + } + let pos: vec2 = tc * 2.0 - 1.0; + let gl_Position = vec4(pos.x, -pos.y, 0.5, 1.0); + + return VertexOutput(tc, gl_Position); +} + + +struct FragmentOutput { + @location(0) o_Target: u32, +} + +fn pack_height(h: f32) -> u32 { + return u32((h - MIN_HEIGHT) / HEIGHT_RANGE * 65535.0); +} + +@group(0) @binding(0) var t_terrain: texture_2d; + +@fragment +fn downsample(@location(0) v_TexCoord: vec2) -> FragmentOutput { + let dim = textureDimensions(t_terrain); + + let id = vec2(v_TexCoord * vec2(dim)); + + + let h0 = unpack_height(textureLoad(t_terrain, id, 0).r); + let h1 = unpack_height(textureLoad(t_terrain, id + vec2(1u, 0u), 0).r); + let h2 = unpack_height(textureLoad(t_terrain, id + vec2(0u, 1u), 0).r); + let h3 = unpack_height(textureLoad(t_terrain, id - vec2(1u, 0u), 0).r); + let h4 = unpack_height(textureLoad(t_terrain, id - vec2(0u, 1u), 0).r); + let h5 = unpack_height(textureLoad(t_terrain, id + vec2(1u, 1u), 0).r); + let h6 = unpack_height(textureLoad(t_terrain, id - vec2(1u, 1u), 0).r); + let h7 = unpack_height(textureLoad(t_terrain, id + vec2(1u, 0u) - vec2(0u, 1u), 0).r); + let h8 = unpack_height(textureLoad(t_terrain, id + vec2(0u, 1u) - vec2(1u, 0u), 0).r); + + let gaussian = h0 * 0.25 + + h1 * 0.125 + + h2 * 0.125 + + h3 * 0.125 + + h4 * 0.125 + + h5 * 0.0625 + + h6 * 0.0625 + + h7 * 0.0625 + + h8 * 0.0625; + let maxv = max(max(max(max(max(max(max(max(h0, h1), h2), h3), h4), h5), h6), h7), h8); + + let final_height = (gaussian + maxv) * 0.5; + + return FragmentOutput(pack_height(final_height)); +} + +@fragment +fn upsample(@location(0) v_TexCoord: vec2) -> FragmentOutput { + let dim = textureDimensions(t_terrain); + + let id = vec2(v_TexCoord * vec2(dim)); + + let h0 = unpack_height(textureLoad(t_terrain, id, 0).r); + let h1 = unpack_height(textureLoad(t_terrain, id + vec2(1u, 0u), 0).r); + let h2 = unpack_height(textureLoad(t_terrain, id + vec2(0u, 1u), 0).r); + let h3 = unpack_height(textureLoad(t_terrain, id + vec2(1u, 1u), 0).r); + + // bilinear interpolation + let x = fract(v_TexCoord.x * vec2(dim)); + let y = fract(v_TexCoord.y * vec2(dim)); + + let h01 = mix(h0, h1, x.x); + let h23 = mix(h2, h3, x.x); + let h = mix(h01, h23, y.y); + + let final_height = h; + + return FragmentOutput(pack_height(final_height)); +} \ No newline at end of file diff --git a/assets/shaders/terrain/terrain.frag.wgsl b/assets/shaders/terrain/terrain.frag.wgsl index 4836fbc0..ab1fba92 100644 --- a/assets/shaders/terrain/terrain.frag.wgsl +++ b/assets/shaders/terrain/terrain.frag.wgsl @@ -13,11 +13,9 @@ struct ChunkData { @group(1) @binding(0) var params: RenderParams; -@group(2) @binding(0) var t_terraindata: texture_2d; -@group(2) @binding(1) var s_terraindata: sampler; -@group(2) @binding(2) var t_grass: texture_2d; -@group(2) @binding(3) var s_grass: sampler; -@group(2) @binding(4) var cdata: ChunkData; +@group(2) @binding(4) var t_grass: texture_2d; +@group(2) @binding(5) var s_grass: sampler; +@group(2) @binding(6) var cdata: ChunkData; @group(3) @binding(0) var t_ssao: texture_2d; @group(3) @binding(1) var s_ssao: sampler; @@ -145,8 +143,8 @@ fn frag(@builtin(position) position: vec4, let V_denorm: vec3 = params.cam_pos.xyz - in_wpos; let depth: f32 = length(V_denorm); let V: vec3 = V_denorm / depth; - let F0: vec3 = vec3(0.02); - let roughness: f32 = 1.0; + let F0: vec3 = vec3(0.00); + let roughness: f32 = 1.3; // avoid specular highlights which look weird on terrain let normal: vec3 = normalize(in_normal); let F_spec: vec3 = F0; // simplified with constant folding: fresnelSchlickRoughness(max(dot(normal, V), 0.0), F0, roughness); diff --git a/assets/shaders/terrain/terrain.vert.wgsl b/assets/shaders/terrain/terrain.vert.wgsl index fd8b1c69..be9c493b 100644 --- a/assets/shaders/terrain/terrain.vert.wgsl +++ b/assets/shaders/terrain/terrain.vert.wgsl @@ -6,12 +6,12 @@ struct Uniforms { } struct VertexOutput { + @builtin(position) member: vec4, @location(0) out_normal: vec3, @location(1) out_wpos: vec3, #ifdef DEBUG @location(2) debug: f32, #endif - @builtin(position) member: vec4, } struct ChunkData { @@ -27,9 +27,11 @@ struct ChunkData { @group(1) @binding(0) var params: RenderParams; -@group(2) @binding(0) var t_terraindata: texture_2d; -@group(2) @binding(1) var s_terraindata: sampler; -@group(2) @binding(4) var cdata: ChunkData; +@group(2) @binding(0) var t_terrain: texture_2d; +@group(2) @binding(1) var s_terrain: sampler; +@group(2) @binding(2) var t_normals: texture_2d; +@group(2) @binding(3) var s_normals: sampler; +@group(2) @binding(6) var cdata: ChunkData; /* normal: vec3(self.cell_size * scale as f32, 0.0, hx - height) @@ -37,6 +39,12 @@ normal: vec3(self.cell_size * scale as f32, 0.0, hx - height) .normalize(), */ +fn sampleHeightDxDy(pos: vec2, lod: i32) -> vec3 { + let height: f32 = unpack_height(textureLoad(t_terrain, pos, lod).r); + let diffs: vec2 = unpack_diffs(textureLoad(t_normals, pos, lod).r, 1.0); + return vec3(height, diffs); +} + @vertex fn vert(@builtin(vertex_index) vid: u32, @location(0) in_off: vec2, @@ -47,45 +55,56 @@ fn vert(@builtin(vertex_index) vid: u32, var in_position: vec2 = vec2(i32(idx_x), i32(idx_y)); - if (idx_x == 0u) { // x_neg - in_position.y &= -1 << ((stitch_dir_flags & 4u) >> 2u); - } - else if (idx_x == cdata.resolution - 1u) { // x_pos - in_position.y &= -1 << (stitch_dir_flags & 1u); - } - if (idx_y == 0u) { // y_neg - in_position.x &= -1 << ((stitch_dir_flags & 8u) >> 3u); - } - else if (idx_y == cdata.resolution - 1u) { // y_pos - in_position.x &= -1 << ((stitch_dir_flags & 2u) >> 1u); - } + //if (idx_x == 0u) { // x_neg + // in_position.y &= -1 << ((stitch_dir_flags & 4u) >> 2u); + //} + //else if (idx_x == cdata.resolution - 1u) { // x_pos + // in_position.y &= -1 << (stitch_dir_flags & 1u); + //} + //if (idx_y == 0u) { // y_neg + // in_position.x &= -1 << ((stitch_dir_flags & 8u) >> 3u); + //} + //else if (idx_y == cdata.resolution - 1u) { // y_pos + // in_position.x &= -1 << ((stitch_dir_flags & 2u) >> 1u); + //} - let tpos: vec2 = in_position * i32(cdata.lod_pow2) + vec2(in_off * cdata.inv_cell_size); + let tpos: vec2 = in_position + vec2(in_off * cdata.inv_cell_size / f32(cdata.lod_pow2)); - let h_dx_dy: vec3 = unpack(textureLoad(t_terraindata, tpos, 0).r, 1.0); + let height_dx_dy: vec3 = sampleHeightDxDy(tpos, i32(cdata.lod)); - let world_pos: vec3 = vec3(vec2(in_position * i32(cdata.lod_pow2)) * cdata.cell_size + in_off, h_dx_dy.x); - let clip_pos: vec4 = global.u_view_proj * vec4(world_pos, 1.0); + var normal: vec3 = normalize(vec3(height_dx_dy.yz, cdata.cell_size * 2.0)); // https://stackoverflow.com/questions/49640250/calculate-normals-from-heightmap - //let dist_to_cam: f32 = length(params.cam_pos.xyz - vec3(pos.xy, 0.0)); - //let transition_alpha: f32 = smoothstep(cdata.distance_lod_cutoff * 0.8, cdata.distance_lod_cutoff, dist_to_cam); + var world_pos: vec3 = vec3(vec2(in_position * i32(cdata.lod_pow2)) * cdata.cell_size + in_off, height_dx_dy.x); - var out_normal: vec3 = normalize(vec3(h_dx_dy.yz, cdata.cell_size * 2.0)); // https://stackoverflow.com/questions/49640250/calculate-normals-from-heightmap + let height_dx_dy_next: vec3 = sampleHeightDxDy(tpos / 2, i32(cdata.lod) + 1); + let normal_next: vec3 = normalize(vec3(height_dx_dy_next.yz, cdata.cell_size * 2.0)); -#ifdef DEBUG var debug = 0.0; - debug = f32(cdata.lod); - if(height >= MAX_HEIGHT) { - debug = diffs.x; - } + if (cdata.lod < 4u) { + let dist_to_cam: f32 = length(params.cam_pos.xyz - vec3(world_pos.xy, 0.0)); + let transition_alpha: f32 = smoothstep(cdata.distance_lod_cutoff * 0.8, cdata.distance_lod_cutoff, dist_to_cam); + +#ifdef DEBUG + debug = (f32(cdata.lod) + transition_alpha + 1.0) / 5.0; #endif - return VertexOutput( - out_normal, + var world_pos_next: vec3 = vec3(vec2(in_position / 2 * i32(cdata.lod_pow2)) * cdata.cell_size * 2.0 + in_off, height_dx_dy_next.x); + + normal = mix(normal, normal_next, transition_alpha); + world_pos = mix(world_pos, world_pos_next, transition_alpha); + } else { + debug = 1.0; + } + + let clip_pos: vec4 = global.u_view_proj * vec4(world_pos, 1.0); + + + return VertexOutput(clip_pos, + normal, world_pos, #ifdef DEBUG - debug, + debug #endif - clip_pos); + ); } diff --git a/assets/shaders/terrain/unpack.wgsl b/assets/shaders/terrain/unpack.wgsl index cf95c28d..200d542a 100644 --- a/assets/shaders/terrain/unpack.wgsl +++ b/assets/shaders/terrain/unpack.wgsl @@ -12,9 +12,3 @@ fn unpack_diffs(v: u32, lod_pow2: f32) -> vec2 { let y = (f32(v & 0xFFu) - 128.0) / 127.0 * (MAX_DIFF * lod_pow2); return vec2(x, y); } - -fn unpack(v: u32, lod_pow2: f32) -> vec3 { - let h = unpack_height(v & 0xFFFFu); - let d = unpack_diffs(v >> 16u, lod_pow2); - return vec3(h, d); -} \ No newline at end of file diff --git a/engine/src/drawables/terrain.rs b/engine/src/drawables/terrain.rs index 88af6c65..919431cf 100644 --- a/engine/src/drawables/terrain.rs +++ b/engine/src/drawables/terrain.rs @@ -5,30 +5,37 @@ use crate::{ use geom::{vec2, vec3, Camera, InfiniteFrustrum, Intersect3, Matrix4, Vec2, AABB3}; use std::sync::Arc; use wgpu::{ - BindGroupDescriptor, BindGroupLayoutDescriptor, BufferUsages, Extent3d, FilterMode, - ImageCopyTexture, ImageDataLayout, IndexFormat, Origin3d, RenderPass, RenderPipeline, - TextureFormat, TextureUsages, VertexAttribute, VertexBufferLayout, + BindGroupDescriptor, BindGroupLayoutDescriptor, BufferUsages, CommandEncoder, + CommandEncoderDescriptor, Extent3d, FilterMode, ImageCopyTexture, ImageDataLayout, IndexFormat, + Origin3d, RenderPass, RenderPipeline, RenderPipelineDescriptor, TextureFormat, TextureView, + VertexAttribute, VertexBufferLayout, }; -const LOD: usize = 4; -const LOD_MIN_DIST_LOG2: f32 = 11.0; // 2^10 = 1024, meaning until 2048m away, we use the highest lod +const LOD: usize = 5; +const LOD_MIN_DIST_LOG2: f32 = 9.5; // 2^10 = 1024, meaning until 2048m away, we use the highest lod const MAX_HEIGHT: f32 = 2008.0; const MIN_HEIGHT: f32 = -40.0; -const MAX_DIFF: f32 = 32.0; +const UPSCALE_LOD: usize = 2; // amount of LOD that are superior to base terrain data pub struct TerrainChunk { pub dirt_id: u32, } +/// CSIZE is the size of a chunk in meters +/// CRESOLUTION is the resolution of a chunk, in vertices, at the chunk data level (not LOD0 since we upsample) pub struct TerrainRender { terrain_tex: Arc, - #[allow(unused)] - grass_tex: Arc, // kept alive + normal_tex: Arc, + indices: [(PBuffer, u32); LOD], instances: [(PBuffer, u32); LOD], bgs: Arc<[wgpu::BindGroup; LOD]>, w: u32, h: u32, + + normal_pipeline: RenderPipeline, + downsample_pipeline: RenderPipeline, + upsample_pipeline: RenderPipeline, } pub struct TerrainPrepared { @@ -38,22 +45,23 @@ pub struct TerrainPrepared { } impl TerrainRender { + const LOD0_RESOLUTION: usize = CRESOLUTION * (1 << UPSCALE_LOD); + pub fn new(gfx: &mut GfxContext, w: u32, h: u32, grass: Arc) -> Self { debug_assert!( - CRESOLUTION >= 1 << LOD, - "TERRAIN RESOLUTION must be >= {}", + Self::LOD0_RESOLUTION >= 1 << LOD, + "LOD0 TERRAIN RESOLUTION must be >= {}", 1 << LOD ); let indices = Self::generate_indices_mesh(gfx); - let tex = TextureBuilder::empty( - w * CRESOLUTION as u32, - h * CRESOLUTION as u32, + let terrain_tex = TextureBuilder::empty( + w * Self::LOD0_RESOLUTION as u32, + h * Self::LOD0_RESOLUTION as u32, 1, - TextureFormat::R32Uint, + TextureFormat::R16Uint, ) - .with_usage(TextureUsages::COPY_DST | TextureUsages::TEXTURE_BINDING) .with_fixed_mipmaps(LOD as u32) .with_sampler(wgpu::SamplerDescriptor { label: Some("terrain sampler"), @@ -67,6 +75,25 @@ impl TerrainRender TerrainRender TerrainRender TerrainRender Option, - get_down: impl Fn(usize) -> Option, - get_right: impl Fn(usize) -> Option, - get_left: impl Fn(usize) -> Option, ) { - fn pack(height: f32, diffx: f32, diffy: f32) -> [u8; 4] { + fn pack(height: f32) -> [u8; 2] { let h_encoded = ((height.clamp(MIN_HEIGHT, MAX_HEIGHT) - MIN_HEIGHT) / (MAX_HEIGHT - MIN_HEIGHT) * u16::MAX as f32) as u16; - - let dx_encoded: u8; - let dy_encoded: u8; - - if height >= MAX_HEIGHT || height <= MIN_HEIGHT { - dx_encoded = 128; - dy_encoded = 128; // normal is zero if we hit height bounds - } else { - dx_encoded = - (diffx.clamp(-MAX_DIFF, MAX_DIFF) / MAX_DIFF * i8::MAX as f32 + 128.0) as u8; - dy_encoded = - (diffy.clamp(-MAX_DIFF, MAX_DIFF) / MAX_DIFF * i8::MAX as f32 + 128.0) as u8; - } - - let packed = (dx_encoded as u32) << 24 | (dy_encoded as u32) << 16 | h_encoded as u32; - packed.to_le_bytes() + h_encoded.to_le_bytes() } - let mut contents = Vec::with_capacity(CRESOLUTION * CRESOLUTION); + let mut contents = Vec::with_capacity(CRESOLUTION * CRESOLUTION * 2); - let mut holder_y_edge: [f32; CRESOLUTION] = [0.0; CRESOLUTION]; - let mut j = 0; - let mut last_ys = &[(); CRESOLUTION].map(|_| { - let height_down = get_down(j).unwrap_or(chunk[0][j]); - j += 1; - height_down - }); for i in 0..CRESOLUTION { - let ys = &chunk[i]; - let next_ys = chunk.get(i + 1).unwrap_or_else(|| { - for j in 0..CRESOLUTION { - holder_y_edge[j] = get_up(j).unwrap_or(ys[j]); - } - &holder_y_edge - }); + let ys: &[f32; CRESOLUTION] = &chunk[i]; - let mut last_height = get_left(i).unwrap_or(ys[0]); for j in 0..CRESOLUTION { - let height = ys[j]; - let dh_x = last_height - - ys.get(j + 1) - .copied() - .unwrap_or_else(|| get_right(i).unwrap_or(height)); - let dh_y = last_ys[j] - next_ys[j]; - - contents.extend(pack(height, dh_x, dh_y)); - last_height = height; + contents.extend(pack(ys[j])); } - - last_ys = ys; } let h = CRESOLUTION as u32; @@ -185,7 +173,7 @@ impl TerrainRender TerrainRender TerrainRender = vec![0; (self.h * self.w) as usize]; + // frustrum culling + lod assignment for y in 0..self.h { for x in 0..self.w { let chunk_corner = vec2(x as f32, y as f32) * CSIZE as f32; let chunk_center = chunk_corner + Vec2::splat(CSIZE as f32 * 0.5); - if !frustrum.intersects(&AABB3::centered( - chunk_center.z0(), - vec3(CSIZE as f32, CSIZE as f32, 2000.0), + if !frustrum.intersects(&AABB3::new( + chunk_corner.z(MIN_HEIGHT), + chunk_corner.z0() + vec3(CSIZE as f32, CSIZE as f32, MAX_HEIGHT + 16.0), )) { continue; } @@ -289,7 +278,7 @@ impl TerrainRender = Vec::with_capacity(6 * resolution * resolution); @@ -319,6 +308,249 @@ impl TerrainRender RenderPipeline { + let normal_module = gfx.get_module("terrain/calc_normals"); + + gfx.device + .create_render_pipeline(&RenderPipelineDescriptor { + label: Some("terrain normals pipeline"), + layout: Some( + &gfx.device + .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("terrain normals pipeline layout"), + bind_group_layouts: &[&Texture::bindgroup_layout(&gfx.device, [TL::UInt])], + push_constant_ranges: &[], + }), + ), + vertex: wgpu::VertexState { + module: &normal_module, + entry_point: "vert", + buffers: &[], + }, + fragment: Some(wgpu::FragmentState { + module: &normal_module, + entry_point: "calc_normals", + targets: &[Some(wgpu::ColorTargetState { + format: normals_tex.format, + blend: None, + write_mask: wgpu::ColorWrites::ALL, + })], + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleStrip, + cull_mode: None, + ..Default::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }) +} + +fn normal_update<'a>( + gfx: &GfxContext, + normal_pipeline: &RenderPipeline, + encoder: &'a mut CommandEncoder, + height_tex: &'a Texture, + normal_view: &TextureView, +) { + let binding = height_tex.bindgroup(&gfx.device, &normal_pipeline.get_bind_group_layout(0)); + let mut rp = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("terrain normals render pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &normal_view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + rp.set_pipeline(&normal_pipeline); + rp.set_bind_group(0, &binding, &[]); + rp.draw(0..4, 0..1); + drop(rp); +} + +fn resample_pipeline(gfx: &GfxContext, height_tex: &Texture, entry_point: &str) -> RenderPipeline { + let resample_module = gfx.get_module("terrain/resample"); + + gfx.device + .create_render_pipeline(&RenderPipelineDescriptor { + label: Some("terrain downsample pipeline"), + layout: Some( + &gfx.device + .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("terrain downsample pipeline layout"), + bind_group_layouts: &[&gfx.device.create_bind_group_layout( + &BindGroupLayoutDescriptor { + label: None, + entries: &[Texture::bindgroup_layout_entries( + 0, + [TL::UInt].into_iter(), + ) + // We don't need a sampler + .next() + .unwrap()], + }, + )], + push_constant_ranges: &[], + }), + ), + vertex: wgpu::VertexState { + module: &resample_module, + entry_point: "vert", + buffers: &[], + }, + fragment: Some(wgpu::FragmentState { + module: &resample_module, + entry_point, + targets: &[Some(wgpu::ColorTargetState { + format: height_tex.format, + blend: None, + write_mask: wgpu::ColorWrites::ALL, + })], + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleStrip, + cull_mode: None, + ..Default::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }) +} + +/// Downsamples the terrain 1 mip up, the mip argument should be the base level +fn downsample_update( + gfx: &GfxContext, + downsample_pipeline: &RenderPipeline, + encoder: &mut CommandEncoder, + height_tex: &Texture, + mip: u32, +) { + let bg = gfx.device.create_bind_group(&wgpu::BindGroupDescriptor { + layout: &downsample_pipeline.get_bind_group_layout(0), + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&height_tex.mip_view(mip)), + }], + label: None, + }); + + let render_view = height_tex.mip_view(mip + 1); + let mut rp = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("terrain downsample render pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &render_view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + rp.set_pipeline(&downsample_pipeline); + rp.set_bind_group(0, &bg, &[]); + rp.draw(0..4, 0..1); + drop(rp); +} + +/// Downsamples the terrain 1 mip down, the mip argument should be the base level +fn upsample_update( + gfx: &GfxContext, + upsample_pipeline: &RenderPipeline, + encoder: &mut CommandEncoder, + height_tex: &Texture, + mip: u32, +) { + let bg = gfx.device.create_bind_group(&wgpu::BindGroupDescriptor { + layout: &upsample_pipeline.get_bind_group_layout(0), + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&height_tex.mip_view(mip)), + }], + label: None, + }); + + let render_view = height_tex.mip_view(mip - 1); + let mut rp = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("terrain upsample render pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &render_view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + rp.set_pipeline(&upsample_pipeline); + rp.set_bind_group(0, &bg, &[]); + rp.draw(0..4, 0..1); + drop(rp); } #[derive(Hash)] @@ -386,11 +618,14 @@ impl PipelineBuilder for TerrainPipeline { let terrainlayout = gfx .device .create_bind_group_layout(&BindGroupLayoutDescriptor { - entries: &Texture::bindgroup_layout_entries(0, [TL::UInt, TL::Float].into_iter()) - .chain(std::iter::once( - Uniform::::bindgroup_layout_entry(4), - )) - .collect::>(), + entries: &Texture::bindgroup_layout_entries( + 0, + [TL::UInt, TL::UInt, TL::Float].into_iter(), + ) + .chain(std::iter::once( + Uniform::::bindgroup_layout_entry(6), + )) + .collect::>(), label: Some("terrain bindgroup layout"), }); let vert = &mk_module("terrain/terrain.vert"); diff --git a/engine/src/lib.rs b/engine/src/lib.rs index aac198c0..bd46370d 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -41,4 +41,5 @@ pub use vertex_types::*; pub use winit::event::ScanCode; pub use winit::window::CursorGrabMode; +pub use image; pub use wgpu; diff --git a/engine/src/texture.rs b/engine/src/texture.rs index 54cccf4b..f3b5536f 100644 --- a/engine/src/texture.rs +++ b/engine/src/texture.rs @@ -269,6 +269,19 @@ impl Texture { ..Default::default() } } + + pub fn mip_view(&self, mip_level: u32) -> wgpu::TextureView { + self.texture.create_view(&TextureViewDescriptor { + label: Some("texture mip"), + format: None, + dimension: None, + aspect: wgpu::TextureAspect::All, + base_mip_level: mip_level, + mip_level_count: Some(1), + base_array_layer: 0, + array_layer_count: None, + }) + } } #[derive(Debug, Display, From)] @@ -277,6 +290,7 @@ pub enum TextureBuildError { Image(image::ImageError), } +#[derive(Clone)] pub struct TextureBuilder<'a> { img: Option, dimensions: (u32, u32, u32), @@ -626,11 +640,11 @@ fn generate_mipmaps( let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { layout: &bind_group_layout, entries: &[ - wgpu::BindGroupEntry { + BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&views[target_mip - 1]), }, - wgpu::BindGroupEntry { + BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&sampler), }, diff --git a/engine_demo/src/main.rs b/engine_demo/src/main.rs index 80aa0d9c..6c515f2e 100644 --- a/engine_demo/src/main.rs +++ b/engine_demo/src/main.rs @@ -23,6 +23,7 @@ trait DemoElement { Self: Sized; fn update(&mut self, ctx: &mut Context); fn render(&mut self, fc: &mut FrameContext, cam: &Camera, frustrum: &InfiniteFrustrum); + fn render_gui(&mut self, _ui: &mut egui::Ui) {} } struct State { @@ -42,7 +43,7 @@ struct State { ms_hist: History, - settings: GfxSettings, + gfx_settings: GfxSettings, sun_angle: Degrees, } @@ -73,6 +74,9 @@ impl engine::framework::State for State { ctx.audio.set_settings(100.0, 100.0, 100.0, 100.0); + let mut gfx_settings = GfxSettings::default(); + gfx_settings.shader_debug = true; + Self { demo_elements: vec![ (Box::new(Spheres::init(ctx)), true), @@ -88,7 +92,7 @@ impl engine::framework::State for State { last_cam: camera, freeze_cam: false, ms_hist: History::new(128), - settings: Default::default(), + gfx_settings, sun_angle: Degrees(0.0), } } @@ -96,7 +100,7 @@ impl engine::framework::State for State { fn update(&mut self, ctx: &mut Context) { self.delta = ctx.delta; - ctx.gfx.update_settings(self.settings); + ctx.gfx.update_settings(self.gfx_settings); self.ms_hist.add_value(ctx.delta); let delta = self.camera_movement(ctx); @@ -177,6 +181,7 @@ impl engine::framework::State for State { for (de, enabled) in &mut self.demo_elements { ui.checkbox(enabled, de.name()); + de.render_gui(ui); } if ui.button("play sound: road_lay").clicked() { @@ -197,22 +202,22 @@ impl engine::framework::State for State { ui.add(egui::Slider::new(&mut self.camera_speed, 1.0..=100.0).text("Camera speed")); ui.checkbox(&mut self.freeze_cam, "Freeze camera"); - ui.checkbox(&mut self.settings.fullscreen, "Fullscreen"); - ui.checkbox(&mut self.settings.vsync, "VSync"); - ui.checkbox(&mut self.settings.fog, "Fog"); - ui.checkbox(&mut self.settings.ssao, "SSAO"); - ui.checkbox(&mut self.settings.terrain_grid, "Terrain grid"); + ui.checkbox(&mut self.gfx_settings.fullscreen, "Fullscreen"); + ui.checkbox(&mut self.gfx_settings.vsync, "VSync"); + ui.checkbox(&mut self.gfx_settings.fog, "Fog"); + ui.checkbox(&mut self.gfx_settings.ssao, "SSAO"); + ui.checkbox(&mut self.gfx_settings.terrain_grid, "Terrain grid"); - let mut shadows = self.settings.shadows.size().is_some(); + let mut shadows = self.gfx_settings.shadows.size().is_some(); ui.checkbox(&mut shadows, "Shadows"); - self.settings.shadows = if shadows { + self.gfx_settings.shadows = if shadows { ShadowQuality::High } else { ShadowQuality::NoShadows }; - ui.checkbox(&mut self.settings.shader_debug, "Shader debug"); - ui.checkbox(&mut self.settings.pbr_enabled, "PBR Environment Update"); + ui.checkbox(&mut self.gfx_settings.shader_debug, "Shader debug"); + ui.checkbox(&mut self.gfx_settings.pbr_enabled, "PBR Environment Update"); }); } } @@ -287,10 +292,10 @@ impl State { * cam_speed; } if ctx.input.keyboard.pressed_scancode.contains(&57) { - self.camera.pos += vec3(0.0, 0.0, 1.0) * cam_speed; + self.camera.pos += vec3(0.0, 0.0, 0.5) * cam_speed; } if ctx.input.keyboard.pressed_scancode.contains(&29) { - self.camera.pos -= vec3(0.0, 0.0, 1.0) * cam_speed; + self.camera.pos -= vec3(0.0, 0.0, 0.5) * cam_speed; } if self.is_captured { diff --git a/engine_demo/src/terrain.rs b/engine_demo/src/terrain.rs index 4ec613f0..32c0be8d 100644 --- a/engine_demo/src/terrain.rs +++ b/engine_demo/src/terrain.rs @@ -1,15 +1,17 @@ use crate::DemoElement; +use engine::image::GenericImageView; use engine::terrain::TerrainRender as EngineTerrainRender; use engine::{Context, FrameContext}; use geom::{vec2, Camera, InfiniteFrustrum}; -const CSIZE: usize = 512; -const CRESO: usize = 32; -const MAP_SIZE: usize = 25; +const CSIZE: usize = 256; +const CRESO: usize = 8; +const MAP_SIZE: usize = 100; pub struct Terrain { terrain: EngineTerrainRender, - heights: Box<[[[[f32; CRESO]; CRESO]; MAP_SIZE]; MAP_SIZE]>, + _heights: Box<[[[[f32; CRESO]; CRESO]; MAP_SIZE]; MAP_SIZE]>, + reload: bool, } impl DemoElement for Terrain { @@ -26,17 +28,18 @@ impl DemoElement for Terrain { .try_into() .unwrap(); - for x in 0..MAP_SIZE { - for y in 0..MAP_SIZE { + for y in 0..MAP_SIZE { + for x in 0..MAP_SIZE { for i in 0..CRESO { for j in 0..CRESO { - heights[y][x][i][j] = 600.0 - * (0.5 - + geom::fnoise( - 0.01 * vec2((x * CRESO + j) as f32, (y * CRESO + i) as f32), - ) - .0 - .powi(2)); + heights[y][x][i][j] = 3000.0 + * geom::fnoise::<6>( + 0.002 * vec2((x * CRESO + j) as f32, (y * CRESO + i) as f32), + ) + .0 + .powi(2); + //heights[y][x][i][j] = + // (CSIZE / CRESO * i) as f32 + 0.5 * (CSIZE / CRESO * j) as f32; } } } @@ -48,44 +51,37 @@ impl DemoElement for Terrain { for x in 0..MAP_SIZE { for y in 0..MAP_SIZE { - terrain.update_chunk( - gfx, - (x as u32, y as u32), - &heights[y][x], - |j: usize| { - if y + 1 == MAP_SIZE || j >= CRESO { - return None; - } - Some(heights[y + 1][x][0][j]) - }, - |j: usize| { - if y == 0 || j >= CRESO { - return None; - } - Some(heights[y - 1][x][CRESO - 1][j]) - }, - |i: usize| { - if x + 1 == MAP_SIZE || i >= CRESO { - return None; - } - Some(heights[y][x + 1][i][0]) - }, - |i: usize| { - if x == 0 || i >= CRESO { - return None; - } - Some(heights[y][x - 1][i][CRESO - 1]) - }, - ); + terrain.update_chunk(gfx, (x as u32, y as u32), &heights[y][x]); } } - Self { terrain, heights } + terrain.invalidate_height_normals(&ctx.gfx); + + Self { + terrain, + _heights: heights, + reload: false, + } } - fn update(&mut self, _ctx: &mut Context) {} + fn update(&mut self, ctx: &mut Context) { + if self.reload { + self.reload = false; + self.terrain.invalidate_height_normals(&ctx.gfx); + } + } fn render(&mut self, fc: &mut FrameContext, cam: &Camera, frustrum: &InfiniteFrustrum) { self.terrain.draw_terrain(cam, frustrum, fc); } + + fn render_gui(&mut self, ui: &mut egui::Ui) { + ui.indent("terrain", |ui| { + if cfg!(debug_assertions) { + if ui.button("reload terrain").clicked() { + self.reload = true; + } + } + }); + } } diff --git a/geom/src/noise.rs b/geom/src/noise.rs index d7d06882..1935d10c 100644 --- a/geom/src/noise.rs +++ b/geom/src/noise.rs @@ -61,14 +61,14 @@ pub fn simplex_noise(pos: Vec2) -> (f32, Vec2) { const FBM_MAG: f32 = 0.4; #[inline] -pub fn fnoise(in_wv: Vec2) -> (f32, Vec2) { +pub fn fnoise(in_wv: Vec2) -> (f32, Vec2) { let mut dec = in_wv; let mut noise: f32 = 0.0; let mut amplitude: f32 = 1.0; let mut grad: Vec2 = Vec2::ZERO; - for _ in 0..4 { + for _ in 0..LAYERS { let (n, g) = simplex_noise(dec); noise += amplitude * n; grad += g; diff --git a/native_app/src/rendering/map_rendering/terrain.rs b/native_app/src/rendering/map_rendering/terrain.rs index b7205f7d..95c2d24c 100644 --- a/native_app/src/rendering/map_rendering/terrain.rs +++ b/native_app/src/rendering/map_rendering/terrain.rs @@ -38,45 +38,17 @@ impl TerrainRender { while let Some(cell) = self.terrain_sub.take_one_updated_chunk() { let chunk = unwrap_retlog!(ter.chunks.get(&cell), "trying to update nonexistent chunk"); - let chunk_up = ter.chunks.get(&(cell.0, cell.1 + 1)); - let chunk_down = ter.chunks.get(&(cell.0, cell.1.wrapping_sub(1))); - let chunk_left = ter.chunks.get(&(cell.0.wrapping_sub(1), cell.1)); - let chunk_right = ter.chunks.get(&(cell.0 + 1, cell.1)); - - self.terrain.update_chunk( - &mut ctx.gfx, - cell, - &chunk.heights, - |i: usize| { - if i >= CRESO { - return None; - } - return Some(chunk_up?.heights[0][i]); - }, - |i: usize| { - if i >= CRESO { - return None; - } - return Some(chunk_down?.heights[CRESO - 1][i]); - }, - |i: usize| { - if i >= CRESO { - return None; - } - return Some(chunk_right?.heights[i][0]); - }, - |i: usize| { - if i >= CRESO { - return None; - } - return Some(chunk_left?.heights[i][CRESO - 1]); - }, - ); + self.terrain + .update_chunk(&mut ctx.gfx, cell, &chunk.heights); update_count += 1; if update_count > 20 { break; } } + + if update_count > 0 { + self.terrain.invalidate_height_normals(&ctx.gfx); + } } } diff --git a/simulation/src/map/procgen/heightmap.rs b/simulation/src/map/procgen/heightmap.rs index bf763d85..cde9e621 100644 --- a/simulation/src/map/procgen/heightmap.rs +++ b/simulation/src/map/procgen/heightmap.rs @@ -1,7 +1,7 @@ use geom::{fnoise, simplex_noise, vec2, Vec2}; pub(crate) fn height(p: Vec2) -> (f32, Vec2) { - let (noise, mut grad) = fnoise(Vec2::splat(70.69) + 0.00006 * p); + let (noise, mut grad) = fnoise::<4>(Vec2::splat(70.69) + 0.00006 * p); grad *= 0.00006; let ratio = 0.00005;