diff --git a/.github/ISSUE_TEMPLATE/new-task.md b/.github/ISSUE_TEMPLATE/new-task.md new file mode 100644 index 0000000000..c61bf28178 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/new-task.md @@ -0,0 +1,14 @@ +--- +name: New Task +about: Create a New Task for the project +title: '' +labels: Task +assignees: Gikster007 + +--- + +Task Description: + + +Acceptance Criteria: +- diff --git a/.gitignore b/.gitignore index 56a3716b45..d31a823537 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ # User-specific files *.sln +*.slnx *.vcxproj *.vcxproj.filters *.suo diff --git a/data/shaders/brdf.hlsl b/data/shaders/brdf.hlsl index 4a87b37185..6e9265e8c6 100644 --- a/data/shaders/brdf.hlsl +++ b/data/shaders/brdf.hlsl @@ -41,6 +41,12 @@ float F_Schlick(float f0, float f90, float v_dot_h) return f0 + (f90 - f0) * pow(1.0 - v_dot_h, 5.0); } +// Inspired by Atlas talk @ GDC19 - https://youtu.be/Dqld965-Vv0?t=1590 +float F_Ocean(float n_dot_v, float roughness, float f0) +{ + return lerp(pow(1.0f - n_dot_v, 5.0f * exp(-2.69f * roughness)) / (1.0f + 22.7f * pow(roughness, 1.5f)), 1.0f, f0); +} + /*------------------------------------------------------------------------------ DIFFUSE ------------------------------------------------------------------------------*/ @@ -56,7 +62,10 @@ float3 Diffuse_Burley(float3 diffuse_color, float roughness, float n_dot_v, floa float3 BRDF_Diffuse(Surface surface, AngularInfo angular_info) { - return Diffuse_Burley(surface.albedo, surface.roughness, angular_info.n_dot_v, angular_info.n_dot_l, angular_info.v_dot_h); + if (!surface.is_ocean()) + return Diffuse_Burley(surface.albedo, surface.roughness, angular_info.n_dot_v, angular_info.n_dot_l, angular_info.v_dot_h); + else + return float3(0.0f, 0.0f, 0.0f); // ocean diffuse comes from scattering, not albedo } /*------------------------------------------------------------------------------ @@ -116,6 +125,12 @@ float V_Neubelt(float n_dot_v, float n_dot_l) return saturate_16(1.0 / (4.0 * (n_dot_l + n_dot_v - n_dot_l * n_dot_v))); } +// single-direction smith G1 for ocean SSS masking, takes alpha^2 +float V1_SmithGGX(float n_dot_l, float a2) +{ + return 2.0f * n_dot_l / (n_dot_l + sqrt(n_dot_l * n_dot_l * (1.0f - a2) + a2) + FLT_MIN); +} + /*------------------------------------------------------------------------------ ENERGY ------------------------------------------------------------------------------*/ diff --git a/data/shaders/common.hlsl b/data/shaders/common.hlsl index dfefefa053..462885243f 100644 --- a/data/shaders/common.hlsl +++ b/data/shaders/common.hlsl @@ -45,6 +45,7 @@ static const uint THREAD_GROUP_COUNT_X = 8; static const uint THREAD_GROUP_COUNT_Y = 8; static const uint THREAD_GROUP_COUNT = 64; static const float DEG_TO_RAD = PI / 180.0f; +static const float G = 9.81f; /*------------------------------------------------------------------------------ SATURATE diff --git a/data/shaders/common_resources.hlsl b/data/shaders/common_resources.hlsl index 4e9c681c58..7c9deeae0e 100644 --- a/data/shaders/common_resources.hlsl +++ b/data/shaders/common_resources.hlsl @@ -121,6 +121,40 @@ struct MaterialParameters float anisotropic_rotation; float clearcoat; float clearcoat_roughness; + + struct OceanParameters + { + float scale; + float spreadBlend; + float swell; + float gamma; + float shortWavesFade; + + float windDirection; + float fetch; + float windSpeed; + float repeatTime; + float angle; + float alpha; + float peakOmega; + + float depth; + float lowCutoff; + float highCutoff; + + float foamDecayRate; + float foamBias; + float foamThreshold; + float foamAdd; + + float displacementScale; + float slopeScale; + float lengthScale; + + float debugDisplacement; + float debugSlope; + float debugSynthesised; + } ocean_parameters; bool has_texture_albedo() { return (flags & (1 << 2)) != 0; } bool has_texture_normal() { return (flags & (1 << 1)) != 0; } @@ -268,7 +302,11 @@ struct DrawData uint material_index; uint is_transparent; uint aabb_index; - uint padding; + float tile_size; + float2 tile_world_pos; + float2 tile_snap_center; + uint tile_res; + float3 padding; }; // bindless draw data - per-draw transforms, material indices, etc. diff --git a/data/shaders/common_structs.hlsl b/data/shaders/common_structs.hlsl index a2312af927..ff19846601 100644 --- a/data/shaders/common_structs.hlsl +++ b/data/shaders/common_structs.hlsl @@ -26,6 +26,40 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #ifndef SPARTAN_COMMON_STRUCT #define SPARTAN_COMMON_STRUCT +struct OceanParameters +{ + float scale; + float spreadBlend; + float swell; + float gamma; + float shortWavesFade; + + float windDirection; + float fetch; + float windSpeed; + float repeatTime; + float angle; + float alpha; + float peakOmega; + + float depth; + float lowCutoff; + float highCutoff; + + float foamDecayRate; + float foamBias; + float foamThreshold; + float foamAdd; + + float displacementScale; + float slopeScale; + float lengthScale; + + float debugDisplacement; + float debugSlope; + float debugSynthesised; +}; + struct Surface { // properties @@ -54,6 +88,10 @@ struct Surface float camera_to_pixel_length; float3 diffuse_energy; + OceanParameters ocean_parameters; + float wave_height; + float foam_mask; + // easy access to certain properties bool has_texture_height() { return flags & uint(1U << 0); } bool has_texture_normal() { return flags & uint(1U << 1); } @@ -70,6 +108,7 @@ struct Surface bool is_flower() { return flags & uint(1U << 12); } bool is_water() { return flags & uint(1U << 13); } bool is_tessellated() { return flags & uint(1U << 14); } + bool is_ocean() { return flags & uint(1U << 16); } bool is_sky() { return alpha == 0.0f; } bool is_opaque() { return alpha == 1.0f; } bool is_transparent() { return alpha > 0.0f && alpha < 1.0f; } @@ -96,7 +135,7 @@ struct Surface roughness = sample_material.r; metallic = sample_material.g; emissive = sample_material.b; - F0 = lerp(0.04f, albedo, metallic); + F0 = flags & uint(1U << 16) ? 0.02f : lerp(0.04f, albedo, metallic); anisotropic = material.anisotropic; anisotropic_rotation = material.anisotropic_rotation; clearcoat = material.clearcoat; @@ -104,7 +143,50 @@ struct Surface sheen = material.sheen; subsurface_scattering = material.subsurface_scattering; diffuse_energy = 1.0f; - + + // jonswap parameters + ocean_parameters.alpha = material.ocean_parameters.alpha; + ocean_parameters.angle = material.ocean_parameters.angle; + ocean_parameters.fetch = material.ocean_parameters.fetch; + ocean_parameters.gamma = material.ocean_parameters.gamma; + ocean_parameters.peakOmega = material.ocean_parameters.peakOmega; + ocean_parameters.repeatTime = material.ocean_parameters.repeatTime; + ocean_parameters.scale = material.ocean_parameters.scale; + ocean_parameters.shortWavesFade = material.ocean_parameters.shortWavesFade; + ocean_parameters.spreadBlend = material.ocean_parameters.spreadBlend; + ocean_parameters.swell = material.ocean_parameters.swell; + ocean_parameters.windDirection = material.ocean_parameters.windDirection; + ocean_parameters.windSpeed = material.ocean_parameters.windSpeed; + ocean_parameters.depth = material.ocean_parameters.depth; + ocean_parameters.lowCutoff = material.ocean_parameters.lowCutoff; + ocean_parameters.highCutoff = material.ocean_parameters.highCutoff; + ocean_parameters.foamDecayRate = material.ocean_parameters.foamDecayRate; + ocean_parameters.foamBias = material.ocean_parameters.foamBias; + ocean_parameters.foamThreshold = material.ocean_parameters.foamThreshold; + ocean_parameters.foamAdd = material.ocean_parameters.foamAdd; + ocean_parameters.displacementScale = material.ocean_parameters.displacementScale; + ocean_parameters.slopeScale = material.ocean_parameters.slopeScale; + ocean_parameters.lengthScale = material.ocean_parameters.lengthScale; + ocean_parameters.debugDisplacement = material.ocean_parameters.debugDisplacement; + ocean_parameters.debugSlope = material.ocean_parameters.debugSlope; + ocean_parameters.debugSynthesised = material.ocean_parameters.debugSynthesised; + // if we are an ocean, wave height is stored in the metallnes and emissive channels of the material gbuffer + // unpack the wave height and reset the metallic and emissive to 0.0f (expected values for an ocean surface) + // additionally, we store the foam mask in the occlusion channel of the material gbuffer as well + if (flags & uint(1U << 16)) + { + // Unpacking + uint high = (uint)(metallic * 255.0f + 0.5f); + uint low = (uint)(emissive * 255.0f + 0.5f); + wave_height = f16tof32((high << 8u) | low); + metallic = 0.0f; + emissive = 0.0f; + + foam_mask = sample_material.a; + occlusion = 0.0f; + } + + // roughness is authored as perceptual roughness, as is convention roughness_alpha = roughness * roughness; diff --git a/data/shaders/common_vertex_processing.hlsl b/data/shaders/common_vertex_processing.hlsl index 1dfbf37774..81c3b9bbd4 100644 --- a/data/shaders/common_vertex_processing.hlsl +++ b/data/shaders/common_vertex_processing.hlsl @@ -51,6 +51,7 @@ struct gbuffer_vertex float4 uv_misc : TEXCOORD; // xy = uv, z = height_percent, w = instance_id - packed together to reduced the interpolators (shader registers) the gpu needs to track float width_percent : TEXCOORD2; // temp, will remove nointerpolation uint material_index : TEXCOORD3; // for indirect draws, material index passed from vs + float2 synth_uv : TEXCOORD4; }; float4x4 compose_instance_transform(min16float instance_position_x, min16float instance_position_y, min16float instance_position_z, uint instance_normal_oct, uint instance_yaw, uint instance_scale) diff --git a/data/shaders/depth_prepass.hlsl b/data/shaders/depth_prepass.hlsl index 8c1871cda8..5442f34aaf 100644 --- a/data/shaders/depth_prepass.hlsl +++ b/data/shaders/depth_prepass.hlsl @@ -22,6 +22,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. //= INCLUDES ====================== #include "common.hlsl" #include "common_tessellation.hlsl" +#include "ocean/synthesise_maps.hlsl" //================================= #ifdef INDIRECT_DRAW @@ -34,10 +35,47 @@ gbuffer_vertex main_vs(Vertex_PosUvNorTan input, uint instance_id : SV_InstanceI _draw = draw_data[buffer_pass.draw_index]; #endif - float3 position_world = 0.0f; + MaterialParameters material = GetMaterial(); + Surface surface; + surface.flags = material.flags; + + // transform to world space + float3 position_world = 0.0f; float3 position_world_previous = 0.0f; - gbuffer_vertex vertex = transform_to_world_space(input, instance_id, _draw.transform, position_world, position_world_previous); - vertex.material_index = _draw.material_index; + gbuffer_vertex vertex = transform_to_world_space(input, instance_id, _draw.transform, position_world, position_world_previous); + + if (surface.is_ocean() && material.ocean_parameters.displacementScale > -1.0f) + { + // Snap to this LOD's vertex grid to prevent swimming + const float vertex_spacing = _draw.tile_size / _draw.tile_res; + position_world.xz = round(position_world.xz / vertex_spacing) * vertex_spacing; + + // Epsilon expansion to close gaps from floating point precision + float2 tile_center = _draw.tile_world_pos + _draw.tile_size * 0.5; + float epsilon = vertex_spacing * 0.1f; + float scale = 1.0f + epsilon / (_draw.tile_size * 0.5f); + position_world.xz = tile_center + (position_world.xz - tile_center) * scale; + + // Morph toward coarser grid near LOD boundaries + float2 from_center = abs(position_world.xz - _draw.tile_snap_center); + float half_extent = _draw.tile_size * 2.0f; + float edge = max(from_center.x, from_center.y) / half_extent; + float morph = smoothstep(0.7f, 1.0f, edge); + + float coarse_spacing = vertex_spacing * 2.0f; + float2 coarse_xz = round(position_world.xz / coarse_spacing) * coarse_spacing; + position_world.xz = lerp(position_world.xz, coarse_xz, morph); + + // Synthesize + float2 synth_uv = position_world.xz / material.ocean_parameters.lengthScale; + float4 displacement = float4(0.0f, 0.0f, 0.0f, 0.0f); + synthesize(tex2, displacement, synth_uv); + + // Apply displacement + position_world.xyz += displacement.xyz; + position_world_previous = position_world; + } + vertex.material_index = _draw.material_index; return transform_to_clip_space(vertex, position_world, position_world_previous); } diff --git a/data/shaders/g_buffer.hlsl b/data/shaders/g_buffer.hlsl index e8a955da5c..f432b9ec53 100644 --- a/data/shaders/g_buffer.hlsl +++ b/data/shaders/g_buffer.hlsl @@ -20,6 +20,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. //= INCLUDES ====================== #include "common.hlsl" #include "common_tessellation.hlsl" +#include "ocean/synthesise_maps.hlsl" //================================= struct gbuffer @@ -122,10 +123,49 @@ gbuffer_vertex main_vs(Vertex_PosUvNorTan input, uint instance_id : SV_InstanceI _draw = draw_data[buffer_pass.draw_index]; #endif - float3 position_world = 0.0f; + MaterialParameters material = GetMaterial(); + Surface surface; + surface.flags = material.flags; + + // transform to world space + float3 position_world = 0.0f; float3 position_world_previous = 0.0f; - gbuffer_vertex vertex = transform_to_world_space(input, instance_id, _draw.transform, position_world, position_world_previous); - vertex.material_index = _draw.material_index; + gbuffer_vertex vertex = transform_to_world_space(input, instance_id, _draw.transform, position_world, position_world_previous); + + if (surface.is_ocean() && material.ocean_parameters.displacementScale > -1.0f) + { + // Snap to this LOD's vertex grid to prevent swimming + const float vertex_spacing = _draw.tile_size / _draw.tile_res; + position_world.xz = round(position_world.xz / vertex_spacing) * vertex_spacing; + + // Epsilon expansion to close gaps from floating point precision + float2 tile_center = _draw.tile_world_pos + _draw.tile_size * 0.5; + float epsilon = vertex_spacing * 0.1f; + float scale = 1.0f + epsilon / (_draw.tile_size * 0.5f); + position_world.xz = tile_center + (position_world.xz - tile_center) * scale; + + // Morph toward coarser grid near LOD boundaries + float2 from_center = abs(position_world.xz - _draw.tile_snap_center); + float half_extent = _draw.tile_size * 2.0f; + float edge = max(from_center.x, from_center.y) / half_extent; + float morph = smoothstep(0.7f, 1.0f, edge); + + float coarse_spacing = vertex_spacing * 2.0f; + float2 coarse_xz = round(position_world.xz / coarse_spacing) * coarse_spacing; + position_world.xz = lerp(position_world.xz, coarse_xz, morph); + + // Synthesize + float2 synth_uv = position_world.xz / material.ocean_parameters.lengthScale; + float4 displacement = float4(0.0f, 0.0f, 0.0f, 0.0f); + synthesize(tex2, displacement, synth_uv); + + // Apply displacement + position_world.xyz += displacement.xyz; + position_world_previous = position_world; + + vertex.synth_uv = synth_uv; + } + vertex.material_index = _draw.material_index; return transform_to_clip_space(vertex, position_world, position_world_previous); } @@ -267,6 +307,50 @@ gbuffer main_ps(gbuffer_vertex vertex, bool is_front_face : SV_IsFrontFace) float3x3 tangent_to_world = make_tangent_to_world_matrix(vertex.normal, vertex.tangent); normal = normalize(mul(tangent_normal, tangent_to_world).xyz); } + else if (surface.is_ocean()) + { + float4 slope = float4(0.0f, 0.0f, 0.0f, 0.0f); + synthesize(tex3, slope, vertex.synth_uv, true); + + slope.rgb = slope.rgb * material.ocean_parameters.slopeScale; + + normal = normalize(float3(-slope.x, vertex.normal.y, -slope.y)); + + // apply foam (foam mask is stored in the alpha channel of slope map) + albedo.rgb = lerp(albedo.rgb, float3(1.0f, 1.0f, 1.0f), slope.a); + + // store fp16 wave height in metallnes and emission channels of material gbuffer + uint half_val = f32tof16(position_world.y); + metalness = (half_val >> 8u) / 255.0f; + emission = (half_val & 0xFFu) / 255.0f; + occlusion = slope.a; // store foam mask in occlusion channel + + if (material.ocean_parameters.debugDisplacement == 1.0f) // displacement + { + if (material.ocean_parameters.debugSynthesised == 1.0f) // show synthesised version + { + float4 displacement = float4(0.0f, 0.0f, 0.0f, 0.0f); + synthesize(tex2, displacement, vertex.synth_uv); + + albedo = displacement; + } + else // show original displacement + albedo = tex2.Sample(samplers[sampler_trilinear_clamp], vertex.uv_misc.xy).rgba; + } + else if (material.ocean_parameters.debugSlope == 1.0f) // slope + { + if (material.ocean_parameters.debugSynthesised == 1.0f) // show synthesised version + albedo = slope; + else // show original slope + albedo = tex3.Sample(samplers[sampler_trilinear_clamp], vertex.uv_misc.xy); + } + + //albedo = tex4.Sample(samplers[sampler_anisotropic_wrap], world_space_tile_uv / float2(6.0f, 6.0f)).rgba; + //albedo = float4(flow_dir * 0.5f + 0.5f, 0.0f, 1.0f); + //albedo = tex5.Sample(samplers[sampler_point_clamp], vertex.uv_misc.xy); + + //albedo = tex2.Sample(samplers[sampler_trilinear_clamp], vertex.uv_misc.xy).rgba; + } // foliage curved normals if (surface.is_grass_blade() || surface.is_flower()) diff --git a/data/shaders/indirect_cull.hlsl b/data/shaders/indirect_cull.hlsl index cf68d2e47e..2e32db2f7f 100644 --- a/data/shaders/indirect_cull.hlsl +++ b/data/shaders/indirect_cull.hlsl @@ -143,7 +143,7 @@ void main_cs(uint3 dispatch_thread_id : SV_DispatchThreadID) // atomically allocate a slot in the output buffers uint output_index; InterlockedAdd(indirect_draw_count[0], 1, output_index); - + // compact the draw arguments and per-draw data indirect_draw_args_out[output_index] = indirect_draw_args[draw_index]; indirect_draw_data_out[output_index] = indirect_draw_data[draw_index]; diff --git a/data/shaders/light.hlsl b/data/shaders/light.hlsl index 378fceb52a..b747655e41 100644 --- a/data/shaders/light.hlsl +++ b/data/shaders/light.hlsl @@ -124,6 +124,39 @@ float3 subsurface_scattering(Surface surface, Light light, AngularInfo angular_i return light_radiance * sss_term * thickness_modulation * sss_strength * sss_color; } +// Ocean subsurface scattering +static const float3 SSS_MODIFIER = float3(0.9f, 1.15f, 0.85f); +float3 ocean_subsurface(Surface surface, Light light, AngularInfo angular_info) +{ + float a = D_GGX_Alpha(surface.roughness); + float a2 = a * a; + + float n_dot_l = angular_info.n_dot_l; + float n_dot_v = angular_info.n_dot_v; + float fresnel = F_Ocean(n_dot_v, surface.roughness, surface.F0.x); + + // masking for SSS attenuation + float v1_light = V1_SmithGGX(n_dot_l, a2); + + // view-to-light alignment + float v_dot_l = max(dot(-surface.camera_to_pixel, light.forward), 0.0f); + float l_dot_n = dot(light.forward, surface.normal); + + // forward scattering through wave crests + float sss_height = max(0.0f, surface.wave_height + 2.5f) + * pow(v_dot_l, 4.0f) + * pow(0.5f - 0.5f * l_dot_n, 3.0f); + + // near-field ambient scattering + float sss_near = 0.5f * pow(n_dot_v, 2.0f); + + // lambertian fallback + float lambertian = 0.5f * n_dot_l; + + // combine + return lerp((sss_height + sss_near) * SSS_MODIFIER / (1.0f + v1_light) + lambertian, float3(1.0f, 1.0f, 1.0f), surface.foam_mask) * (1.0f - fresnel) * light.radiance; +} + [numthreads(THREAD_GROUP_COUNT_X, THREAD_GROUP_COUNT_Y, 1)] void main_cs(uint3 thread_id : SV_DispatchThreadID) { @@ -202,32 +235,42 @@ void main_cs(uint3 thread_id : SV_DispatchThreadID) // compute specular brdf lobes { - // main specular lobe (anisotropic or isotropic) - if (surface.anisotropic > 0.0f) + // ocean specific specular and subsurface + if (surface.is_ocean()) { - L_specular_sum += BRDF_Specular_Anisotropic(surface, angular_info); + L_specular_sum = BRDF_Specular_Isotropic(surface, angular_info); + + L_subsurface = ocean_subsurface(surface, light, angular_info); } else { - L_specular_sum += BRDF_Specular_Isotropic(surface, angular_info); - } + // main specular lobe (anisotropic or isotropic) + if (surface.anisotropic > 0.0f) + { + L_specular_sum += BRDF_Specular_Anisotropic(surface, angular_info); + } + else if (!surface.is_ocean()) + { + L_specular_sum += BRDF_Specular_Isotropic(surface, angular_info); + } // clearcoat layer (secondary specular) - if (surface.clearcoat > 0.0f) - { - L_specular_sum += BRDF_Specular_Clearcoat(surface, angular_info); - } + if (surface.clearcoat > 0.0f) + { + L_specular_sum += BRDF_Specular_Clearcoat(surface, angular_info); + } // sheen layer (cloth-like materials) - if (surface.sheen > 0.0f) - { - L_specular_sum += BRDF_Specular_Sheen(surface, angular_info); - } + if (surface.sheen > 0.0f) + { + L_specular_sum += BRDF_Specular_Sheen(surface, angular_info); + } // subsurface scattering (translucent materials) - if (surface.subsurface_scattering > 0.0f) - { - L_subsurface += subsurface_scattering(surface, light, angular_info); + if (surface.subsurface_scattering > 0.0f) + { + L_subsurface += subsurface_scattering(surface, light, angular_info); + } } } diff --git a/data/shaders/ocean/advance_spectrum.hlsl b/data/shaders/ocean/advance_spectrum.hlsl new file mode 100644 index 0000000000..148585c45b --- /dev/null +++ b/data/shaders/ocean/advance_spectrum.hlsl @@ -0,0 +1,89 @@ +/* +Copyright(c) 2025 George Bolba + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions : + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +// Inspired by Acerola's Implementation: +// https://github.com/GarrettGunnell/Water/blob/main/Assets/Shaders/FFTWater.compute + +#include "common_ocean.hlsl" + +float2 ComplexMult(float2 a, float2 b) +{ + return float2(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x); +} + +float2 EulerFormula(float x) +{ + return float2(cos(x), sin(x)); +} + +[numthreads(8, 8, 1)] +void main_cs(uint3 thread_id : SV_DispatchThreadID) +{ + float4 initialSignal = initial_spectrum[thread_id.xy]; + float2 h0 = initialSignal.rg; + float2 h0conj = initialSignal.ba; + + float2 resolution_out; + initial_spectrum.GetDimensions(resolution_out.x, resolution_out.y); + Surface surface; + surface.Build(thread_id.xy, resolution_out, true, false); + OceanParameters params = surface.ocean_parameters; + + float halfN = SPECTRUM_TEX_SIZE / 2.0f; + float2 K = (thread_id.xy - halfN) * 2.0f * PI / params.lengthScale; + float kMag = length(K); + float kMagRcp = rcp(kMag); + + if (kMag < 0.0001f) + { + kMagRcp = 1.0f; + } + + + float repeatTime = params.repeatTime; + float w_0 = 2.0f * PI / repeatTime; + float dispersion = Dispersion(kMag, params.depth) * buffer_frame.time; + + float2 exponent = EulerFormula(dispersion); + + float2 htilde = ComplexMult(h0, exponent) + ComplexMult(h0conj, float2(exponent.x, -exponent.y)); + float2 ih = float2(-htilde.y, htilde.x); + + float2 displacementX = ih * K.x * kMagRcp; + float2 displacementY = htilde; + float2 displacementZ = ih * K.y * kMagRcp; + + float2 displacementX_dx = -htilde * K.x * K.x * kMagRcp; + float2 displacementY_dx = ih * K.x; + float2 displacementZ_dx = -htilde * K.x * K.y * kMagRcp; + + float2 displacementY_dz = ih * K.y; + float2 displacementZ_dz = -htilde * K.y * K.y * kMagRcp; + + float2 htildeDisplacementX = float2(displacementX.x - displacementZ.y, displacementX.y + displacementZ.x); + float2 htildeDisplacementZ = float2(displacementY.x - displacementZ_dx.y, displacementY.y + displacementZ_dx.x); + + float2 htildeSlopeX = float2(displacementY_dx.x - displacementY_dz.y, displacementY_dx.y + displacementY_dz.x); + float2 htildeSlopeZ = float2(displacementX_dx.x - displacementZ_dz.y, displacementX_dx.y + displacementZ_dz.x); + + displacement_spectrum[thread_id.xy] = float4(htildeDisplacementX, htildeDisplacementZ); + slope_spectrum[thread_id.xy] = float4(htildeSlopeX, htildeSlopeZ); +} diff --git a/data/shaders/ocean/common_ocean.hlsl b/data/shaders/ocean/common_ocean.hlsl new file mode 100644 index 0000000000..08500e1f51 --- /dev/null +++ b/data/shaders/ocean/common_ocean.hlsl @@ -0,0 +1,104 @@ +/* +Copyright(c) 2025 George Bolba + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions : + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +#include "../common.hlsl" + +#ifndef SPARTAN_COMMON_OCEAN +#define SPARTAN_COMMON_OCEAN + +static const uint SPECTRUM_TEX_SIZE = 512; + +RWTexture2D initial_spectrum : register(u9); +RWTexture2D displacement_spectrum : register(u10); +RWTexture2D slope_spectrum : register(u11); +RWTexture2D displacement_map : register(u12); +RWTexture2D slope_map : register(u13); +RWTexture2D synthesised_displacement : register(u14); +RWTexture2D synthesised_slope : register(u15); + +float Dispersion(float kMag, float depth) +{ + return sqrt(G * kMag * tanh(min(kMag * depth, 20))); +} + +float hash21(float2 p) +{ + p = frac(p * float2(123.34f, 456.21f)); + p += dot(p, p + 78.233f); + return frac(p.x * p.y); +} + +float noise_2d(float2 p) +{ + float2 i = floor(p); + float2 f = frac(p); + float a = hash21(i); + float b = hash21(i + float2(1.0f, 0.0f)); + float c = hash21(i + float2(0.0f, 1.0f)); + float d = hash21(i + float2(1.0f, 1.0f)); + float2 u = f * f * (3.0f - 2.0f * f); + return lerp(lerp(a, b, u.x), lerp(c, d, u.x), u.y); +} + +float fbm_noise(float2 uv) +{ + float val = 0.0f; + float amp = 0.5f; + float freq = 1.0f; + for (int i = 0; i < 5; i++) + { + val += amp * noise_2d(uv * freq); + freq *= 2.0f; + amp *= 0.5f; + } + return val; +} + +float worley(float2 p) +{ + float2 i = floor(p); + float2 f = frac(p); + float min_dist = 1.0f; + for (int y = -1; y <= 1; y++) + { + for (int x = -1; x <= 1; x++) + { + float2 neighbor = float2(x, y); + float2 p = hash21(i + neighbor) * float2(1.0f, 1.0f); + float2 diff = neighbor + p - f; + float d = dot(diff, diff); + min_dist = min(min_dist, d); + } + } + return min_dist; +} + +float compute_foam_noise(float2 uv, float time) +{ + float2 noise_uv = uv * 32.0f + time * float2(0.1f, 0.05f); + + float fbm = fbm_noise(noise_uv); + float bubbles = 1.0f - saturate(sqrt(worley(noise_uv * 2.0f))); + + return saturate(fbm * bubbles); +} + +#endif // SPARTAN_COMMON_OCEAN diff --git a/data/shaders/ocean/fft_common.hlsl b/data/shaders/ocean/fft_common.hlsl new file mode 100644 index 0000000000..f6a14758a0 --- /dev/null +++ b/data/shaders/ocean/fft_common.hlsl @@ -0,0 +1,77 @@ +/* +Copyright(c) 2025 George Bolba + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions : + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +// Inspired by Acerola's Implementation: +// https://github.com/GarrettGunnell/Water/blob/main/Assets/Shaders/FFTWater.compute + +#ifndef SPARTAN_FFT_COMMON +#define SPARTAN_FFT_COMMON + +#include "common_ocean.hlsl" + +static const uint LOG_SIZE = log(SPECTRUM_TEX_SIZE) / log(2); // result of Log base 2 of SPECTRUM_TEX_SIZE + +groupshared float4 fftGroupBuffer[2][SPECTRUM_TEX_SIZE]; + +void ButterflyValues(uint step, uint index, out uint2 indices, out float2 twiddle) +{ + const float twoPi = 6.28318530718; + uint b = SPECTRUM_TEX_SIZE >> (step + 1); + uint w = b * (index / b); + uint i = (w + index) % SPECTRUM_TEX_SIZE; + sincos(-twoPi / SPECTRUM_TEX_SIZE * w, twiddle.y, twiddle.x); + + // This is what makes it the inverse FFT + twiddle.y = -twiddle.y; + indices = uint2(i, i + b); +} + +float2 ComplexMult(float2 a, float2 b) +{ + return float2(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x); +} + +float4 FFT(uint threadIndex, float4 input) +{ + fftGroupBuffer[0][threadIndex] = input; + GroupMemoryBarrierWithGroupSync(); + + bool flag = false; + + /*[unroll]*/ + for (uint step = 0; step < LOG_SIZE; ++step) + { + uint2 inputsIndices; + float2 twiddle; + ButterflyValues(step, threadIndex, inputsIndices, twiddle); + + float4 v = fftGroupBuffer[flag][inputsIndices.y]; + fftGroupBuffer[!flag][threadIndex] = + fftGroupBuffer[flag][inputsIndices.x] + float4(ComplexMult(twiddle, v.xy), ComplexMult(twiddle, v.zw)); + + flag = !flag; + GroupMemoryBarrierWithGroupSync(); + } + + return fftGroupBuffer[flag][threadIndex]; +} + +#endif // SPARTAN_FFT_COMMON diff --git a/data/shaders/ocean/generate_maps.hlsl b/data/shaders/ocean/generate_maps.hlsl new file mode 100644 index 0000000000..bbc8ea4289 --- /dev/null +++ b/data/shaders/ocean/generate_maps.hlsl @@ -0,0 +1,71 @@ +/* +Copyright(c) 2025 George Bolba + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions : + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +// Inspired by Acerola's Implementation: +// https://github.com/GarrettGunnell/Water/blob/main/Assets/Shaders/FFTWater.compute + +#include "common_ocean.hlsl" + +float4 Permute(float4 data, float3 id) +{ + return data * (1.0f - 2.0f * ((id.x + id.y) % 2)); +} + +[numthreads(8, 8, 1)] +void main_cs(uint3 thread_id : SV_DispatchThreadID) +{ + const float2 Lambda = float2(1.0f, 1.0f); + + float4 htildeDisplacement = Permute(displacement_spectrum[thread_id.xy], thread_id); + float4 htildeSlope = Permute(slope_spectrum[thread_id.xy], thread_id); + + float2 dxdz = htildeDisplacement.rg; + float2 dydxz = htildeDisplacement.ba; + float2 dyxdyz = htildeSlope.rg; + float2 dxxdzz = htildeSlope.ba; + + float3 displacement = float3(Lambda.x * dxdz.x, dydxz.x, Lambda.y * dxdz.y); + float2 slopes = dyxdyz.xy / (1 + abs(dxxdzz * Lambda)); + + float jacobian = (1.0f + Lambda.x * dxxdzz.x) * (1.0f + Lambda.y * dxxdzz.y) - Lambda.x * Lambda.y * dydxz.y * dydxz.y; + float covariance = slopes.x * slopes.y; + + // create surface + float2 resolution_out; + slope_map.GetDimensions(resolution_out.x, resolution_out.y); + Surface surface; + surface.Build(thread_id.xy, resolution_out, false, false); + + OceanParameters params = surface.ocean_parameters; + + float foam = slope_map[thread_id.xy].a; + foam *= exp(-params.foamDecayRate); + + float biasedJacobian = max(0.0f, -(jacobian - params.foamBias)); + + if (biasedJacobian > params.foamThreshold) + foam += params.foamAdd * biasedJacobian; + + foam = saturate(foam); + + displacement_map[thread_id.xy] = float4(displacement, 1.0f); + slope_map[thread_id.xy] = float4(slopes, 0.0f, foam); +} diff --git a/data/shaders/ocean/horizontal_fft.hlsl b/data/shaders/ocean/horizontal_fft.hlsl new file mode 100644 index 0000000000..f55a8c7ab1 --- /dev/null +++ b/data/shaders/ocean/horizontal_fft.hlsl @@ -0,0 +1,32 @@ +/* +Copyright(c) 2025 George Bolba + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions : + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +// Inspired by Acerola's Implementation: +// https://github.com/GarrettGunnell/Water/blob/main/Assets/Shaders/FFTWater.compute + +#include "fft_common.hlsl" + +[numthreads(SPECTRUM_TEX_SIZE, 1, 1)] +void main_cs(uint3 thread_id : SV_DispatchThreadID) +{ + displacement_spectrum[thread_id.xy] = FFT(thread_id.x, displacement_spectrum[thread_id.xy]); + slope_spectrum[thread_id.xy] = FFT(thread_id.x, slope_spectrum[thread_id.xy]); +} diff --git a/data/shaders/ocean/initial_spectrum.hlsl b/data/shaders/ocean/initial_spectrum.hlsl new file mode 100644 index 0000000000..bc54b738f3 --- /dev/null +++ b/data/shaders/ocean/initial_spectrum.hlsl @@ -0,0 +1,167 @@ +/* +Copyright(c) 2025 George Bolba + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions : + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +// Inspired by Acerola's Implementation: +// https://github.com/GarrettGunnell/Water/blob/main/Assets/Shaders/FFTWater.compute + +#include "common_ocean.hlsl" + +float2 UniformToGaussian(float u1, float u2) +{ + float R = sqrt(-2.0f * log(u1)); + float theta = 2.0f * PI * u2; + + return float2(R * cos(theta), R * sin(theta)); +} + +float DispersionDerivative(float kMag, float depth) +{ + float th = tanh(min(kMag * depth, 20)); + float ch = cosh(kMag * depth); + return G * (depth * kMag / ch / ch + th) / Dispersion(kMag, depth) / 2.0f; +} + +float NormalizationFactor(float s) +{ + float s2 = s * s; + float s3 = s2 * s; + float s4 = s3 * s; + if (s < 5) + return -0.000564f * s4 + 0.00776f * s3 - 0.044f * s2 + 0.192f * s + 0.163f; + else + return -4.80e-08f * s4 + 1.07e-05f * s3 - 9.53e-04f * s2 + 5.90e-02f * s + 3.93e-01f; +} + +float DonelanBannerBeta(float x) +{ + if (x < 0.95f) + return 2.61f * pow(abs(x), 1.3f); + if (x < 1.6f) + return 2.28f * pow(abs(x), -1.3f); + + float p = -0.4f + 0.8393f * exp(-0.567f * log(x * x)); + return pow(10.0f, p); +} + +float DonelanBanner(float theta, float omega, float peakOmega) +{ + float beta = DonelanBannerBeta(omega / peakOmega); + float sech = 1.0f / cosh(beta * theta); + return beta / 2.0f / tanh(beta * 3.1416f) * sech * sech; +} + +float Cosine2s(float theta, float s) +{ + return NormalizationFactor(s) * pow(abs(cos(0.5f * theta)), 2.0f * s); +} + +float SpreadPower(float omega, float peakOmega) +{ + if (omega > peakOmega) + return 9.77f * pow(abs(omega / peakOmega), -2.5f); + else + return 6.97f * pow(abs(omega / peakOmega), 5.0f); +} + +float DirectionSpectrum(float theta, float omega, float peakOmega, float swell, float spreadBlend, float angle) +{ + float s = SpreadPower(omega, peakOmega) + 16 * tanh(min(omega / peakOmega, 20)) * swell * swell; + + return lerp(2.0f / 3.1415f * cos(theta) * cos(theta), Cosine2s(theta - angle, s), spreadBlend); +} + +float TMACorrection(float omega, float depth) +{ + float omegaH = omega * sqrt(depth / G); + if (omegaH <= 1.0f) + return 0.5f * omegaH * omegaH; + if (omegaH < 2.0f) + return 1.0f - 0.5f * (2.0f - omegaH) * (2.0f - omegaH); + + return 1.0f; +} + +float JONSWAP(float omega, float peakOmega, float gamma, float scale, float alpha, float depth) +{ + float sigma = (omega <= peakOmega) ? 0.07f : 0.09f; + + float r = exp(-(omega - peakOmega) * (omega - peakOmega) / 2.0f / sigma / sigma / peakOmega / peakOmega); + + float oneOverOmega = 1.0f / (omega + 1e-6f); + float peakOmegaOverOmega = peakOmega / omega; + return scale * TMACorrection(omega, depth) * alpha * G * G + * oneOverOmega * oneOverOmega * oneOverOmega * oneOverOmega * oneOverOmega + * exp(-1.25f * peakOmegaOverOmega * peakOmegaOverOmega * peakOmegaOverOmega * peakOmegaOverOmega) + * pow(abs(gamma), r); +} + +float ShortWavesFade(float kLength, float shortWavesFade) +{ + return exp(-shortWavesFade * shortWavesFade * kLength * kLength); +} + +float hash(uint n) +{ + // integer hash copied from Hugo Elias + n = (n << 13U) ^ n; + n = n * (n * n * 15731U + 0x789221U) + 0x1376312589U; + return float(n & uint(0x7fffffffU)) / float(0x7fffffff); +} + +[numthreads(8, 8, 1)] +void main_cs(uint3 thread_id : SV_DispatchThreadID) +{ + float2 resolution_out; + initial_spectrum.GetDimensions(resolution_out.x, resolution_out.y); + Surface surface; + surface.Build(thread_id.xy, resolution_out, true, false); + + uint seed = thread_id.x + SPECTRUM_TEX_SIZE * thread_id.y + SPECTRUM_TEX_SIZE; + seed += buffer_frame.frame; + + OceanParameters params = surface.ocean_parameters; + + float halfN = SPECTRUM_TEX_SIZE / 2.0f; + + float deltaK = 2.0f * PI / params.lengthScale; + float2 K = (thread_id.xy - halfN) * deltaK; + float kLength = length(K); + + seed += hash(seed) * 10; + float4 uniformRandSamples = float4(hash(seed), hash(seed * 2), hash(seed * 3), hash(seed * 4)); + float2 gauss1 = UniformToGaussian(uniformRandSamples.x, uniformRandSamples.y); + float2 gauss2 = UniformToGaussian(uniformRandSamples.z, uniformRandSamples.w); + + if (params.lowCutoff <= kLength && kLength <= params.highCutoff) + { + float kAngle = atan2(K.y, K.x); + float omega = Dispersion(kLength, params.depth); + float dOmegadk = DispersionDerivative(kLength, params.depth); + + float spectrum = JONSWAP(omega, params.peakOmega, params.gamma, params.scale, params.alpha, params.depth) * DirectionSpectrum(kAngle, omega, params.peakOmega, params.swell, params.spreadBlend, params.angle) * ShortWavesFade(kLength, params.shortWavesFade); + + initial_spectrum[thread_id.xy] = float4(float2(gauss2.x, gauss1.y) * float2(sqrt(2 * spectrum * abs(dOmegadk) / kLength * deltaK * deltaK).xx), 0.0f, 0.0f); + } + else + { + initial_spectrum[thread_id.xy] = float4(0.0f, 0.0f, 0.0f, 0.0f); + } +} diff --git a/data/shaders/ocean/pack_spectrum.hlsl b/data/shaders/ocean/pack_spectrum.hlsl new file mode 100644 index 0000000000..b187a9fd81 --- /dev/null +++ b/data/shaders/ocean/pack_spectrum.hlsl @@ -0,0 +1,34 @@ +/* +Copyright(c) 2025 George Bolba + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions : + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +// Inspired by Acerola's Implementation: +// https://github.com/GarrettGunnell/Water/blob/main/Assets/Shaders/FFTWater.compute + +#include "common_ocean.hlsl" + +[numthreads(8, 8, 1)] +void main_cs(uint3 thread_id : SV_DispatchThreadID) +{ + float2 h0 = initial_spectrum[thread_id.xy].rg; + float2 h0conj = initial_spectrum[uint2((SPECTRUM_TEX_SIZE - thread_id.x) % SPECTRUM_TEX_SIZE, (SPECTRUM_TEX_SIZE - thread_id.y) % SPECTRUM_TEX_SIZE)].rg; + + initial_spectrum[thread_id.xy] = float4(h0, h0conj.x, -h0conj.y); +} diff --git a/data/shaders/ocean/synthesise_maps.hlsl b/data/shaders/ocean/synthesise_maps.hlsl new file mode 100644 index 0000000000..22bf9bd90c --- /dev/null +++ b/data/shaders/ocean/synthesise_maps.hlsl @@ -0,0 +1,171 @@ +/* +Copyright(c) 2025 George Bolba + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions : + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +#include "common_ocean.hlsl" + +// Inspired by https://www.shadertoy.com/view/tsVGRd + +static const float2 hex_ratio = float2(1.0f, sqrt(3.0f)); + +float4 get_hex_grid_info(float2 uv) +{ + const float4 hex_index = round(float4(uv, uv - float2(0.5f, 1.0f)) / hex_ratio.xyxy); + const float4 hex_center = float4(hex_index.xy * hex_ratio, (hex_index.zw + 0.5f) * hex_ratio); + const float4 offset = uv.xyxy - hex_center; + return dot(offset.xy, offset.xy) < dot(offset.zw, offset.zw) ? float4(hex_center.xy, hex_index.xy) : float4(hex_center.zw, hex_index.zw); +} + +float get_hex_sdf(float2 p) +{ + p = abs(p); + return 0.5f - max(dot(p, hex_ratio * 0.5f), p.x); +} + +//xy: node pos, z: weight +float3 get_triangle_interp_node(float2 pos, float freq, int node_index) +{ + const float2 node_offsets[3] = + { + float2(0.0f, 0.0f), + float2(1.0f, 1.0f), + float2(1.0f, -1.0f) + }; + + const float2 uv = pos * freq + node_offsets[node_index] / hex_ratio.xy * 0.5f; + const float4 hex_info = get_hex_grid_info(uv); + const float dist = get_hex_sdf(uv - hex_info.xy) * 2.0f; + return float3(hex_info.xy / freq, dist); +} + +float3 hash33(float3 p) +{ + p = float3(dot(p, float3(127.1f, 311.7f, 74.7f)), + dot(p, float3(269.5f, 183.3f, 246.1f)), + dot(p, float3(113.5f, 271.9f, 124.6f))); + + return frac(sin(p) * 43758.5453123f); +} + +float4 get_texture_sample(Texture2D texture, float2 pos, float freq, float2 node_point) +{ + const float3 hash = hash33(float3(node_point.xy, 0.0f)); + + const float2 uv = pos * freq + hash.yz; + return texture.SampleLevel(samplers[sampler_bilinear_wrap], uv, 0); +} + +void preserve_variance(out float4 linear_color, float4 mean_color, float moment2) +{ + linear_color = (linear_color - mean_color) / sqrt(moment2) + mean_color; +} + +void synthesize(Texture2D example, out float4 output, float2 uv, bool preserve_foam = false) +{ + const float tex_freq = 1.0f; + const float tile_freq = 2.0f; + + output = float4(0.0f, 0.0f, 0.0f, 0.0f); // init to 0.0f for safety reasons + float moment2 = 0.0f; + + for (int i = 0; i < 3; i++) + { + const float3 interp_node = get_triangle_interp_node(uv, tile_freq, i); + output += get_texture_sample(example, uv, tex_freq, interp_node.xy) * interp_node.z; + + moment2 += interp_node.z * interp_node.z; + } + // assumes example is a mip mapped 512x512 texture and samples lowest mip (1x1) + const float4 mean_example = example.SampleLevel(samplers[sampler_point_clamp], uv, 9); + + if (preserve_foam) + { + const float foam = saturate(output.a); + preserve_variance(output, mean_example, moment2); + output.a = foam; + } + else + { + preserve_variance(output, mean_example, moment2); + } +} + +void synthesize_with_flow(Texture2D example, out float4 output, Texture2D flowmap, float2 tile_xz_pos, float wind_dir_deg, float2 tile_local_uv, bool debug_flow = false) +{ + const float tex_freq = 1.0f; + const float tile_freq = 2.0f; + + // convert wind dir from degrees to a 2d vector + const float wind_dir_rad = radians(wind_dir_deg); + const float2 wind_dir = float2(cos(wind_dir_rad), sin(wind_dir_rad)) * float2(-1.0f, -1.0f); + + output = float4(0.0f, 0.0f, 0.0f, 0.0f); // init to 0.0f for safety reasons + float moment2 = 0.0f; + float2 output_flow = float2(0.0f, 0.0f); + + for (int i = 0; i < 3; i++) + { + const float3 interp_node = get_triangle_interp_node(tile_local_uv, tile_freq, i); + + float2 node_world_pos = tile_xz_pos; + float2 node_world_uv = (node_world_pos - (-3069.0f)) / (3069.0f - (-3069.0f)); + + float2 flow_dir = flowmap.SampleLevel(samplers[sampler_bilinear_wrap], node_world_uv, 0).rg; + flow_dir = normalize(flow_dir * 2.0f - 1.0f); + flow_dir = float2(flow_dir.x, flow_dir.y) * interp_node.z; + output_flow += flow_dir; + + //float2 to_center = interp_node.xy - float2(3.0f, 3.0f); + //float2 flow_dir = normalize(float2(to_center.y, to_center.x)); + + const float theta = atan2(flow_dir.y, flow_dir.x) - atan2(wind_dir.y, wind_dir.x); + const float cosT = cos(theta); + const float sinT = sin(theta); + const float2x2 R2 = float2x2(cosT, -sinT, sinT, cosT); + const float3x3 R3 = float3x3(cosT, 0, sinT, + 0, 1, 0, + -sinT, 0, cosT); + const float2 rotated_pos = interp_node.xy + mul(R2, tile_local_uv - interp_node.xy); + + float4 sample = get_texture_sample(example, rotated_pos, tex_freq, interp_node.xy); + + sample.xyz = mul(R3, sample.xyz); + + output += sample * interp_node.z; + moment2 += interp_node.z * interp_node.z; + } + // assumes example is a mip mapped 512x512 texture and samples lowest mip (1x1) + const float4 mean_example = example.SampleLevel(samplers[sampler_point_clamp], tile_local_uv, 9); + + preserve_variance(output, mean_example, moment2); + + if (debug_flow) + { + //output = float4(output_flow * 0.5f + 0.5f, 0.0f, 1.0f); + output = float4(output_flow, 0.0f, 1.0f); + } +} + +float2 ocean_get_world_space_uvs(float2 uv, float2 tile_xz_pos, float tile_size) +{ + const float2 tile_origin_uv_space = float2(tile_xz_pos.x / tile_size, tile_xz_pos.y / tile_size); + + return tile_origin_uv_space + uv; +} diff --git a/data/shaders/ocean/vertical_fft.hlsl b/data/shaders/ocean/vertical_fft.hlsl new file mode 100644 index 0000000000..14fdaa1c94 --- /dev/null +++ b/data/shaders/ocean/vertical_fft.hlsl @@ -0,0 +1,32 @@ +/* +Copyright(c) 2025 George Bolba + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions : + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +// Inspired by Acerola's Implementation: +// https://github.com/GarrettGunnell/Water/blob/main/Assets/Shaders/FFTWater.compute + +#include "fft_common.hlsl" + +[numthreads(SPECTRUM_TEX_SIZE, 1, 1)] +void main_cs(uint3 thread_id : SV_DispatchThreadID) +{ + displacement_spectrum[thread_id.yx] = FFT(thread_id.x, displacement_spectrum[thread_id.yx]); + slope_spectrum[thread_id.yx] = FFT(thread_id.x, slope_spectrum[thread_id.yx]); +} diff --git a/source/editor/Widgets/Properties.cpp b/source/editor/Widgets/Properties.cpp index 89b356fd42..4f24798404 100644 --- a/source/editor/Widgets/Properties.cpp +++ b/source/editor/Widgets/Properties.cpp @@ -1558,6 +1558,82 @@ void Properties::ShowMaterial(Material* material) const ImGui::PopItemWidth(); } + // ocean properties + if (material->GetProperty(MaterialProperty::IsOcean)) + { + const auto show_jonswap_params = [this, &material](const char* name, const char* tooltip, const OceanParameters params) + { + bool show_modifier = params != OceanParameters::Max; + + // name + if (name) + { + ImGui::Text(name); + + if (tooltip) + { + ImGuiSp::tooltip(tooltip); + } + + if (show_modifier) + { + ImGui::SameLine(); + } + } + + if (show_modifier) + { + // constrain width to available space + const float available_width = ImGui::GetContentRegionAvail().x; + const float slider_width = ImMin(available_width, 120.0f); + + float value = material->GetOceanProperty(params); + + ImGui::PushItemWidth(slider_width); + if (ImGuiSp::draw_float_wrap("##val", &value, 0.004f, 0.0f)) + { + material->SetOceanProperty(params, value); + } + ImGui::PopItemWidth(); + } + }; + + show_jonswap_params("Alpha", "", OceanParameters::Alpha); + show_jonswap_params("Angle", "", OceanParameters::Angle); + show_jonswap_params("Fetch", "", OceanParameters::Fetch); + show_jonswap_params("Gamma", "", OceanParameters::Gamma); + show_jonswap_params("Peak Omega", "", OceanParameters::PeakOmega); + show_jonswap_params("Repeat Time", "", OceanParameters::RepeatTime); + show_jonswap_params("Scale", "", OceanParameters::Scale); + show_jonswap_params("Short Waves Fade", "", OceanParameters::ShortWavesFade); + show_jonswap_params("Spread Blend", "", OceanParameters::SpreadBlend); + show_jonswap_params("Swell", "", OceanParameters::Swell); + show_jonswap_params("Wind Direction", "", OceanParameters::WindDirection); + show_jonswap_params("Wind Speed", "", OceanParameters::WindSpeed); + show_jonswap_params("Depth", "", OceanParameters::Depth); + show_jonswap_params("Low Cutoff", "", OceanParameters::LowCutoff); + show_jonswap_params("High Cutoff", "", OceanParameters::HighCutoff); + show_jonswap_params("Foam Decay Rate", "", OceanParameters::FoamDecayRate); + show_jonswap_params("Foam Bias", "", OceanParameters::FoamBias); + show_jonswap_params("Foam Threshold", "", OceanParameters::FoamThreshold); + show_jonswap_params("Foam Add", "", OceanParameters::FoamAdd); + show_jonswap_params("Displacement Scale", "", OceanParameters::DisplacementScale); + show_jonswap_params("Slope Scale", "", OceanParameters::SlopeScale); + show_jonswap_params("Length Scale", "", OceanParameters::LengthScale); + + bool show_displacement = material->GetOceanProperty(OceanParameters::DebugDisplacement) == 1.0f ? true : false; + ImGui::Checkbox("Show Displacement Map", &show_displacement); + material->SetOceanProperty(OceanParameters::DebugDisplacement, show_displacement ? 1.0f : 0.0f); + + bool show_slope = material->GetOceanProperty(OceanParameters::DebugSlope) == 1.0f ? true : false; + ImGui::Checkbox("Show Slope Map", &show_slope); + material->SetOceanProperty(OceanParameters::DebugSlope, show_slope ? 1.0f : 0.0f); + + bool show_synthesised = material->GetOceanProperty(OceanParameters::DebugSynthesised) == 1.0f ? true : false; + ImGui::Checkbox("Show Synthesised Version", &show_synthesised); + material->SetOceanProperty(OceanParameters::DebugSynthesised, show_synthesised ? 1.0f : 0.0f); + } + // offset { layout::begin_property("Offset", "texture offset"); diff --git a/source/editor/Windows/WorldSelector.cpp b/source/editor/Windows/WorldSelector.cpp index fe594c50c3..08e739402d 100644 --- a/source/editor/Windows/WorldSelector.cpp +++ b/source/editor/Windows/WorldSelector.cpp @@ -56,7 +56,8 @@ namespace { "Sponza 4K", "High-resolution textures & meshes", "Complete" , "Demanding", 2600 }, { "Cornell Box", "Classic ray tracing test scene", "Complete" , "Light", 2100 }, { "San Miguel", "Detailed courtyard scene with complex geometry and lighting", "Complete" , "Demanding", 2600 }, - { "Basic", "Light, camera, floor", "Complete" , "Light", 2100 } + { "Basic", "Light, camera, floor", "Complete" , "Light", 2100 }, + { "Ocean", "Light, camera, ocean", "WIP" , "Light", 2100 } }; constexpr int default_world_count = sizeof(default_worlds) / sizeof(default_worlds[0]); diff --git a/source/runtime/Core/Debugging.h b/source/runtime/Core/Debugging.h index ad6bef5b58..cbbd65cda3 100644 --- a/source/runtime/Core/Debugging.h +++ b/source/runtime/Core/Debugging.h @@ -36,13 +36,13 @@ namespace spartan static bool IsBreadcrumbsEnabled() { return m_breadcrumbs_enabled; } private: - inline static bool m_validation_layer_enabled = false; // enables vulkan validation layers for api error detection and debug message reporting - inline static bool m_gpu_assisted_validation_enabled = false; // enables gpu-assisted validation to detect memory and synchronization errors during rendering - inline static bool m_logging_to_file_enabled = false; // writes diagnostic and validation messages to a persistent log file - inline static bool m_breadcrumbs_enabled = false; // records gpu execution markers to help identify the cause of gpu crashes - inline static bool m_renderdoc_enabled = false; // enables integration with renderdoc for frame capture and gpu debugging - inline static bool m_gpu_marking_enabled = true; // labels gpu resources and command markers to improve debugging and profiling readability - inline static bool m_gpu_timing_enabled = true; // measures gpu execution times for profiling and performance analysis - inline static bool m_shader_optimization_enabled = true; // enables shader compiler optimizations to improve performance and efficiency + inline static bool m_validation_layer_enabled = false; // enables vulkan diagnostic layers, incurs significant per-draw cpu performance overhead + inline static bool m_gpu_assisted_validation_enabled = false; // performs gpu-based validation with substantial cpu and gpu performance impact + inline static bool m_logging_to_file_enabled = false; // writes diagnostic logs to disk, causes high cpu overhead due to file I/O operations + inline static bool m_breadcrumbs_enabled = false; // tracks gpu crash information in breadcrumbs.txt, minimal overhead (amd gpus only) - crashes in debug mode - outputs unreliable data in release mode - issue reported to amd + inline static bool m_renderdoc_enabled = false; // integrates RenderDoc graphics debugging, introduces high cpu overhead from api wrapping + inline static bool m_gpu_marking_enabled = true; // enables gpu resource marking with negligible performance cost + inline static bool m_gpu_timing_enabled = true; // enables gpu performance timing with negligible performance cost + inline static bool m_shader_optimization_enabled = true; // controls shader optimization, disabling has significant performance impact }; } diff --git a/source/runtime/Game/Game.cpp b/source/runtime/Game/Game.cpp index c7c18ecf51..2529cdb810 100644 --- a/source/runtime/Game/Game.cpp +++ b/source/runtime/Game/Game.cpp @@ -56,6 +56,7 @@ namespace spartan namespace cornell { void create(); } namespace san_miguel { void create(); } namespace basic { void create(); } + namespace ocean { void create(); void tick(); } } //========================================================= @@ -77,6 +78,7 @@ namespace spartan Entity* default_light_directional = nullptr; Entity* default_metal_cube = nullptr; Entity* default_water = nullptr; + Entity* default_ocean = nullptr; std::vector> meshes; //========================================== @@ -94,6 +96,7 @@ namespace spartan worlds::cornell::create, worlds::san_miguel::create, worlds::basic::create, + worlds::ocean::create, }; constexpr tick_fn world_tick[] = @@ -105,6 +108,7 @@ namespace spartan nullptr, nullptr, nullptr, + worlds::ocean::tick, }; static_assert(size(world_create) == static_cast(DefaultWorld::Max), "world_create out of sync with DefaultWorld enum"); @@ -345,6 +349,61 @@ namespace spartan return water; } + + Entity* ocean(std::shared_ptr material, const Vector3& position) + { + // entity + Entity* water = World::CreateEntity(); + water->SetObjectName("ocean"); + water->SetPosition(position); + water->SetScale({ 1.0f, 1.0f, 1.0f }); + + // material + { + material->SetObjectName("material_ocean"); + material->SetResourceFilePath("ocean" + string(EXTENSION_MATERIAL)); + + material->LoadFromFile(material->GetResourceFilePath()); + + material->MarkSpectrumAsComputed(false); + //material->SetTexture(MaterialTextureType::Flowmap, "project\\materials\\water\\flowmap.png"); + + // if material fails to load from file + if (material->GetProperty(MaterialProperty::IsOcean) != 1.0f) + { + material->SetColor(Color(0.0f, 142.0f / 255.0f, 229.0f / 255.0f, 255.0f / 255.0f)); + material->SetProperty(MaterialProperty::IsOcean, 1.0f); + + material->SetOceanProperty(OceanParameters::Angle, 0.0f); //handled internally + material->SetOceanProperty(OceanParameters::Alpha, 0.0f); // handled internally + material->SetOceanProperty(OceanParameters::PeakOmega, 0.0f); // handled internally + + material->SetOceanProperty(OceanParameters::Scale, 1.0f); + material->SetOceanProperty(OceanParameters::SpreadBlend, 0.9f); + material->SetOceanProperty(OceanParameters::Swell, 1.0f); + material->SetOceanProperty(OceanParameters::Fetch, 1280000.0f); + material->SetOceanProperty(OceanParameters::WindDirection, 135.0f); + material->SetOceanProperty(OceanParameters::WindSpeed, 2.8f); + material->SetOceanProperty(OceanParameters::Gamma, 3.3f); + material->SetOceanProperty(OceanParameters::ShortWavesFade, 0.0f); + material->SetOceanProperty(OceanParameters::RepeatTime, 200.0f); + + material->SetOceanProperty(OceanParameters::Depth, 20.0f); + material->SetOceanProperty(OceanParameters::LowCutoff, 0.001f); + material->SetOceanProperty(OceanParameters::HighCutoff, 1000.0f); + + material->SetOceanProperty(OceanParameters::FoamDecayRate, 3.0f); + material->SetOceanProperty(OceanParameters::FoamThreshold, 0.5f); + material->SetOceanProperty(OceanParameters::FoamBias, 1.2f); + material->SetOceanProperty(OceanParameters::FoamAdd, 1.0f); + + material->SetOceanProperty(OceanParameters::DisplacementScale, 1.0f); + material->SetOceanProperty(OceanParameters::SlopeScale, 1.0f); + material->SetOceanProperty(OceanParameters::LengthScale, 128.0f); + } + } + return water; + } } //======================================================================================== @@ -1535,6 +1594,220 @@ namespace spartan } //==================================================================================== + //== Ocean =========================================================================== + namespace ocean + { + shared_ptr material = make_shared(); + + // Edge flag bitmask + namespace EdgeFlags + { + constexpr uint32_t None = 0; + constexpr uint32_t Left = 1 << 0; // tx == -2 + constexpr uint32_t Right = 1 << 1; // tx == 1 + constexpr uint32_t Back = 1 << 2; // tz == -2 + constexpr uint32_t Front = 1 << 3; // tz == 1 + }; + + struct TileInstance + { + Vector2 world_offset; // world-space XZ origin of this tile + Vector2 snapped_center; // used for per lod level snapping and vertex morphing + float tile_scale; // world-space size of this tile + uint32_t lod_level; // which LOD level (for transition blending) + uint32_t edge = EdgeFlags::None; // used when generating skirt geometry + }; + std::vector instances {}; + std::vector tile_entities {}; + uint32_t tile_resolution = 128; // vertices per tile side (one tile = res^2 vertices) + float base_tile_size = 32.0f; // world-space units for LOD0 tile + uint32_t lod_levels = 4; + + static void apply_skirt(std::vector* vertices, uint32_t resolution, uint32_t edge_flags, float skirt = 100000.0f) + { + for (uint32_t z = 0; z <= resolution; z++) + { + for (uint32_t x = 0; x <= resolution; x++) + { + auto& v = (*vertices)[z * (resolution + 1) + x]; + + if (x == 0 && (edge_flags & EdgeFlags::Left)) v.pos[0] -= skirt; + if (x == resolution && (edge_flags & EdgeFlags::Right)) v.pos[0] += skirt; + if (z == 0 && (edge_flags & EdgeFlags::Back)) v.pos[2] -= skirt; + if (z == resolution && (edge_flags & EdgeFlags::Front)) v.pos[2] += skirt; + } + } + } + + static void build_clipmap(Vector3 camera_pos) + { + instances.clear(); + + // Single snap from finest level - guarantees alignment across all LODs + const float finest_snap = base_tile_size / tile_resolution * 2.0f; + const Vector2 snapped_center = { + floor(camera_pos.x / finest_snap) * finest_snap, + floor(camera_pos.z / finest_snap) * finest_snap + }; + + for (uint32_t lod = 0; lod < lod_levels; lod++) + { + const float tile_size = base_tile_size * (float)(1 << lod); + const bool is_outermost = (lod == lod_levels - 1); + + for (int tz = -2; tz < 2; tz++) + { + for (int tx = -2; tx < 2; tx++) + { + if (lod > 0 && tx >= -1 && tx <= 0 && tz >= -1 && tz <= 0) + continue; + + TileInstance inst{}; + inst.world_offset = { + snapped_center.x + tx * tile_size, + snapped_center.y + tz * tile_size + }; + inst.tile_scale = tile_size; + inst.lod_level = lod; + inst.snapped_center = snapped_center; + + inst.edge = EdgeFlags::None; + if (is_outermost) + { + if (tx == -2) inst.edge |= EdgeFlags::Left; + if (tx == 1) inst.edge |= EdgeFlags::Right; + if (tz == -2) inst.edge |= EdgeFlags::Back; + if (tz == 1) inst.edge |= EdgeFlags::Front; + } + instances.push_back(inst); + } + } + } + } + + void create() + { + entities::camera(false); + entities::sun(LightPreset::day, true); + + default_camera->RemoveComponent(); + + default_ocean = entities::ocean(material, { 0.0f, 0.0f, 0.0f }); + + //geometry_generation::generate_tile(&tile_vertices, &tile_indices, tile_resolution); + // Generate normal tile + std::vector normal_verts{}; + std::vector normal_indices{}; + geometry_generation::generate_tile(&normal_verts, &normal_indices, tile_resolution); + + auto make_mesh = [&](const std::string& name, + std::vector& verts, + std::vector& indices) -> shared_ptr + { + auto mesh = meshes.emplace_back(make_shared()); + mesh->SetObjectName(name); + mesh->SetRootEntity(default_ocean); + mesh->SetFlag(static_cast(MeshFlags::PostProcessOptimize), false); + mesh->SetFlag(static_cast(MeshFlags::PostProcessNormalizeScale), false); + mesh->AddGeometry(verts, indices, false); + mesh->CreateGpuBuffers(); + return mesh; + }; + + auto make_skirt_mesh = [&](uint32_t flags) -> shared_ptr + { + auto verts = normal_verts; + apply_skirt(&verts, tile_resolution, flags); + return make_mesh("Clipmap Skirt Tile", verts, normal_indices); + }; + + // Create meshes + shared_ptr mesh_normal = make_mesh("Clipmap Tile", normal_verts, normal_indices); + + std::unordered_map> skirt_meshes; + skirt_meshes[EdgeFlags::Left] = make_skirt_mesh(EdgeFlags::Left); + skirt_meshes[EdgeFlags::Right] = make_skirt_mesh(EdgeFlags::Right); + skirt_meshes[EdgeFlags::Back] = make_skirt_mesh(EdgeFlags::Back); + skirt_meshes[EdgeFlags::Front] = make_skirt_mesh(EdgeFlags::Front); + skirt_meshes[EdgeFlags::Left | EdgeFlags::Back] = make_skirt_mesh(EdgeFlags::Left | EdgeFlags::Back); + skirt_meshes[EdgeFlags::Left | EdgeFlags::Front] = make_skirt_mesh(EdgeFlags::Left | EdgeFlags::Front); + skirt_meshes[EdgeFlags::Right | EdgeFlags::Back] = make_skirt_mesh(EdgeFlags::Right | EdgeFlags::Back); + skirt_meshes[EdgeFlags::Right | EdgeFlags::Front] = make_skirt_mesh(EdgeFlags::Right | EdgeFlags::Front); + + // Build initial clipmap and create entities + build_clipmap(default_camera->GetPosition()); + + for (const auto& tile_inst : instances) + { + Entity* entity_tile = World::CreateEntity(); + entity_tile->SetParent(default_ocean); + entity_tile->SetPosition({ tile_inst.world_offset.x, 0.0f, tile_inst.world_offset.y }); + entity_tile->SetScale({ tile_inst.tile_scale, 1.0f, tile_inst.tile_scale }); + + Mesh* mesh = (tile_inst.edge != EdgeFlags::None) + ? skirt_meshes[tile_inst.edge].get() + : mesh_normal.get(); + + material->SetClipmapTileRes(tile_resolution); + + if (Renderable* renderable = entity_tile->AddComponent()) + { + renderable->SetMesh(mesh); + renderable->SetMaterial(material); + renderable->SetFlag(RenderableFlags::CastsShadows, false); + renderable->SetOceanClipmapTileScale(tile_inst.tile_scale); + renderable->SetOceanClipmapTilePos(tile_inst.world_offset); + renderable->SetOceanClipmapTileSnapCenter(tile_inst.snapped_center); + //renderable->SetOceanClipmapLodLevel(tile_inst.lod_level); + } + + tile_entities.emplace_back(entity_tile); + } + + default_light_directional->GetComponent()->SetFlag(LightFlags::ShadowsScreenSpace, false); + } + + void tick() + { + if (!material) + return; + + Camera* camera = default_camera->GetChildByIndex(0)->GetComponent(); + const Vector3 camera_pos = camera->GetEntity()->GetPosition(); + + build_clipmap(camera_pos); + + for (size_t i = 0; i < instances.size(); i++) + { + const auto& inst = instances[i]; + Entity* entity = tile_entities[i]; + + entity->SetPosition({ inst.world_offset.x, 0.0f, inst.world_offset.y }); + entity->SetScale({ inst.tile_scale, 1.0f, inst.tile_scale }); + + if (Renderable* renderable = entity->GetComponent()) + { + renderable->SetOceanClipmapTileScale(inst.tile_scale); + renderable->SetOceanClipmapTilePos(inst.world_offset); + renderable->SetOceanClipmapTileSnapCenter(inst.snapped_center); + } + } + } + + void shutdown() + { + if (!default_ocean) + return; + + if (!material) + SP_ASSERT_MSG(false, "Failed to get ocean material"); + + material->SaveToFile(material->GetResourceFilePath()); + + default_ocean = nullptr; + } + } + //======================================================================================== } //======================================================================================== @@ -1549,15 +1822,23 @@ namespace spartan default_terrain = nullptr; default_car = nullptr; default_metal_cube = nullptr; + default_water = nullptr; // reset world-specific state worlds::showroom::texture_brand_logo = nullptr; + worlds::ocean::shutdown(); Car::ShutdownAll(); meshes.clear(); } void Game::Tick() { + // ocean-specific tick + //if (loaded_world == DefaultWorld::Ocean) + //{ + // worlds::ocean::tick(); + //} + // world-specific tick if (loaded_world != DefaultWorld::Max) { diff --git a/source/runtime/Game/Game.h b/source/runtime/Game/Game.h index 670b3c1057..15a1527480 100644 --- a/source/runtime/Game/Game.h +++ b/source/runtime/Game/Game.h @@ -32,6 +32,7 @@ namespace spartan Cornell, SanMiguel, Basic, + Ocean, Max }; diff --git a/source/runtime/Geometry/GeometryGeneration.h b/source/runtime/Geometry/GeometryGeneration.h index a38d3a93fe..25d8694973 100644 --- a/source/runtime/Geometry/GeometryGeneration.h +++ b/source/runtime/Geometry/GeometryGeneration.h @@ -110,6 +110,39 @@ namespace spartan::geometry_generation indices->emplace_back(1); } + static void generate_tile(std::vector* vertices, std::vector* indices, uint32_t resolution) + { + using namespace math; + + for (uint32_t z = 0; z <= resolution; z++) + { + for (uint32_t x = 0; x <= resolution; x++) + { + // Normalized [0,1] coordinates within the tile + const Vector3 pos = Vector3((float)x / resolution, 0.0f, (float)z / resolution ); + const Vector2 uv = Vector2((float)x / resolution, (float)z / resolution); + const Vector3 normal = Vector3(0, 1, 0); + const Vector3 tangent = Vector3(0, 0, 0); + + vertices->emplace_back(pos, uv, normal, tangent); + } + } + + for (uint32_t z = 0; z < resolution; z++) + { + for (uint32_t x = 0; x < resolution; x++) + { + const uint32_t i = z * (resolution + 1u) + x; + indices->emplace_back(i); + indices->emplace_back(i + resolution + 1u); + indices->emplace_back(i + 1u); + indices->emplace_back(i + 1u); + indices->emplace_back(i + resolution + 1u); + indices->emplace_back(i + resolution + 2u); + } + } + } + static void generate_grid(std::vector* vertices, std::vector* indices, uint32_t grid_points_per_dimension, float extent) { using namespace math; diff --git a/source/runtime/Rendering/Material.cpp b/source/runtime/Rendering/Material.cpp index 830aaa51c2..5d898dee9a 100644 --- a/source/runtime/Rendering/Material.cpp +++ b/source/runtime/Rendering/Material.cpp @@ -89,6 +89,7 @@ namespace spartan case MaterialProperty::WindAnimation: return "wind_animation"; case MaterialProperty::ColorVariationFromInstance: return "color_variation_from_instance"; case MaterialProperty::IsWater: return "vertex_animate_water"; + case MaterialProperty::IsOcean: return "is_ocean"; // Render settings case MaterialProperty::CullMode: return "cull_mode"; @@ -103,6 +104,51 @@ namespace spartan } } } + + const char* ocean_property_to_char_ptr(OceanParameters property) + { + switch (property) + { + case spartan::OceanParameters::Scale: return "scale"; + case spartan::OceanParameters::SpreadBlend: return "spread_blend"; + case spartan::OceanParameters::Swell: return "swell"; + case spartan::OceanParameters::Gamma: return "gamma"; + case spartan::OceanParameters::ShortWavesFade: return "short_waves_fade"; + case spartan::OceanParameters::WindDirection: return "wind_direction"; + case spartan::OceanParameters::Fetch: return "fetch"; + case spartan::OceanParameters::WindSpeed: return "wind_speed"; + case spartan::OceanParameters::RepeatTime: return "repeat_time"; + case spartan::OceanParameters::Angle: return "angle"; + case spartan::OceanParameters::Alpha: return "alpha"; + case spartan::OceanParameters::PeakOmega: return "peak_omega"; + case spartan::OceanParameters::Depth: return "depth"; + case spartan::OceanParameters::LowCutoff: return "low_cutoff"; + case spartan::OceanParameters::HighCutoff: return "high_cutoff"; + case spartan::OceanParameters::FoamDecayRate: return "foam_decay_rate"; + case spartan::OceanParameters::FoamThreshold: return "foam_threshold"; + case spartan::OceanParameters::FoamAdd: return "foam_add"; + case spartan::OceanParameters::FoamBias: return "foam_bias"; + case spartan::OceanParameters::DisplacementScale: return "displacement_scale"; + case spartan::OceanParameters::SlopeScale: return "slope_scale"; + case spartan::OceanParameters::LengthScale: return "length_scale"; + case spartan::OceanParameters::DebugDisplacement: return "debug_displacement"; + case spartan::OceanParameters::DebugSlope: return "debug_slope"; + case spartan::OceanParameters::DebugSynthesised: return "debug_synthesised"; + case spartan::OceanParameters::Max: return "max"; + default: + { + SP_ASSERT_MSG(false, "Unknown ocean property"); + return nullptr; + } + } + } + + float jonswap_alpha(float fetch, float windSpeed) { return 0.076f * pow(9.81f * fetch / windSpeed / windSpeed, -0.22f); } + float jonswap_peak_frequency(float fetch, float windSpeed) { + float g = 9.81f; + float dimensionlessFetch = g * fetch / (windSpeed * windSpeed); + return 22.0f * (g / windSpeed) * pow(dimensionlessFetch, -0.33f); + } } namespace texture_processing @@ -606,6 +652,12 @@ namespace spartan const char* attribute_name = material_property_to_char_ptr(static_cast(i)); m_properties[i] = node_material.child(attribute_name).text().as_float(); } + + for (uint32_t i = 0; i < static_cast(OceanParameters::Max); ++i) + { + const char* attribute_name = ocean_property_to_char_ptr(static_cast(i)); + m_ocean_properties[i] = node_material.child(attribute_name).text().as_float(); + } // load textures (skip packed textures as they're regenerated from source textures during PrepareForGpu) pugi::xml_node textures_node = node_material.child("textures"); @@ -660,6 +712,16 @@ namespace spartan const char* attribute_name = material_property_to_char_ptr(static_cast(i)); material_node.append_child(attribute_name).text().set(m_properties[i]); } + + // save ocean properties + if (GetProperty(MaterialProperty::IsOcean) == 1.0f) + { + for (uint32_t i = 0; i < static_cast(OceanParameters::Max); ++i) + { + const char* attribute_name = ocean_property_to_char_ptr(static_cast(i)); + material_node.append_child(attribute_name).text().set(m_ocean_properties[i]); + } + } // save textures (skip packed textures as they're regenerated from source textures during PrepareForGpu) pugi::xml_node textures_node = material_node.append_child("textures"); @@ -902,6 +964,53 @@ namespace spartan SaveToFile(GetResourceFilePath()); } + float Material::GetOceanProperty(const OceanParameters property_type) const + { + if (m_properties[static_cast(MaterialProperty::IsOcean)] != 1.0f) + return 0.0f; + + return m_ocean_properties[static_cast(property_type)]; + } + + void Material::SetOceanProperty(const OceanParameters property_type, const float value) + { + SP_ASSERT_MSG(m_properties[static_cast(MaterialProperty::IsOcean)] == 1.0f, "Only ocean materials can have ocean properties"); + + // special cases + if (property_type == OceanParameters::Fetch || property_type == OceanParameters::WindSpeed) + { + // update fetch or windspeed + m_ocean_properties[static_cast(property_type)] = value; + + // get fetch and windspeed + float fetch = m_ocean_properties[static_cast(OceanParameters::Fetch)]; + float windSpeed = m_ocean_properties[static_cast(OceanParameters::WindSpeed)]; + + // update alpha and peakOmega + m_ocean_properties[static_cast(OceanParameters::Alpha)] = jonswap_alpha(fetch, windSpeed); + m_ocean_properties[static_cast(OceanParameters::PeakOmega)] = jonswap_peak_frequency(fetch, windSpeed); + } + else if (property_type == OceanParameters::WindDirection) + { + // update wind direction + m_ocean_properties[static_cast(property_type)] = value; + + // update angle, based on the wind direction + float angle = value / 180.0f * pi; + m_ocean_properties[static_cast(OceanParameters::Angle)] = angle; + } + else + { + if (m_ocean_properties[static_cast(property_type)] == value) + return; + + m_ocean_properties[static_cast(property_type)] = value; + } + + // mark for spectrum re-computation + m_should_compute_spectrum = true; + } + void Material::SetColor(const Color& color) { SetProperty(MaterialProperty::ColorR, color.r); diff --git a/source/runtime/Rendering/Material.h b/source/runtime/Rendering/Material.h index 4d28b340d2..0165e140a6 100644 --- a/source/runtime/Rendering/Material.h +++ b/source/runtime/Rendering/Material.h @@ -43,6 +43,7 @@ namespace spartan Height, // packed a AlphaMask, // packed into color a Packed, // occlusion, roughness, metalness, height + Flowmap, Max }; @@ -92,6 +93,7 @@ namespace spartan WindAnimation, // vertex wind animation ColorVariationFromInstance, // per-instance color variation IsWater, // water flow animation + IsOcean, // fft ocean rendering // render settings CullMode, // face culling mode @@ -111,6 +113,44 @@ namespace spartan Max }; + // used for ocean calculations + enum class OceanParameters + { + Scale, // used to scale the Spectrum [1.0f, 5.0f] --> Value Range + SpreadBlend, // used to blend between agitated water motion, and windDirection [0.0f, 1.0f] + Swell, // influences wave choppines, the bigger the swell, the longer the wave length [0.0f, 1.0f] + Gamma, // defines the Spectrum Peak [0.0f, 7.0f] + + ShortWavesFade, // [0.0f, 1.0f] + WindDirection, // [0.0f, 360.0f] + Fetch, // distance over which Wind impacts Wave Formation [0.0f, 10000.0f] + WindSpeed, // [0.0f, 100.0f] + + RepeatTime, + Angle, + Alpha, + PeakOmega, + + Depth, + LowCutoff, + HighCutoff, + + FoamDecayRate, + FoamBias, + FoamThreshold, + FoamAdd, + + DisplacementScale, + SlopeScale, + LengthScale, + + DebugDisplacement, + DebugSlope, + DebugSynthesised, + + Max + }; + class Material : public IResource { public: @@ -141,6 +181,8 @@ namespace spartan // properties float GetProperty(const MaterialProperty property_type) const { return m_properties[static_cast(property_type)]; } void SetProperty(const MaterialProperty property_type, const float value); + float GetOceanProperty(const OceanParameters property_type) const; + void SetOceanProperty(const OceanParameters property_type, const float value); void SetColor(const Color& color); bool IsTransparent() const { return GetProperty(MaterialProperty::ColorA) < 1.0f; } bool IsAlphaTested(); @@ -150,7 +192,16 @@ namespace spartan uint32_t GetUsedSlotCount() const; void SetIndex(const uint32_t index) { m_index = index; } uint32_t GetIndex() const { return m_index; } + + // ocean + bool ShouldComputeSpectrum() const { return m_should_compute_spectrum; } + void MarkSpectrumAsComputed(const bool flag) { m_should_compute_spectrum = !flag; } + uint32_t GetClipmapTileRes() const { return m_clipmap_tile_res; } + void SetClipmapTileRes(const uint32_t res) { m_clipmap_tile_res = res; } + bool IsOcean() const { return GetProperty(MaterialProperty::IsOcean) == 1.0f; } + const std::array(MaterialProperty::Max)>& GetProperties() const { return m_properties; } + const std::array(OceanParameters::Max)>& GetOceanProperties() const { return m_ocean_properties; } void ClearPackedTextures(); private: @@ -158,8 +209,13 @@ namespace spartan std::array(MaterialTextureType::Max) * slots_per_texture> m_textures; std::array(MaterialProperty::Max)> m_properties; + std::array(OceanParameters::Max)> m_ocean_properties; uint32_t m_index = 0; bool m_needs_repack = true; // starts true so first PrepareForGpu() packs textures std::mutex m_mutex; + + // ocean + bool m_should_compute_spectrum = false; + uint32_t m_clipmap_tile_res = 0; }; } diff --git a/source/runtime/Rendering/Renderer.cpp b/source/runtime/Rendering/Renderer.cpp index 8ce968685b..163c7634b6 100644 --- a/source/runtime/Rendering/Renderer.cpp +++ b/source/runtime/Rendering/Renderer.cpp @@ -787,7 +787,8 @@ namespace spartan bool is_instanced = draw_call.instance_count > 1; bool is_alpha_tested = material->IsAlphaTested(); bool is_non_standard_cull = static_cast(material->GetProperty(MaterialProperty::CullMode)) != RHI_CullMode::Back; - return is_tessellated || is_instanced || is_alpha_tested || is_non_standard_cull; + //bool is_ocean = material->IsOcean(); // TEMP: for now, ocean is cpu driven. Might wanna move it to gpu driven + return is_tessellated || is_instanced || is_alpha_tested || is_non_standard_cull; //|| is_ocean; } void Renderer::SetCommonTextures(RHI_CommandList* cmd_list) @@ -815,7 +816,7 @@ namespace spartan entry.material_index = material_index; entry.is_transparent = is_transparent; entry.aabb_index = 0; - entry.padding = 0; + entry.padding = math::Vector3::Zero; // write directly to the mapped gpu buffer RHI_Buffer* buffer = GetBuffer(Renderer_Buffer::DrawData); @@ -867,6 +868,33 @@ namespace spartan properties[count].subsurface_scattering = material->GetProperty(MaterialProperty::SubsurfaceScattering); properties[count].world_space_uv = material->GetProperty(MaterialProperty::WorldSpaceUv); + // ocean + properties[count].jonswap_parameters.alpha = material->GetOceanProperty(OceanParameters::Alpha); + properties[count].jonswap_parameters.angle = material->GetOceanProperty(OceanParameters::Angle); + properties[count].jonswap_parameters.fetch = material->GetOceanProperty(OceanParameters::Fetch); + properties[count].jonswap_parameters.gamma = material->GetOceanProperty(OceanParameters::Gamma); + properties[count].jonswap_parameters.peakOmega = material->GetOceanProperty(OceanParameters::PeakOmega); + properties[count].jonswap_parameters.repeatTime = material->GetOceanProperty(OceanParameters::RepeatTime); + properties[count].jonswap_parameters.scale = material->GetOceanProperty(OceanParameters::Scale); + properties[count].jonswap_parameters.shortWavesFade = material->GetOceanProperty(OceanParameters::ShortWavesFade); + properties[count].jonswap_parameters.spreadBlend = material->GetOceanProperty(OceanParameters::SpreadBlend); + properties[count].jonswap_parameters.swell = material->GetOceanProperty(OceanParameters::Swell); + properties[count].jonswap_parameters.windDirection = material->GetOceanProperty(OceanParameters::WindDirection); + properties[count].jonswap_parameters.windSpeed = material->GetOceanProperty(OceanParameters::WindSpeed); + properties[count].jonswap_parameters.depth = material->GetOceanProperty(OceanParameters::Depth); + properties[count].jonswap_parameters.lowCutoff = material->GetOceanProperty(OceanParameters::LowCutoff); + properties[count].jonswap_parameters.highCutoff = material->GetOceanProperty(OceanParameters::HighCutoff); + properties[count].jonswap_parameters.foamDecayRate = material->GetOceanProperty(OceanParameters::FoamDecayRate); + properties[count].jonswap_parameters.foamBias = material->GetOceanProperty(OceanParameters::FoamBias); + properties[count].jonswap_parameters.foamThreshold = material->GetOceanProperty(OceanParameters::FoamThreshold); + properties[count].jonswap_parameters.foamAdd = material->GetOceanProperty(OceanParameters::FoamAdd); + properties[count].jonswap_parameters.displacementScale = material->GetOceanProperty(OceanParameters::DisplacementScale); + properties[count].jonswap_parameters.slopeScale = material->GetOceanProperty(OceanParameters::SlopeScale); + properties[count].jonswap_parameters.lengthScale = material->GetOceanProperty(OceanParameters::LengthScale); + properties[count].jonswap_parameters.debugDisplacement = material->GetOceanProperty(OceanParameters::DebugDisplacement); + properties[count].jonswap_parameters.debugSlope = material->GetOceanProperty(OceanParameters::DebugSlope); + properties[count].jonswap_parameters.debugSynthesised = material->GetOceanProperty(OceanParameters::DebugSynthesised); + // flags properties[count].flags = material->HasTextureOfType(MaterialTextureType::Height) ? (1U << 0) : 0; properties[count].flags |= material->HasTextureOfType(MaterialTextureType::Normal) ? (1U << 1) : 0; @@ -884,6 +912,7 @@ namespace spartan properties[count].flags |= material->GetProperty(MaterialProperty::IsWater) ? (1U << 13) : 0; properties[count].flags |= material->GetProperty(MaterialProperty::Tessellation) ? (1U << 14) : 0; properties[count].flags |= material->GetProperty(MaterialProperty::EmissiveFromAlbedo) ? (1U << 15) : 0; + properties[count].flags |= material->GetProperty(MaterialProperty::IsOcean) ? (1U << 16) : 0; // keep in sync with Surface struct in common_structs.hlsl } @@ -1230,12 +1259,17 @@ namespace spartan // per-draw data (aabb_index sits after prepass aabbs) Sb_DrawData& data = m_indirect_draw_data[idx]; Entity* entity = renderable->GetEntity(); + math::Vector3 ent_pos = entity->GetPosition(); data.transform = entity->GetMatrix(); data.transform_previous = entity->GetMatrixPrevious(); data.material_index = material->GetIndex(); data.is_transparent = 0; data.aabb_index = m_draw_calls_prepass_count + idx; - data.padding = 0; + data.tile_size = renderable->GetOceanClipmapTileScale(); + data.tile_world_pos = renderable->GetOceanClipmapTilePos(); + data.tile_snap_center = renderable->GetOceanClipmapTileSnapCenter(); + data.tile_res = material->GetClipmapTileRes(); + data.padding = math::Vector3::Zero; } } diff --git a/source/runtime/Rendering/Renderer.h b/source/runtime/Rendering/Renderer.h index 538f848694..d96a0fb4cb 100644 --- a/source/runtime/Rendering/Renderer.h +++ b/source/runtime/Rendering/Renderer.h @@ -222,6 +222,13 @@ namespace spartan // passes - volumetric clouds static void Pass_CloudNoise(RHI_CommandList* cmd_list); static void Pass_CloudShadow(RHI_CommandList* cmd_list); + // passes - ocean + static void Pass_ComputeInitialSpectrum(RHI_CommandList* cmd_list); + static void Pass_PackSpectrum(RHI_CommandList* cmd_list); + static void Pass_AdvanceSpectrum(RHI_CommandList* cmd_list); + static void Pass_ApplyHorizontalFFT(RHI_CommandList* cmd_list); + static void Pass_ApplyVerticalFFT(RHI_CommandList* cmd_list); + static void Pass_GenerateMaps(RHI_CommandList* cmd_list); // passes - debug/editor static void Pass_Grid(RHI_CommandList* cmd_list, RHI_Texture* tex_out); static void Pass_Lines(RHI_CommandList* cmd_list, RHI_Texture* tex_out); diff --git a/source/runtime/Rendering/Renderer_Buffers.h b/source/runtime/Rendering/Renderer_Buffers.h index 17c2c4b2ca..e819aafa8f 100644 --- a/source/runtime/Rendering/Renderer_Buffers.h +++ b/source/runtime/Rendering/Renderer_Buffers.h @@ -153,6 +153,40 @@ namespace spartan float anisotropic_rotation; float clearcoat; float clearcoat_roughness; + + struct OceanParameters + { + float scale; + float spreadBlend; + float swell; + float gamma; + float shortWavesFade; + + float windDirection; + float fetch; + float windSpeed; + float repeatTime; + float angle; + float alpha; + float peakOmega; + + float depth; + float lowCutoff; + float highCutoff; + + float foamDecayRate; + float foamBias; + float foamThreshold; + float foamAdd; + + float displacementScale; + float slopeScale; + float lengthScale; + + float debugDisplacement; + float debugSlope; + float debugSynthesised; + } jonswap_parameters; }; struct Sb_Light @@ -210,7 +244,11 @@ namespace spartan uint32_t material_index = 0; // index into the bindless material parameters array uint32_t is_transparent = 0; // transparency flag uint32_t aabb_index = 0; // index into the aabb buffer for culling - uint32_t padding = 0; + float tile_size = 0.0f; // used for ocean clipmap + math::Vector2 tile_world_pos; // used for ocean clipmap + math::Vector2 tile_snap_center; // used for ocean clipmap + uint32_t tile_res = 0; // used for ocean clipmap + math::Vector3 padding; }; // gpu particle (matches hlsl Particle struct, 64 bytes) diff --git a/source/runtime/Rendering/Renderer_Definitions.h b/source/runtime/Rendering/Renderer_Definitions.h index 8f93985e22..6c4ea1c579 100644 --- a/source/runtime/Rendering/Renderer_Definitions.h +++ b/source/runtime/Rendering/Renderer_Definitions.h @@ -152,6 +152,13 @@ namespace spartan tex_sss = 5, sb_spd = 7, tex_spd = 8, + ocean_initial_spectrum = 9, + ocean_displacement_spectrum = 10, + ocean_slope_spectrum = 11, + ocean_displacement_map = 12, + ocean_slope_map = 13, + ocean_synthesised_displacement = 14, + ocean_synthesised_slope = 15, geometry_info = 20, // ray tracing geometry info buffer // restir reservoir uav bindings reservoir0 = 21, @@ -266,6 +273,13 @@ namespace spartan particle_render_c, // gpu texture compression texture_compress_bc3_c, + // ocean + ocean_initial_spectrum_c, + ocean_pack_spectrum_c, + ocean_advance_spectrum_c, + ocean_horizontal_fft_c, + ocean_vertical_fft_c, + ocean_generate_maps_c, max }; @@ -337,6 +351,12 @@ namespace spartan nrd_spec_radiance_hitdist, nrd_out_diff_radiance_hitdist, nrd_out_spec_radiance_hitdist, + // ocean + ocean_initial_spectrum, + ocean_displacement_spectrum, + ocean_slope_spectrum, + ocean_displacement_map, + ocean_slope_map, // debug debug_output, max diff --git a/source/runtime/Rendering/Renderer_Passes.cpp b/source/runtime/Rendering/Renderer_Passes.cpp index 7c7da9c3f4..cbc9369ffc 100644 --- a/source/runtime/Rendering/Renderer_Passes.cpp +++ b/source/runtime/Rendering/Renderer_Passes.cpp @@ -139,6 +139,37 @@ namespace spartan { Pass_VariableRateShading(cmd_list_graphics_present); + // Ocean Passes + Material* prev_material = nullptr; + for (uint32_t i = 0; i < m_draw_call_count; i++) + { + const Renderer_DrawCall& draw_call = m_draw_calls[i]; + Renderable* renderable = draw_call.renderable; + Material* material = renderable->GetMaterial(); + + // get ocean material + if (!material->IsOcean() || material == prev_material) + continue; + + prev_material = material; + + if (material->ShouldComputeSpectrum()) + { + //SP_LOG_INFO("Computing Ocean Spectrum..."); + Pass_ComputeInitialSpectrum(cmd_list_graphics_present); + // calculates conjugate and stores it in BA channels of the initial spectrum + Pass_PackSpectrum(cmd_list_graphics_present); + + material->MarkSpectrumAsComputed(true); + } + + // computes displacement and slope maps + Pass_AdvanceSpectrum(cmd_list_graphics_present); + Pass_ApplyHorizontalFFT(cmd_list_graphics_present); + Pass_ApplyVerticalFFT(cmd_list_graphics_present); + Pass_GenerateMaps(cmd_list_graphics_present); + } + // graphics phase 1: geometry { bool is_transparent = false; @@ -208,6 +239,7 @@ namespace spartan if (m_transparents_present) { bool is_transparent = true; + Pass_GBuffer(cmd_list_graphics_present, is_transparent); Pass_Light(cmd_list_graphics_present, is_transparent); Pass_Light_Composition(cmd_list_graphics_present, is_transparent); @@ -220,6 +252,7 @@ namespace spartan Pass_Light_Reflections(cmd_list_graphics_present); Pass_TransparencyReflectionRefraction(cmd_list_graphics_present); + Pass_AA_Upscale(cmd_list_graphics_present); Pass_PostProcess(cmd_list_graphics_present); } @@ -514,6 +547,31 @@ namespace spartan cmd_list->SetBuffer(Renderer_BindingsUav::indirect_draw_data_out, GetBuffer(Renderer_Buffer::IndirectDrawDataOut)); cmd_list->SetCullMode(RHI_CullMode::Back); + for (uint32_t i = 0; i < m_draw_call_count; i++) + { + const Renderer_DrawCall& draw_call = m_draw_calls[i]; + Renderable* renderable = draw_call.renderable; + Material* material = renderable->GetMaterial(); + if (!material || !draw_call.camera_visible) + continue; + + Entity* entity = renderable->GetEntity(); + + //if (material->IsOcean()) + //{ + // const Vector3 tile_pos = renderable->GetEntity()->GetPosition(); + // m_pcb_pass_cpu.set_f3_value2(tile_pos.x, tile_pos.z, material->GetOceanTileSize()); + //} + + cmd_list->PushConstants(m_pcb_pass_cpu); + + if (material->IsOcean()) + { + RHI_Texture* displacement_map = GetRenderTarget(Renderer_RenderTarget::ocean_displacement_map); + cmd_list->SetTexture(Renderer_BindingsSrv::tex2, displacement_map); + } + } + cmd_list->DrawIndexedIndirectCount( GetBuffer(Renderer_Buffer::IndirectDrawArgsOut), 0, @@ -572,6 +630,13 @@ namespace spartan m_pcb_pass_cpu.is_transparent = 0; m_pcb_pass_cpu.material_index = material->GetIndex(); m_pcb_pass_cpu.set_f3_value(0.0f, has_color_texture ? 1.0f : 0.0f, static_cast(i)); + + //if (material->IsOcean()) + //{ + // const Vector3 tile_pos = renderable->GetEntity()->GetPosition(); + // m_pcb_pass_cpu.set_f3_value2(tile_pos.x, tile_pos.z, material->GetOceanTileSize()); + //} + cmd_list->PushConstants(m_pcb_pass_cpu); } @@ -582,6 +647,12 @@ namespace spartan cmd_list->SetBufferVertex(renderable->GetVertexBuffer(), renderable->GetInstanceBuffer()); cmd_list->SetBufferIndex(renderable->GetIndexBuffer()); + if (material->IsOcean()) + { + RHI_Texture* displacement_map = GetRenderTarget(Renderer_RenderTarget::ocean_displacement_map); + cmd_list->SetTexture(Renderer_BindingsSrv::tex2, displacement_map); + } + cmd_list->DrawIndexed( renderable->GetIndexCount(draw_call.lod_index), renderable->GetIndexOffset(draw_call.lod_index), @@ -644,6 +715,41 @@ namespace spartan cmd_list->SetBuffer(Renderer_BindingsUav::indirect_draw_data_out, GetBuffer(Renderer_Buffer::IndirectDrawDataOut)); cmd_list->SetCullMode(RHI_CullMode::Back); + for (uint32_t i = 0; i < m_draw_call_count; i++) + { + const Renderer_DrawCall& draw_call = m_draw_calls[i]; + Renderable* renderable = draw_call.renderable; + Material* material = renderable->GetMaterial(); + if (!material || !draw_call.camera_visible) + continue; + + Entity* entity = renderable->GetEntity(); + + //if (material->IsOcean()) + //{ + // const Vector3 tile_pos = entity->GetPosition(); + // m_pcb_pass_cpu.set_f3_value(tile_pos.x, tile_pos.z, material->GetOceanTileSize()); + //} + + cmd_list->PushConstants(m_pcb_pass_cpu); + + if (material->IsOcean()) + { + RHI_Texture* displacement_map = GetRenderTarget(Renderer_RenderTarget::ocean_displacement_map); + cmd_list->SetTexture(Renderer_BindingsSrv::tex2, displacement_map); + + RHI_Texture* slope_map = GetRenderTarget(Renderer_RenderTarget::ocean_slope_map); + cmd_list->SetTexture(Renderer_BindingsSrv::tex3, slope_map); + + RHI_Texture* heightmap = material->GetTexture(MaterialTextureType::Height); + cmd_list->SetTexture(Renderer_BindingsSrv::tex4, heightmap); + + RHI_Texture* flowmap = material->GetTexture(MaterialTextureType::Flowmap); + cmd_list->SetTexture(Renderer_BindingsSrv::tex5, flowmap); + } + } + + // single indirect draw call replaces the entire opaque draw loop cmd_list->DrawIndexedIndirectCount( GetBuffer(Renderer_Buffer::IndirectDrawArgsOut), 0, @@ -727,6 +833,13 @@ namespace spartan m_pcb_pass_cpu.draw_index = draw_call.draw_data_index; m_pcb_pass_cpu.is_transparent = is_transparent_pass ? 1 : 0; m_pcb_pass_cpu.material_index = material->GetIndex(); + + //if (material->IsOcean()) + //{ + // const Vector3 tile_pos = entity->GetPosition(); + // m_pcb_pass_cpu.set_f3_value(tile_pos.x, tile_pos.z, material->GetOceanTileSize()); + //} + cmd_list->PushConstants(m_pcb_pass_cpu); entity->SetMatrixPrevious(entity->GetMatrix()); @@ -737,6 +850,21 @@ namespace spartan cmd_list->SetBufferVertex(renderable->GetVertexBuffer(), renderable->GetInstanceBuffer()); cmd_list->SetBufferIndex(renderable->GetIndexBuffer()); + if (material->IsOcean()) + { + RHI_Texture* displacement_map = GetRenderTarget(Renderer_RenderTarget::ocean_displacement_map); + cmd_list->SetTexture(Renderer_BindingsSrv::tex2, displacement_map); + + RHI_Texture* slope_map = GetRenderTarget(Renderer_RenderTarget::ocean_slope_map); + cmd_list->SetTexture(Renderer_BindingsSrv::tex3, slope_map); + + RHI_Texture* heightmap = material->GetTexture(MaterialTextureType::Height); + cmd_list->SetTexture(Renderer_BindingsSrv::tex4, heightmap); + + RHI_Texture* flowmap = material->GetTexture(MaterialTextureType::Flowmap); + cmd_list->SetTexture(Renderer_BindingsSrv::tex5, flowmap); + } + cmd_list->DrawIndexed( renderable->GetIndexCount(draw_call.lod_index), renderable->GetIndexOffset(draw_call.lod_index), @@ -1455,6 +1583,7 @@ namespace spartan { RHI_Shader* shader = GetShader(Renderer_Shader::light_image_based_c); RHI_Texture* tex_out = GetRenderTarget(Renderer_RenderTarget::frame_render); + RHI_Texture* slope_map = GetRenderTarget(Renderer_RenderTarget::ocean_slope_map); cmd_list->BeginTimeblock("light_image_based"); { @@ -1628,6 +1757,141 @@ namespace spartan cmd_list->EndTimeblock(); } + void Renderer::Pass_ComputeInitialSpectrum(RHI_CommandList* cmd_list) + { + RHI_Texture* initial_spectrum = GetRenderTarget(Renderer_RenderTarget::ocean_initial_spectrum); + RHI_Texture* tex_normal = GetRenderTarget(Renderer_RenderTarget::gbuffer_normal); + + cmd_list->BeginTimeblock("ocean_intial_spectrum"); + { + RHI_PipelineState pso; + pso.name = "ocean_initial_spectrum"; + pso.shaders[Compute] = GetShader(Renderer_Shader::ocean_initial_spectrum_c); + cmd_list->SetPipelineState(pso); + + cmd_list->SetTexture(Renderer_BindingsUav::ocean_initial_spectrum, initial_spectrum); + cmd_list->SetTexture(Renderer_BindingsSrv::gbuffer_normal, tex_normal); + cmd_list->Dispatch(initial_spectrum); + } + cmd_list->EndTimeblock(); + } + + void Renderer::Pass_PackSpectrum(RHI_CommandList* cmd_list) + { + RHI_Texture* initial_spectrum = GetRenderTarget(Renderer_RenderTarget::ocean_initial_spectrum); + + cmd_list->BeginTimeblock("ocean_pack_spectrum"); + { + RHI_PipelineState pso; + pso.name = "ocean_pack_spectrum"; + pso.shaders[Compute] = GetShader(Renderer_Shader::ocean_pack_spectrum_c); + cmd_list->SetPipelineState(pso); + + cmd_list->SetTexture(Renderer_BindingsUav::ocean_initial_spectrum, initial_spectrum); + cmd_list->Dispatch(initial_spectrum); + + // for the lifetime of the engine, this will be read as an srv, so transition here + initial_spectrum->SetLayout(RHI_Image_Layout::Shader_Read, cmd_list); + } + cmd_list->EndTimeblock(); + } + + void Renderer::Pass_AdvanceSpectrum(RHI_CommandList* cmd_list) + { + RHI_Texture* initial_spectrum = GetRenderTarget(Renderer_RenderTarget::ocean_initial_spectrum); + RHI_Texture* displacement_spectrum = GetRenderTarget(Renderer_RenderTarget::ocean_displacement_spectrum); + RHI_Texture* slope_spectrum = GetRenderTarget(Renderer_RenderTarget::ocean_slope_spectrum); + RHI_Texture* tex_normal = GetRenderTarget(Renderer_RenderTarget::gbuffer_normal); + + cmd_list->BeginTimeblock("ocean_advance_spectrum"); + { + RHI_PipelineState pso; + pso.name = "ocean_advance_spectrum"; + pso.shaders[Compute] = GetShader(Renderer_Shader::ocean_advance_spectrum_c); + cmd_list->SetPipelineState(pso); + + cmd_list->SetTexture(Renderer_BindingsUav::ocean_initial_spectrum, initial_spectrum); + cmd_list->SetTexture(Renderer_BindingsUav::ocean_displacement_spectrum, displacement_spectrum); + cmd_list->SetTexture(Renderer_BindingsUav::ocean_slope_spectrum, slope_spectrum); + cmd_list->SetTexture(Renderer_BindingsSrv::gbuffer_normal, tex_normal); + cmd_list->Dispatch(initial_spectrum); + } + cmd_list->EndTimeblock(); + } + + void Renderer::Pass_ApplyHorizontalFFT(RHI_CommandList* cmd_list) + { + RHI_Texture* displacement_spectrum = GetRenderTarget(Renderer_RenderTarget::ocean_displacement_spectrum); + RHI_Texture* slope_spectrum = GetRenderTarget(Renderer_RenderTarget::ocean_slope_spectrum); + + cmd_list->BeginTimeblock("ocean_horizontal_fft"); + { + RHI_PipelineState pso; + pso.name = "ocean_horizontal_fft"; + pso.shaders[Compute] = GetShader(Renderer_Shader::ocean_horizontal_fft_c); + cmd_list->SetPipelineState(pso); + + cmd_list->SetTexture(Renderer_BindingsUav::ocean_displacement_spectrum, displacement_spectrum); + cmd_list->SetTexture(Renderer_BindingsUav::ocean_slope_spectrum, slope_spectrum); + cmd_list->Dispatch(1, displacement_spectrum->GetHeight(), 1); + + displacement_spectrum->SetLayout(RHI_Image_Layout::Attachment, cmd_list); + slope_spectrum->SetLayout(RHI_Image_Layout::Attachment, cmd_list); + } + cmd_list->EndTimeblock(); + } + + void Renderer::Pass_ApplyVerticalFFT(RHI_CommandList* cmd_list) + { + RHI_Texture* displacement_spectrum = GetRenderTarget(Renderer_RenderTarget::ocean_displacement_spectrum); + RHI_Texture* slope_spectrum = GetRenderTarget(Renderer_RenderTarget::ocean_slope_spectrum); + + cmd_list->BeginTimeblock("ocean_vertical_fft"); + { + RHI_PipelineState pso; + pso.name = "ocean_vertical_fft"; + pso.shaders[Compute] = GetShader(Renderer_Shader::ocean_vertical_fft_c); + cmd_list->SetPipelineState(pso); + + cmd_list->SetTexture(Renderer_BindingsUav::ocean_displacement_spectrum, displacement_spectrum); + cmd_list->SetTexture(Renderer_BindingsUav::ocean_slope_spectrum, slope_spectrum); + cmd_list->Dispatch(1, displacement_spectrum->GetHeight(), 1); + } + cmd_list->EndTimeblock(); + } + + void Renderer::Pass_GenerateMaps(RHI_CommandList* cmd_list) + { + RHI_Texture* displacement_spectrum = GetRenderTarget(Renderer_RenderTarget::ocean_displacement_spectrum); + RHI_Texture* slope_spectrum = GetRenderTarget(Renderer_RenderTarget::ocean_slope_spectrum); + + RHI_Texture* displacement_map = GetRenderTarget(Renderer_RenderTarget::ocean_displacement_map); + RHI_Texture* slope_map = GetRenderTarget(Renderer_RenderTarget::ocean_slope_map); + + RHI_Texture* tex_normal = GetRenderTarget(Renderer_RenderTarget::gbuffer_normal); + + cmd_list->BeginTimeblock("ocean_map_generation"); + { + RHI_PipelineState pso; + pso.name = "ocean_map_generation"; + pso.shaders[Compute] = GetShader(Renderer_Shader::ocean_generate_maps_c); + cmd_list->SetPipelineState(pso); + + cmd_list->SetTexture(Renderer_BindingsUav::ocean_displacement_spectrum, displacement_spectrum); + cmd_list->SetTexture(Renderer_BindingsUav::ocean_slope_spectrum, slope_spectrum); + cmd_list->SetTexture(Renderer_BindingsUav::ocean_displacement_map, displacement_map); + cmd_list->SetTexture(Renderer_BindingsUav::ocean_slope_map, slope_map); + cmd_list->SetTexture(Renderer_BindingsSrv::gbuffer_normal, tex_normal); + cmd_list->Dispatch(displacement_map); + + Pass_Downscale(cmd_list, displacement_map, Renderer_DownsampleFilter::Average); + Pass_Downscale(cmd_list, slope_map, Renderer_DownsampleFilter::Average); + + slope_map->SetLayout(RHI_Image_Layout::Shader_Read, cmd_list); + } + cmd_list->EndTimeblock(); + } + void Renderer::Pass_PostProcess(RHI_CommandList* cmd_list) { RHI_Texture* rt_frame_output = GetRenderTarget(Renderer_RenderTarget::frame_output); diff --git a/source/runtime/Rendering/Renderer_Resources.cpp b/source/runtime/Rendering/Renderer_Resources.cpp index edaa7438f7..a378a5aca0 100644 --- a/source/runtime/Rendering/Renderer_Resources.cpp +++ b/source/runtime/Rendering/Renderer_Resources.cpp @@ -363,6 +363,23 @@ namespace spartan render_target(Renderer_RenderTarget::light_volumetric) = make_shared(RHI_Texture_Type::Type2D, width_render, height_render, 1, 1, RHI_Format::R11G11B10_Float, flags, "light_volumetric"); } + // ocean + { + const uint32_t flags = RHI_Texture_Uav | RHI_Texture_Srv; + const uint32_t texture_size = 512; + const RHI_Format texture_format = RHI_Format::R16G16B16A16_Float; + + render_target(Renderer_RenderTarget::ocean_initial_spectrum) = make_shared(RHI_Texture_Type::Type2D, texture_size, texture_size, 1, 1, texture_format, flags, "ocean_initial_spectrum"); + + render_target(Renderer_RenderTarget::ocean_displacement_spectrum) = make_shared(RHI_Texture_Type::Type2D, texture_size, texture_size, 1, 1, texture_format, flags, "ocean_displacement_spectrum"); + + render_target(Renderer_RenderTarget::ocean_slope_spectrum) = make_shared(RHI_Texture_Type::Type2D, texture_size, texture_size, 1, 1, texture_format, flags, "ocean_slope_spectrum"); + + render_target(Renderer_RenderTarget::ocean_displacement_map) = make_shared(RHI_Texture_Type::Type2D, texture_size, texture_size, 1, 10, texture_format, flags | RHI_Texture_PerMipViews, "ocean_displacement_map"); + + render_target(Renderer_RenderTarget::ocean_slope_map) = make_shared(RHI_Texture_Type::Type2D, texture_size, texture_size, 1, 10, texture_format, flags | RHI_Texture_PerMipViews, "ocean_slope_map"); + } + // occlusion { // amd depth format restrictions: separate texture for uav + manual blit @@ -532,6 +549,27 @@ namespace spartan shader(Renderer_Shader::light_image_based_c)->Compile(RHI_Shader_Type::Compute, shader_dir + "light_image_based.hlsl", async); } + // ocean + { + shader(Renderer_Shader::ocean_initial_spectrum_c) = make_shared(); + shader(Renderer_Shader::ocean_initial_spectrum_c)->Compile(RHI_Shader_Type::Compute, shader_dir + "ocean\\initial_spectrum.hlsl", false); + + shader(Renderer_Shader::ocean_pack_spectrum_c) = make_shared(); + shader(Renderer_Shader::ocean_pack_spectrum_c)->Compile(RHI_Shader_Type::Compute, shader_dir + "ocean\\pack_spectrum.hlsl", false); + + shader(Renderer_Shader::ocean_advance_spectrum_c) = make_shared(); + shader(Renderer_Shader::ocean_advance_spectrum_c)->Compile(RHI_Shader_Type::Compute, shader_dir + "ocean\\advance_spectrum.hlsl", false); + + shader(Renderer_Shader::ocean_horizontal_fft_c) = make_shared(); + shader(Renderer_Shader::ocean_horizontal_fft_c)->Compile(RHI_Shader_Type::Compute, shader_dir + "ocean\\horizontal_fft.hlsl", false); + + shader(Renderer_Shader::ocean_vertical_fft_c) = make_shared(); + shader(Renderer_Shader::ocean_vertical_fft_c)->Compile(RHI_Shader_Type::Compute, shader_dir + "ocean\\vertical_fft.hlsl", false); + + shader(Renderer_Shader::ocean_generate_maps_c) = make_shared(); + shader(Renderer_Shader::ocean_generate_maps_c)->Compile(RHI_Shader_Type::Compute, shader_dir + "ocean\\generate_maps.hlsl", false); + } + // blur { // gaussian diff --git a/source/runtime/World/Components/Renderable.h b/source/runtime/World/Components/Renderable.h index 6e93b5b877..f4ee084027 100644 --- a/source/runtime/World/Components/Renderable.h +++ b/source/runtime/World/Components/Renderable.h @@ -76,6 +76,14 @@ namespace spartan const math::BoundingBox& GetBoundingBox() const { return m_bounding_box; } const math::BoundingBox& GetBoundingBoxMesh() const { return m_bounding_box_mesh; } + // ocean clipmap + void SetOceanClipmapTilePos(const math::Vector2 pos) { m_tile_world_pos = pos; } + math::Vector2 GetOceanClipmapTilePos() const { return m_tile_world_pos; } + void SetOceanClipmapTileSnapCenter(const math::Vector2 snap) { m_tile_snap_center = snap; } + math::Vector2 GetOceanClipmapTileSnapCenter() const { return m_tile_snap_center; } + void SetOceanClipmapTileScale(const float scale) { m_tile_size = scale; } + float GetOceanClipmapTileScale() const { return m_tile_size; } + // material void SetMaterial(const std::shared_ptr& material); void SetMaterial(const std::string& file_path); @@ -124,6 +132,11 @@ namespace spartan math::BoundingBox m_bounding_box_mesh = math::BoundingBox::Unit; math::BoundingBox m_bounding_box = math::BoundingBox::Unit; + // ocean clipmap + math::Vector2 m_tile_world_pos; + math::Vector2 m_tile_snap_center; + float m_tile_size; + // material bool m_material_default = false; Material* m_material = nullptr; diff --git a/source/runtime/World/World.cpp b/source/runtime/World/World.cpp index ecda6d9265..7b89ccda3b 100644 --- a/source/runtime/World/World.cpp +++ b/source/runtime/World/World.cpp @@ -120,6 +120,10 @@ namespace spartan { hash = (hash * 31) ^ std::hash{}(prop); } + for (const float oceanProp : material->GetOceanProperties()) + { + hash = (hash * 31) ^ std::hash{}(oceanProp); + } return hash; }