diff --git a/Cargo.toml b/Cargo.toml index 8b554598d4..7036a87971 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,8 @@ members = [ "fyrox-texture", "fyrox-autotile", "fyrox-material", - "fyrox-graphics-gl"] + "fyrox-graphics-gl", + "fyrox-graphics-wgpu"] resolver = "2" [profile.editor-standalone] diff --git a/editor-standalone/Cargo.toml b/editor-standalone/Cargo.toml index b7dbeb56e2..1a93ac5cfd 100644 --- a/editor-standalone/Cargo.toml +++ b/editor-standalone/Cargo.toml @@ -14,5 +14,10 @@ include = ["/src/**/*", "/Cargo.toml", "/LICENSE", "/README.md"] [dependencies] fyrox = { version = "2.0.0-rc.1", path = "../fyrox" } -fyroxed_base = { version = "2.0.0-rc.1", path = "../editor" } +fyroxed_base = { version = "2.0.0-rc.1", path = "../editor", default-features = false } clap = { version = "4", features = ["derive"] } + +[features] +default = ["backend_opengl"] +backend_opengl = ["fyroxed_base/backend_opengl"] +backend_wgpu = ["fyroxed_base/backend_wgpu"] diff --git a/editor/Cargo.toml b/editor/Cargo.toml index 9f68926b66..82e2a8bfa5 100644 --- a/editor/Cargo.toml +++ b/editor/Cargo.toml @@ -32,5 +32,7 @@ notify = "8" bitflags = "2.9.1" [features] -default = ["fyrox/default"] +default = ["fyrox/default", "backend_opengl"] dylib_engine = ["fyrox/dylib"] +backend_opengl = ["fyrox/backend_opengl"] +backend_wgpu = ["fyrox/backend_wgpu"] diff --git a/editor/resources/shaders/gizmo.shader b/editor/resources/shaders/opengl/gizmo.shader similarity index 100% rename from editor/resources/shaders/gizmo.shader rename to editor/resources/shaders/opengl/gizmo.shader diff --git a/editor/resources/shaders/grid.shader b/editor/resources/shaders/opengl/grid.shader similarity index 100% rename from editor/resources/shaders/grid.shader rename to editor/resources/shaders/opengl/grid.shader diff --git a/editor/resources/shaders/highlight.shader b/editor/resources/shaders/opengl/highlight.shader similarity index 100% rename from editor/resources/shaders/highlight.shader rename to editor/resources/shaders/opengl/highlight.shader diff --git a/editor/resources/shaders/overlay.shader b/editor/resources/shaders/opengl/overlay.shader similarity index 100% rename from editor/resources/shaders/overlay.shader rename to editor/resources/shaders/opengl/overlay.shader diff --git a/editor/resources/shaders/sprite_gizmo.shader b/editor/resources/shaders/opengl/sprite_gizmo.shader similarity index 100% rename from editor/resources/shaders/sprite_gizmo.shader rename to editor/resources/shaders/opengl/sprite_gizmo.shader diff --git a/editor/resources/shaders/wgpu/gizmo.shader b/editor/resources/shaders/wgpu/gizmo.shader new file mode 100644 index 0000000000..5bff2e2cbb --- /dev/null +++ b/editor/resources/shaders/wgpu/gizmo.shader @@ -0,0 +1,98 @@ +( + name: "GizmoShader", + + resources: [ + ( + name: "properties", + kind: PropertyGroup([ + ( + name: "diffuseColor", + kind: Color(r: 255, g: 255, b: 255, a: 255), + ), + ]), + binding: 0 + ), + ( + name: "fyrox_instanceData", + kind: PropertyGroup([ + // Autogenerated + ]), + binding: 1 + ), + ], + + disabled_passes: ["GBuffer", "DirectionalShadow", "PointShadow", "SpotShadow"], + + passes: [ + ( + name: "Forward", + draw_parameters: DrawParameters( + cull_face: None, + color_write: ColorMask( + red: true, + green: true, + blue: true, + alpha: true, + ), + depth_write: true, + stencil_test: None, + depth_test: Some(Less), + blend: Some(BlendParameters( + func: BlendFunc( + sfactor: SrcAlpha, + dfactor: OneMinusSrcAlpha, + alpha_sfactor: SrcAlpha, + alpha_dfactor: OneMinusSrcAlpha, + ), + equation: BlendEquation( + rgb: Add, + alpha: Add + ) + )), + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Keep, + write_mask: 0xFFFF_FFFF, + ), + scissor_box: None + ), + vertex_shader: + r#" + struct VertexInput { + @location(0) vertexPosition: vec3f, + }; + + struct VertexOutput { + @builtin(position) position: vec4f, + }; + + @vertex + fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + output.position = fyrox_instanceData.worldViewProjection * vec4f(input.vertexPosition, 1.0); + return output; + } + "#, + + fragment_shader: + r#" + struct FragOutput { + @location(0) color: vec4f, + @builtin(frag_depth) depth: f32, + }; + + @fragment + fn fs_main(@builtin(position) fragCoord: vec4f) -> FragOutput { + var output: FragOutput; + output.color = properties.diffuseColor; + + // Pull depth towards near clipping plane so the gizmo will be drawn on top + // of everything, but its parts will be correctly handled by depth test. + output.depth = fragCoord.z * 0.001; + return output; + } + "#, + ), + ], +) diff --git a/editor/resources/shaders/wgpu/grid.shader b/editor/resources/shaders/wgpu/grid.shader new file mode 100644 index 0000000000..60508580b3 --- /dev/null +++ b/editor/resources/shaders/wgpu/grid.shader @@ -0,0 +1,215 @@ +( + name: "GridShader", + + resources: [ + ( + name: "properties", + kind: PropertyGroup([ + ( + name: "diffuseColor", + kind: Color(r: 40, g: 40, b: 40, a: 255), + ), + ( + name: "xAxisColor", + kind: Color(r: 255, g: 0, b: 0, a: 255), + ), + ( + name: "yAxisColor", + kind: Color(r: 0, g: 255, b: 0, a: 255), + ), + ( + name: "zAxisColor", + kind: Color(r: 0, g: 0, b: 255, a: 255), + ), + ( + name: "orientation", + kind: Int(value: 0), + ), + ( + name: "isPerspective", + kind: Bool(value: false) + ), + ( + name: "scale", + kind: Vector2(value: (1.0, 1.0)) + ) + ]), + binding: 0 + ), + ( + name: "fyrox_cameraData", + kind: PropertyGroup([ + // Autogenerated + ]), + binding: 1 + ), + ], + + disabled_passes: ["GBuffer", "DirectionalShadow", "PointShadow", "SpotShadow"], + + passes: [ + ( + name: "Forward", + draw_parameters: DrawParameters( + cull_face: None, + color_write: ColorMask( + red: true, + green: true, + blue: true, + alpha: true, + ), + depth_write: true, + stencil_test: None, + depth_test: Some(Less), + blend: Some(BlendParameters( + func: BlendFunc( + sfactor: SrcAlpha, + dfactor: OneMinusSrcAlpha, + alpha_sfactor: SrcAlpha, + alpha_dfactor: OneMinusSrcAlpha, + ), + equation: BlendEquation( + rgb: Add, + alpha: Add + ) + )), + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Keep, + write_mask: 0xFFFF_FFFF, + ), + scissor_box: None + ), + vertex_shader: + r#" + struct VertexInput { + @location(0) vertexPosition: vec3f, + }; + + struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) nearPoint: vec3f, + @location(1) farPoint: vec3f, + }; + + fn Unproject(x: f32, y: f32, z: f32, matrix: mat4x4f) -> vec3f { + var position = matrix * vec4f(x, y, z, 1.0); + return position.xyz / position.w; + } + + @vertex + fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + var invViewProj = inverse_mat4(fyrox_cameraData.viewProjectionMatrix); + output.nearPoint = Unproject(input.vertexPosition.x, input.vertexPosition.y, 0.0, invViewProj); + output.farPoint = Unproject(input.vertexPosition.x, input.vertexPosition.y, 1.0, invViewProj); + output.position = vec4f(input.vertexPosition, 1.0); + return output; + } + "#, + + fragment_shader: + r#" + // Original code: https://asliceofrendering.com/scene%20helper/2020/01/05/InfiniteGrid/ + // Fixed and adapted for Fyrox. + + struct FragOutput { + @location(0) color: vec4f, + @builtin(frag_depth) depth: f32, + }; + + fn grid(fragPos3D: vec3f) -> vec4f { + var projection: vec2f; + var planeNormal: vec3f; + var xColor: vec4f; + var yColor: vec4f; + if (properties.orientation == 0) { + projection = fragPos3D.xz; + planeNormal = vec3f(0.0, 1.0, 0.0); + xColor = properties.xAxisColor; + yColor = properties.zAxisColor; + } else if (properties.orientation == 1) { + projection = fragPos3D.xy; + planeNormal = vec3f(0.0, 0.0, 1.0); + xColor = properties.yAxisColor; + yColor = properties.xAxisColor; + } else if (properties.orientation == 2) { + projection = fragPos3D.zy; + planeNormal = vec3f(1.0, 0.0, 0.0); + xColor = properties.zAxisColor; + yColor = properties.yAxisColor; + } + + var coord = projection * properties.scale; + var derivative = fwidth(coord); + var gridVal = abs(fract(coord - vec2f(0.5)) - vec2f(0.5)) / derivative; + var lineVal = min(gridVal.x, gridVal.y); + var minX = 0.5 * min(derivative.x, 1.0); + var minY = 0.5 * min(derivative.y, 1.0); + + var color = properties.diffuseColor; + var alpha = 1.0 - min(lineVal, 1.0); + // Sharpen lines a bit. + color.a = select(0.0, 1.0, alpha >= 0.5); + + if (projection.x > -minX && projection.x < minX) { + color = vec4f(xColor.xyz, color.a); + } else if (projection.y > -minY && projection.y < minY) { + color = vec4f(yColor.xyz, color.a); + } else { + var viewDir = fragPos3D - fyrox_cameraData.position; + // This helps to negate moire pattern at large distances. + var cosAngle = abs(dot(planeNormal, normalize(viewDir))); + color.a *= cosAngle; + } + + return color; + } + + fn computeDepth(pos: vec3f) -> f32 { + var clip_space_pos = fyrox_cameraData.viewProjectionMatrix * vec4f(pos.xyz, 1.0); + return (clip_space_pos.z / clip_space_pos.w); + } + + @fragment + fn fs_main(@location(0) nearPoint: vec3f, @location(1) farPoint: vec3f, @builtin(position) fragCoord: vec4f) -> FragOutput { + var nearCoord: f32; + var farCoord: f32; + if (properties.orientation == 0) { + nearCoord = nearPoint.y; + farCoord = farPoint.y; + } else if (properties.orientation == 1) { + nearCoord = nearPoint.z; + farCoord = farPoint.z; + } else if (properties.orientation == 2) { + nearCoord = nearPoint.x; + farCoord = farPoint.x; + } + + var denominator = farCoord - nearCoord; + var t = select(0.0, -nearCoord / denominator, denominator != 0.0); + + var fragPos3D = nearPoint + t * (farPoint - nearPoint); + + var depth = computeDepth(fragPos3D); + + var output: FragOutput; + output.depth = depth; + + output.color = grid(fragPos3D); + if (properties.isPerspective != 0u) { + output.color.a *= f32(t > 0.0); + } + + // Alpha test to prevent blending issues. + if (output.color.a < 0.01) { + discard; + } + + return output; + } + "#, + ), + ], +) diff --git a/editor/resources/shaders/wgpu/highlight.shader b/editor/resources/shaders/wgpu/highlight.shader new file mode 100644 index 0000000000..aec3bd11c4 --- /dev/null +++ b/editor/resources/shaders/wgpu/highlight.shader @@ -0,0 +1,104 @@ +( + name: "Highlight", + resources: [ + ( + name: "frameTexture", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 0 + ), + ( + name: "properties", + kind: PropertyGroup([ + (name: "worldViewProjection", kind: Matrix4()), + (name: "color", kind: Vector4()), + ]), + binding: 0 + ), + ], + passes: [ + ( + name: "Primary", + + draw_parameters: DrawParameters( + cull_face: None, + color_write: ColorMask( + red: true, + green: true, + blue: true, + alpha: true, + ), + depth_write: false, + stencil_test: None, + depth_test: None, + blend: Some(BlendParameters( + func: BlendFunc( + sfactor: SrcAlpha, + dfactor: OneMinusSrcAlpha, + alpha_sfactor: SrcAlpha, + alpha_dfactor: OneMinusSrcAlpha, + ), + equation: BlendEquation( + rgb: Add, + alpha: Add + ) + )), + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Keep, + write_mask: 0xFFFF_FFFF, + ), + scissor_box: None + ), + + vertex_shader: + r#" + struct VertexInput { + @location(0) vertexPosition: vec3f, + @location(1) vertexTexCoord: vec2f, + }; + + struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) texCoord: vec2f, + }; + + @vertex + fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + output.texCoord = input.vertexTexCoord; + output.position = properties.worldViewProjection * vec4f(input.vertexPosition, 1.0); + return output; + } + "#, + + fragment_shader: + r#" + @fragment + fn fs_main(@location(0) texCoord: vec2f) -> @location(0) vec4f { + var size = textureDimensions(frameTexture_tex); + + var w = 1.0 / f32(size.x); + var h = 1.0 / f32(size.y); + + var n: array; + n[0] = textureSample(frameTexture_tex, frameTexture_samp, texCoord + vec2f(-w, -h)).a; + n[1] = textureSample(frameTexture_tex, frameTexture_samp, texCoord + vec2f(0.0, -h)).a; + n[2] = textureSample(frameTexture_tex, frameTexture_samp, texCoord + vec2f(w, -h)).a; + n[3] = textureSample(frameTexture_tex, frameTexture_samp, texCoord + vec2f(-w, 0.0)).a; + n[4] = textureSample(frameTexture_tex, frameTexture_samp, texCoord).a; + n[5] = textureSample(frameTexture_tex, frameTexture_samp, texCoord + vec2f(w, 0.0)).a; + n[6] = textureSample(frameTexture_tex, frameTexture_samp, texCoord + vec2f(-w, h)).a; + n[7] = textureSample(frameTexture_tex, frameTexture_samp, texCoord + vec2f(0.0, h)).a; + n[8] = textureSample(frameTexture_tex, frameTexture_samp, texCoord + vec2f(w, h)).a; + + var sobel_edge_h = n[2] + (2.0 * n[5]) + n[8] - (n[0] + (2.0 * n[3]) + n[6]); + var sobel_edge_v = n[0] + (2.0 * n[1]) + n[2] - (n[6] + (2.0 * n[7]) + n[8]); + var sobel = sqrt((sobel_edge_h * sobel_edge_h) + (sobel_edge_v * sobel_edge_v)); + + return vec4f(properties.color.rgb, properties.color.a * sobel); + } + "#, + ) + ] +) diff --git a/editor/resources/shaders/wgpu/overlay.shader b/editor/resources/shaders/wgpu/overlay.shader new file mode 100644 index 0000000000..2cc92ef6f3 --- /dev/null +++ b/editor/resources/shaders/wgpu/overlay.shader @@ -0,0 +1,90 @@ +( + name: "Overlay", + resources: [ + ( + name: "diffuseTexture", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 0 + ), + ( + name: "properties", + kind: PropertyGroup([ + (name: "viewProjectionMatrix", kind: Matrix4()), + (name: "worldMatrix", kind: Matrix4()), + (name: "cameraSideVector", kind: Vector3()), + (name: "cameraUpVector", kind: Vector3()), + (name: "size", kind: Float()), + ]), + binding: 0 + ), + ], + passes: [ + ( + name: "Primary", + + draw_parameters: DrawParameters( + cull_face: None, + color_write: ColorMask( + red: true, + green: true, + blue: true, + alpha: true, + ), + depth_write: false, + stencil_test: None, + depth_test: None, + blend: Some(BlendParameters( + func: BlendFunc( + sfactor: SrcAlpha, + dfactor: OneMinusSrcAlpha, + alpha_sfactor: SrcAlpha, + alpha_dfactor: OneMinusSrcAlpha, + ), + equation: BlendEquation( + rgb: Add, + alpha: Add + ) + )), + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Keep, + write_mask: 0xFFFF_FFFF, + ), + scissor_box: None + ), + + vertex_shader: + r#" + struct VertexInput { + @location(0) vertexPosition: vec3f, + @location(1) vertexTexCoord: vec2f, + }; + + struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) texCoord: vec2f, + }; + + @vertex + fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + output.texCoord = input.vertexTexCoord; + var vertexOffset = input.vertexTexCoord * 2.0 - 1.0; + var worldPosition = properties.worldMatrix * vec4f(input.vertexPosition, 1.0); + var offset = (vertexOffset.x * properties.cameraSideVector + vertexOffset.y * properties.cameraUpVector) * properties.size; + output.position = properties.viewProjectionMatrix * (worldPosition + vec4f(offset.x, offset.y, offset.z, 0.0)); + return output; + } + "#, + + fragment_shader: + r#" + @fragment + fn fs_main(@location(0) texCoord: vec2f) -> @location(0) vec4f { + return textureSample(diffuseTexture_tex, diffuseTexture_samp, texCoord); + } + "#, + ) + ] +) diff --git a/editor/resources/shaders/wgpu/sprite_gizmo.shader b/editor/resources/shaders/wgpu/sprite_gizmo.shader new file mode 100644 index 0000000000..bb8e3feffb --- /dev/null +++ b/editor/resources/shaders/wgpu/sprite_gizmo.shader @@ -0,0 +1,113 @@ +( + name: "SpriteGizmoShader", + + resources: [ + ( + name: "diffuseTexture", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 0 + ), + ( + name: "fyrox_instanceData", + kind: PropertyGroup([ + // Autogenerated + ]), + binding: 0 + ), + ( + name: "fyrox_cameraData", + kind: PropertyGroup([ + // Autogenerated + ]), + binding: 1 + ), + ], + + disabled_passes: ["GBuffer", "DirectionalShadow", "PointShadow", "SpotShadow"], + + passes: [ + ( + name: "Forward", + draw_parameters: DrawParameters( + cull_face: None, + color_write: ColorMask( + red: true, + green: true, + blue: true, + alpha: true, + ), + depth_write: true, + stencil_test: None, + depth_test: Some(Less), + blend: Some(BlendParameters( + func: BlendFunc( + sfactor: SrcAlpha, + dfactor: OneMinusSrcAlpha, + alpha_sfactor: SrcAlpha, + alpha_dfactor: OneMinusSrcAlpha, + ), + equation: BlendEquation( + rgb: Add, + alpha: Add + ) + )), + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Keep, + write_mask: 0xFFFF_FFFF, + ), + scissor_box: None + ), + vertex_shader: + r#" + struct VertexInput { + @location(0) vertexPosition: vec3f, + @location(1) vertexTexCoord: vec2f, + @location(2) vertexParams: vec2f, + @location(3) vertexColor: vec4f, + }; + + struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) texCoord: vec2f, + @location(1) color: vec4f, + }; + + @vertex + fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + var size = input.vertexParams.x; + var rotation = input.vertexParams.y; + + output.texCoord = input.vertexTexCoord; + output.color = input.vertexColor; + var vertexOffset = S_RotateVec2(input.vertexTexCoord * 2.0 - 1.0, rotation); + var worldPosition = fyrox_instanceData.worldMatrix * vec4f(input.vertexPosition, 1.0); + var offset = (vertexOffset.x * fyrox_cameraData.sideVector + vertexOffset.y * fyrox_cameraData.upVector) * size; + output.position = fyrox_cameraData.viewProjectionMatrix * (worldPosition + vec4f(offset.x, offset.y, offset.z, 0.0)); + return output; + } + "#, + + fragment_shader: + r#" + struct FragOutput { + @location(0) color: vec4f, + @builtin(frag_depth) depth: f32, + }; + + @fragment + fn fs_main(@location(0) texCoord: vec2f, @location(1) color: vec4f, @builtin(position) fragCoord: vec4f) -> FragOutput { + var output: FragOutput; + output.color = color * S_SRGBToLinear(textureSample(diffuseTexture_tex, diffuseTexture_samp, texCoord)); + + // Pull depth towards near clipping plane so the gizmo will be drawn on top + // of everything, but its parts will be correctly handled by depth test. + output.depth = fragCoord.z * 0.001; + return output; + } + "#, + ), + ], +) diff --git a/editor/src/highlight.rs b/editor/src/highlight.rs index f4c36d7b21..a8bd404be1 100644 --- a/editor/src/highlight.rs +++ b/editor/src/highlight.rs @@ -42,6 +42,12 @@ use crate::{ }; use std::{any::TypeId, cell::RefCell, rc::Rc}; +#[cfg(feature = "backend_opengl")] +const HIGHLIGHT_SHADER_SRC: &str = include_str!("../resources/shaders/opengl/highlight.shader"); + +#[cfg(feature = "backend_wgpu")] +const HIGHLIGHT_SHADER_SRC: &str = include_str!("../resources/shaders/wgpu/highlight.shader"); + pub struct HighlightRenderPass { framebuffer: GpuFrameBuffer, edge_detect_shader: RenderPassContainer, @@ -79,7 +85,7 @@ impl HighlightRenderPass { framebuffer: Self::create_frame_buffer(server, width, height), edge_detect_shader: RenderPassContainer::from_str( server, - include_str!("../resources/shaders/highlight.shader"), + HIGHLIGHT_SHADER_SRC, ) .unwrap(), scene_handle: Default::default(), @@ -107,6 +113,13 @@ impl SceneRenderPass for HighlightRenderPass { return Ok(Default::default()); } + // Skip highlight rendering when nothing is selected. + // Without this, the edge-detect shader reads stale FBO content on wgpu + // because the deferred clear never executes when no draw calls happen. + if self.nodes_to_highlight.is_empty() { + return Ok(Default::default()); + } + // Draw selected nodes in the temporary frame buffer first. { let render_pass_name = ImmutableString::new("Forward"); diff --git a/editor/src/lib.rs b/editor/src/lib.rs index de93c8d2f6..9190d1c06a 100644 --- a/editor/src/lib.rs +++ b/editor/src/lib.rs @@ -273,15 +273,26 @@ macro_rules! load_image { }; } +#[cfg(feature = "backend_opengl")] static GIZMO_SHADER: LazyLock = LazyLock::new(|| { ShaderResource::from_str( Uuid::new_v4(), - include_str!("../resources/shaders/gizmo.shader"), + include_str!("../resources/shaders/opengl/gizmo.shader"), Default::default(), ) .unwrap() }); +#[cfg(feature = "backend_wgpu")] +static GIZMO_SHADER: LazyLock = LazyLock::new(|| { + ShaderResource::from_str( + Uuid::new_v4(), + include_str!("../resources/shaders/wgpu/gizmo.shader"), + Default::default(), + ) + .unwrap() +}); + pub fn make_color_material(color: Color) -> MaterialResource { let mut material = Material::from_shader(GIZMO_SHADER.clone()); material.set_property("diffuseColor", color); diff --git a/editor/src/overlay.rs b/editor/src/overlay.rs index d162c25927..a0900b8686 100644 --- a/editor/src/overlay.rs +++ b/editor/src/overlay.rs @@ -44,6 +44,12 @@ use crate::{ use fyrox::core::uuid::Uuid; use std::{any::TypeId, cell::RefCell, rc::Rc}; +#[cfg(feature = "backend_opengl")] +const OVERLAY_SHADER_SRC: &str = include_str!("../resources/shaders/opengl/overlay.shader"); + +#[cfg(feature = "backend_wgpu")] +const OVERLAY_SHADER_SRC: &str = include_str!("../resources/shaders/wgpu/overlay.shader"); + pub struct OverlayRenderPass { quad: GpuGeometryBuffer, shader: RenderPassContainer, @@ -65,7 +71,7 @@ impl OverlayRenderPass { .unwrap(), shader: RenderPassContainer::from_str( server, - include_str!("../resources/shaders/overlay.shader"), + OVERLAY_SHADER_SRC, ) .unwrap(), sound_icon: TextureResource::load_from_memory( diff --git a/editor/src/plugins/collider/mod.rs b/editor/src/plugins/collider/mod.rs index 27782f42ab..ec82ea9e1d 100644 --- a/editor/src/plugins/collider/mod.rs +++ b/editor/src/plugins/collider/mod.rs @@ -270,15 +270,26 @@ fn make_shape_gizmo( } } +#[cfg(feature = "backend_opengl")] static GIZMO_SHADER: LazyLock = LazyLock::new(|| { ShaderResource::from_str( Uuid::new_v4(), - include_str!("../../../resources/shaders/sprite_gizmo.shader"), + include_str!("../../../resources/shaders/opengl/sprite_gizmo.shader"), Default::default(), ) .unwrap() }); +#[cfg(feature = "backend_wgpu")] +static GIZMO_SHADER: LazyLock = LazyLock::new(|| { + ShaderResource::from_str( + Uuid::new_v4(), + include_str!("../../../resources/shaders/wgpu/sprite_gizmo.shader"), + Default::default(), + ) + .unwrap() +}); + fn make_handle(scene: &mut Scene, root: Handle, visible: bool) -> Handle { let mut material = Material::from_shader(GIZMO_SHADER.clone()); diff --git a/editor/src/scene/mod.rs b/editor/src/scene/mod.rs index 77b68d629e..95b3b86774 100644 --- a/editor/src/scene/mod.rs +++ b/editor/src/scene/mod.rs @@ -152,15 +152,26 @@ pub struct GameScene { pub settings_receiver: Receiver, } +#[cfg(feature = "backend_opengl")] static GRID_SHADER: LazyLock = LazyLock::new(|| { ShaderResource::from_str( Uuid::new_v4(), - include_str!("../../resources/shaders/grid.shader"), + include_str!("../../resources/shaders/opengl/grid.shader"), Default::default(), ) .unwrap() }); +#[cfg(feature = "backend_wgpu")] +static GRID_SHADER: LazyLock = LazyLock::new(|| { + ShaderResource::from_str( + Uuid::new_v4(), + include_str!("../../resources/shaders/wgpu/grid.shader"), + Default::default(), + ) + .unwrap() +}); + fn make_grid_material() -> MaterialResource { let material = Material::from_shader(GRID_SHADER.clone()); MaterialResource::new_embedded(material) diff --git a/fyrox-graphics-gl/src/program.rs b/fyrox-graphics-gl/src/program.rs index 096ab1f259..a4a1efefde 100644 --- a/fyrox-graphics-gl/src/program.rs +++ b/fyrox-graphics-gl/src/program.rs @@ -44,6 +44,8 @@ fn glsl_sampler_name(sampler: SamplerKind) -> &'static str { SamplerKind::USampler2D => "usampler2D", SamplerKind::USampler3D => "usampler3D", SamplerKind::USamplerCube => "usamplerCube", + SamplerKind::DepthSampler2D => "sampler2D", + SamplerKind::DepthSamplerCube => "samplerCube", } } diff --git a/fyrox-graphics-gl/src/server.rs b/fyrox-graphics-gl/src/server.rs index d62fbfece4..1202ce8520 100644 --- a/fyrox-graphics-gl/src/server.rs +++ b/fyrox-graphics-gl/src/server.rs @@ -1494,7 +1494,7 @@ impl GraphicsServer for GlGraphicsServer { let gl = &self.gl; unsafe { ServerCapabilities { - max_uniform_block_size: gl.get_parameter_i32(glow::MAX_UNIFORM_BLOCK_SIZE) as usize, + max_uniform_buffer_binding_size: gl.get_parameter_i32(glow::MAX_UNIFORM_BLOCK_SIZE) as usize, uniform_buffer_offset_alignment: gl .get_parameter_i32(glow::UNIFORM_BUFFER_OFFSET_ALIGNMENT) as usize, diff --git a/fyrox-graphics-wgpu/Cargo.toml b/fyrox-graphics-wgpu/Cargo.toml new file mode 100644 index 0000000000..f1c626b693 --- /dev/null +++ b/fyrox-graphics-wgpu/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "fyrox-graphics-wgpu" +version = "2.0.0-rc.1" +edition = "2021" +license = "MIT" +description = "Wgpu-based graphics server for Fyrox Game Engine" +keywords = ["graphics", "gapi"] +categories = ["graphics", "rendering::graphics-api"] +include = ["/src/**/*", "/Cargo.toml", "/LICENSE", "/README.md"] +homepage = "https://fyrox.rs" +documentation = "https://docs.rs/fyrox-graphics-wgpu" +repository = "https://github.com/FyroxEngine/Fyrox" +rust-version = "1.87" + +[dependencies] +winit = { version = "0.30" } +fyrox-graphics = { version = "2.0.0-rc.1", path = "../fyrox-graphics" } +fyrox-core = { version = "2.0.0-rc.1", path = "../fyrox-core" } +wgpu = { version = "30.0.0" } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +raw-window-handle = "0.6" diff --git a/fyrox-graphics-wgpu/src/buffer.rs b/fyrox-graphics-wgpu/src/buffer.rs new file mode 100644 index 0000000000..e6b8e0eace --- /dev/null +++ b/fyrox-graphics-wgpu/src/buffer.rs @@ -0,0 +1,193 @@ +// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors. +// +// 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. + +use crate::server::WgpuGraphicsServer; +use fyrox_graphics::{ + buffer::{BufferKind, BufferUsage, GpuBufferDescriptor, GpuBufferTrait}, + error::FrameworkError, +}; +use std::cell::{Cell, RefCell}; +use std::rc::Weak; + +/// Maps a Fyrox [`BufferKind`] to the corresponding wgpu [`BufferUsages`] flags. +/// +/// All buffers additionally get `COPY_DST` to allow data uploads via `queue.write_buffer`. +/// `PixelWrite` buffers also get `COPY_SRC` to allow copying to readback buffers. +fn buffer_usage_to_wgpu(kind: BufferKind) -> wgpu::BufferUsages { + let mut flags = match kind { + BufferKind::Vertex => wgpu::BufferUsages::VERTEX, + BufferKind::Index => wgpu::BufferUsages::INDEX, + BufferKind::Uniform => wgpu::BufferUsages::UNIFORM, + BufferKind::PixelRead => wgpu::BufferUsages::MAP_READ, + BufferKind::PixelWrite => wgpu::BufferUsages::MAP_WRITE, + }; + flags |= wgpu::BufferUsages::COPY_DST; + if kind == BufferKind::PixelWrite { + flags |= wgpu::BufferUsages::COPY_SRC; + } + flags +} + +/// Wgpu implementation of [`GpuBufferTrait`](fyrox_graphics::buffer::GpuBufferTrait). +/// +/// Wraps a [`wgpu::Buffer`] and tracks its memory usage via [`ServerMemoryUsage`]. +/// When `write_data` receives data larger than the current buffer capacity, the buffer +/// is transparently reallocated to fit — matching the OpenGL backend behavior. Memory +/// accounting is updated accordingly. +pub struct WgpuBuffer { + server: Weak, + buffer: RefCell, + size: Cell, + kind: BufferKind, + usage: BufferUsage, +} + +impl WgpuBuffer { + /// Creates a new GPU buffer from the given descriptor. + /// + /// The buffer is created with at least 1 byte of capacity (wgpu requires non-zero size). + /// Memory usage is tracked on the server. + pub fn new( + server: &WgpuGraphicsServer, + desc: GpuBufferDescriptor, + ) -> Result { + let wgpu_usage = buffer_usage_to_wgpu(desc.kind); + let buffer = server.state.device.create_buffer(&wgpu::BufferDescriptor { + label: if server.named_objects { + Some(desc.name) + } else { + None + }, + size: desc.size.max(1) as u64, + usage: wgpu_usage, + mapped_at_creation: false, + }); + server.memory_usage.borrow_mut().buffers += desc.size; + Ok(Self { + server: server.weak_ref(), + buffer: RefCell::new(buffer), + size: Cell::new(desc.size), + kind: desc.kind, + usage: desc.usage, + }) + } + + /// Returns a reference to the underlying [`wgpu::Buffer`]. + /// + /// # Safety + /// + /// The returned reference must not be held across a call to `write_data`, + /// which may replace the inner buffer. In practice this is always satisfied + /// because `write_data` takes `&self` and completes before external access. + pub unsafe fn wgpu_buffer_raw(&self) -> &wgpu::Buffer { + // SAFETY: The caller guarantees no mutable borrow is active. + unsafe { &*self.buffer.as_ptr() } + } +} + +impl Drop for WgpuBuffer { + fn drop(&mut self) { + if let Some(server) = self.server.upgrade() { + server.memory_usage.borrow_mut().buffers -= self.size.get(); + } + } +} + +impl GpuBufferTrait for WgpuBuffer { + fn usage(&self) -> BufferUsage { + self.usage + } + fn kind(&self) -> BufferKind { + self.kind + } + fn size(&self) -> usize { + self.size.get() + } + + fn write_data(&self, data: &[u8]) -> Result<(), FrameworkError> { + if data.is_empty() { + return Ok(()); + } + let Some(server) = self.server.upgrade() else { + return Err(FrameworkError::GraphicsServerUnavailable); + }; + if data.len() <= self.size.get() { + server.state.queue.write_buffer(&self.buffer.borrow(), 0, data); + } else { + // Reallocate the buffer to fit the larger data, matching the GL backend + // behavior. This prevents silent data truncation that caused rendering + // artifacts (e.g. broken skeletal animation when bone count changes). + let new_size = data.len(); + let wgpu_usage = buffer_usage_to_wgpu(self.kind); + let new_buffer = server.state.device.create_buffer(&wgpu::BufferDescriptor { + label: None, + size: new_size as u64, + usage: wgpu_usage, + mapped_at_creation: false, + }); + server.state.queue.write_buffer(&new_buffer, 0, data); + + let mut mem = server.memory_usage.borrow_mut(); + mem.buffers -= self.size.get(); + mem.buffers += new_size; + + *self.buffer.borrow_mut() = new_buffer; + self.size.set(new_size); + } + Ok(()) + } + + fn read_data(&self, data: &mut [u8]) -> Result<(), FrameworkError> { + let Some(server) = self.server.upgrade() else { + return Err(FrameworkError::GraphicsServerUnavailable); + }; + + let buf = self.buffer.borrow(); + let buffer_slice = buf.slice(..data.len() as u64); + let (tx, rx) = std::sync::mpsc::channel(); + + buffer_slice.map_async(wgpu::MapMode::Read, move |result| { + tx.send(result).ok(); + }); + + server + .state + .device + .poll(wgpu::PollType::Wait { + submission_index: None, + timeout: None, + }) + .ok(); + + rx.recv() + .map_err(|_| FrameworkError::Custom("Channel closed".into()))? + .map_err(|e| FrameworkError::Custom(format!("Buffer map failed: {e}")))?; + + let mapped = buffer_slice + .get_mapped_range() + .map_err(|e| FrameworkError::Custom(format!("Failed to get mapped range: {e}")))?; + + data.copy_from_slice(&mapped); + drop(mapped); + buf.unmap(); + + Ok(()) + } +} diff --git a/fyrox-graphics-wgpu/src/format_helpers.rs b/fyrox-graphics-wgpu/src/format_helpers.rs new file mode 100644 index 0000000000..82b48b33c0 --- /dev/null +++ b/fyrox-graphics-wgpu/src/format_helpers.rs @@ -0,0 +1,126 @@ +// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors. +// +// 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. + +//! Shared texture format helpers and binding offset constants. +//! +//! This module centralizes format-related utilities used across the crate to avoid +//! duplication and keep the binding convention in one place. + +/// Offset added to a texture binding slot to obtain the corresponding sampler slot. +/// +/// For a texture bound at slot `N`, its sampler is bound at `N + SAMPLER_BINDING_OFFSET`. +pub const SAMPLER_BINDING_OFFSET: usize = 100; + +/// Offset added to a resource binding slot to obtain the uniform buffer slot. +/// +/// For a property group at binding `N`, its uniform buffer is bound at +/// `N + UNIFORM_BINDING_OFFSET`. +pub const UNIFORM_BINDING_OFFSET: usize = 200; + +/// Returns `true` if the given texture format supports filtering samplers. +/// +/// Non-filterable formats (32-bit float, uint, sint) require +/// [`wgpu::SamplerBindingType::NonFiltering`] when creating bind group layouts. +/// +/// # Examples +/// +/// ```ignore +/// use fyrox_graphics_wgpu::format_helpers::is_filterable_format; +/// +/// assert!(is_filterable_format(wgpu::TextureFormat::Rgba8Unorm)); +/// assert!(!is_filterable_format(wgpu::TextureFormat::R32Float)); +/// ``` +pub fn is_filterable_format(fmt: wgpu::TextureFormat) -> bool { + !matches!( + fmt, + wgpu::TextureFormat::R32Float + | wgpu::TextureFormat::Rg32Float + | wgpu::TextureFormat::Rgba32Float + | wgpu::TextureFormat::R8Uint + | wgpu::TextureFormat::R16Uint + | wgpu::TextureFormat::R32Uint + | wgpu::TextureFormat::R8Sint + | wgpu::TextureFormat::R16Sint + | wgpu::TextureFormat::R32Sint + ) +} + +/// Returns `true` if the given texture format is an integer format. +/// +/// Hardware blending (e.g., alpha blending) relies on floating-point arithmetic. +/// Integer formats (such as `R8Uint`, `R16Sint`, etc.) cannot be blended by the GPU. +/// When configuring a [`wgpu::ColorTargetState`] for these formats, the `blend` +/// field MUST be explicitly set to `None` to prevent `wgpu` validation errors. +/// +/// # Examples +/// +/// ```ignore +/// use fyrox_graphics_wgpu::format_helpers::is_integer_format; +/// +/// assert!(is_integer_format(wgpu::TextureFormat::R8Uint)); +/// assert!(!is_integer_format(wgpu::TextureFormat::Rgba8Unorm)); +/// ``` +pub fn is_integer_format(fmt: wgpu::TextureFormat) -> bool { + use wgpu::TextureFormat as F; + matches!( + fmt, + F::R8Uint | F::R8Sint | F::R16Uint | F::R16Sint | F::R32Uint | F::R32Sint | + F::Rg8Uint | F::Rg8Sint | F::Rg16Uint | F::Rg16Sint | F::Rg32Uint | F::Rg32Sint | + F::Rgba8Uint | F::Rgba8Sint | F::Rgba16Uint | F::Rgba16Sint | F::Rgba32Uint | F::Rgba32Sint + ) +} + +/// Returns the appropriate [`wgpu::TextureSampleType`] for the given texture format. +/// +/// Used when creating bind group layouts from actual texture formats rather than +/// from [`SamplerKind`](fyrox_graphics::gpu_program::SamplerKind) inference. +/// Handles depth, uint, sint, and non-filterable float formats correctly. +pub fn sample_type_for_format(fmt: wgpu::TextureFormat) -> wgpu::TextureSampleType { + use wgpu::TextureFormat as F; + match fmt { + F::Depth16Unorm + | F::Depth24Plus + | F::Depth24PlusStencil8 + | F::Depth32Float + | F::Depth32FloatStencil8 => wgpu::TextureSampleType::Depth, + F::R8Uint + | F::R16Uint + | F::R32Uint + | F::Rg8Uint + | F::Rg16Uint + | F::Rg32Uint + | F::Rgba8Uint + | F::Rgba16Uint + | F::Rgba32Uint => wgpu::TextureSampleType::Uint, + F::R8Sint + | F::R16Sint + | F::R32Sint + | F::Rg8Sint + | F::Rg16Sint + | F::Rg32Sint + | F::Rgba8Sint + | F::Rgba16Sint + | F::Rgba32Sint => wgpu::TextureSampleType::Sint, + F::R32Float | F::Rg32Float | F::Rgba32Float => { + wgpu::TextureSampleType::Float { filterable: false } + } + _ => wgpu::TextureSampleType::Float { filterable: true }, + } +} diff --git a/fyrox-graphics-wgpu/src/framebuffer.rs b/fyrox-graphics-wgpu/src/framebuffer.rs new file mode 100644 index 0000000000..ac57e83470 --- /dev/null +++ b/fyrox-graphics-wgpu/src/framebuffer.rs @@ -0,0 +1,1137 @@ +// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors. +// +// 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. + +use crate::buffer::WgpuBuffer; +use crate::format_helpers::{is_filterable_format, is_integer_format, sample_type_for_format, SAMPLER_BINDING_OFFSET, UNIFORM_BINDING_OFFSET}; +use crate::geometry_buffer::WgpuGeometryBuffer; +use crate::program::WgpuProgram; +use crate::sampler::WgpuSampler; +use crate::server::WgpuGraphicsServer; +use crate::texture::WgpuTexture; +use fyrox_core::log::Log; +use fyrox_graphics::{ + core::{color::Color, math::Rect}, + error::FrameworkError, + framebuffer::{ + Attachment, BufferDataUsage, DrawCallStatistics, GpuFrameBuffer, GpuFrameBufferTrait, + ReadTarget, ResourceBindGroup, ResourceBinding, + }, + geometry_buffer::GpuGeometryBuffer, + gpu_program::GpuProgram, + gpu_texture::{CubeMapFace, GpuTexture, GpuTextureKind}, + BlendMode, CompareFunc, CullFace, DrawParameters, ElementRange, +}; +use std::cell::{Cell, RefCell}; +use std::collections::hash_map::DefaultHasher; +use std::hash::{Hash, Hasher}; +use std::rc::Weak; + +/// Maps a Fyrox [`CompareFunc`] to a wgpu [`CompareFunction`]. +fn compare_func_to_wgpu(f: CompareFunc) -> wgpu::CompareFunction { + match f { + CompareFunc::Never => wgpu::CompareFunction::Never, + CompareFunc::Less => wgpu::CompareFunction::Less, + CompareFunc::Equal => wgpu::CompareFunction::Equal, + CompareFunc::LessOrEqual => wgpu::CompareFunction::LessEqual, + CompareFunc::Greater => wgpu::CompareFunction::Greater, + CompareFunc::NotEqual => wgpu::CompareFunction::NotEqual, + CompareFunc::GreaterOrEqual => wgpu::CompareFunction::GreaterEqual, + CompareFunc::Always => wgpu::CompareFunction::Always, + } +} + +/// Maps a Fyrox [`BlendMode`] to a wgpu [`BlendOperation`]. +fn blend_mode_to_wgpu(m: BlendMode) -> wgpu::BlendOperation { + match m { + BlendMode::Add => wgpu::BlendOperation::Add, + BlendMode::Subtract => wgpu::BlendOperation::Subtract, + BlendMode::ReverseSubtract => wgpu::BlendOperation::ReverseSubtract, + BlendMode::Min => wgpu::BlendOperation::Min, + BlendMode::Max => wgpu::BlendOperation::Max, + } +} + +/// Maps a Fyrox [`BlendFactor`] to a wgpu [`BlendFactor`]. +/// +/// Note: `ConstantColor`/`ConstantAlpha` both map to `Constant` (wgpu has a single +/// constant color set via `set_blend_constant`). +fn blend_factor_to_wgpu(f: fyrox_graphics::BlendFactor) -> wgpu::BlendFactor { + use fyrox_graphics::BlendFactor; + match f { + BlendFactor::Zero => wgpu::BlendFactor::Zero, + BlendFactor::One => wgpu::BlendFactor::One, + BlendFactor::SrcColor => wgpu::BlendFactor::Src, + BlendFactor::OneMinusSrcColor => wgpu::BlendFactor::OneMinusSrc, + BlendFactor::DstColor => wgpu::BlendFactor::Dst, + BlendFactor::OneMinusDstColor => wgpu::BlendFactor::OneMinusDst, + BlendFactor::SrcAlpha => wgpu::BlendFactor::SrcAlpha, + BlendFactor::OneMinusSrcAlpha => wgpu::BlendFactor::OneMinusSrcAlpha, + BlendFactor::DstAlpha => wgpu::BlendFactor::DstAlpha, + BlendFactor::OneMinusDstAlpha => wgpu::BlendFactor::OneMinusDstAlpha, + BlendFactor::ConstantColor | BlendFactor::ConstantAlpha => wgpu::BlendFactor::Constant, + BlendFactor::OneMinusConstantColor | BlendFactor::OneMinusConstantAlpha => { + wgpu::BlendFactor::OneMinusConstant + } + BlendFactor::SrcAlphaSaturate => wgpu::BlendFactor::SrcAlphaSaturated, + BlendFactor::Src1Color => wgpu::BlendFactor::Src, + BlendFactor::OneMinusSrc1Color => wgpu::BlendFactor::OneMinusSrc, + BlendFactor::Src1Alpha => wgpu::BlendFactor::SrcAlpha, + BlendFactor::OneMinusSrc1Alpha => wgpu::BlendFactor::OneMinusSrcAlpha, + } +} + +/// Maps a Fyrox [`StencilAction`] to a wgpu [`StencilOperation`]. +fn stencil_action_to_wgpu(a: fyrox_graphics::StencilAction) -> wgpu::StencilOperation { + match a { + fyrox_graphics::StencilAction::Keep => wgpu::StencilOperation::Keep, + fyrox_graphics::StencilAction::Zero => wgpu::StencilOperation::Zero, + fyrox_graphics::StencilAction::Replace => wgpu::StencilOperation::Replace, + fyrox_graphics::StencilAction::Incr => wgpu::StencilOperation::IncrementClamp, + fyrox_graphics::StencilAction::IncrWrap => wgpu::StencilOperation::IncrementWrap, + fyrox_graphics::StencilAction::Decr => wgpu::StencilOperation::DecrementClamp, + fyrox_graphics::StencilAction::DecrWrap => wgpu::StencilOperation::DecrementWrap, + fyrox_graphics::StencilAction::Invert => wgpu::StencilOperation::Invert, + } +} + +/// Builds a [`StencilFaceState`] from a compare function and stencil operation. +fn stencil_face_state( + compare: CompareFunc, + op: &fyrox_graphics::StencilOp, +) -> wgpu::StencilFaceState { + wgpu::StencilFaceState { + compare: compare_func_to_wgpu(compare), + fail_op: stencil_action_to_wgpu(op.fail), + depth_fail_op: stencil_action_to_wgpu(op.zfail), + pass_op: stencil_action_to_wgpu(op.zpass), + } +} + +/// Returns `true` if the texture format includes a stencil component. +fn format_has_stencil(fmt: wgpu::TextureFormat) -> bool { + matches!( + fmt, + wgpu::TextureFormat::Depth24PlusStencil8 | wgpu::TextureFormat::Depth32FloatStencil8 + ) +} + +/// Maps a [`CubeMapFace`] to its array layer index (0-5). +fn cubemap_face_to_layer(face: CubeMapFace) -> u32 { + match face { + CubeMapFace::PositiveX => 0, + CubeMapFace::NegativeX => 1, + CubeMapFace::PositiveY => 2, + CubeMapFace::NegativeY => 3, + CubeMapFace::PositiveZ => 4, + CubeMapFace::NegativeZ => 5, + } +} + +fn texture_format_for_attachment(tex: &GpuTexture) -> Option { + Some(tex.as_any().downcast_ref::()?.format()) +} + +/// Hashable key for the render pipeline cache. +/// +/// Encodes all state that affects pipeline creation: program identity, color/depth +/// formats, sample count, blend/depth/stencil/cull mode, resource texture formats +/// (which determine the bind group layout), and the number of extra vertex buffer +/// slots. Two draw calls with identical keys can share a pipeline. +#[derive(Hash, PartialEq, Eq, Clone)] +pub struct PipelineKey { + /// Identity of the shader program (pointer-based). + program_ptr: usize, + /// Color attachment formats for the render pass. + color_formats: Vec, + /// Depth-stencil attachment format, if present. + depth_format: Option, + /// MSAA sample count. + sample_count: u32, + /// Whether alpha blending is enabled. + blend: bool, + /// Whether depth testing is enabled. + depth_test: bool, + /// Whether depth writes are enabled. + depth_write: bool, + /// Whether stencil operations are configured. + stencil: bool, + /// Whether the pipeline has at least one color target. + has_color: bool, + /// Cull mode encoded as u8 (0=None, 1=Front, 2=Back). + cull: u8, + /// Number of extra vertex buffer slots (filled with dummy buffer). + extra_vert_count: u8, + /// Polygon fill mode encoded as u8 (0=Point, 1=Line, 2=Fill). + polygon_fill_mode: u8, + /// Resource texture formats that determine the bind group layout. + /// Ensures pipeline is recreated when texture formats change (e.g., R32Float is non-filterable). + texture_resource_sample_types: Vec<(usize, wgpu::TextureSampleType)>, +} + +/// Wgpu implementation of [`GpuFrameBufferTrait`](fyrox_graphics::framebuffer::GpuFrameBufferTrait). +/// +/// Represents a render target with optional depth and color attachments. Supports +/// both offscreen framebuffers and the screen backbuffer. Contains the core draw +/// call logic, pipeline caching, and bind group creation. +/// +/// # Clear Behavior +/// +/// Clearing is deferred: [`clear`](Self::clear) stores the values and sets a flag. +/// The actual `LoadOp::Clear` is applied at the next [`draw`](Self::draw) call. +/// The backbuffer clears once per frame (flag set by `swap_buffers`). +pub struct WgpuFrameBuffer { + server: Weak, + depth_attachment: Option, + color_attachments: Vec, + is_backbuffer: bool, + needs_clear: Cell, + pending_clear_color: RefCell, + pending_clear_depth: RefCell, + pending_clear_stencil: RefCell, + backbuffer_depth_cache: RefCell>, +} + +impl WgpuFrameBuffer { + /// Creates a new offscreen framebuffer with the given depth and color attachments. + pub fn new( + server: &WgpuGraphicsServer, + depth: Option, + colors: Vec, + ) -> Result { + Ok(Self { + server: server.weak_ref(), + depth_attachment: depth, + color_attachments: colors, + is_backbuffer: false, + needs_clear: Cell::new(false), + pending_clear_color: RefCell::new(wgpu::Color::BLACK), + pending_clear_depth: RefCell::new(1.0), + pending_clear_stencil: RefCell::new(0), + backbuffer_depth_cache: RefCell::new(None), + }) + } + + /// Creates a backbuffer framebuffer that renders to the screen surface. + /// + /// The backbuffer acquires a surface texture on the first draw call per frame + /// and presents it via [`swap_buffers`](WgpuGraphicsServer::swap_buffers). + pub fn backbuffer(server: &WgpuGraphicsServer, depth: Option) -> Self { + Self { + server: server.weak_ref(), + depth_attachment: depth, + color_attachments: Default::default(), + is_backbuffer: true, + needs_clear: Cell::new(false), + pending_clear_color: RefCell::new(wgpu::Color::BLACK), + pending_clear_depth: RefCell::new(1.0), + pending_clear_stencil: RefCell::new(0), + backbuffer_depth_cache: RefCell::new(None), + } + } + + fn get_or_create_pipeline( + &self, + server: &WgpuGraphicsServer, + program: &WgpuProgram, + params: &DrawParameters, + all_layouts: &[wgpu::VertexBufferLayout<'static>], + color_formats: &[wgpu::TextureFormat], + df: Option, + pipeline_layout: &wgpu::PipelineLayout, + element_kind: fyrox_graphics::ElementKind, + has_color: bool, + texture_resource_formats: &[(usize, wgpu::TextureFormat)], + ) -> wgpu::RenderPipeline { + let needs_stencil = params.stencil_test.is_some() + || params.stencil_op.zpass != fyrox_graphics::StencilAction::Keep + || params.stencil_op.fail != fyrox_graphics::StencilAction::Keep + || params.stencil_op.zfail != fyrox_graphics::StencilAction::Keep; + let depth_fmt = df.unwrap_or(wgpu::TextureFormat::Depth32Float); + let stencil_supported = format_has_stencil(depth_fmt); + let effective_stencil = needs_stencil && stencil_supported; + + let sample_types: Vec<_> = texture_resource_formats + .iter() + .map(|(loc, fmt)| (*loc, sample_type_for_format(*fmt))) + .collect(); + + let device_features = server.state.device.features(); + let supports_line = device_features.contains(wgpu::Features::POLYGON_MODE_LINE); + let supports_point = device_features.contains(wgpu::Features::POLYGON_MODE_POINT); + + let actual_polygon_mode = match server.polygon_fill_mode() { + fyrox_graphics::PolygonFillMode::Line if supports_line => wgpu::PolygonMode::Line, + fyrox_graphics::PolygonFillMode::Point if supports_point => wgpu::PolygonMode::Point, + _ => wgpu::PolygonMode::Fill, + }; + + let key = PipelineKey { + program_ptr: program as *const WgpuProgram as usize, + color_formats: color_formats.to_vec(), + depth_format: df, + sample_count: server.msaa_sample_count, + blend: params.blend.is_some(), + depth_test: params.depth_test.is_some(), + depth_write: params.depth_write, + stencil: effective_stencil, + has_color, + cull: match params.cull_face { + Some(CullFace::Back) => 2, + Some(CullFace::Front) => 1, + None => 0, + }, + extra_vert_count: all_layouts.len() as u8, + polygon_fill_mode: match actual_polygon_mode { + wgpu::PolygonMode::Point => 0, + wgpu::PolygonMode::Line => 1, + wgpu::PolygonMode::Fill => 2, + }, + texture_resource_sample_types: sample_types, + }; + { + let cache = server.pipeline_cache.borrow(); + if let Some(p) = cache.get(&key) { + return p.clone(); + } + } + + let blend_state = params.blend.as_ref().map(|bp| { + let rgb_op = blend_mode_to_wgpu(bp.equation.rgb); + let alpha_op = blend_mode_to_wgpu(bp.equation.alpha); + let is_minmax_rgb = matches!( + rgb_op, + wgpu::BlendOperation::Min | wgpu::BlendOperation::Max + ); + let is_minmax_alpha = matches!( + alpha_op, + wgpu::BlendOperation::Min | wgpu::BlendOperation::Max + ); + wgpu::BlendState { + color: wgpu::BlendComponent { + src_factor: if is_minmax_rgb { + wgpu::BlendFactor::One + } else { + blend_factor_to_wgpu(bp.func.sfactor) + }, + dst_factor: if is_minmax_rgb { + wgpu::BlendFactor::One + } else { + blend_factor_to_wgpu(bp.func.dfactor) + }, + operation: rgb_op, + }, + alpha: wgpu::BlendComponent { + src_factor: if is_minmax_alpha { + wgpu::BlendFactor::One + } else { + blend_factor_to_wgpu(bp.func.alpha_sfactor) + }, + dst_factor: if is_minmax_alpha { + wgpu::BlendFactor::One + } else { + blend_factor_to_wgpu(bp.func.alpha_dfactor) + }, + operation: alpha_op, + }, + } + }); + + let wgpu_stencil_state = if effective_stencil { + let default_face = stencil_face_state(CompareFunc::Always, ¶ms.stencil_op); + let sf = params + .stencil_test + .as_ref() + .map(|st| stencil_face_state(st.func, ¶ms.stencil_op)) + .unwrap_or(default_face); + let read_mask = params + .stencil_test + .as_ref() + .map(|st| st.mask) + .unwrap_or(0xFFFF_FFFF); + wgpu::StencilState { + front: sf, + back: sf, + read_mask, + write_mask: params.stencil_op.write_mask, + } + } else { + wgpu::StencilState::default() + }; + + let depth_stencil = + if params.depth_test.is_some() || params.depth_write || effective_stencil { + Some(wgpu::DepthStencilState { + format: depth_fmt, + depth_write_enabled: Some(params.depth_write), + depth_compare: Some( + params + .depth_test + .map(compare_func_to_wgpu) + .unwrap_or(wgpu::CompareFunction::Always), + ), + stencil: wgpu_stencil_state, + bias: wgpu::DepthBiasState::default(), + }) + } else { + df.map(|f| wgpu::DepthStencilState { + format: f, + depth_write_enabled: Some(false), + depth_compare: Some(wgpu::CompareFunction::Always), + stencil: wgpu::StencilState::default(), + bias: wgpu::DepthBiasState::default(), + }) + }; + + let cull = match params.cull_face { + Some(CullFace::Back) => Some(wgpu::Face::Back), + Some(CullFace::Front) => Some(wgpu::Face::Front), + None => None, + }; + + let topo = match element_kind { + fyrox_graphics::ElementKind::Triangle => wgpu::PrimitiveTopology::TriangleList, + fyrox_graphics::ElementKind::Line => wgpu::PrimitiveTopology::LineList, + fyrox_graphics::ElementKind::Point => wgpu::PrimitiveTopology::PointList, + }; + + let color_targets: Vec> = color_formats + .iter() + .map(|&format| { + let blend = if is_integer_format(format) { + None + } else { + blend_state.clone() + }; + + Some(wgpu::ColorTargetState { + format, + blend, + write_mask: wgpu::ColorWrites::ALL, + }) + }) + .collect(); + let fragment_state = if has_color { + Some(wgpu::FragmentState { + module: program.fragment_module(), + entry_point: Some("fs_main"), + targets: &color_targets, + compilation_options: Default::default(), + }) + } else { + None + }; + + let optional_layouts: Vec>> = all_layouts + .iter() + .cloned() + .map(Some) + .collect(); + + let pipeline = + server + .state + .device + .create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("RP"), + layout: Some(pipeline_layout), + vertex: wgpu::VertexState { + module: program.vertex_module(), + entry_point: Some("vs_main"), + buffers: &optional_layouts, + compilation_options: Default::default(), + }, + fragment: fragment_state, + primitive: wgpu::PrimitiveState { + topology: topo, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: cull, + polygon_mode: actual_polygon_mode, + unclipped_depth: false, + conservative: false, + }, + depth_stencil, + multisample: wgpu::MultisampleState { + count: server.msaa_sample_count, + mask: !0, + alpha_to_coverage_enabled: false, + }, + multiview_mask: None, + cache: None, + }); + + server + .pipeline_cache + .borrow_mut() + .insert(key, pipeline.clone()); + pipeline + } + + fn do_draw( + &self, + instance_count: u32, + geometry: &GpuGeometryBuffer, + viewport: Rect, + program: &GpuProgram, + params: &DrawParameters, + resources: &[ResourceBindGroup], + element_range: ElementRange, + ) -> Result { + let server = self.server.upgrade().ok_or(FrameworkError::GraphicsServerUnavailable)?; + let geo = geometry.as_any().downcast_ref::().ok_or_else(|| FrameworkError::Custom("Expected WgpuGeometryBuffer".into()))?; + let prog = program.as_any().downcast_ref::().ok_or_else(|| FrameworkError::Custom("Expected WgpuProgram".into()))?; + + let (offset, count) = match element_range { + ElementRange::Full => (0, geo.element_count()), + ElementRange::Specific { offset, count } => (offset, count), + }; + if offset + count > geo.element_count() { return Err(FrameworkError::InvalidElementRange { start: offset, end: offset + count, total: geo.element_count() }); } + if count == 0 { return Ok(DrawCallStatistics { triangles: 0 }); } + + let color_formats: Vec = if self.is_backbuffer { + vec![server.surface_config.read().unwrap().format] + } else { + self.color_attachments.iter().map(|a| texture_format_for_attachment(&a.texture).unwrap_or(wgpu::TextureFormat::Rgba8Unorm)).collect() + }; + let df = if self.is_backbuffer { Some(wgpu::TextureFormat::Depth24PlusStencil8) } else { self.depth_attachment.as_ref().and_then(|a| texture_format_for_attachment(&a.texture)) }; + + let mut texture_formats: Vec<(usize, wgpu::TextureFormat)> = Vec::new(); + for group in resources { + for binding in group.bindings { + if let ResourceBinding::Texture { texture, binding: loc, .. } = binding { + let wt = texture.as_any().downcast_ref::().ok_or_else(|| FrameworkError::Custom("Expected WgpuTexture".into()))?; + texture_formats.push((*loc, wt.format())); + } + } + } + + let (_bind_group_layout, pipeline_layout) = prog.get_or_create_layouts(&texture_formats); + let (all_layouts, extra_vert_count) = build_vertex_layouts(geo); + let has_color = self.is_backbuffer || !self.color_attachments.is_empty(); + + let pipeline = self.get_or_create_pipeline(&server, prog, params, &all_layouts, &color_formats, df, &pipeline_layout, geo.element_kind(), has_color, &texture_formats); + let bind_group = create_bind_group(&server, prog, resources); + + let fb_id = self as *const _ as usize; + let requires_new_pass = { + let pass = server.active_pass.borrow(); + pass.is_none() + || pass.as_ref().unwrap().framebuffer_id != fb_id + || (self.is_backbuffer && server.backbuffer_needs_clear.get()) + || (!self.is_backbuffer && self.needs_clear.get()) + }; + + if requires_new_pass { + server.flush_active_pass(); + + let mut current_width = 0; + let mut current_height = 0; + + let surface_tex = if self.is_backbuffer { + if server.current_frame.borrow().is_none() { + match server.surface.get_current_texture() { + wgpu::CurrentSurfaceTexture::Success(t) | wgpu::CurrentSurfaceTexture::Suboptimal(t) => { + *server.current_frame.borrow_mut() = Some(t); + } + wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Lost | wgpu::CurrentSurfaceTexture::Outdated => { + return Ok(DrawCallStatistics { triangles: 0 }); + } + other => return Err(FrameworkError::Custom(format!("Surface texture error: {other:?}"))) + } + } + let frame = server.current_frame.borrow(); + let frame_ref = frame.as_ref().unwrap(); + current_width = frame_ref.texture.size().width; + current_height = frame_ref.texture.size().height; + Some(frame_ref.texture.create_view(&wgpu::TextureViewDescriptor::default())) + } else { None }; + + let color_views: Vec = if self.is_backbuffer { + vec![surface_tex.unwrap()] + } else { + self.color_attachments.iter().map(|att| { + if let Some(face) = att.cube_map_face() { + let wt = att.texture.as_any().downcast_ref::().unwrap(); + wt.wgpu_texture().create_view(&wgpu::TextureViewDescriptor { + dimension: Some(wgpu::TextureViewDimension::D2), + base_array_layer: cubemap_face_to_layer(face), + array_layer_count: Some(1), + mip_level_count: Some(1), + ..Default::default() + }) + } else { + att.texture.as_any().downcast_ref::().unwrap().wgpu_view().clone() + } + }).collect() + }; + + let depth_view = if self.is_backbuffer { + let mut cache = self.backbuffer_depth_cache.borrow_mut(); + if match cache.as_ref() { + Some((cw, ch, _)) => *cw != current_width || *ch != current_height, + None => true, + } && current_width > 0 && current_height > 0 { + let depth_texture = server.state.device.create_texture(&wgpu::TextureDescriptor { + label: None, + size: wgpu::Extent3d { width: current_width, height: current_height, depth_or_array_layers: 1 }, + mip_level_count: 1, + sample_count: server.msaa_sample_count, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Depth24PlusStencil8, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }); + *cache = Some((current_width, current_height, depth_texture)); + } + cache.as_ref().map(|(_, _, tex)| tex.create_view(&wgpu::TextureViewDescriptor::default())) + } else { + self.depth_attachment.as_ref().map(|a| { + let wt = a.texture.as_any().downcast_ref::().unwrap(); + if let Some(face) = a.cube_map_face() { + wt.wgpu_texture().create_view(&wgpu::TextureViewDescriptor { + dimension: Some(wgpu::TextureViewDimension::D2), + base_array_layer: cubemap_face_to_layer(face), + array_layer_count: Some(1), + mip_level_count: Some(1), + ..Default::default() + }) + } else { wt.wgpu_view().clone() } + }) + }; + + let has_stencil = df.map(format_has_stencil).unwrap_or(false); + let (color_load, depth_load, stencil_load) = if self.is_backbuffer && server.backbuffer_needs_clear.replace(false) { + (wgpu::LoadOp::Clear(wgpu::Color::BLACK), wgpu::LoadOp::Clear(1.0), if has_stencil { Some(wgpu::LoadOp::Clear(0)) } else { None }) + } else if !self.is_backbuffer && self.needs_clear.replace(false) { + (wgpu::LoadOp::Clear(*self.pending_clear_color.borrow()), wgpu::LoadOp::Clear(*self.pending_clear_depth.borrow()), if has_stencil { Some(wgpu::LoadOp::Clear(*self.pending_clear_stencil.borrow())) } else { None }) + } else { + (wgpu::LoadOp::Load, wgpu::LoadOp::Load, if has_stencil { Some(wgpu::LoadOp::Load) } else { None }) + }; + + *server.active_pass.borrow_mut() = Some(crate::server::ActivePass { + framebuffer_id: fb_id, + color_views, + depth_view, + color_load, + depth_load, + stencil_load, + commands: Vec::new(), + }); + } + + let ipe = geo.element_kind().index_per_element(); + + server.active_pass.borrow_mut().as_mut().unwrap().commands.push(crate::server::DrawCommand { + pipeline, + bind_group, + vertex_buffers: geo.vertex_buffers().iter().cloned().collect(), + extra_verts: extra_vert_count, + index_buffer: geo.element_buffer().clone(), + viewport, + stencil_ref: params.stencil_test.as_ref().map(|s| s.ref_value), + scissor_box: params.scissor_box, + start_idx: (offset * ipe) as u32, + end_idx: ((offset + count) * ipe) as u32, + instances: instance_count, + }); + + Ok(DrawCallStatistics { + triangles: count * instance_count as usize, + }) + } +} + +fn copy_attachment_texture( + encoder: &mut wgpu::CommandEncoder, + src: &Attachment, + dst: &Attachment, + src_x: u32, + src_y: u32, + dst_x: u32, + dst_y: u32, + width: u32, + height: u32, +) { + let Some(src_tex) = src.texture.as_any().downcast_ref::() else { + return; + }; + let Some(dst_tex) = dst.texture.as_any().downcast_ref::() else { + return; + }; + encoder.copy_texture_to_texture( + wgpu::TexelCopyTextureInfo { + texture: src_tex.wgpu_texture(), + mip_level: src.level() as u32, + origin: wgpu::Origin3d { + x: src_x, + y: src_y, + z: 0, + }, + aspect: wgpu::TextureAspect::All, + }, + wgpu::TexelCopyTextureInfo { + texture: dst_tex.wgpu_texture(), + mip_level: dst.level() as u32, + origin: wgpu::Origin3d { + x: dst_x, + y: dst_y, + z: 0, + }, + aspect: wgpu::TextureAspect::All, + }, + wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + ); +} + +impl GpuFrameBufferTrait for WgpuFrameBuffer { + fn color_attachments(&self) -> &[Attachment] { + &self.color_attachments + } + fn depth_attachment(&self) -> Option<&Attachment> { + self.depth_attachment.as_ref() + } + fn set_cubemap_face(&self, i: usize, face: CubeMapFace, level: usize) { + if let Some(a) = self.color_attachments.get(i) { + a.set_cube_map_face(Some(face)); + a.set_level(level); + } + } + fn blit_to( + &self, + dest: &GpuFrameBuffer, + src_x0: i32, + src_y0: i32, + src_x1: i32, + src_y1: i32, + dst_x0: i32, + dst_y0: i32, + dst_x1: i32, + dst_y1: i32, + copy_color: bool, + copy_depth: bool, + copy_stencil: bool, + ) { + let Some(server) = self.server.upgrade() else { + return; + }; + let Some(dest) = dest.as_any().downcast_ref::() else { + return; + }; + + let src_w = (src_x1 - src_x0) as u32; + let src_h = (src_y1 - src_y0) as u32; + let dst_w = (dst_x1 - dst_x0) as u32; + let dst_h = (dst_y1 - dst_y0) as u32; + + if src_w != dst_w || src_h != dst_h { + Log::warn("blit_to: scaling not supported in wgpu backend, skipping"); + return; + } + + server.flush_active_pass(); + + let mut encoder = server + .frame_encoder + .borrow_mut() + .take() + .unwrap_or_else(|| { + server + .state + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }) + }); + + if copy_color { + for (src_att, dst_att) in + self.color_attachments.iter().zip(&dest.color_attachments) + { + copy_attachment_texture( + &mut encoder, + src_att, + dst_att, + src_x0 as u32, + src_y0 as u32, + dst_x0 as u32, + dst_y0 as u32, + src_w, + src_h, + ); + } + } + + if copy_depth || copy_stencil { + if let (Some(src_att), Some(dst_att)) = + (&self.depth_attachment, &dest.depth_attachment) + { + copy_attachment_texture( + &mut encoder, + src_att, + dst_att, + src_x0 as u32, + src_y0 as u32, + dst_x0 as u32, + dst_y0 as u32, + src_w, + src_h, + ); + } + } + + *server.frame_encoder.borrow_mut() = Some(encoder); + } + fn read_pixels(&self, read_target: ReadTarget) -> Option> { + let server = self.server.upgrade()?; + server.flush_active_pass(); + // Flush any pending frame encoder so prior draws are submitted before readback. + if let Some(encoder) = server.frame_encoder.borrow_mut().take() { + server.state.queue.submit(std::iter::once(encoder.finish())); + } + let texture = match read_target { + ReadTarget::Depth | ReadTarget::Stencil => &self.depth_attachment.as_ref()?.texture, + ReadTarget::Color(i) => &self.color_attachments.get(i)?.texture, + }; + let wtex = texture.as_any().downcast_ref::()?; + if let GpuTextureKind::Rectangle { width, height } = texture.kind() { + let fmt = wtex.format(); + let bps = fmt.block_copy_size(None).unwrap_or(4) as usize; + let bytes_per_row = (width * bps).max(256); + let padded_total = bytes_per_row * (height - 1) + width * bps; + let buf = server.state.device.create_buffer(&wgpu::BufferDescriptor { + label: Some("ReadPx"), + size: padded_total as u64, + usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + let mut enc = + server + .state + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: None, + }); + enc.copy_texture_to_buffer( + wgpu::TexelCopyTextureInfo { + texture: wtex.wgpu_texture(), + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + wgpu::TexelCopyBufferInfo { + buffer: &buf, + layout: wgpu::TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(bytes_per_row as u32), + rows_per_image: Some(height as u32), + }, + }, + wgpu::Extent3d { + width: width as u32, + height: height as u32, + depth_or_array_layers: 1, + }, + ); + server.state.queue.submit(std::iter::once(enc.finish())); + let slice = buf.slice(..); + let (tx, rx) = std::sync::mpsc::channel(); + slice.map_async(wgpu::MapMode::Read, move |r| { + tx.send(r).ok(); + }); + server + .state + .device + .poll(wgpu::PollType::Wait { + submission_index: None, + timeout: None, + }) + .ok(); + rx.recv().ok()?.ok()?; + let mapped = slice.get_mapped_range().ok()?; + let unpadded_row = width * bps; + let mut result = vec![0u8; unpadded_row * height]; + if bytes_per_row == unpadded_row { + result.copy_from_slice(&mapped); + } else { + for y in 0..height { + let src_off = y * bytes_per_row; + let dst_off = y * unpadded_row; + result[dst_off..dst_off + unpadded_row] + .copy_from_slice(&mapped[src_off..src_off + unpadded_row]); + } + } + drop(mapped); + buf.unmap(); + Some(result) + } else { + None + } + } + fn clear( + &self, + _viewport: Rect, + color: Option, + depth: Option, + stencil: Option, + ) { + if let Some(c) = color { + *self.pending_clear_color.borrow_mut() = wgpu::Color { + r: c.r as f64 / 255.0, + g: c.g as f64 / 255.0, + b: c.b as f64 / 255.0, + a: c.a as f64 / 255.0, + }; + } + if let Some(d) = depth { + *self.pending_clear_depth.borrow_mut() = d; + } + if let Some(s) = stencil { + *self.pending_clear_stencil.borrow_mut() = s as u32; + } + self.needs_clear.set(true); + } + fn draw( + &self, + geometry: &GpuGeometryBuffer, + viewport: Rect, + program: &GpuProgram, + params: &DrawParameters, + resources: &[ResourceBindGroup], + element_range: ElementRange, + ) -> Result { + self.do_draw( + 1, + geometry, + viewport, + program, + params, + resources, + element_range, + ) + } + fn draw_instances( + &self, + instance_count: usize, + geometry: &GpuGeometryBuffer, + viewport: Rect, + program: &GpuProgram, + params: &DrawParameters, + resources: &[ResourceBindGroup], + element_range: ElementRange, + ) -> Result { + self.do_draw( + instance_count as u32, + geometry, + viewport, + program, + params, + resources, + element_range, + ) + } +} + +/// Expected vertex attribute locations that the standard shaders may need but +/// geometry might not provide (e.g. boneWeights, boneIndices, vertexSecondTexCoord). +/// +/// Each entry is a `(location, format, &'static [VertexAttribute])` triple. The +/// attribute arrays are `const` statics so they have `'static` lifetime without +/// needing `Box::leak`, avoiding per-draw-call memory leaks. +const EXTRA_VERTEX_LAYOUTS: &[(u32, &[wgpu::VertexAttribute])] = &[ + ( + 4, + &[wgpu::VertexAttribute { + format: wgpu::VertexFormat::Float32x4, + offset: 0, + shader_location: 4, + }], + ), // boneWeights + ( + 5, + &[wgpu::VertexAttribute { + format: wgpu::VertexFormat::Float32x4, + offset: 0, + shader_location: 5, + }], + ), // boneIndices + ( + 6, + &[wgpu::VertexAttribute { + format: wgpu::VertexFormat::Float32x2, + offset: 0, + shader_location: 6, + }], + ), // vertexSecondTexCoord +]; + +/// Builds the full vertex buffer layout list, adding dummy entries for attributes +/// the shader expects but the geometry doesn't provide. Returns `(layouts, extra_count)`. +/// +/// Extra layouts use `array_stride: 0` and point to a small dummy vertex buffer +/// on the server, so the shader reads valid (zeroed) data for missing attributes. +fn build_vertex_layouts(geo: &WgpuGeometryBuffer) -> (Vec>, u32) { + let geo_layouts = geo.vertex_buffer_layouts(); + + let mut provided_mask = 0u32; + for layout in geo_layouts { + for attr in layout.attributes { + provided_mask |= 1 << attr.shader_location; + } + } + + let mut all: Vec> = + Vec::with_capacity(geo_layouts.len() + EXTRA_VERTEX_LAYOUTS.len()); + + all.extend_from_slice(geo_layouts); + + let mut extra = 0u32; + for &(loc, attrs) in EXTRA_VERTEX_LAYOUTS { + if (provided_mask & (1 << loc)) == 0 { + all.push(wgpu::VertexBufferLayout { + array_stride: 0, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: attrs, + }); + extra += 1; + } + } + + (all, extra) +} + +fn create_bind_group( + server: &WgpuGraphicsServer, + program: &WgpuProgram, + groups: &[ResourceBindGroup], +) -> Option { + let mut entries = Vec::new(); + let mut texture_formats: Vec<(usize, wgpu::TextureFormat)> = Vec::new(); + + // Compute a cache key from all resource pointers and formats + let mut hasher = DefaultHasher::new(); + // Include program identity in the hash + hasher.write_usize(program as *const WgpuProgram as usize); + + for group in groups { + for binding in group.bindings { + match binding { + ResourceBinding::Texture { + texture, + sampler, + binding: loc, + } => { + let wt = texture.as_any().downcast_ref::()?; + let ws = sampler.as_any().downcast_ref::()?; + texture_formats.push((*loc, wt.format())); + // Hash the WgpuTexture and WgpuSampler thin pointers + format + binding + hasher.write_usize(wt as *const WgpuTexture as usize); + hasher.write_usize(ws as *const WgpuSampler as usize); + hasher.write_u32(*loc as u32); + entries.push(wgpu::BindGroupEntry { + binding: *loc as u32, + resource: wgpu::BindingResource::TextureView(wt.wgpu_binding_view()), + }); + let sampler_res = if is_filterable_format(wt.format()) { + wgpu::BindingResource::Sampler(ws.wgpu_sampler()) + } else { + wgpu::BindingResource::Sampler(server.non_filtering_sampler()) + }; + entries.push(wgpu::BindGroupEntry { + binding: (*loc + SAMPLER_BINDING_OFFSET) as u32, + resource: sampler_res, + }); + } + ResourceBinding::Buffer { + buffer, + binding: loc, + data_usage, + } => { + let wb = buffer.as_any().downcast_ref::()?; + // Hash the WgpuBuffer thin pointer + binding + data usage + hasher.write_usize(wb as *const WgpuBuffer as usize); + hasher.write_u32(*loc as u32); + // SAFETY: No write_data call is active at this point (we're building + // bind groups between draw calls), so the buffer reference is stable. + let wb_buf = unsafe { wb.wgpu_buffer_raw() }; + match data_usage { + BufferDataUsage::UseEverything => { + hasher.write_u64(0); + entries.push(wgpu::BindGroupEntry { + binding: (*loc + UNIFORM_BINDING_OFFSET) as u32, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: wb_buf, + offset: 0, + size: None, + }), + }); + } + BufferDataUsage::UseSegment { offset, size } => { + hasher.write_u64(*offset as u64); + hasher.write_u64(*size as u64); + let nonzero_size = std::num::NonZeroU64::new(*size as u64) + .expect("BufferDataUsage::UseSegment size must be non-zero"); + entries.push(wgpu::BindGroupEntry { + binding: (*loc + UNIFORM_BINDING_OFFSET) as u32, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: wb_buf, + offset: *offset as u64, + size: Some(nonzero_size), + }), + }) + } + } + } + } + } + } + if entries.is_empty() { + return None; + } + + let key = hasher.finish(); + + // Check cache first + { + let cache = server.bind_group_cache.borrow(); + if let Some(bg) = cache.get(&key) { + return Some(bg.clone()); + } + } + + let (bgl, _) = program.get_or_create_layouts(&texture_formats); + let bind_group = server + .state + .device + .create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("BG"), + layout: &bgl, + entries: &entries, + }); + + // Store in cache + server + .bind_group_cache + .borrow_mut() + .insert(key, bind_group.clone()); + + Some(bind_group) +} diff --git a/fyrox-graphics-wgpu/src/geometry_buffer.rs b/fyrox-graphics-wgpu/src/geometry_buffer.rs new file mode 100644 index 0000000000..bec8014ccf --- /dev/null +++ b/fyrox-graphics-wgpu/src/geometry_buffer.rs @@ -0,0 +1,241 @@ +// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors. +// +// 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. + +use crate::server::WgpuGraphicsServer; +use fyrox_graphics::{ + core::{array_as_u8_slice, math::TriangleDefinition}, + error::FrameworkError, + geometry_buffer::{ + AttributeKind, ElementsDescriptor, GpuGeometryBufferDescriptor, GpuGeometryBufferTrait, + }, + ElementKind, +}; +use std::cell::{Cell, RefCell}; +use std::rc::Weak; + +/// Maps a Fyrox [`AttributeKind`] + component count + normalization flag to a +/// wgpu [`VertexFormat`]. +fn attribute_format(kind: AttributeKind, cc: usize, normalized: bool) -> wgpu::VertexFormat { + match (kind, cc, normalized) { + (AttributeKind::Float, 1, _) => wgpu::VertexFormat::Float32, + (AttributeKind::Float, 2, _) => wgpu::VertexFormat::Float32x2, + (AttributeKind::Float, 3, _) => wgpu::VertexFormat::Float32x3, + (AttributeKind::Float, 4, _) => wgpu::VertexFormat::Float32x4, + (AttributeKind::UnsignedByte, 4, true) => wgpu::VertexFormat::Unorm8x4, + (AttributeKind::UnsignedByte, 4, false) => wgpu::VertexFormat::Uint8x4, + (AttributeKind::UnsignedShort, 2, true) => wgpu::VertexFormat::Unorm16x2, + (AttributeKind::UnsignedShort, 2, false) => wgpu::VertexFormat::Uint16x2, + (AttributeKind::UnsignedShort, 4, true) => wgpu::VertexFormat::Unorm16x4, + (AttributeKind::UnsignedShort, 4, false) => wgpu::VertexFormat::Uint16x4, + (AttributeKind::UnsignedInt, 1, _) => wgpu::VertexFormat::Uint32, + (AttributeKind::UnsignedInt, 2, _) => wgpu::VertexFormat::Uint32x2, + (AttributeKind::UnsignedInt, 3, _) => wgpu::VertexFormat::Uint32x3, + (AttributeKind::UnsignedInt, 4, _) => wgpu::VertexFormat::Uint32x4, + _ => wgpu::VertexFormat::Float32x4, + } +} + +/// Wgpu implementation of [`GpuGeometryBufferTrait`](fyrox_graphics::geometry_buffer::GpuGeometryBufferTrait). +/// +/// Holds vertex buffers, their layouts, and an index (element) buffer for indexed +/// drawing. Supports triangles, lines, and points as element types. +/// +/// Vertex attribute layouts are leaked once at construction to satisfy wgpu's +/// `'static` lifetime requirement — see the comment in [`new`](Self::new) for details. +pub struct WgpuGeometryBuffer { + _server: Weak, + vertex_buffers: RefCell>, + vertex_buffer_layouts: Vec>, + element_buffer: RefCell, + element_count: Cell, + element_kind: ElementKind, +} + +impl WgpuGeometryBuffer { + /// Creates a new geometry buffer from the given descriptor. + /// + /// Creates vertex buffers with their attribute layouts and an index buffer. + /// Attribute layouts are leaked once to satisfy wgpu's `'static` lifetime + /// requirement for `VertexBufferLayout::attributes`. + pub fn new( + server: &WgpuGraphicsServer, + desc: GpuGeometryBufferDescriptor, + ) -> Result { + let mut vertex_buffers = Vec::new(); + let mut vertex_buffer_layouts = Vec::new(); + + for (i, buf) in desc.buffers.iter().enumerate() { + let data_size = buf.data.bytes.map(|b| b.len()).unwrap_or(0); + let label_str = format!("{}VB{i}", desc.name); + let buffer = server.state.device.create_buffer(&wgpu::BufferDescriptor { + label: if server.named_objects { + Some(&label_str) + } else { + None + }, + size: data_size.max(1) as u64, + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + if let Some(data) = buf.data.bytes { + if !data.is_empty() { + server.state.queue.write_buffer(&buffer, 0, data); + } + } + + let mut attributes = Vec::new(); + let mut offset = 0u64; + for attr in buf.attributes { + attributes.push(wgpu::VertexAttribute { + format: attribute_format(attr.kind, attr.component_count, attr.normalized), + offset, + shader_location: attr.location, + }); + offset += (attr.kind.size() * attr.component_count) as u64; + } + + let step_mode = if buf.attributes.iter().any(|a| a.divisor > 0) { + wgpu::VertexStepMode::Instance + } else { + wgpu::VertexStepMode::Vertex + }; + // NOTE: Attributes are leaked to satisfy wgpu's `'static` lifetime requirement + // for `VertexBufferLayout::attributes`. Each geometry buffer leaks one small + // `Vec` per vertex buffer (typically < 100 bytes). This is + // acceptable because geometry buffers are long-lived (tied to mesh lifetime) + // and relatively few in number. + vertex_buffer_layouts.push(wgpu::VertexBufferLayout { + array_stride: buf.data.element_size as u64, + step_mode, + attributes: attributes.leak(), + }); + vertex_buffers.push(buffer); + } + + let (element_count, element_data) = match desc.elements { + ElementsDescriptor::Triangles(t) => (t.len(), array_as_u8_slice(t)), + ElementsDescriptor::Lines(l) => (l.len(), array_as_u8_slice(l)), + ElementsDescriptor::Points(p) => (p.len(), array_as_u8_slice(p)), + }; + + let ib_label = format!("{}IB", desc.name); + let element_buffer = server.state.device.create_buffer(&wgpu::BufferDescriptor { + label: if server.named_objects { + Some(&ib_label) + } else { + None + }, + size: element_data.len().max(1) as u64, + usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + if !element_data.is_empty() { + server + .state + .queue + .write_buffer(&element_buffer, 0, element_data); + } + + Ok(Self { + _server: server.weak_ref(), + vertex_buffers: RefCell::new(vertex_buffers), + vertex_buffer_layouts, + element_buffer: RefCell::new(element_buffer), + element_count: Cell::new(element_count), + element_kind: desc.elements.element_kind(), + }) + } + + /// Returns the number of elements (triangles, lines, or points) in the index buffer. + pub fn element_count(&self) -> usize { + self.element_count.get() + } + /// Returns the type of elements stored in the index buffer. + pub fn element_kind(&self) -> ElementKind { + self.element_kind + } + /// Returns a borrow of the vertex buffer list. + pub fn vertex_buffers(&self) -> std::cell::Ref<'_, Vec> { + self.vertex_buffers.borrow() + } + /// Returns the vertex buffer layout descriptors for all vertex buffers. + pub fn vertex_buffer_layouts(&self) -> &[wgpu::VertexBufferLayout<'static>] { + &self.vertex_buffer_layouts + } + /// Returns a borrow of the index (element) buffer. + pub fn element_buffer(&self) -> std::cell::Ref<'_, wgpu::Buffer> { + self.element_buffer.borrow() + } + + /// Writes index data to the element buffer, recreating it if the new data is larger + /// than the current buffer capacity. + fn write_element_data(&self, count: usize, data: &[u8]) { + if let Some(server) = self._server.upgrade() { + self.element_count.set(count); + let mut eb = self.element_buffer.borrow_mut(); + if (data.len() as u64) <= eb.size() { + server.state.queue.write_buffer(&eb, 0, data); + } else { + let new_buf = server.state.device.create_buffer(&wgpu::BufferDescriptor { + label: None, + size: data.len() as u64, + usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + server.state.queue.write_buffer(&new_buf, 0, data); + *eb = new_buf; + } + } + } +} + +impl GpuGeometryBufferTrait for WgpuGeometryBuffer { + fn set_buffer_data(&self, buffer_idx: usize, data: &[u8]) { + if let Some(server) = self._server.upgrade() { + let mut bufs = self.vertex_buffers.borrow_mut(); + if let Some(buf) = bufs.get(buffer_idx) { + if (data.len() as u64) <= buf.size() { + server.state.queue.write_buffer(buf, 0, data); + } else { + let new_buf = server.state.device.create_buffer(&wgpu::BufferDescriptor { + label: None, + size: data.len() as u64, + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + server.state.queue.write_buffer(&new_buf, 0, data); + bufs[buffer_idx] = new_buf; + } + } + } + } + fn element_count(&self) -> usize { + self.element_count.get() + } + fn set_triangles(&self, triangles: &[TriangleDefinition]) { + self.write_element_data(triangles.len(), array_as_u8_slice(triangles)); + } + fn set_lines(&self, lines: &[[u32; 2]]) { + self.write_element_data(lines.len(), array_as_u8_slice(lines)); + } + fn set_points(&self, points: &[u32]) { + self.write_element_data(points.len(), array_as_u8_slice(points)); + } +} diff --git a/fyrox-graphics-wgpu/src/lib.rs b/fyrox-graphics-wgpu/src/lib.rs new file mode 100644 index 0000000000..70bb5e00e8 --- /dev/null +++ b/fyrox-graphics-wgpu/src/lib.rs @@ -0,0 +1,90 @@ +// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors. +// +// 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. + +//! Wgpu-based graphics server implementation for the Fyrox game engine. +//! +//! This crate provides a concrete [`GraphicsServer`](fyrox_graphics::server::GraphicsServer) +//! implementation backed by the [wgpu](https://wgpu.rs/) graphics API. It is the primary +//! rendering backend for Fyrox and supports desktop (Vulkan/Metal/DX12) and web (WebGL2/WebGPU) +//! targets. +//! +//! # Architecture +//! +//! The entry point is [`server::WgpuGraphicsServer`], which creates and owns the wgpu +//! device, queue, and surface. All GPU resources (textures, buffers, shaders, etc.) are +//! created through the server and implement the corresponding trait from [`fyrox_graphics`]. +//! +//! ```text +//! WgpuGraphicsServer +//! ├── WgpuTexture (texture.rs) — 1D/2D/3D/Cube textures +//! ├── WgpuBuffer (buffer.rs) — uniform/vertex/index buffers +//! ├── WgpuSampler (sampler.rs) — texture sampling state +//! ├── WgpuGeometryBuffer (geometry_buffer.rs) — mesh vertex + index data +//! ├── WgpuFrameBuffer (framebuffer.rs) — render targets + draw calls +//! ├── WgpuProgram (program.rs) — compiled shader programs +//! ├── WgpuQuery (query.rs) — GPU queries (stub) +//! └── WgpuAsyncReadBuffer(read_buffer.rs) — async pixel readback +//! ``` +//! +//! # Binding Layout +//! +//! All shader resources use a fixed binding scheme within bind group 0: +//! +//! | Resource kind | Binding slot | +//! |---------------------|------------------------| +//! | Texture view | `N` | +//! | Sampler | `N + 100` | +//! | Uniform buffer | `N + 200` | +//! +//! Where `N` is the resource's declared binding index from [`ShaderResourceDefinition`]. +//! +//! # Pipeline Caching +//! +//! Render pipelines are immutable and expensive to create. The backend caches them +//! by a hash of the full render state (program, formats, blend, depth, stencil, cull) +//! in a per-server [`HashMap`](std::collections::HashMap). +//! +//! # Shared Shader Library +//! +//! Every compiled shader includes `shaders/shared.wgsl`, which provides PBR lighting, +//! shadow mapping, color space conversion, and other common utilities. + +#![warn(missing_docs)] + +/// Generic GPU buffer implementation (uniform, vertex, index, pixel read/write). +pub mod buffer; +/// Texture format helpers and binding offset constants. +pub mod format_helpers; +/// Render target (framebuffer) implementation with draw call logic and pipeline caching. +pub mod framebuffer; +/// Geometry buffer implementation (vertex + index buffers for mesh rendering). +pub mod geometry_buffer; +/// Shader compilation and program management. +pub mod program; +/// GPU query stub implementation. +pub mod query; +/// Async pixel readback buffer implementation. +pub mod read_buffer; +/// Texture sampler implementation. +pub mod sampler; +/// The main graphics server — entry point for all GPU resource creation. +pub mod server; +/// Texture creation, upload, and format mapping. +pub mod texture; diff --git a/fyrox-graphics-wgpu/src/program.rs b/fyrox-graphics-wgpu/src/program.rs new file mode 100644 index 0000000000..0c00db017b --- /dev/null +++ b/fyrox-graphics-wgpu/src/program.rs @@ -0,0 +1,600 @@ +// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors. +// +// 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. + +use crate::format_helpers::{ + sample_type_for_format, SAMPLER_BINDING_OFFSET, UNIFORM_BINDING_OFFSET, +}; +use crate::server::WgpuGraphicsServer; +use fyrox_graphics::{ + core::log::{Log, MessageKind}, + error::FrameworkError, + gpu_program::{ + GpuProgramTrait, GpuShaderTrait, SamplerKind, ShaderKind, ShaderPropertyKind, + ShaderResourceDefinition, ShaderResourceKind, + }, +}; +use std::borrow::Cow; +use std::cell::RefCell; +use std::collections::HashMap; +use std::rc::Weak; + +/// Returns the WGSL texture type for a given sampler kind. +fn wgsl_texture_type(kind: SamplerKind) -> &'static str { + match kind { + SamplerKind::Sampler1D => "texture_1d", + SamplerKind::Sampler2D => "texture_2d", + SamplerKind::Sampler3D => "texture_3d", + SamplerKind::SamplerCube => "texture_cube", + SamplerKind::USampler1D => "texture_1d", + SamplerKind::USampler2D => "texture_2d", + SamplerKind::USampler3D => "texture_3d", + SamplerKind::USamplerCube => "texture_cube", + SamplerKind::DepthSampler2D => "texture_depth_2d", + SamplerKind::DepthSamplerCube => "texture_depth_cube", + } +} + +/// Returns the WGSL type string for a shader property kind. +fn wgsl_property_type(kind: &ShaderPropertyKind) -> String { + match kind { + ShaderPropertyKind::Float { .. } => "f32".into(), + ShaderPropertyKind::FloatArray { max_len, .. } => format!("array"), + ShaderPropertyKind::Int { .. } => "i32".into(), + ShaderPropertyKind::IntArray { max_len, .. } => format!("array"), + ShaderPropertyKind::UInt { .. } => "u32".into(), + ShaderPropertyKind::UIntArray { max_len, .. } => format!("array"), + ShaderPropertyKind::Bool { .. } => "u32".into(), + ShaderPropertyKind::Vector2 { .. } => "vec2f".into(), + ShaderPropertyKind::Vector2Array { max_len, .. } => format!("array"), + ShaderPropertyKind::Vector3 { .. } => "vec3f".into(), + ShaderPropertyKind::Vector3Array { max_len, .. } => format!("array"), + ShaderPropertyKind::Vector4 { .. } => "vec4f".into(), + ShaderPropertyKind::Vector4Array { max_len, .. } => format!("array"), + ShaderPropertyKind::Matrix2 { .. } => "mat2x2f".into(), + ShaderPropertyKind::Matrix2Array { max_len, .. } => format!("array"), + ShaderPropertyKind::Matrix3 { .. } => "mat3x3f".into(), + ShaderPropertyKind::Matrix3Array { max_len, .. } => format!("array"), + ShaderPropertyKind::Matrix4 { .. } => "mat4x4f".into(), + ShaderPropertyKind::Matrix4Array { max_len, .. } => format!("array"), + ShaderPropertyKind::Color { .. } => "vec4f".into(), + } +} + +/// Generates WGSL `@group(0) @binding(N)` declarations for textures, samplers, +/// and uniform buffers from the shader resource definitions. +fn generate_wgsl_declarations(resources: &[ShaderResourceDefinition]) -> String { + let mut decls = String::new(); + + for res in resources { + match res.kind { + ShaderResourceKind::Texture { kind, .. } => { + let tex_type = wgsl_texture_type(kind); + decls += &format!( + "@group(0) @binding({}) var {}_tex: {};\n", + res.binding, res.name, tex_type + ); + decls += &format!( + "@group(0) @binding({}) var {}_samp: sampler;\n", + res.binding + SAMPLER_BINDING_OFFSET, + res.name + ); + } + ShaderResourceKind::PropertyGroup(ref fields) => { + if fields.is_empty() { + continue; + } + decls += &format!("struct T{} {{\n", res.name); + for f in fields { + let n = &f.name; + let ty = wgsl_property_type(&f.kind); + decls += &format!(" {n}: {ty},\n"); + } + decls += "}\n"; + decls += &format!( + "@group(0) @binding({}) var {}: T{};\n", + res.binding + UNIFORM_BINDING_OFFSET, + res.name, + res.name + ); + } + } + } + + decls +} + +/// Compiles WGSL source into a wgpu shader module. +fn compile_wgsl( + device: &wgpu::Device, + name: &str, + wgsl: &str, +) -> Result { + let shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some(name), + source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(wgsl)), + }); + Log::writeln( + MessageKind::Information, + format!("Shader {name} compiled successfully!"), + ); + Ok(shader_module) +} + +/// Creates bind group layout using actual texture formats when available. +/// `texture_formats` provides the actual wgpu format for specific bindings. +fn create_bind_group_layout_with_formats( + device: &wgpu::Device, + resources: &[ShaderResourceDefinition], + texture_formats: &[(usize, wgpu::TextureFormat)], +) -> wgpu::BindGroupLayout { + let fmt_map: std::collections::HashMap = + texture_formats.iter().copied().collect(); + let mut entries = Vec::new(); + for res in resources { + match res.kind { + ShaderResourceKind::Texture { kind, .. } => { + let vd = match kind { + SamplerKind::Sampler1D | SamplerKind::USampler1D => { + wgpu::TextureViewDimension::D1 + } + SamplerKind::Sampler2D + | SamplerKind::USampler2D + | SamplerKind::DepthSampler2D => wgpu::TextureViewDimension::D2, + SamplerKind::Sampler3D | SamplerKind::USampler3D => { + wgpu::TextureViewDimension::D3 + } + SamplerKind::SamplerCube + | SamplerKind::USamplerCube + | SamplerKind::DepthSamplerCube => wgpu::TextureViewDimension::Cube, + }; + // Use actual texture format if available, otherwise fall back to kind-based inference + let st = if let Some(&fmt) = fmt_map.get(&res.binding) { + sample_type_for_format(fmt) + } else { + match kind { + SamplerKind::DepthSampler2D | SamplerKind::DepthSamplerCube => { + wgpu::TextureSampleType::Depth + } + SamplerKind::USampler1D + | SamplerKind::USampler2D + | SamplerKind::USampler3D + | SamplerKind::USamplerCube => wgpu::TextureSampleType::Uint, + _ => wgpu::TextureSampleType::Float { filterable: true }, + } + }; + entries.push(wgpu::BindGroupLayoutEntry { + binding: res.binding as u32, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: st, + view_dimension: vd, + multisampled: false, + }, + count: None, + }); + let sampler_binding = if matches!( + st, + wgpu::TextureSampleType::Float { filterable: false } + | wgpu::TextureSampleType::Uint + | wgpu::TextureSampleType::Sint + ) { + wgpu::SamplerBindingType::NonFiltering + } else { + wgpu::SamplerBindingType::Filtering + }; + entries.push(wgpu::BindGroupLayoutEntry { + binding: (res.binding + SAMPLER_BINDING_OFFSET) as u32, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(sampler_binding), + count: None, + }); + } + ShaderResourceKind::PropertyGroup { .. } => { + entries.push(wgpu::BindGroupLayoutEntry { + binding: (res.binding + UNIFORM_BINDING_OFFSET) as u32, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }); + } + } + } + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("ShaderBindGroupLayout"), + entries: &entries, + }) +} + +/// Wgpu implementation of [`GpuShaderTrait`](fyrox_graphics::gpu_program::GpuShaderTrait). +/// +/// Wraps a compiled [`wgpu::ShaderModule`]. Shaders are compiled from WGSL source +/// with automatically generated resource binding declarations prepended. +pub struct WgpuShader { + _server: Weak, + module: wgpu::ShaderModule, + _kind: ShaderKind, +} + +impl GpuShaderTrait for WgpuShader {} + +impl WgpuShader { + /// Compiles a WGSL shader from source with resource binding declarations. + /// + /// The compilation pipeline: + /// 1. Validates resource definitions for duplicate bindings/names + /// 2. Generates `@group(0) @binding(N)` WGSL declarations via [`generate_wgsl_declarations`] + /// 3. Prepends declarations + [`shared.wgsl`](shaders/shared.wgsl) to the user source + /// 4. Compiles the combined WGSL as a [`wgpu::ShaderModule`] + pub fn new( + server: &WgpuGraphicsServer, + name: String, + kind: ShaderKind, + source: String, + resources: &[ShaderResourceDefinition], + _line_offset: isize, + ) -> Result { + for r in resources { + for o in resources { + if std::ptr::eq(r, o) { + continue; + } + if std::mem::discriminant(&r.kind) == std::mem::discriminant(&o.kind) { + if r.binding == o.binding { + return Err(FrameworkError::Custom(format!( + "Resource {} and {} same binding {}", + r.name, o.name, r.binding + ))); + } + if r.name == o.name { + return Err(FrameworkError::Custom(format!( + "Duplicate resource name {}", + r.name + ))); + } + } + } + } + + let declarations = generate_wgsl_declarations(resources); + let shared = include_str!("shaders/shared.wgsl"); + + let mut wgsl = String::new(); + wgsl += &declarations; + wgsl += shared; + wgsl += "\n"; + wgsl += &source; + + let module = compile_wgsl(&server.state.device, &name, &wgsl)?; + Ok(Self { + _server: server.weak_ref(), + module, + _kind: kind, + }) + } + + /// Returns a reference to the compiled [`wgpu::ShaderModule`]. + pub fn wgpu_module(&self) -> &wgpu::ShaderModule { + &self.module + } +} + +/// Wgpu implementation of [`GpuProgramTrait`](fyrox_graphics::gpu_program::GpuProgramTrait). +/// +/// A shader program consisting of a vertex and fragment [`wgpu::ShaderModule`], +/// along with resource definitions and lazily-cached bind group / pipeline layouts. +/// +/// The layout cache is keyed on the actual texture formats passed to +/// [`get_or_create_layouts`](Self::get_or_create_layouts), ensuring correct +/// sample type inference for each unique set of bound textures. +pub struct WgpuProgram { + server: Weak, + name: String, + vertex_module: wgpu::ShaderModule, + fragment_module: wgpu::ShaderModule, + resources: Vec, + cached_layouts: RefCell< + HashMap< + Vec<(usize, wgpu::TextureSampleType)>, + (wgpu::BindGroupLayout, wgpu::PipelineLayout), + >, + >, +} + +impl GpuProgramTrait for WgpuProgram {} + +impl WgpuProgram { + /// Creates a program by compiling vertex and fragment shaders from source. + /// + /// Both shaders share the same resource definitions. The vertex shader is + /// named `{name}_VS` and the fragment shader `{name}_FS`. + pub fn from_source( + server: &WgpuGraphicsServer, + name: &str, + vs: String, + vs_off: isize, + fs: String, + fs_off: isize, + resources: &[ShaderResourceDefinition], + ) -> Result { + let vert = WgpuShader::new( + server, + format!("{name}_VS"), + ShaderKind::Vertex, + vs, + resources, + vs_off, + )?; + let frag = WgpuShader::new( + server, + format!("{name}_FS"), + ShaderKind::Fragment, + fs, + resources, + fs_off, + )?; + Self::from_modules(server, name, &vert, &frag, resources) + } + + /// Creates a program from pre-compiled shaders. + /// + /// The shaders must be [`WgpuShader`] instances (downcast from trait objects). + pub fn from_shaders( + server: &WgpuGraphicsServer, + name: &str, + vs: &fyrox_graphics::gpu_program::GpuShader, + fs: &fyrox_graphics::gpu_program::GpuShader, + resources: &[ShaderResourceDefinition], + ) -> Result { + let vert = vs + .as_any() + .downcast_ref::() + .ok_or_else(|| FrameworkError::Custom("Expected WgpuShader".into()))?; + let frag = fs + .as_any() + .downcast_ref::() + .ok_or_else(|| FrameworkError::Custom("Expected WgpuShader".into()))?; + Self::from_modules(server, name, vert, frag, resources) + } + + fn from_modules( + server: &WgpuGraphicsServer, + name: &str, + vert: &WgpuShader, + frag: &WgpuShader, + resources: &[ShaderResourceDefinition], + ) -> Result { + Ok(Self { + server: server.weak_ref(), + name: name.to_owned(), + vertex_module: vert.module.clone(), + fragment_module: frag.module.clone(), + resources: resources.to_vec(), + cached_layouts: RefCell::new(HashMap::new()), + }) + } + + /// Lazily create bind group layout + pipeline layout based on actual texture formats. + /// `texture_formats` maps resource binding -> actual wgpu texture format for textures. + pub fn get_or_create_layouts( + &self, + texture_formats: &[(usize, wgpu::TextureFormat)], + ) -> (wgpu::BindGroupLayout, wgpu::PipelineLayout) { + let sample_types: Vec<_> = texture_formats + .iter() + .map(|(loc, fmt)| (*loc, sample_type_for_format(*fmt))) + .collect(); + + let mut cache = self.cached_layouts.borrow_mut(); + + if let Some((bgl, pl)) = cache.get(&sample_types) { + return (bgl.clone(), pl.clone()); + } + + let server = self + .server + .upgrade() + .expect("WgpuGraphicsServer dropped before WgpuProgram"); + + let bgl = create_bind_group_layout_with_formats( + &server.state.device, + &self.resources, + texture_formats, + ); + + let pl = server + .state + .device + .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some(&format!("{}_PL", self.name)), + bind_group_layouts: &[Some(&bgl)], + ..Default::default() + }); + + let result = (bgl.clone(), pl.clone()); + + cache.insert(sample_types, result.clone()); + + result + } + + /// Returns a reference to the vertex shader module. + pub fn vertex_module(&self) -> &wgpu::ShaderModule { + &self.vertex_module + } + /// Returns a reference to the fragment shader module. + pub fn fragment_module(&self) -> &wgpu::ShaderModule { + &self.fragment_module + } + /// Returns the resource definitions for this program. + pub fn resources(&self) -> &[ShaderResourceDefinition] { + &self.resources + } + /// Returns the program name (used for debug labels). + pub fn name(&self) -> &str { + &self.name + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_generate_wgsl_declarations_textures() { + let resources = vec![ + ShaderResourceDefinition { + name: "diffuseTexture".into(), + kind: ShaderResourceKind::Texture { + kind: SamplerKind::Sampler2D, + fallback: Default::default(), + }, + binding: 0, + }, + ShaderResourceDefinition { + name: "shadowMap".into(), + kind: ShaderResourceKind::Texture { + kind: SamplerKind::SamplerCube, + fallback: Default::default(), + }, + binding: 1, + }, + ]; + let decls = generate_wgsl_declarations(&resources); + assert!(decls.contains("@group(0) @binding(0) var diffuseTexture_tex: texture_2d;")); + assert!(decls.contains("@group(0) @binding(100) var diffuseTexture_samp: sampler;")); + assert!(decls.contains("@group(0) @binding(1) var shadowMap_tex: texture_cube;")); + assert!(decls.contains("@group(0) @binding(101) var shadowMap_samp: sampler;")); + } + + #[test] + fn test_generate_wgsl_declarations_uniforms() { + use fyrox_graphics::gpu_program::ShaderProperty; + let resources = vec![ShaderResourceDefinition { + name: "properties".into(), + kind: ShaderResourceKind::PropertyGroup(vec![ + ShaderProperty::new("field0", ShaderPropertyKind::Float { value: 0.0 }), + ShaderProperty::new( + "field1", + ShaderPropertyKind::Vector3 { + value: Default::default(), + }, + ), + ShaderProperty::new("field2", ShaderPropertyKind::Bool { value: false }), + ]), + binding: 0, + }]; + let decls = generate_wgsl_declarations(&resources); + assert!(decls.contains("struct Tproperties {")); + assert!(decls.contains("field0: f32,")); + assert!(decls.contains("field1: vec3f,")); + assert!(decls.contains("field2: u32,")); // Bool → u32 + assert!(decls.contains("@group(0) @binding(200) var properties: Tproperties;")); + } + + #[test] + fn test_wgsl_texture_type_mapping() { + assert_eq!(wgsl_texture_type(SamplerKind::Sampler2D), "texture_2d"); + assert_eq!(wgsl_texture_type(SamplerKind::Sampler3D), "texture_3d"); + assert_eq!( + wgsl_texture_type(SamplerKind::SamplerCube), + "texture_cube" + ); + assert_eq!( + wgsl_texture_type(SamplerKind::USampler2D), + "texture_2d" + ); + } + + #[test] + fn test_wgsl_property_type_mapping() { + assert_eq!( + wgsl_property_type(&ShaderPropertyKind::Float { value: 0.0 }), + "f32" + ); + assert_eq!( + wgsl_property_type(&ShaderPropertyKind::Int { value: 0 }), + "i32" + ); + assert_eq!( + wgsl_property_type(&ShaderPropertyKind::UInt { value: 0 }), + "u32" + ); + assert_eq!( + wgsl_property_type(&ShaderPropertyKind::Bool { value: false }), + "u32" + ); + assert_eq!( + wgsl_property_type(&ShaderPropertyKind::Vector2 { + value: Default::default() + }), + "vec2f" + ); + assert_eq!( + wgsl_property_type(&ShaderPropertyKind::Vector3 { + value: Default::default() + }), + "vec3f" + ); + assert_eq!( + wgsl_property_type(&ShaderPropertyKind::Vector4 { + value: Default::default() + }), + "vec4f" + ); + assert_eq!( + wgsl_property_type(&ShaderPropertyKind::Matrix2 { + value: Default::default() + }), + "mat2x2f" + ); + assert_eq!( + wgsl_property_type(&ShaderPropertyKind::Matrix3 { + value: Default::default() + }), + "mat3x3f" + ); + assert_eq!( + wgsl_property_type(&ShaderPropertyKind::Matrix4 { + value: Default::default() + }), + "mat4x4f" + ); + assert_eq!( + wgsl_property_type(&ShaderPropertyKind::Color { + r: 255, + g: 255, + b: 255, + a: 255 + }), + "vec4f" + ); + assert_eq!( + wgsl_property_type(&ShaderPropertyKind::FloatArray { + max_len: 32, + value: vec![] + }), + "array" + ); + } +} diff --git a/fyrox-graphics-wgpu/src/query.rs b/fyrox-graphics-wgpu/src/query.rs new file mode 100644 index 0000000000..24f804ed40 --- /dev/null +++ b/fyrox-graphics-wgpu/src/query.rs @@ -0,0 +1,74 @@ +// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors. +// +// 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. + +use crate::server::WgpuGraphicsServer; +use fyrox_graphics::{ + error::FrameworkError, + query::{GpuQueryTrait, QueryKind, QueryResult}, +}; +use std::cell::Cell; +use std::fmt::Debug; +use std::rc::Weak; + +/// Wgpu implementation of [`GpuQueryTrait`](fyrox_graphics::query::GpuQueryTrait). +/// +/// **Stub implementation**: wgpu does not currently expose occlusion queries, +/// so this type tracks the active state but always returns `u32::MAX` from +/// [`try_get_result`](Self::try_get_result). This is sufficient for the engine's +/// query-based culling to work (everything is considered visible). +pub struct WgpuQuery { + _server: Weak, + active: Cell, +} + +impl Debug for WgpuQuery { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("WgpuQuery") + .field("active", &self.active.get()) + .finish() + } +} + +impl WgpuQuery { + /// Creates a new stub query. + pub fn new(server: &WgpuGraphicsServer) -> Result { + Ok(Self { + _server: server.weak_ref(), + active: Cell::new(false), + }) + } +} + +impl GpuQueryTrait for WgpuQuery { + fn begin(&self, _kind: QueryKind) { + self.active.set(true); + } + fn end(&self) { + self.active.set(false); + } + fn is_started(&self) -> bool { + self.active.get() + } + /// Always returns `Some(QueryResult::SamplesPassed(u32::MAX))` — see the + /// struct-level documentation for details. + fn try_get_result(&self) -> Option { + Some(QueryResult::SamplesPassed(u32::MAX)) + } +} diff --git a/fyrox-graphics-wgpu/src/read_buffer.rs b/fyrox-graphics-wgpu/src/read_buffer.rs new file mode 100644 index 0000000000..d50f2812b5 --- /dev/null +++ b/fyrox-graphics-wgpu/src/read_buffer.rs @@ -0,0 +1,183 @@ +// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors. +// +// 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. + +use crate::server::WgpuGraphicsServer; +use fyrox_graphics::{ + core::math::Rect, error::FrameworkError, framebuffer::GpuFrameBufferTrait, + gpu_texture::GpuTextureKind, read_buffer::GpuAsyncReadBufferTrait, +}; +use std::cell::Cell; +use std::rc::Weak; + +/// Wgpu implementation of [`GpuAsyncReadBufferTrait`](fyrox_graphics::read_buffer::GpuAsyncReadBufferTrait). +/// +/// Provides asynchronous pixel readback from a framebuffer's color attachment to +/// CPU-accessible memory. Internally uses a `MAP_READ | COPY_DST` buffer and +/// `copy_texture_to_buffer` to transfer pixel data. +/// +/// # Usage +/// +/// 1. Create via [`GraphicsServer::create_async_read_buffer`](fyrox_graphics::server::GraphicsServer::create_async_read_buffer) +/// 2. Call [`schedule_pixels_transfer`](Self::schedule_pixels_transfer) to start the transfer +/// 3. Poll [`try_read`](Self::try_read) each frame until the data is ready +pub struct WgpuAsyncReadBuffer { + server: Weak, + buffer: wgpu::Buffer, + _pixel_count: usize, + pixel_size: usize, + request_pending: Cell, + size_bytes: usize, +} + +impl WgpuAsyncReadBuffer { + /// Creates a new async read buffer with enough capacity for `pixel_count` pixels + /// of `pixel_size` bytes each. + pub fn new( + server: &WgpuGraphicsServer, + name: &str, + pixel_size: usize, + pixel_count: usize, + ) -> Result { + let size_bytes = pixel_count * pixel_size; + let buffer = server.state.device.create_buffer(&wgpu::BufferDescriptor { + label: if server.named_objects { + Some(name) + } else { + None + }, + size: size_bytes.max(1) as u64, + usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + Ok(Self { + server: server.weak_ref(), + buffer, + _pixel_count: pixel_count, + pixel_size, + request_pending: Cell::new(false), + size_bytes, + }) + } +} + +impl GpuAsyncReadBufferTrait for WgpuAsyncReadBuffer { + fn schedule_pixels_transfer( + &self, + framebuffer: &dyn GpuFrameBufferTrait, + color_buffer_index: u32, + _rect: Option>, + ) -> Result<(), FrameworkError> { + if self.request_pending.get() { + return Ok(()); + } + let Some(server) = self.server.upgrade() else { + return Err(FrameworkError::GraphicsServerUnavailable); + }; + let color_attachment = framebuffer + .color_attachments() + .get(color_buffer_index as usize) + .ok_or_else(|| FrameworkError::Custom("No color attachment".into()))?; + let wgpu_tex = color_attachment + .texture + .as_any() + .downcast_ref::() + .ok_or_else(|| FrameworkError::Custom("Expected WgpuTexture".into()))?; + let (w, h) = match color_attachment.texture.kind() { + GpuTextureKind::Rectangle { width, height } => (width, height), + _ => return Err(FrameworkError::Custom("Only rectangular textures".into())), + }; + let bpp = self.pixel_size; + + let mut encoder = + server + .state + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: None + }); + encoder.copy_texture_to_buffer( + wgpu::TexelCopyTextureInfo { + texture: wgpu_tex.wgpu_texture(), + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + wgpu::TexelCopyBufferInfo { + buffer: &self.buffer, + layout: wgpu::TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some((w as u32 * bpp as u32).max(256)), + rows_per_image: Some(h as u32), + }, + }, + wgpu::Extent3d { + width: w as u32, + height: h as u32, + depth_or_array_layers: 1, + }, + ); + server.state.queue.submit(std::iter::once(encoder.finish())); + self.request_pending.set(true); + Ok(()) + } + + fn is_request_running(&self) -> bool { + self.request_pending.get() + } + + fn try_read(&self) -> Option> { + if !self.request_pending.get() { + return None; + } + let server = self.server.upgrade()?; + let slice = self.buffer.slice(..self.size_bytes as u64); + let (tx, rx) = std::sync::mpsc::channel(); + slice.map_async(wgpu::MapMode::Read, move |r| { + tx.send(r).ok(); + }); + server + .state + .device + .poll(wgpu::PollType::Wait { + submission_index: None, + timeout: None, + }) + .ok(); + match rx.recv() { + Ok(Ok(())) => { + if let Ok(mapped) = slice.get_mapped_range() { + let mut result = vec![0u8; self.size_bytes]; + result.copy_from_slice(&mapped); + drop(mapped); + self.buffer.unmap(); + self.request_pending.set(false); + Some(result) + } else { + self.request_pending.set(false); + None + } + } + _ => { + self.request_pending.set(false); + None + } + } + } +} diff --git a/fyrox-graphics-wgpu/src/sampler.rs b/fyrox-graphics-wgpu/src/sampler.rs new file mode 100644 index 0000000000..585d595dcc --- /dev/null +++ b/fyrox-graphics-wgpu/src/sampler.rs @@ -0,0 +1,144 @@ +// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors. +// +// 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. + +use crate::server::WgpuGraphicsServer; +use fyrox_graphics::{ + error::FrameworkError, + sampler::{ + GpuSamplerDescriptor, GpuSamplerTrait, MagnificationFilter, MinificationFilter, WrapMode, + }, +}; +use std::fmt::Debug; +use std::rc::Weak; + +/// Maps a Fyrox [`MinificationFilter`] to a wgpu [`FilterMode`]. +/// +/// Nearest-mipmap variants map to `Nearest`; all others map to `Linear`. +fn min_filter_to_wgpu(f: MinificationFilter) -> wgpu::FilterMode { + match f { + MinificationFilter::Nearest + | MinificationFilter::NearestMipMapNearest + | MinificationFilter::NearestMipMapLinear => wgpu::FilterMode::Nearest, + _ => wgpu::FilterMode::Linear, + } +} + +/// Maps a Fyrox [`MagnificationFilter`] to a wgpu [`FilterMode`]. +fn mag_filter_to_wgpu(f: MagnificationFilter) -> wgpu::FilterMode { + match f { + MagnificationFilter::Nearest => wgpu::FilterMode::Nearest, + _ => wgpu::FilterMode::Linear, + } +} + +/// Maps a Fyrox [`MinificationFilter`] to a wgpu [`MipmapFilterMode`]. +/// +/// Only `LinearMipMap*` variants produce linear mipmap filtering. +fn mipmap_filter_to_wgpu(f: MinificationFilter) -> wgpu::MipmapFilterMode { + match f { + MinificationFilter::NearestMipMapLinear | MinificationFilter::LinearMipMapLinear => { + wgpu::MipmapFilterMode::Linear + } + _ => wgpu::MipmapFilterMode::Nearest, + } +} + +/// Maps a Fyrox [`WrapMode`] to a wgpu [`AddressMode`]. +fn wrap_mode_to_wgpu(m: WrapMode) -> wgpu::AddressMode { + match m { + WrapMode::Repeat => wgpu::AddressMode::Repeat, + WrapMode::ClampToEdge => wgpu::AddressMode::ClampToEdge, + WrapMode::ClampToBorder => wgpu::AddressMode::ClampToBorder, + WrapMode::MirroredRepeat | WrapMode::MirrorClampToEdge => wgpu::AddressMode::MirrorRepeat, + } +} + +/// Wgpu implementation of [`GpuSamplerTrait`](fyrox_graphics::sampler::GpuSamplerTrait). +/// +/// Wraps a [`wgpu::Sampler`] configured from a [`GpuSamplerDescriptor`]. When +/// anisotropy is greater than 1, all filters are forced to `Linear` (this is a +/// wgpu/WebGPU requirement). +pub struct WgpuSampler { + _server: Weak, + sampler: wgpu::Sampler, +} + +impl Debug for WgpuSampler { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("WgpuSampler").finish() + } +} +impl GpuSamplerTrait for WgpuSampler {} + +impl WgpuSampler { + /// Creates a new sampler from the given descriptor. + /// + /// If `anisotropy > 1`, all min/mag/mipmap filters are forced to `Linear` + /// regardless of the descriptor settings. + pub fn new( + server: &WgpuGraphicsServer, + desc: GpuSamplerDescriptor, + ) -> Result { + let aniso = desc.anisotropy.clamp(1.0, 16.0) as u16; + let use_linear = aniso > 1; + let sampler = server + .state + .device + .create_sampler(&wgpu::SamplerDescriptor { + label: if server.named_objects { + Some("Sampler") + } else { + None + }, + address_mode_u: wrap_mode_to_wgpu(desc.s_wrap_mode), + address_mode_v: wrap_mode_to_wgpu(desc.t_wrap_mode), + address_mode_w: wrap_mode_to_wgpu(desc.r_wrap_mode), + mag_filter: if use_linear { + wgpu::FilterMode::Linear + } else { + mag_filter_to_wgpu(desc.mag_filter) + }, + min_filter: if use_linear { + wgpu::FilterMode::Linear + } else { + min_filter_to_wgpu(desc.min_filter) + }, + mipmap_filter: if use_linear { + wgpu::MipmapFilterMode::Linear + } else { + mipmap_filter_to_wgpu(desc.min_filter) + }, + lod_min_clamp: desc.min_lod.max(0.0), + lod_max_clamp: desc.max_lod.max(0.0), + anisotropy_clamp: aniso, + compare: None, + border_color: None, + }); + Ok(Self { + _server: server.weak_ref(), + sampler, + }) + } + + /// Returns a reference to the underlying [`wgpu::Sampler`]. + pub fn wgpu_sampler(&self) -> &wgpu::Sampler { + &self.sampler + } +} diff --git a/fyrox-graphics-wgpu/src/server.rs b/fyrox-graphics-wgpu/src/server.rs new file mode 100644 index 0000000000..4ebb7ecffe --- /dev/null +++ b/fyrox-graphics-wgpu/src/server.rs @@ -0,0 +1,870 @@ +// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors. +// +// 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. + +use crate::buffer::WgpuBuffer; +use crate::format_helpers::is_filterable_format; +use crate::framebuffer::{PipelineKey, WgpuFrameBuffer}; +use crate::geometry_buffer::WgpuGeometryBuffer; +use crate::program::{WgpuProgram, WgpuShader}; +use crate::query::WgpuQuery; +use crate::read_buffer::WgpuAsyncReadBuffer; +use crate::sampler::WgpuSampler; +use crate::texture::{texture_size, WgpuTexture}; +use fyrox_core::futures::executor::block_on; +use fyrox_core::log::Log; +use fyrox_core::math::Rect; +use fyrox_graphics::buffer::{GpuBuffer, GpuBufferDescriptor}; +use fyrox_graphics::error::FrameworkError; +use fyrox_graphics::framebuffer::{Attachment, GpuFrameBuffer}; +use fyrox_graphics::geometry_buffer::{GpuGeometryBuffer, GpuGeometryBufferDescriptor}; +use fyrox_graphics::gpu_program::{GpuProgram, GpuShader, ShaderKind, ShaderResourceDefinition}; +use fyrox_graphics::gpu_texture::{GpuTexture, GpuTextureDescriptor}; +use fyrox_graphics::query::GpuQuery; +use fyrox_graphics::read_buffer::GpuAsyncReadBuffer; +use fyrox_graphics::sampler::{GpuSampler, GpuSamplerDescriptor}; +use fyrox_graphics::server::{ + GraphicsServer, ServerCapabilities, ServerMemoryUsage, SharedGraphicsServer, +}; +use fyrox_graphics::stats::PipelineStatistics; +use fyrox_graphics::{PolygonFace, PolygonFillMode, ScissorBox}; +use std::cell::{Cell, OnceCell, RefCell}; +use std::collections::HashMap; +use std::rc::{Rc, Weak}; +use std::sync::{Arc, RwLock}; +use winit::event_loop::ActiveEventLoop; +use winit::window::{Window, WindowAttributes}; + +const MIPMAP_SHADER_SRC: &str = r#" +@vertex +fn vs_main(@builtin(vertex_index) idx: u32) -> @builtin(position) vec4 { + var pos = array, 3>( + vec2(-1.0, -1.0), + vec2( 3.0, -1.0), + vec2(-1.0, 3.0) + ); + return vec4(pos[idx], 0.0, 1.0); +} + +@group(0) @binding(0) var src_tex: texture_2d; +@group(0) @binding(1) var src_sampler: sampler; + +@fragment +fn fs_main(@builtin(position) pos: vec4) -> @location(0) vec4 { + return textureLoad(src_tex, vec2(pos.xy), 0); +} +"#; + +/// A single draw call recorded into an [`ActivePass`]. Captures all GPU state +/// needed to replay the call when the pass is flushed. +pub struct DrawCommand { + /// The compiled render pipeline for this draw. + pub pipeline: wgpu::RenderPipeline, + /// Resource bind group (textures, samplers, uniforms). + pub bind_group: Option, + /// Vertex buffers bound for this draw. + pub vertex_buffers: Vec, + /// Number of extra vertex buffer slots filled with the dummy buffer. + pub extra_verts: u32, + /// Index buffer for indexed drawing. + pub index_buffer: wgpu::Buffer, + /// Viewport rectangle. + pub viewport: Rect, + /// Stencil reference value, if stencil testing is enabled. + pub stencil_ref: Option, + /// Optional scissor clipping rectangle. + pub scissor_box: Option, + /// Start index in the index buffer. + pub start_idx: u32, + /// End index in the index buffer. + pub end_idx: u32, + /// Number of instances to draw. + pub instances: u32, +} + +/// A batched render pass that accumulates draw commands for a single framebuffer. +/// +/// Created when the first draw call targets a framebuffer, and flushed +/// (encoded into a `wgpu::RenderPass`) when the target changes or the frame ends. +pub struct ActivePass { + /// Identity of the target framebuffer (pointer-based). + pub framebuffer_id: usize, + /// Color attachment views for the render pass. + pub color_views: Vec, + /// Depth-stencil attachment view, if present. + pub depth_view: Option, + /// Load operation for color attachments. + pub color_load: wgpu::LoadOp, + /// Load operation for the depth attachment. + pub depth_load: wgpu::LoadOp, + /// Load operation for the stencil attachment, if present. + pub stencil_load: Option>, + /// Accumulated draw commands to replay when the pass is flushed. + pub commands: Vec, +} + +/// Core wgpu objects shared across the server. +/// +/// Holds the wgpu instance, adapter, device, and queue. Shared via [`Arc`] so that +/// GPU commands can be submitted from any resource type. +pub struct WgpuState { + /// The wgpu instance (entry point for creating adapters and surfaces). + pub instance: wgpu::Instance, + /// The selected physical device. + pub adapter: wgpu::Adapter, + /// The logical device for creating GPU resources. + pub device: wgpu::Device, + /// The command queue for submitting GPU work. + pub queue: wgpu::Queue, +} + +/// The main wgpu-based graphics server. +/// +/// Implements [`GraphicsServer`](fyrox_graphics::server::GraphicsServer) and serves +/// as the entry point for all GPU resource creation. Manages the wgpu device, surface, +/// pipeline cache, and memory usage tracking. +/// +/// # Pipeline Cache +/// +/// Render pipelines are cached by a hash of [`PipelineKey`] to avoid recreating +/// identical pipelines across draw calls. The cache is stored in a [`RefCell`] +/// and grows monotonically during the session. +/// +/// # Bind Group Cache +/// +/// Bind groups are cached by a hash of resource pointers and texture formats to avoid +/// recreating identical bind groups across draw calls. This is critical for performance +/// since bind group creation is expensive. +/// +/// # Backbuffer Lifecycle +/// +/// The backbuffer acquires a surface texture on the first draw call per frame. +/// Multiple draw calls to the backbuffer reuse the same texture. The frame is +/// presented via [`swap_buffers`](Self::swap_buffers), which also sets the +/// clear flag for the next frame. +pub struct WgpuGraphicsServer { + /// Core wgpu objects (instance, adapter, device, queue). + pub state: Arc, + /// The rendering surface (window or canvas). + pub surface: wgpu::Surface<'static>, + /// Current surface configuration (format, size, present mode). + pub surface_config: RwLock, + /// Whether to set debug labels on GPU objects. + pub named_objects: bool, + /// MSAA sample count (currently forced to 1). + pub msaa_sample_count: u32, + /// Hash-based cache of render pipelines, keyed by [`PipelineKey`]. + pub pipeline_cache: RefCell>, + /// Hash-based cache of bind groups, keyed by resource pointers and texture formats. + pub bind_group_cache: RefCell>, + weak_self: RefCell>>, + /// Tracked GPU memory usage (buffers + textures). + pub memory_usage: RefCell, + pipeline_statistics: RefCell, + /// Small buffer bound to extra vertex slots when geometry lacks attributes the shader expects. + pub dummy_vertex_buffer: wgpu::Buffer, + /// Non-filtering sampler for textures with non-filterable formats (e.g. R32Float). + non_filtering_sampler: wgpu::Sampler, + /// Holds the acquired surface frame between do_draw and swap_buffers. + pub current_frame: RefCell>, + /// Whether the backbuffer needs clearing at the start of the next frame. + pub backbuffer_needs_clear: Cell, + /// Cached depth-stencil texture for the backbuffer, with its (width, height). + backbuffer_depth_stencil: RefCell>, + /// Per-frame command encoder. Lazily created on first draw, submitted in swap_buffers. + /// Storing it on the server (not per-framebuffer) because multiple framebuffers + /// (G-Buffer, HDR, backbuffer) share the same frame and benefit from a single submit. + pub frame_encoder: RefCell>, + /// Currently accumulating render pass. Draw commands are batched here and + /// flushed when the target changes or the frame ends. + pub active_pass: RefCell>, + /// Sampler used for mipmap generation (linear filtering). + mipmap_sampler: wgpu::Sampler, + /// Bind group layout for the mipmap generation shader. + mipmap_bind_group_layout: wgpu::BindGroupLayout, + /// Cached mipmap render pipelines, keyed by texture format. + mipmap_pipeline_cache: RefCell>, + /// Shared shader module for mipmap generation (created once). + mipmap_shader: OnceCell, + /// Shared pipeline layout for mipmap generation (created once). + mipmap_pipeline_layout: OnceCell, + /// Current polygon fill mode (Fill, Line, or Point). Baked into new pipelines. + polygon_fill_mode: Cell, +} + +impl WgpuGraphicsServer { + /// Creates a new wgpu graphics server with a window and GPU device. + /// + /// Initializes wgpu with the primary backend (Vulkan/Metal/DX12 on native, + /// WebGL2 on WASM). Prefers non-sRGB surface formats to avoid double gamma + /// correction (the engine applies its own in the HDR tone-mapping pass). + /// + /// Returns the created [`Window`] and a shared [`GraphicsServer`] handle. + /// + /// # Arguments + /// + /// * `vsync` — enable vertical sync (`PresentMode::AutoVsync` vs `AutoNoVsync`) + /// * `_msaa_sample_count` — currently ignored (MSAA not yet implemented) + /// * `window_target` — the winit event loop for window/surface creation + /// * `window_attributes` — initial window configuration + /// * `named_objects` — whether to set debug labels on GPU objects + pub fn new( + vsync: bool, + _msaa_sample_count: Option, + window_target: &ActiveEventLoop, + window_attributes: WindowAttributes, + named_objects: bool, + ) -> Result<(Window, SharedGraphicsServer), FrameworkError> { + let window = window_target + .create_window(window_attributes) + .map_err(|e| FrameworkError::Custom(format!("Failed to create window: {e}")))?; + let size = window.inner_size(); + + #[cfg(not(target_arch = "wasm32"))] + let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { + backends: wgpu::Backends::PRIMARY, + ..wgpu::InstanceDescriptor::new_with_display_handle(Box::new( + window_target.owned_display_handle(), + )) + }); + + #[cfg(target_arch = "wasm32")] + let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { + backends: wgpu::Backends::GL, + ..wgpu::InstanceDescriptor::new_without_display_handle() + }); + + #[cfg(not(target_arch = "wasm32"))] + let surface = unsafe { + let target = wgpu::SurfaceTargetUnsafe::from_window(&window) + .map_err(|e| FrameworkError::Custom(format!("Failed to get window handle: {e}")))?; + instance + .create_surface_unsafe(target) + .map_err(|e| FrameworkError::Custom(format!("Failed to create surface: {e}")))? + }; + + #[cfg(target_arch = "wasm32")] + let surface = { + use fyrox_core::wasm_bindgen::JsCast; + use winit::platform::web::WindowExtWebSys; + let canvas = window.canvas().unwrap(); + let web_window = fyrox_core::web_sys::window().unwrap(); + let document = web_window.document().unwrap(); + let body = document.body().unwrap(); + body.append_child(&canvas) + .expect("Append canvas to HTML body"); + instance + .create_surface(wgpu::SurfaceTarget::Canvas(canvas)) + .map_err(|e| FrameworkError::Custom(format!("Failed to create surface: {e}")))? + }; + + let adapter = block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::HighPerformance, + compatible_surface: Some(&surface), + force_fallback_adapter: false, + apply_limit_buckets: false, + })) + .map_err(|e| FrameworkError::Custom(format!("No suitable WGPU adapter found: {e}")))?; + + let adapter_features = adapter.features(); + let mut required_features = wgpu::Features::empty(); + + if adapter_features.contains(wgpu::Features::POLYGON_MODE_LINE) { + required_features |= wgpu::Features::POLYGON_MODE_LINE; + } + + let (device, queue) = block_on(adapter.request_device(&wgpu::DeviceDescriptor { + label: None, + required_features, + required_limits: if cfg!(target_arch = "wasm32") { + wgpu::Limits::downlevel_webgl2_defaults() + } else { + adapter.limits() + }, + memory_hints: wgpu::MemoryHints::Performance, + ..Default::default() + })) + .map_err(|e| FrameworkError::Custom(format!("Failed to request device: {e}")))?; + + let surface_caps = surface.get_capabilities(&adapter); + // Prefer linear (non-sRGB) formats to avoid double gamma correction. + // The engine applies its own gamma correction in the HDR tone-mapping pass. + let surface_format = surface_caps + .formats + .iter() + .copied() + .find(|f| !f.is_srgb()) + .or_else(|| surface_caps.formats.first().copied()) + .ok_or_else(|| FrameworkError::Custom("Surface has no supported formats".into()))?; + + let present_mode = if vsync { + wgpu::PresentMode::AutoVsync + } else { + wgpu::PresentMode::AutoNoVsync + }; + + let surface_config = wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format: surface_format, + color_space: Default::default(), + width: size.width, + height: size.height, + present_mode, + alpha_mode: surface_caps.alpha_modes[0], + view_formats: vec![], + desired_maximum_frame_latency: 2, + }; + surface.configure(&device, &surface_config); + + // TODO: Force `msaa_sample_count` to 1 in the wgpu backend. Full MSAA support requires creating multisampled render targets and resolve targets, which is a larger feature. + let msaa = 1u32; // msaa_sample_count.unwrap_or(1).max(1) as u32; + + let non_filtering_sampler = device.create_sampler(&wgpu::SamplerDescriptor { + label: Some("NonFilteringSampler"), + mag_filter: wgpu::FilterMode::Nearest, + min_filter: wgpu::FilterMode::Nearest, + mipmap_filter: wgpu::MipmapFilterMode::Nearest, + ..Default::default() + }); + + let mipmap_sampler = device.create_sampler(&wgpu::SamplerDescriptor { + label: Some("MipmapSampler"), + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + mipmap_filter: wgpu::MipmapFilterMode::Nearest, + ..Default::default() + }); + + let mipmap_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("MipmapBGL"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + }); + + let dummy_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("DummyVB"), + size: 16, // enough for vec4f + usage: wgpu::BufferUsages::VERTEX, + mapped_at_creation: false, + }); + + let server = Rc::new(Self { + state: Arc::new(WgpuState { + instance, + adapter, + device, + queue, + }), + surface, + surface_config: RwLock::new(surface_config), + named_objects, + msaa_sample_count: msaa, + pipeline_cache: RefCell::new(HashMap::new()), + bind_group_cache: RefCell::new(HashMap::new()), + weak_self: RefCell::new(None), + memory_usage: RefCell::new(ServerMemoryUsage::default()), + pipeline_statistics: RefCell::new(PipelineStatistics::default()), + dummy_vertex_buffer, + non_filtering_sampler, + current_frame: RefCell::new(None), + backbuffer_needs_clear: Cell::new(true), + backbuffer_depth_stencil: RefCell::new(None), + frame_encoder: RefCell::new(None), + active_pass: RefCell::new(None), + mipmap_sampler, + mipmap_bind_group_layout, + mipmap_pipeline_cache: RefCell::new(HashMap::new()), + mipmap_shader: OnceCell::new(), + mipmap_pipeline_layout: OnceCell::new(), + polygon_fill_mode: Cell::new(PolygonFillMode::Fill), + }); + + *server.weak_self.borrow_mut() = Some(Rc::downgrade(&server)); + + Ok((window, server)) + } + + /// Returns a [`Weak`] reference to this server. + /// + /// Used by resource types to avoid reference cycles. Resources store a weak + /// reference and call [`upgrade`](Weak::upgrade) when they need the server. + pub fn weak_ref(&self) -> Weak { + self.weak_self.borrow().clone().unwrap() + } + /// Returns the non-filtering sampler used for textures with non-filterable formats + /// (e.g. `R32Float`, `R32Uint`). These formats require + /// [`SamplerBindingType::NonFiltering`](wgpu::SamplerBindingType::NonFiltering). + pub fn non_filtering_sampler(&self) -> &wgpu::Sampler { + &self.non_filtering_sampler + } + /// Returns the current polygon fill mode. Baked into new render pipelines. + pub fn polygon_fill_mode(&self) -> PolygonFillMode { + self.polygon_fill_mode.get() + } + + /// Encodes the current [`ActivePass`] into the frame command encoder and clears it. + /// + /// All buffered draw commands are replayed into a `wgpu::RenderPass`, which is + /// then dropped (ending the pass). The encoder is stored back for subsequent + /// operations. Does nothing if there is no active pass. + pub fn flush_active_pass(&self) { + let Some(pass) = self.active_pass.borrow_mut().take() else { return; }; + + let mut encoder = self.frame_encoder.borrow_mut().take().unwrap_or_else(|| { + self.state.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }) + }); + + let color_attachments: Vec<_> = pass.color_views.iter().map(|view| { + Some(wgpu::RenderPassColorAttachment { + view, + resolve_target: None, + depth_slice: None, + ops: wgpu::Operations { load: pass.color_load, store: wgpu::StoreOp::Store }, + }) + }).collect(); + + let mut rp = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: None, + color_attachments: &color_attachments, + depth_stencil_attachment: pass.depth_view.as_ref().map(|v| { + wgpu::RenderPassDepthStencilAttachment { + view: v, + depth_ops: Some(wgpu::Operations { load: pass.depth_load, store: wgpu::StoreOp::Store }), + stencil_ops: pass.stencil_load.map(|load| wgpu::Operations { + load, + store: wgpu::StoreOp::Store, + }), + } + }), + ..Default::default() + }); + + for cmd in pass.commands { + rp.set_viewport(cmd.viewport.x() as f32, cmd.viewport.y() as f32, cmd.viewport.w() as f32, cmd.viewport.h() as f32, 0.0, 1.0); + rp.set_pipeline(&cmd.pipeline); + if let Some(bg) = &cmd.bind_group { + rp.set_bind_group(0, bg, &[]); + } + if let Some(st) = cmd.stencil_ref { + rp.set_stencil_reference(st); + } + match cmd.scissor_box { + Some(sb) => { + // The ScissorBox Y is computed for OpenGL (origin at bottom-left): + // y_gl = viewport_h - (pos_y + size_h) + // wgpu uses top-left origin (same as UI coords), so convert: + // y_wgpu = viewport_h - y_gl - height = pos_y + let rt_h = cmd.viewport.h(); + let wgpu_y = (rt_h - sb.y - sb.height).max(0); + rp.set_scissor_rect( + sb.x.max(0) as u32, + wgpu_y as u32, + sb.width.max(0) as u32, + sb.height.max(0) as u32, + ); + } + None => rp.set_scissor_rect( + cmd.viewport.x().max(0) as u32, + cmd.viewport.y().max(0) as u32, + cmd.viewport.w().max(0) as u32, + cmd.viewport.h().max(0) as u32, + ), + } + for (i, vb) in cmd.vertex_buffers.iter().enumerate() { + rp.set_vertex_buffer(i as u32, vb.slice(..)); + } + let geo_buf_count = cmd.vertex_buffers.len() as u32; + for i in 0..cmd.extra_verts { + rp.set_vertex_buffer(geo_buf_count + i, self.dummy_vertex_buffer.slice(..)); + } + rp.set_index_buffer(cmd.index_buffer.slice(..), wgpu::IndexFormat::Uint32); + rp.draw_indexed(cmd.start_idx..cmd.end_idx, 0, 0..cmd.instances); + } + + drop(rp); + *self.frame_encoder.borrow_mut() = Some(encoder); + } +} + +impl GraphicsServer for WgpuGraphicsServer { + fn create_buffer(&self, desc: GpuBufferDescriptor) -> Result { + Ok(GpuBuffer(Rc::new(WgpuBuffer::new(self, desc)?))) + } + fn create_texture(&self, desc: GpuTextureDescriptor) -> Result { + Ok(GpuTexture(Rc::new(WgpuTexture::new(self, desc)?))) + } + fn create_sampler(&self, desc: GpuSamplerDescriptor) -> Result { + Ok(GpuSampler(Rc::new(WgpuSampler::new(self, desc)?))) + } + fn create_frame_buffer( + &self, + depth: Option, + colors: Vec, + ) -> Result { + Ok(GpuFrameBuffer(Rc::new(WgpuFrameBuffer::new( + self, depth, colors, + )?))) + } + fn back_buffer(&self) -> GpuFrameBuffer { + let config = self.surface_config.read().unwrap(); + let (w, h) = (config.width, config.height); + drop(config); + + let mut cache = self.backbuffer_depth_stencil.borrow_mut(); + let needs_recreate = match cache.as_ref() { + Some((cw, ch, _)) => *cw != w || *ch != h, + None => true, + }; + if needs_recreate { + if w > 0 && h > 0 { + match self.create_2d_render_target( + "BackbufferDepthStencil", + fyrox_graphics::gpu_texture::PixelKind::D24S8, + w as usize, + h as usize, + ) { + Ok(tex) => { + *cache = Some((w, h, tex)); + } + Err(e) => { + Log::warn(format!("Failed to create backbuffer depth-stencil: {e}")); + *cache = None; + } + } + } else { + *cache = None; + } + } + let depth_attachment = cache + .as_ref() + .map(|(_, _, tex)| Attachment::depth_stencil(tex.clone())); + GpuFrameBuffer(Rc::new(WgpuFrameBuffer::backbuffer(self, depth_attachment))) + } + fn create_query(&self) -> Result { + Ok(GpuQuery(Rc::new(WgpuQuery::new(self)?))) + } + fn create_shader( + &self, + name: String, + kind: ShaderKind, + source: String, + resources: &[ShaderResourceDefinition], + line_offset: isize, + ) -> Result { + Ok(GpuShader(Rc::new(WgpuShader::new( + self, + name, + kind, + source, + resources, + line_offset, + )?))) + } + fn create_program( + &self, + name: &str, + vs: String, + vs_offset: isize, + fs: String, + fs_offset: isize, + resources: &[ShaderResourceDefinition], + ) -> Result { + Ok(GpuProgram(Rc::new(WgpuProgram::from_source( + self, name, vs, vs_offset, fs, fs_offset, resources, + )?))) + } + fn create_program_from_shaders( + &self, + name: &str, + vs: &GpuShader, + fs: &GpuShader, + resources: &[ShaderResourceDefinition], + ) -> Result { + Ok(GpuProgram(Rc::new(WgpuProgram::from_shaders( + self, name, vs, fs, resources, + )?))) + } + fn create_async_read_buffer( + &self, + name: &str, + pixel_size: usize, + pixel_count: usize, + ) -> Result { + Ok(GpuAsyncReadBuffer(Rc::new(WgpuAsyncReadBuffer::new( + self, + name, + pixel_size, + pixel_count, + )?))) + } + fn create_geometry_buffer( + &self, + desc: GpuGeometryBufferDescriptor, + ) -> Result { + Ok(GpuGeometryBuffer(Rc::new(WgpuGeometryBuffer::new( + self, desc, + )?))) + } + fn weak(&self) -> Weak { + self.weak_ref() as Weak + } + fn flush(&self) { + // flush in Fyrox means "send the accumulated commands to the video card right now." + // An empty submit is not needed here, just close and send the encoder, if there is one. + self.flush_active_pass(); + if let Some(encoder) = self.frame_encoder.borrow_mut().take() { + self.state.queue.submit(std::iter::once(encoder.finish())); + } + } + fn finish(&self) { + self.state + .device + .poll(wgpu::PollType::Wait { + submission_index: None, + timeout: None, + }) + .ok(); + } + fn invalidate_resource_bindings_cache(&self) { + *self.pipeline_statistics.borrow_mut() = Default::default(); + } + fn pipeline_statistics(&self) -> PipelineStatistics { + *self.pipeline_statistics.borrow() + } + fn swap_buffers(&self) -> Result<(), FrameworkError> { + // Submit all batched draw commands from this frame + self.flush_active_pass(); + if let Some(encoder) = self.frame_encoder.borrow_mut().take() { + self.state.queue.submit(std::iter::once(encoder.finish())); + } + + if let Some(frame) = self.current_frame.borrow_mut().take() { + self.state.queue.present(frame); + } + + self.backbuffer_needs_clear.replace(true); + Ok(()) + } + fn set_frame_size(&self, new_size: (u32, u32)) { + if new_size.0 > 0 && new_size.1 > 0 { + let mut config = self.surface_config.write().unwrap(); + + if config.width == new_size.0 && config.height == new_size.1 { + return; + } + + config.width = new_size.0; + config.height = new_size.1; + + self.flush_active_pass(); + if let Some(encoder) = self.frame_encoder.borrow_mut().take() { + self.state.queue.submit(std::iter::once(encoder.finish())); + } + + self.current_frame.borrow_mut().take(); + + self.surface.configure(&self.state.device, &config); + } + } + fn capabilities(&self) -> ServerCapabilities { + let limits = self.state.device.limits(); + ServerCapabilities { + max_uniform_buffer_binding_size: limits.max_uniform_buffer_binding_size as usize, + uniform_buffer_offset_alignment: limits.min_uniform_buffer_offset_alignment as usize, + max_lod_bias: 16.0, + } + } + fn set_polygon_fill_mode(&self, _face: PolygonFace, mode: PolygonFillMode) { + self.polygon_fill_mode.set(mode); + } + fn generate_mipmap(&self, texture: &GpuTexture) { + let Some(wtex) = texture.as_any().downcast_ref::() else { + return; + }; + let format = wtex.format(); + if !is_filterable_format(format) { + Log::warn("generate_mipmap: format is not filterable, skipping"); + return; + } + + let (width, height, _depth) = texture_size(texture.kind()); + let mip_count = wtex.wgpu_texture().mip_level_count(); + if mip_count <= 1 || width <= 1 || height <= 1 { + return; + } + + self.flush_active_pass(); + + let shader = self.mipmap_shader.get_or_init(|| { + self.state + .device + .create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("MipmapShader"), + source: wgpu::ShaderSource::Wgsl(MIPMAP_SHADER_SRC.into()), + }) + }); + let layout = self.mipmap_pipeline_layout.get_or_init(|| { + self.state + .device + .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("MipmapPL"), + bind_group_layouts: &[Some(&self.mipmap_bind_group_layout)], + ..Default::default() + }) + }); + + if !self.mipmap_pipeline_cache.borrow().contains_key(&format) { + let pipeline = + self.state + .device + .create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("MipmapPipeline"), + layout: Some(layout), + vertex: wgpu::VertexState { + module: shader, + entry_point: Some("vs_main"), + buffers: &[], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: shader, + entry_point: Some("fs_main"), + targets: &[Some(wgpu::ColorTargetState { + format, + blend: None, + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + ..Default::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + multiview_mask: None, + cache: None, + }); + self.mipmap_pipeline_cache + .borrow_mut() + .insert(format, pipeline); + } + + let pipeline_cache = self.mipmap_pipeline_cache.borrow(); + let pipeline = pipeline_cache.get(&format).unwrap(); + + let mut encoder = self + .frame_encoder + .borrow_mut() + .take() + .unwrap_or_else(|| { + self.state + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }) + }); + + let mut mip_w = width; + let mut mip_h = height; + + for level in 1..mip_count { + mip_w = (mip_w / 2).max(1); + mip_h = (mip_h / 2).max(1); + + let src_view = + wtex.wgpu_texture().create_view(&wgpu::TextureViewDescriptor { + dimension: Some(wgpu::TextureViewDimension::D2), + base_mip_level: level - 1, + mip_level_count: Some(1), + ..Default::default() + }); + + let dst_view = + wtex.wgpu_texture().create_view(&wgpu::TextureViewDescriptor { + dimension: Some(wgpu::TextureViewDimension::D2), + base_mip_level: level, + mip_level_count: Some(1), + ..Default::default() + }); + + let bind_group = self.state.device.create_bind_group(&wgpu::BindGroupDescriptor { + label: None, + layout: &self.mipmap_bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&src_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&self.mipmap_sampler), + }, + ], + }); + + let mut rp = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: None, + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &dst_view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), + store: wgpu::StoreOp::Store, + }, + depth_slice: None, + })], + depth_stencil_attachment: None, + ..Default::default() + }); + + rp.set_viewport(0.0, 0.0, mip_w as f32, mip_h as f32, 0.0, 1.0); + rp.set_pipeline(&pipeline); + rp.set_bind_group(0, &bind_group, &[]); + rp.draw(0..3, 0..1); + drop(rp); + } + + *self.frame_encoder.borrow_mut() = Some(encoder); + } + fn memory_usage(&self) -> ServerMemoryUsage { + self.memory_usage.borrow().clone() + } + fn push_debug_group(&self, _name: &str) {} + fn pop_debug_group(&self) {} +} diff --git a/fyrox-graphics-wgpu/src/shaders/shared.wgsl b/fyrox-graphics-wgpu/src/shaders/shared.wgsl new file mode 100644 index 0000000000..0e2f80e995 --- /dev/null +++ b/fyrox-graphics-wgpu/src/shaders/shared.wgsl @@ -0,0 +1,454 @@ +// Shared functions for all shaders in the engine. +// WGSL version for wgpu backend. + +const PI: f32 = 3.14159; + +fn inverse_mat4(m: mat4x4f) -> mat4x4f { + let m00 = m[0][0]; let m01 = m[0][1]; let m02 = m[0][2]; let m03 = m[0][3]; + let m10 = m[1][0]; let m11 = m[1][1]; let m12 = m[1][2]; let m13 = m[1][3]; + let m20 = m[2][0]; let m21 = m[2][1]; let m22 = m[2][2]; let m23 = m[2][3]; + let m30 = m[3][0]; let m31 = m[3][1]; let m32 = m[3][2]; let m33 = m[3][3]; + + let b00 = m00 * m11 - m01 * m10; + let b01 = m00 * m12 - m02 * m10; + let b02 = m00 * m13 - m03 * m10; + let b03 = m01 * m12 - m02 * m11; + let b04 = m01 * m13 - m03 * m11; + let b05 = m02 * m13 - m03 * m12; + let b06 = m20 * m31 - m21 * m30; + let b07 = m20 * m32 - m22 * m30; + let b08 = m20 * m33 - m23 * m30; + let b09 = m21 * m32 - m22 * m31; + let b10 = m21 * m33 - m23 * m31; + let b11 = m22 * m33 - m23 * m32; + + var inv = mat4x4f(); + inv[0][0] = m11 * b11 - m12 * b10 + m13 * b09; + inv[0][1] = m02 * b10 - m01 * b11 - m03 * b09; + inv[0][2] = m31 * b05 - m32 * b04 + m33 * b03; + inv[0][3] = m22 * b04 - m21 * b05 - m23 * b03; + inv[1][0] = m12 * b08 - m10 * b11 - m13 * b07; + inv[1][1] = m00 * b11 - m02 * b08 + m03 * b07; + inv[1][2] = m32 * b02 - m30 * b05 - m33 * b01; + inv[1][3] = m20 * b05 - m22 * b02 + m23 * b01; + inv[2][0] = m10 * b10 - m11 * b08 + m13 * b06; + inv[2][1] = m01 * b08 - m00 * b10 - m03 * b06; + inv[2][2] = m30 * b04 - m31 * b02 + m33 * b00; + inv[2][3] = m21 * b02 - m20 * b04 - m23 * b00; + inv[3][0] = m11 * b07 - m10 * b09 - m12 * b06; + inv[3][1] = m00 * b09 - m01 * b07 + m02 * b06; + inv[3][2] = m31 * b01 - m30 * b03 - m32 * b00; + inv[3][3] = m20 * b03 - m21 * b01 + m22 * b00; + + let det = m00 * b00 - m01 * b01 + m02 * b02 - m03 * b03; + let invDet = 1.0 / det; + return inv * invDet; +} + +fn S_SolveQuadraticEq(a: f32, b: f32, c: f32, minT: ptr, maxT: ptr) -> bool { + let twoA = 2.0 * a; + let det = b * b - 2.0 * twoA * c; + if (det < 0.0) { *minT = 0.0; *maxT = 0.0; return false; } + let sqrtDet = sqrt(det); + let root1 = (-b - sqrtDet) / twoA; + let root2 = (-b + sqrtDet) / twoA; + *minT = min(root1, root2); + *maxT = max(root1, root2); + return true; +} + +fn S_LightDistanceAttenuation(distance: f32, radius: f32) -> f32 { + return clamp(1.0 - distance * distance / (radius * radius), 0.0, 1.0); +} + +fn S_Project(worldPosition: vec3f, matrix: mat4x4f) -> vec3f { + let screenPos = matrix * vec4f(worldPosition, 1.0); + return (screenPos.xyz / screenPos.w) * 0.5 + 0.5; +} + +fn S_UnProject(screenPos: vec3f, matrix: mat4x4f) -> vec3f { + let clipSpacePos = vec4f(screenPos * 2.0 - 1.0, 1.0); + let position = matrix * clipSpacePos; + return position.xyz / position.w; +} + +fn S_DistributionGGX(N: vec3f, H: vec3f, roughness: f32) -> f32 { + let a = roughness * roughness; + let a2 = a * a; + let NdotH = max(dot(N, H), 0.0); + let NdotH2 = NdotH * NdotH; + let nom = a2; + var denom = (NdotH2 * (a2 - 1.0) + 1.0); + denom = PI * denom * denom; + return nom / denom; +} + +fn S_GeometrySchlickGGX(NdotV: f32, roughness: f32) -> f32 { + let r = (roughness + 1.0); + let k = (r * r) / 8.0; + let nom = NdotV; + let denom = NdotV * (1.0 - k) + k; + return nom / denom; +} + +fn S_GeometrySmith(N: vec3f, V: vec3f, L: vec3f, roughness: f32) -> f32 { + let NdotV = max(dot(N, V), 0.0); + let NdotL = max(dot(N, L), 0.0); + let ggx2 = S_GeometrySchlickGGX(NdotV, roughness); + let ggx1 = S_GeometrySchlickGGX(NdotL, roughness); + return ggx1 * ggx2; +} + +fn S_FresnelSchlick(cosTheta: f32, F0: vec3f) -> vec3f { + return F0 + (1.0 - F0) * pow(max(1.0 - cosTheta, 0.0), 5.0); +} + +fn S_FresnelSchlickRoughness(cosTheta: f32, F0: vec3f, roughness: f32) -> vec3f { + return F0 + (max(vec3f(1.0 - roughness), F0) - F0) * pow(1.0 - cosTheta, 5.0); +} + +struct TPBRContext { + lightColor: vec3f, + viewVector: vec3f, + fragmentToLight: vec3f, + fragmentNormal: vec3f, + metallic: f32, + roughness: f32, + albedo: vec3f, +}; + +fn S_PBR_CalculateLight(ctx: TPBRContext) -> vec3f { + let F0 = mix(vec3f(0.04), ctx.albedo, ctx.metallic); + let L = ctx.fragmentToLight; + let H = normalize(ctx.viewVector + L); + let NDF = S_DistributionGGX(ctx.fragmentNormal, H, ctx.roughness); + let G = S_GeometrySmith(ctx.fragmentNormal, ctx.viewVector, L, ctx.roughness); + let F = S_FresnelSchlick(max(dot(H, ctx.viewVector), 0.0), F0); + let numerator = NDF * G * F; + let denominator = 4.0 * max(dot(ctx.fragmentNormal, ctx.viewVector), 0.0) * max(dot(ctx.fragmentNormal, L), 0.0) + 0.001; + let specular = numerator / denominator; + let kS = F; + var kD = vec3f(1.0) - kS; + kD *= 1.0 - ctx.metallic; + let NdotL = max(dot(ctx.fragmentNormal, L), 0.0); + return (kD * ctx.albedo / PI + specular) * ctx.lightColor * NdotL; +} + +fn S_InScatter(start: vec3f, dir: vec3f, lightPos: vec3f, d: f32) -> f32 { + let q = start - lightPos; + let b = dot(dir, q); + let c = dot(q, q); + let s = 1.0 / sqrt(c - b * b); + let l = s * (atan((d + b) * s) - atan(b * s)); + return l; +} + +fn S_RayleighScatter(start: vec3f, dir: vec3f, lightPos: vec3f, d: f32) -> vec3f { + let scatter = S_InScatter(start, dir, lightPos, d); + return vec3f(0.55, 0.75, 1.0) * scatter; +} + +fn S_RaySphereIntersection(origin: vec3f, dir: vec3f, center: vec3f, radius: f32, minT: ptr, maxT: ptr) -> bool { + let d = origin - center; + let a = dot(dir, dir); + let b = 2.0 * dot(dir, d); + let c = dot(d, d) - radius * radius; + return S_SolveQuadraticEq(a, b, c, minT, maxT); +} + +fn S_PointShadow( + shadowsEnabled: bool, + softShadows: bool, + fragmentDistance: f32, + shadowBias: f32, + toLight: vec3f, + shadowMap_tex: texture_cube, + shadowMap_samp: sampler) -> f32 +{ + if (shadowsEnabled) { + let biasedFragmentDistance = fragmentDistance - shadowBias; + + if (softShadows) { + const directions = array( + vec3f(1, 1, 1), vec3f(1, -1, 1), vec3f(-1, -1, 1), vec3f(-1, 1, 1), + vec3f(1, 1, -1), vec3f(1, -1, -1), vec3f(-1, -1, -1), vec3f(-1, 1, -1), + vec3f(1, 1, 0), vec3f(1, -1, 0), vec3f(-1, -1, 0), vec3f(-1, 1, 0), + vec3f(1, 0, 1), vec3f(-1, 0, 1), vec3f(1, 0, -1), vec3f(-1, 0, -1), + vec3f(0, 1, 1), vec3f(0, -1, 1), vec3f(0, -1, -1), vec3f(0, 1, -1) + ); + + const diskRadius = 0.0025; + + var accumulator = 0.0; + + for (var i = 0; i < 20; i++) { + let fetchDirection = -toLight + directions[i] * diskRadius; + let shadowDistanceToLight = textureSample(shadowMap_tex, shadowMap_samp, fetchDirection).r; + if (biasedFragmentDistance > shadowDistanceToLight) { + accumulator += 1.0; + } + } + + return clamp(1.0 - accumulator / 20.0, 0.0, 1.0); + } else { + let shadowDistanceToLight = textureSample(shadowMap_tex, shadowMap_samp, -toLight).r; + return select(1.0, 0.0, biasedFragmentDistance > shadowDistanceToLight); + } + } else { + return 1.0; + } +} + +fn S_SpotShadowFactor( + shadowsEnabled: bool, + softShadows: bool, + shadowBias: f32, + fragmentPosition: vec3f, + lightViewProjMatrix: mat4x4f, + shadowMapInvSize: f32, + spotShadowTexture_tex: texture_2d, + spotShadowTexture_samp: sampler) -> f32 +{ + if (shadowsEnabled) { + let lightSpacePosition = S_Project(fragmentPosition, lightViewProjMatrix); + + let biasedLightSpaceFragmentDepth = lightSpacePosition.z - shadowBias; + + if (softShadows) { + var accumulator = 0.0; + + let stepSize = 0.5; + let kernelHalfSize = 2.0; + let kernelSize = 2.0 * kernelHalfSize; + let totalSamples = pow(kernelSize / stepSize, 2.0); + + var y = -kernelHalfSize; + loop { + if (y > kernelHalfSize) { break; } + var x = -kernelHalfSize; + loop { + if (x > kernelHalfSize) { break; } + let fetchTexCoord = lightSpacePosition.xy + vec2f(x, y) * shadowMapInvSize; + if (biasedLightSpaceFragmentDepth > textureSample(spotShadowTexture_tex, spotShadowTexture_samp, fetchTexCoord).r) { + accumulator += 1.0; + } + x += stepSize; + } + y += stepSize; + } + + return clamp(1.0 - accumulator / totalSamples, 0.0, 1.0); + } else { + return select(1.0, 0.0, biasedLightSpaceFragmentDepth > textureSample(spotShadowTexture_tex, spotShadowTexture_samp, lightSpacePosition.xy).r); + } + } else { + return 1.0; + } +} + +// Depth-texture variant of S_PointShadow (textureSample returns f32, no .r needed) +fn S_PointShadow_Depth( + shadowsEnabled: bool, + softShadows: bool, + fragmentDistance: f32, + shadowBias: f32, + toLight: vec3f, + shadowMap_tex: texture_depth_cube, + shadowMap_samp: sampler) -> f32 +{ + if (shadowsEnabled) { + let biasedFragmentDistance = fragmentDistance - shadowBias; + if (softShadows) { + const directions = array( + vec3f(1, 1, 1), vec3f(1, -1, 1), vec3f(-1, -1, 1), vec3f(-1, 1, 1), + vec3f(1, 1, -1), vec3f(1, -1, -1), vec3f(-1, -1, -1), vec3f(-1, 1, -1), + vec3f(1, 1, 0), vec3f(1, -1, 0), vec3f(-1, -1, 0), vec3f(-1, 1, 0), + vec3f(1, 0, 1), vec3f(-1, 0, 1), vec3f(1, 0, -1), vec3f(-1, 0, -1), + vec3f(0, 1, 1), vec3f(0, -1, 1), vec3f(0, -1, -1), vec3f(0, 1, -1) + ); + const diskRadius = 0.0025; + var accumulator = 0.0; + for (var i = 0; i < 20; i++) { + let fetchDirection = -toLight + directions[i] * diskRadius; + let shadowDistanceToLight = textureSample(shadowMap_tex, shadowMap_samp, fetchDirection); + if (biasedFragmentDistance > shadowDistanceToLight) { accumulator += 1.0; } + } + return clamp(1.0 - accumulator / 20.0, 0.0, 1.0); + } else { + let shadowDistanceToLight = textureSample(shadowMap_tex, shadowMap_samp, -toLight); + return select(1.0, 0.0, biasedFragmentDistance > shadowDistanceToLight); + } + } else { return 1.0; } +} + +// Depth-texture variant of S_SpotShadowFactor +fn S_SpotShadowFactor_Depth( + shadowsEnabled: bool, + softShadows: bool, + shadowBias: f32, + fragmentPosition: vec3f, + lightViewProjMatrix: mat4x4f, + shadowMapInvSize: f32, + spotShadowTexture_tex: texture_depth_2d, + spotShadowTexture_samp: sampler) -> f32 +{ + if (shadowsEnabled) { + let lightSpacePosition = S_Project(fragmentPosition, lightViewProjMatrix); + let biasedLightSpaceFragmentDepth = lightSpacePosition.z - shadowBias; + if (softShadows) { + var accumulator = 0.0; + let stepSize = 0.5; + let kernelHalfSize = 2.0; + let kernelSize = 2.0 * kernelHalfSize; + let totalSamples = pow(kernelSize / stepSize, 2.0); + var y = -kernelHalfSize; + loop { + if (y > kernelHalfSize) { break; } + var x = -kernelHalfSize; + loop { + if (x > kernelHalfSize) { break; } + let fetchTexCoord = lightSpacePosition.xy + vec2f(x, y) * shadowMapInvSize; + if (biasedLightSpaceFragmentDepth > textureSample(spotShadowTexture_tex, spotShadowTexture_samp, fetchTexCoord)) { accumulator += 1.0; } + x += stepSize; + } + y += stepSize; + } + return clamp(1.0 - accumulator / totalSamples, 0.0, 1.0); + } else { + return select(1.0, 0.0, biasedLightSpaceFragmentDepth > textureSample(spotShadowTexture_tex, spotShadowTexture_samp, lightSpacePosition.xy)); + } + } else { return 1.0; } +} + +fn Internal_FetchHeight(heightTexture_tex: texture_2d, heightTexture_samp: sampler, texCoords: vec2f, center: f32) -> f32 { + return clamp(textureSample(heightTexture_tex, heightTexture_samp, texCoords).r - center, 0.0, 1.0); +} + +fn S_ComputeParallaxTextureCoordinates(heightTexture_tex: texture_2d, heightTexture_samp: sampler, eyeVec: vec3f, texCoords: vec2f, center: f32, scale: f32) -> vec2f { + const minLayers = 8.0; + const maxLayers = 15.0; + const maxIterations = 15; + + let t = max(0.0, abs(dot(vec3f(0.0, 0.0, 1.0), eyeVec))); + let numLayers = mix(maxLayers, minLayers, t); + let layerDepth = 1.0 / numLayers; + var currentLayerDepth = 0.0; + + let deltaTexCoords = scale * eyeVec.xy / numLayers; + + var currentTexCoords = texCoords; + var currentDepthMapValue = Internal_FetchHeight(heightTexture_tex, heightTexture_samp, currentTexCoords, center); + + for (var i = 0; i < maxIterations; i++) { + if (currentLayerDepth < currentDepthMapValue) { + currentTexCoords -= deltaTexCoords; + currentDepthMapValue = Internal_FetchHeight(heightTexture_tex, heightTexture_samp, currentTexCoords, center); + currentLayerDepth += layerDepth; + } else { + break; + } + } + + let prev = currentTexCoords + deltaTexCoords; + let nextH = currentDepthMapValue - currentLayerDepth; + let prevH = Internal_FetchHeight(heightTexture_tex, heightTexture_samp, prev, center) - currentLayerDepth + layerDepth; + + let weight = nextH / (nextH - prevH); + + return prev * weight + currentTexCoords * (1.0 - weight); +} + +fn S_LinearIndexToPosition(index: i32, textureWidth: i32) -> vec2i { + let y = index / textureWidth; + let x = index - textureWidth * y; + return vec2i(x, y); +} + +fn S_FetchMatrix(storage_tex: texture_2d, storage_samp: sampler, index: i32) -> mat4x4f { + let textureWidth = i32(textureDimensions(storage_tex, 0).x); + let pos = S_LinearIndexToPosition(4 * index, textureWidth); + + let col1 = textureLoad(storage_tex, pos, 0); + let col2 = textureLoad(storage_tex, vec2i(pos.x + 1, pos.y), 0); + let col3 = textureLoad(storage_tex, vec2i(pos.x + 2, pos.y), 0); + let col4 = textureLoad(storage_tex, vec2i(pos.x + 3, pos.y), 0); + + return mat4x4f(col1, col2, col3, col4); +} + +struct TBlendShapeOffsets { + position: vec3f, + normal: vec3f, + tangent: vec3f, +}; + +fn S_FetchBlendShapeOffsets(storage_tex: texture_3d, storage_samp: sampler, vertexIndex: i32, blendShapeIndex: i32) -> TBlendShapeOffsets { + let textureWidth = i32(textureDimensions(storage_tex, 0).x); + let pos = vec3i(S_LinearIndexToPosition(3 * vertexIndex, textureWidth), blendShapeIndex); + let position = textureLoad(storage_tex, pos, 0).xyz; + let normal = textureLoad(storage_tex, vec3i(pos.x + 1, pos.y, pos.z), 0).xyz; + let tangent = textureLoad(storage_tex, vec3i(pos.x + 2, pos.y, pos.z), 0).xyz; + return TBlendShapeOffsets(position, normal, tangent); +} + +fn S_LinearToSRGB(color: vec4f) -> vec4f { + let a = 12.92 * color.rgb; + let b = 1.055 * pow(color.rgb, vec3f(1.0 / 2.4)) - 0.055; + let c = step(vec3f(0.0031308), color.rgb); + return vec4f(mix(a, b, c), color.a); +} + +fn S_SRGBToLinear(color: vec4f) -> vec4f { + let a = color.rgb / 12.92; + let b = pow((color.rgb + 0.055) / 1.055, vec3f(2.4)); + let c = step(vec3f(0.04045), color.rgb); + return vec4f(mix(a, b, c), color.a); +} + +fn S_Luminance(x: vec3f) -> f32 { + return dot(x, vec3f(0.2125, 0.7154, 0.0721)); +} + +fn S_RotateVec2(v: vec2f, angle: f32) -> vec2f { + let c = cos(angle); + let s = sin(angle); + let m = mat2x2f(c, -s, s, c); + return m * v; +} + +fn S_ConvertRgbToXyz(rgb: vec3f) -> vec3f { + var xyz: vec3f; + xyz.x = dot(vec3f(0.4124564, 0.3575761, 0.1804375), rgb); + xyz.y = dot(vec3f(0.2126729, 0.7151522, 0.0721750), rgb); + xyz.z = dot(vec3f(0.0193339, 0.1191920, 0.9503041), rgb); + return xyz; +} + +fn S_ConvertXyzToRgb(xyz: vec3f) -> vec3f { + var rgb: vec3f; + rgb.x = dot(vec3f(3.2404542, -1.5371385, -0.4985314), xyz); + rgb.y = dot(vec3f(-0.9692660, 1.8760108, 0.0415560), xyz); + rgb.z = dot(vec3f(0.0556434, -0.2040259, 1.0572252), xyz); + return rgb; +} + +fn S_ConvertXyzToYxy(xyz: vec3f) -> vec3f { + let inv = 1.0 / dot(xyz, vec3f(1.0, 1.0, 1.0)); + return vec3f(xyz.y, xyz.x * inv, xyz.y * inv); +} + +fn S_ConvertYxyToXyz(Yxy: vec3f) -> vec3f { + var xyz: vec3f; + xyz.x = Yxy.x * Yxy.y / Yxy.z; + xyz.y = Yxy.x; + xyz.z = Yxy.x * (1.0 - Yxy.y - Yxy.z) / Yxy.z; + return xyz; +} + +fn S_ConvertRgbToYxy(rgb: vec3f) -> vec3f { + return S_ConvertXyzToYxy(S_ConvertRgbToXyz(rgb)); +} + +fn S_ConvertYxyToRgb(Yxy: vec3f) -> vec3f { + return S_ConvertXyzToRgb(S_ConvertYxyToXyz(Yxy)); +} diff --git a/fyrox-graphics-wgpu/src/texture.rs b/fyrox-graphics-wgpu/src/texture.rs new file mode 100644 index 0000000000..5ece68d2a2 --- /dev/null +++ b/fyrox-graphics-wgpu/src/texture.rs @@ -0,0 +1,525 @@ +// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors. +// +// 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. + +use crate::server::WgpuGraphicsServer; +use fyrox_graphics::{ + error::FrameworkError, + gpu_texture::{ + image_1d_size_bytes, image_2d_size_bytes, image_3d_size_bytes, GpuTextureDescriptor, + GpuTextureKind, GpuTextureTrait, PixelKind, + }, +}; +use std::cell::Cell; +use std::rc::Weak; + +/// Maps a Fyrox [`PixelKind`] to the corresponding wgpu [`TextureFormat`]. +/// +/// 3-component formats (RGB8, BGR8, SRGB8, RGB16F, RGB32F) are mapped to their +/// 4-component equivalents because wgpu has no 3-component texture formats. +/// The actual data expansion happens in [`expand_to_rgba`]. +fn pixel_kind_to_wgpu_format(kind: PixelKind) -> wgpu::TextureFormat { + match kind { + PixelKind::R32F => wgpu::TextureFormat::R32Float, + PixelKind::R32UI => wgpu::TextureFormat::R32Uint, + PixelKind::R16F => wgpu::TextureFormat::R16Float, + PixelKind::RG16F => wgpu::TextureFormat::Rg16Float, + PixelKind::D32F => wgpu::TextureFormat::Depth32Float, + PixelKind::D16 => wgpu::TextureFormat::Depth16Unorm, + PixelKind::D24S8 => wgpu::TextureFormat::Depth24PlusStencil8, + PixelKind::RGBA8 | PixelKind::RGB8 => wgpu::TextureFormat::Rgba8Unorm, + PixelKind::SRGBA8 | PixelKind::SRGB8 => wgpu::TextureFormat::Rgba8UnormSrgb, + PixelKind::BGRA8 | PixelKind::BGR8 => wgpu::TextureFormat::Bgra8Unorm, + PixelKind::RG8 | PixelKind::LA8 => wgpu::TextureFormat::Rg8Unorm, + PixelKind::R8 | PixelKind::L8 => wgpu::TextureFormat::R8Unorm, + PixelKind::R8UI => wgpu::TextureFormat::R8Uint, + PixelKind::RGBA16F => wgpu::TextureFormat::Rgba16Float, + PixelKind::RGB16F => wgpu::TextureFormat::Rgba16Float, + PixelKind::RGBA32F | PixelKind::RGB32F => wgpu::TextureFormat::Rgba32Float, + PixelKind::R11G11B10F => wgpu::TextureFormat::Rg11b10Ufloat, + PixelKind::RGB10A2 => wgpu::TextureFormat::Rgb10a2Unorm, + _ => wgpu::TextureFormat::Rgba8Unorm, + } +} + +/// Returns true if the pixel kind is 3-component and needs expansion to 4-component +/// for wgpu compatibility (wgpu has no 3-component texture formats). +fn needs_rgba_expansion(pk: PixelKind) -> bool { + matches!( + pk, + PixelKind::RGB8 + | PixelKind::BGR8 + | PixelKind::SRGB8 + | PixelKind::RGB16F + | PixelKind::RGB32F + ) +} + +/// Expands 3-component pixel data to 4-component by adding an opaque alpha channel. +fn expand_to_rgba(pk: PixelKind, data: &[u8]) -> Vec { + match pk { + PixelKind::RGB8 | PixelKind::SRGB8 | PixelKind::BGR8 => { + // 3 bytes → 4 bytes per pixel (add 0xFF alpha) + let pixel_count = data.len() / 3; + let mut out = Vec::with_capacity(pixel_count * 4); + for chunk in data.chunks(3) { + out.extend_from_slice(chunk); + out.push(0xFF); + } + out + } + PixelKind::RGB16F => { + // 6 bytes → 8 bytes per pixel (add 1.0f16 = 0x3C00 as alpha) + let pixel_count = data.len() / 6; + let mut out = Vec::with_capacity(pixel_count * 8); + for chunk in data.chunks(6) { + out.extend_from_slice(chunk); + out.extend_from_slice(&[0x00, 0x3C]); + } + out + } + PixelKind::RGB32F => { + // 12 bytes → 16 bytes per pixel (add 1.0f32 = 0x3F800000 as alpha) + let pixel_count = data.len() / 12; + let mut out = Vec::with_capacity(pixel_count * 16); + for chunk in data.chunks(12) { + out.extend_from_slice(chunk); + out.extend_from_slice(&[0x00, 0x00, 0x80, 0x3F]); + } + out + } + _ => data.to_vec(), + } +} + +fn texture_dimension(kind: GpuTextureKind) -> wgpu::TextureDimension { + match kind { + GpuTextureKind::Line { .. } => wgpu::TextureDimension::D1, + GpuTextureKind::Rectangle { .. } | GpuTextureKind::Cube { .. } => { + wgpu::TextureDimension::D2 + } + GpuTextureKind::Volume { .. } => wgpu::TextureDimension::D3, + } +} + +pub(crate) fn texture_size(kind: GpuTextureKind) -> (u32, u32, u32) { + match kind { + GpuTextureKind::Line { length } => (length as u32, 1, 1), + GpuTextureKind::Rectangle { width, height } => (width as u32, height as u32, 1), + GpuTextureKind::Cube { size } => (size as u32, size as u32, 6), + GpuTextureKind::Volume { + width, + height, + depth, + } => (width as u32, height as u32, depth as u32), + } +} + +fn texture_view_dimension(kind: GpuTextureKind) -> wgpu::TextureViewDimension { + match kind { + GpuTextureKind::Line { .. } => wgpu::TextureViewDimension::D1, + GpuTextureKind::Rectangle { .. } => wgpu::TextureViewDimension::D2, + GpuTextureKind::Cube { .. } => wgpu::TextureViewDimension::Cube, + GpuTextureKind::Volume { .. } => wgpu::TextureViewDimension::D3, + } +} + +/// Writes a single mip level (or cubemap face) to a texture, expanding 3-component +/// pixel data to 4-component if needed for wgpu compatibility. +fn write_mip_data( + queue: &wgpu::Queue, + texture: &wgpu::Texture, + mip: u32, + origin: wgpu::Origin3d, + extent: wgpu::Extent3d, + pk: PixelKind, + data: &[u8], + fmt: wgpu::TextureFormat, +) { + let expanded; + let upload_data = if needs_rgba_expansion(pk) { + expanded = expand_to_rgba(pk, data); + expanded.as_slice() + } else { + data + }; + let bps = fmt.block_copy_size(None).unwrap_or(4); + queue.write_texture( + wgpu::TexelCopyTextureInfo { + texture, + mip_level: mip, + origin, + aspect: wgpu::TextureAspect::All, + }, + upload_data, + wgpu::TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some((extent.width * bps).max(1)), + rows_per_image: Some(extent.height.max(1)), + }, + extent, + ); +} + +/// Wgpu implementation of [`GpuTextureTrait`](fyrox_graphics::gpu_texture::GpuTextureTrait). +/// +/// Wraps a [`wgpu::Texture`] with two views: a full-aspect view for rendering and +/// a separate `DepthOnly` view for shader bindings (depth-stencil textures require +/// this because wgpu rejects `Depth+Stencil` aspect on texture bindings). +/// +/// Memory usage is tracked on the server and decremented on drop. +pub struct WgpuTexture { + server: Weak, + texture: wgpu::Texture, + view: wgpu::TextureView, + /// Separate view with DepthOnly aspect for shader bindings of depth-stencil textures. + /// For non-depth-stencil textures, this is the same as `view`. + binding_view: wgpu::TextureView, + kind: Cell, + pixel_kind: Cell, + size_bytes: Cell, +} + +impl WgpuTexture { + /// Creates a new GPU texture from the given descriptor. + /// + /// Creates the wgpu texture, views (including a separate `DepthOnly` binding + /// view for depth-stencil formats), uploads initial data if provided, and + /// tracks memory usage on the server. + pub fn new( + server: &WgpuGraphicsServer, + desc: GpuTextureDescriptor, + ) -> Result { + let format = pixel_kind_to_wgpu_format(desc.pixel_kind); + let dimension = texture_dimension(desc.kind); + let (raw_w, raw_h, depth_or_layers) = texture_size(desc.kind); + let width = raw_w.max(1); + let height = raw_h.max(1); + let mip_count = desc.mip_count.max(1) as u32; + + let texture = server + .state + .device + .create_texture(&wgpu::TextureDescriptor { + label: if server.named_objects { + Some(desc.name) + } else { + None + }, + size: wgpu::Extent3d { + width, + height, + depth_or_array_layers: depth_or_layers, + }, + mip_level_count: mip_count, + sample_count: 1, + dimension, + format, + usage: wgpu::TextureUsages::TEXTURE_BINDING + | wgpu::TextureUsages::COPY_DST + | wgpu::TextureUsages::COPY_SRC + | wgpu::TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }); + + let view = texture.create_view(&wgpu::TextureViewDescriptor { + label: None, + format: None, + dimension: Some(texture_view_dimension(desc.kind)), + aspect: wgpu::TextureAspect::All, + ..Default::default() + }); + + // For depth-stencil textures, create a separate view with DepthOnly aspect + // for shader bindings. wgpu rejects Depth+Stencil aspect on texture bindings. + let binding_view = match format { + wgpu::TextureFormat::Depth24PlusStencil8 + | wgpu::TextureFormat::Depth32FloatStencil8 => { + texture.create_view(&wgpu::TextureViewDescriptor { + label: None, + format: None, + dimension: Some(texture_view_dimension(desc.kind)), + aspect: wgpu::TextureAspect::DepthOnly, + ..Default::default() + }) + } + _ => view.clone(), + }; + + let size_bytes = calc_size_bytes(desc.kind, desc.pixel_kind, desc.mip_count); + if let Some(data) = desc.data { + Self::upload( + &server.state.queue, + &texture, + desc.kind, + desc.pixel_kind, + data, + desc.mip_count, + )?; + } + server.memory_usage.borrow_mut().textures += size_bytes; + + Ok(Self { + server: server.weak_ref(), + texture, + view, + binding_view, + kind: Cell::new(desc.kind), + pixel_kind: Cell::new(desc.pixel_kind), + size_bytes: Cell::new(size_bytes), + }) + } + + fn upload( + queue: &wgpu::Queue, + texture: &wgpu::Texture, + kind: GpuTextureKind, + pk: PixelKind, + data: &[u8], + mip_count: usize, + ) -> Result<(), FrameworkError> { + let mip_count = mip_count.max(1); + let fmt = pixel_kind_to_wgpu_format(pk); + let mut offset = 0; + for mip in 0..mip_count { + match kind { + GpuTextureKind::Line { length } => { + if let Some(l) = length.checked_shr(mip as u32) { + let sz = image_1d_size_bytes(pk, l); + if offset + sz > data.len() { + break; + } + write_mip_data( + queue, + texture, + mip as u32, + wgpu::Origin3d::ZERO, + wgpu::Extent3d { + width: l as u32, + height: 1, + depth_or_array_layers: 1, + }, + pk, + &data[offset..offset + sz], + fmt, + ); + offset += sz; + } + } + GpuTextureKind::Rectangle { width, height } => { + if let (Some(w), Some(h)) = ( + width.checked_shr(mip as u32), + height.checked_shr(mip as u32), + ) { + let sz = image_2d_size_bytes(pk, w, h); + if offset + sz > data.len() { + break; + } + write_mip_data( + queue, + texture, + mip as u32, + wgpu::Origin3d::ZERO, + wgpu::Extent3d { + width: w as u32, + height: h as u32, + depth_or_array_layers: 1, + }, + pk, + &data[offset..offset + sz], + fmt, + ); + offset += sz; + } + } + GpuTextureKind::Cube { size } => { + if let Some(s) = size.checked_shr(mip as u32) { + let bpf = image_2d_size_bytes(pk, s, s); + for face in 0..6u32 { + let fo = offset + (face as usize) * bpf; + if fo + bpf > data.len() { + break; + } + write_mip_data( + queue, + texture, + mip as u32, + wgpu::Origin3d { + x: 0, + y: 0, + z: face, + }, + wgpu::Extent3d { + width: s as u32, + height: s as u32, + depth_or_array_layers: 1, + }, + pk, + &data[fo..fo + bpf], + fmt, + ); + } + offset += 6 * bpf; + } + } + GpuTextureKind::Volume { + width, + height, + depth, + } => { + if let (Some(w), Some(h), Some(d)) = ( + width.checked_shr(mip as u32), + height.checked_shr(mip as u32), + depth.checked_shr(mip as u32), + ) { + let sz = image_3d_size_bytes(pk, w, h, d); + if offset + sz > data.len() { + break; + } + write_mip_data( + queue, + texture, + mip as u32, + wgpu::Origin3d::ZERO, + wgpu::Extent3d { + width: w as u32, + height: h as u32, + depth_or_array_layers: d as u32, + }, + pk, + &data[offset..offset + sz], + fmt, + ); + offset += sz; + } + } + } + } + Ok(()) + } + + /// Returns a reference to the underlying [`wgpu::Texture`]. + pub fn wgpu_texture(&self) -> &wgpu::Texture { + &self.texture + } + /// Returns a reference to the full-aspect [`wgpu::TextureView`]. + pub fn wgpu_view(&self) -> &wgpu::TextureView { + &self.view + } + /// Returns a view suitable for shader bindings. + /// + /// For depth-stencil textures, this is a `DepthOnly`-aspect view (wgpu rejects + /// `Depth+Stencil` aspect on texture bindings). For all other textures, this + /// is the same as [`wgpu_view`](Self::wgpu_view). + pub fn wgpu_binding_view(&self) -> &wgpu::TextureView { + &self.binding_view + } + /// Returns the wgpu texture format of this texture. + pub fn format(&self) -> wgpu::TextureFormat { + self.texture.format() + } +} + +fn calc_size_bytes(kind: GpuTextureKind, pk: PixelKind, mip_count: usize) -> usize { + let mip_count = mip_count.max(1); + let mut total = 0; + for mip in 0..mip_count { + match kind { + GpuTextureKind::Line { length } => { + if let Some(l) = length.checked_shr(mip as u32) { + total += image_1d_size_bytes(pk, l); + } + } + GpuTextureKind::Rectangle { width, height } => { + if let (Some(w), Some(h)) = ( + width.checked_shr(mip as u32), + height.checked_shr(mip as u32), + ) { + total += image_2d_size_bytes(pk, w, h); + } + } + GpuTextureKind::Cube { size } => { + if let Some(s) = size.checked_shr(mip as u32) { + total += 6 * image_2d_size_bytes(pk, s, s); + } + } + GpuTextureKind::Volume { + width, + height, + depth, + } => { + if let (Some(w), Some(h), Some(d)) = ( + width.checked_shr(mip as u32), + height.checked_shr(mip as u32), + depth.checked_shr(mip as u32), + ) { + total += image_3d_size_bytes(pk, w, h, d); + } + } + } + } + total +} + +impl Drop for WgpuTexture { + fn drop(&mut self) { + if let Some(server) = self.server.upgrade() { + server.memory_usage.borrow_mut().textures -= self.size_bytes.get(); + } + } +} + +impl GpuTextureTrait for WgpuTexture { + fn set_data( + &self, + kind: GpuTextureKind, + pk: PixelKind, + mip_count: usize, + data: Option<&[u8]>, + ) -> Result { + let Some(server) = self.server.upgrade() else { + return Err(FrameworkError::GraphicsServerUnavailable); + }; + let new_size = calc_size_bytes(kind, pk, mip_count); + let mut mu = server.memory_usage.borrow_mut(); + mu.textures -= self.size_bytes.get(); + mu.textures += new_size; + drop(mu); + self.size_bytes.set(new_size); + self.kind.set(kind); + self.pixel_kind.set(pk); + if let Some(data) = data { + Self::upload( + &server.state.queue, + &self.texture, + kind, + pk, + data, + mip_count, + )?; + } + Ok(new_size) + } + fn kind(&self) -> GpuTextureKind { + self.kind.get() + } + fn pixel_kind(&self) -> PixelKind { + self.pixel_kind.get() + } +} diff --git a/fyrox-graphics/src/gpu_program.rs b/fyrox-graphics/src/gpu_program.rs index 70886c6af1..f6f150e161 100644 --- a/fyrox-graphics/src/gpu_program.rs +++ b/fyrox-graphics/src/gpu_program.rs @@ -153,6 +153,10 @@ pub enum SamplerKind { /// The sampler follows the direction of the coordinates until it finds a place on one of the six faces of the cube. /// Each component of the resulting value is an unsigned integer. USamplerCube, + /// A sampler for a 2D depth texture. + DepthSampler2D, + /// A sampler for a cube depth texture. + DepthSamplerCube, } /// Shader property with default value. diff --git a/fyrox-graphics/src/server.rs b/fyrox-graphics/src/server.rs index 19150cbac2..cdd260ff6f 100644 --- a/fyrox-graphics/src/server.rs +++ b/fyrox-graphics/src/server.rs @@ -43,8 +43,10 @@ use std::rc::{Rc, Weak}; /// Graphics server capabilities. #[derive(Debug)] pub struct ServerCapabilities { - /// The maximum size in basic machine units of a uniform block, which must be at least 16384. - pub max_uniform_block_size: usize, + /// The maximum size in bytes of a single uniform buffer binding in a bind group. + /// This corresponds to `GL_MAX_UNIFORM_BLOCK_SIZE` (OpenGL) and + /// `max_uniform_buffer_binding_size` (wgpu). Must be at least 16384. + pub max_uniform_buffer_binding_size: usize, /// The minimum required alignment for uniform buffer sizes and offset. The initial value is 1. pub uniform_buffer_offset_alignment: usize, /// The maximum, absolute value of the texture level-of-detail bias. The value must be at least diff --git a/fyrox-impl/Cargo.toml b/fyrox-impl/Cargo.toml index 32bf75b71d..2343e2ed01 100644 --- a/fyrox-impl/Cargo.toml +++ b/fyrox-impl/Cargo.toml @@ -23,10 +23,11 @@ fyrox-resource = { path = "../fyrox-resource", version = "2.0.0-rc.1" } fyrox-animation = { path = "../fyrox-animation", version = "2.0.0-rc.1" } fyrox-graph = { path = "../fyrox-graph", version = "2.0.0-rc.1" } fyrox-graphics = { path = "../fyrox-graphics", version = "2.0.0-rc.1" } -fyrox-graphics-gl = { path = "../fyrox-graphics-gl", version = "2.0.0-rc.1" } fyrox-texture = { path = "../fyrox-texture", version = "2.0.0-rc.1" } fyrox-autotile = { path = "../fyrox-autotile", version = "2.0.0-rc.1" } fyrox-material = { path = "../fyrox-material", version = "2.0.0-rc.1" } +fyrox-graphics-gl = { path = "../fyrox-graphics-gl", version = "2.0.0-rc.1", optional = true } +fyrox-graphics-wgpu = { path = "../fyrox-graphics-wgpu", version = "2.0.0-rc.1", optional = true } rapier2d = { version = "0.33", features = ["debug-render", ] } rapier3d = { version = "0.33", features = ["debug-render"] } image = { version = "0.25.1", default-features = false, features = ["gif", "jpeg", "png", "tga", "tiff", "bmp"] } @@ -93,6 +94,8 @@ opener = { version = "0.8", default-features = false, features = ["reveal"] } # === end of additional deps === [features] +backend_opengl = ["dep:fyrox-graphics-gl", "fyrox-material/backend_opengl"] +backend_wgpu = ["dep:fyrox-graphics-wgpu", "fyrox-material/backend_wgpu"] mesh_analysis = [] [target.'cfg(target_os = "android")'.dependencies] diff --git a/fyrox-impl/src/engine/mod.rs b/fyrox-impl/src/engine/mod.rs index 9cc6e2617a..9fc35706f8 100644 --- a/fyrox-impl/src/engine/mod.rs +++ b/fyrox-impl/src/engine/mod.rs @@ -113,7 +113,7 @@ use fyrox_animation::AnimationTracksData; use fyrox_core::dyntype::DynTypeConstructorContainer; use fyrox_core::NameProvider; use fyrox_graphics::server::SharedGraphicsServer; -use fyrox_graphics_gl::server::GlGraphicsServer; + use fyrox_sound::{ buffer::{loader::SoundBufferLoader, SoundBuffer}, renderer::hrtf::{HrirSphereLoader, HrirSphereResourceData}, @@ -1022,11 +1022,29 @@ pub type GraphicsServerConstructorCallback = dyn Fn( #[derive(Clone)] pub struct GraphicsServerConstructor(Rc); +#[cfg(feature = "backend_opengl")] +impl Default for GraphicsServerConstructor { + fn default() -> Self { + Self(Rc::new( + |params, window_target, window_builder, named_objects| { + fyrox_graphics_gl::server::GlGraphicsServer::new( + params.vsync, + params.msaa_sample_count, + window_target, + window_builder, + named_objects, + ) + }, + )) + } +} + +#[cfg(all(feature = "backend_wgpu", not(feature = "backend_opengl")))] impl Default for GraphicsServerConstructor { fn default() -> Self { Self(Rc::new( |params, window_target, window_builder, named_objects| { - GlGraphicsServer::new( + fyrox_graphics_wgpu::server::WgpuGraphicsServer::new( params.vsync, params.msaa_sample_count, window_target, diff --git a/fyrox-impl/src/lib.rs b/fyrox-impl/src/lib.rs index f5285a03a7..e4b9972081 100644 --- a/fyrox-impl/src/lib.rs +++ b/fyrox-impl/src/lib.rs @@ -69,9 +69,6 @@ pub use fyrox_autotile as autotile; #[doc(inline)] pub use fyrox_graphics as graphics; -#[doc(inline)] -pub use fyrox_graphics_gl as graphics_gl; - /// Defines a builder's `with_xxx` method. #[macro_export] macro_rules! define_with { diff --git a/fyrox-impl/src/renderer/cache/uniform.rs b/fyrox-impl/src/renderer/cache/uniform.rs index 50b882cc19..8d23393cc8 100644 --- a/fyrox-impl/src/renderer/cache/uniform.rs +++ b/fyrox-impl/src/renderer/cache/uniform.rs @@ -88,7 +88,15 @@ impl UniformBufferCache { pub fn get_or_create(&self, size: usize) -> Result { let mut cache = self.cache.borrow_mut(); let set = cache.entry(size).or_default(); - set.get_or_create(size, &*self.server) + let buffer = set.get_or_create(size, &*self.server)?; + debug_assert_eq!( + buffer.size(), + size, + "UniformBufferCache returned buffer of size {} but {} was requested", + buffer.size(), + size + ); + Ok(buffer) } /// Fetches a suitable (or creates new one) GPU uniform buffer for the given CPU uniform buffer @@ -140,17 +148,20 @@ pub struct UniformBlockLocation { pub struct UniformMemoryAllocator { gpu_buffers: Vec, block_alignment: usize, - max_uniform_buffer_size: usize, + /// Maximum size of a single uniform buffer binding. This is the GPU's + /// `max_uniform_buffer_binding_size` and is used as both the page capacity + /// and the per-allocation size limit. + max_uniform_buffer_binding_size: usize, pages: Vec, blocks: Vec, } impl UniformMemoryAllocator { - pub fn new(max_uniform_buffer_size: usize, block_alignment: usize) -> Self { + pub fn new(max_uniform_buffer_binding_size: usize, block_alignment: usize) -> Self { Self { gpu_buffers: Default::default(), block_alignment, - max_uniform_buffer_size, + max_uniform_buffer_binding_size, pages: Default::default(), blocks: Default::default(), } @@ -170,20 +181,20 @@ impl UniformMemoryAllocator { { let data = buffer.finish(); assert!(data.bytes_count() > 0); - assert!(data.bytes_count() < self.max_uniform_buffer_size); + assert!(data.bytes_count() < self.max_uniform_buffer_binding_size); let page_index = match self.pages.iter().position(|page| { let write_position = page .dynamic .next_write_aligned_position(self.block_alignment); - self.max_uniform_buffer_size - write_position >= data.bytes_count() + self.max_uniform_buffer_binding_size - write_position >= data.bytes_count() }) { Some(page_index) => page_index, None => { let page_index = self.pages.len(); self.pages.push(Page { dynamic: UniformBuffer::with_storage(Vec::with_capacity( - self.max_uniform_buffer_size, + self.max_uniform_buffer_binding_size, )), is_submitted: false, }); @@ -211,7 +222,7 @@ impl UniformMemoryAllocator { for _ in 0..(self.pages.len() - self.gpu_buffers.len()) { let buffer = server.create_buffer(GpuBufferDescriptor { name: &format!("UniformMemoryPage{}", self.gpu_buffers.len()), - size: self.max_uniform_buffer_size, + size: self.max_uniform_buffer_binding_size, kind: BufferKind::Uniform, usage: BufferUsage::StreamCopy, })?; @@ -222,7 +233,7 @@ impl UniformMemoryAllocator { for (page, gpu_buffer) in self.pages.iter_mut().zip(self.gpu_buffers.iter()) { if !page.is_submitted { let bytes = page.dynamic.storage().bytes(); - assert!(bytes.len() <= self.max_uniform_buffer_size); + assert!(bytes.len() <= self.max_uniform_buffer_binding_size); gpu_buffer.write_data(bytes)?; page.is_submitted = true; } diff --git a/fyrox-impl/src/renderer/light.rs b/fyrox-impl/src/renderer/light.rs index 2bc4704690..4d45348737 100644 --- a/fyrox-impl/src/renderer/light.rs +++ b/fyrox-impl/src/renderer/light.rs @@ -48,7 +48,6 @@ use crate::{ framework::GeometryBufferExt, gbuffer::GBuffer, light_volume::LightVolumeRenderer, - make_viewport_matrix, observer::Observer, resources::RendererResources, shadow::{ @@ -70,6 +69,7 @@ use crate::{ EnvironmentLightingSource, Scene, }, }; +use crate::renderer::make_deferred_viewport_matrix; pub struct DeferredLightRenderer { sphere: GpuGeometryBuffer, @@ -291,7 +291,8 @@ impl DeferredLightRenderer { Frustum::from_view_projection_matrix(observer.position.view_projection_matrix) .unwrap_or_default(); - let frame_matrix = make_viewport_matrix(viewport); + // TODO: A temporary fix until we figure out what's going on. For some reason, the deferred light renderer requires an inverted viewport matrix. + let frame_matrix = make_deferred_viewport_matrix(viewport); let inv_projection = observer .position diff --git a/fyrox-impl/src/renderer/mod.rs b/fyrox-impl/src/renderer/mod.rs index bac430c76c..e0925f7a37 100644 --- a/fyrox-impl/src/renderer/mod.rs +++ b/fyrox-impl/src/renderer/mod.rs @@ -168,16 +168,21 @@ fn recreate_render_data_if_needed( frame_size: Vector2, final_frame_texture: FrameTextureKind, ) -> Result<(), FrameworkError> { - if data.gbuffer.width != frame_size.x as i32 || data.gbuffer.height != frame_size.y as i32 { + let new_w = frame_size.x as i32; + let new_h = frame_size.y as i32; + // Use >1px tolerance to prevent oscillation from fractional DPI scaling. + if (data.gbuffer.width - new_w).unsigned_abs() > 1 + || (data.gbuffer.height - new_h).unsigned_abs() > 1 + { Log::info(format!( "Associated scene rendering data was re-created for {} ({}), because render \ - frame size was changed. Old is {}x{}, new {}x{}!", + frame size was changed. Old is {}x{}, new is {}x{}!", parent, std::any::type_name::(), data.gbuffer.width, data.gbuffer.height, - frame_size.x, - frame_size.y + new_w, + new_h )); *data = RenderDataContainer::new(server, frame_size, final_frame_texture)?; @@ -358,7 +363,8 @@ impl RenderDataContainer { } } -/// Creates a view-projection matrix that projects unit quad a screen with the specified viewport. +/// Creates a view-projection matrix that projects unit quad to screen with the specified viewport. +/// Uses OpenGL-style orthographic parameters (bottom=h, top=0). pub fn make_viewport_matrix(viewport: Rect) -> Matrix4 { Matrix4::new_orthographic( 0.0, @@ -374,6 +380,36 @@ pub fn make_viewport_matrix(viewport: Rect) -> Matrix4 { )) } +/// Viewport matrix for deferred passes that reconstruct world positions from G-Buffer depth. +/// +/// In wgpu, the viewport Y-axis is flipped relative to OpenGL (Y=0 at top vs bottom). +/// Deferred passes that use `S_UnProject` with depth need the orthographic bottom/top +/// swapped so that texture coordinates match the G-Buffer's pixel layout. +/// +/// Non-deferred passes (bloom, FXAA, HDR) should use [`make_viewport_matrix`] instead. +#[cfg(feature = "backend_wgpu")] +pub fn make_deferred_viewport_matrix(viewport: Rect) -> Matrix4 { + Matrix4::new_orthographic( + 0.0, + viewport.w() as f32, + 0.0, + viewport.h() as f32, + -1.0, + 1.0, + ) * Matrix4::new_nonuniform_scaling(&Vector3::new( + viewport.w() as f32, + viewport.h() as f32, + 0.0, + )) +} + +/// Viewport matrix for deferred passes that reconstruct world positions from G-Buffer depth. +/// On OpenGL this is identical to [`make_viewport_matrix`]. +#[cfg(not(feature = "backend_wgpu"))] +pub fn make_deferred_viewport_matrix(viewport: Rect) -> Matrix4 { + make_viewport_matrix(viewport) +} + /// See module docs. pub struct Renderer { backbuffer: GpuFrameBuffer, @@ -639,12 +675,11 @@ impl Renderer { let shader_cache = ShaderCache::default(); - let one_megabyte = 1024 * 1024; let uniform_memory_allocator = UniformMemoryAllocator::new( - // Clamp max uniform block size from the upper bound, to prevent allocating huge - // uniform buffers when GPU supports it. Some AMD GPUs are able to allocate ~500 Mb - // uniform buffers, which will lead to ridiculous VRAM consumption. - caps.max_uniform_block_size.min(one_megabyte), + // Use the GPU's max uniform buffer binding size as the page size limit. + // This ensures individual bindings never exceed the limit enforced by + // create_bind_group(). On most GPUs this is 64KB. + caps.max_uniform_buffer_binding_size, caps.uniform_buffer_offset_alignment, ); @@ -1246,6 +1281,12 @@ impl Renderer { dt: f32, resource_manager: &ResourceManager, ) -> Result<&SceneRenderData, FrameworkError> { + if scene_handle.is_none() { + return Err(FrameworkError::Custom( + "Attempted to render scene with invalid handle".into(), + )); + } + let graph = &scene.graph; let _debug_scope = self.server.begin_scope(&format!("Scene {:p}", scene)); diff --git a/fyrox-impl/src/renderer/resources.rs b/fyrox-impl/src/renderer/resources.rs index 3a830b2049..4e92a53a75 100644 --- a/fyrox-impl/src/renderer/resources.rs +++ b/fyrox-impl/src/renderer/resources.rs @@ -21,7 +21,7 @@ //! A set of textures of certain kinds. See [`RendererResources`] docs for more info. use crate::{ - core::{algebra::Matrix4, array_as_u8_slice}, + core::algebra::Matrix4, graphics::{ buffer::GpuBufferDescriptor, buffer::{BufferKind, BufferUsage, GpuBuffer}, @@ -98,79 +98,48 @@ pub struct ShadersContainer { pub environment_map_irradiance_convolution: RenderPassContainer, } +#[cfg(feature = "backend_opengl")] +macro_rules! include_shader { + ($file:literal) => { + include_str!(concat!("shaders/opengl/", $file)) + }; +} + +#[cfg(feature = "backend_wgpu")] +macro_rules! include_shader { + ($file:literal) => { + include_str!(concat!("shaders/wgpu/", $file)) + }; +} + impl ShadersContainer { - /// Creates a new shaders container. + /// Creates a new shaders' container. pub fn new(server: &dyn GraphicsServer) -> Result { Ok(Self { - decal: RenderPassContainer::from_str(server, include_str!("shaders/decal.shader"))?, - spot_light: RenderPassContainer::from_str( - server, - include_str!("shaders/deferred_spot_light.shader"), - )?, - point_light: RenderPassContainer::from_str( - server, - include_str!("shaders/deferred_point_light.shader"), - )?, - directional_light: RenderPassContainer::from_str( - server, - include_str!("shaders/deferred_directional_light.shader"), - )?, - ambient_light: RenderPassContainer::from_str( - server, - include_str!("shaders/ambient_light.shader"), - )?, - volume_marker_lit: RenderPassContainer::from_str( - server, - include_str!("shaders/volume_marker_lit.shader"), - )?, - pixel_counter: RenderPassContainer::from_str( - server, - include_str!("shaders/pixel_counter.shader"), - )?, - debug: RenderPassContainer::from_str(server, include_str!("shaders/debug.shader"))?, - fxaa: RenderPassContainer::from_str(server, include_str!("shaders/fxaa.shader"))?, - spot_light_volume: RenderPassContainer::from_str( - server, - include_str!("shaders/spot_volumetric.shader"), - )?, - point_light_volume: RenderPassContainer::from_str( - server, - include_str!("shaders/point_volumetric.shader"), - )?, - volume_marker_vol: RenderPassContainer::from_str( - server, - include_str!("shaders/volume_marker_vol.shader"), - )?, - visibility_optimizer: RenderPassContainer::from_str( - server, - include_str!("shaders/visibility_optimizer.shader"), - )?, - ssao: RenderPassContainer::from_str(server, include_str!("shaders/ssao.shader"))?, - visibility: RenderPassContainer::from_str( - server, - include_str!("shaders/visibility.shader"), - )?, - blit: RenderPassContainer::from_str(server, include_str!("shaders/blit.shader"))?, - hdr_adaptation: RenderPassContainer::from_str( - server, - include_str!("shaders/hdr_adaptation.shader"), - )?, - hdr_luminance: RenderPassContainer::from_str( - server, - include_str!("shaders/hdr_luminance.shader"), - )?, - hdr_downscale: RenderPassContainer::from_str( - server, - include_str!("shaders/hdr_downscale.shader"), - )?, - hdr_map: RenderPassContainer::from_str(server, include_str!("shaders/hdr_map.shader"))?, - bloom: RenderPassContainer::from_str(server, include_str!("shaders/bloom.shader"))?, - skybox: RenderPassContainer::from_str(server, include_str!("shaders/skybox.shader"))?, - gaussian_blur: RenderPassContainer::from_str( - server, - include_str!("shaders/gaussian_blur.shader"), - )?, - box_blur: RenderPassContainer::from_str(server, include_str!("shaders/blur.shader"))?, + decal: RenderPassContainer::from_str(server, include_shader!("decal.shader"))?, + spot_light: RenderPassContainer::from_str(server, include_shader!("deferred_spot_light.shader"))?, + point_light: RenderPassContainer::from_str(server, include_shader!("deferred_point_light.shader"))?, + directional_light: RenderPassContainer::from_str(server, include_shader!("deferred_directional_light.shader"))?, + ambient_light: RenderPassContainer::from_str(server, include_shader!("ambient_light.shader"))?, + volume_marker_lit: RenderPassContainer::from_str(server, include_shader!("volume_marker_lit.shader"))?, + pixel_counter: RenderPassContainer::from_str(server, include_shader!("pixel_counter.shader"))?, + debug: RenderPassContainer::from_str(server, include_shader!("debug.shader"))?, + fxaa: RenderPassContainer::from_str(server, include_shader!("fxaa.shader"))?, + spot_light_volume: RenderPassContainer::from_str(server, include_shader!("spot_volumetric.shader"))?, + point_light_volume: RenderPassContainer::from_str(server, include_shader!("point_volumetric.shader"))?, + volume_marker_vol: RenderPassContainer::from_str(server, include_shader!("volume_marker_vol.shader"))?, + visibility_optimizer: RenderPassContainer::from_str(server, include_shader!("visibility_optimizer.shader"))?, + ssao: RenderPassContainer::from_str(server, include_shader!("ssao.shader"))?, + visibility: RenderPassContainer::from_str(server, include_shader!("visibility.shader"))?, + blit: RenderPassContainer::from_str(server, include_shader!("blit.shader"))?, + hdr_adaptation: RenderPassContainer::from_str(server, include_shader!("hdr_adaptation.shader"))?, + hdr_luminance: RenderPassContainer::from_str(server, include_shader!("hdr_luminance.shader"))?, + hdr_downscale: RenderPassContainer::from_str(server, include_shader!("hdr_downscale.shader"))?, + hdr_map: RenderPassContainer::from_str(server, include_shader!("hdr_map.shader"))?, + bloom: RenderPassContainer::from_str(server, include_shader!("bloom.shader"))?, + skybox: RenderPassContainer::from_str(server, include_shader!("skybox.shader"))?, + gaussian_blur: RenderPassContainer::from_str(server, include_shader!("gaussian_blur.shader"))?, + box_blur: RenderPassContainer::from_str(server, include_shader!("blur.shader"))?, ui: RenderPassContainer::from_str( server, str::from_utf8( @@ -181,16 +150,10 @@ impl ShadersContainer { .bytes .as_ref(), ) - .unwrap(), - )?, - environment_map_specular_convolution: RenderPassContainer::from_str( - server, - include_str!("shaders/prefilter.shader"), - )?, - environment_map_irradiance_convolution: RenderPassContainer::from_str( - server, - include_str!("shaders/irradiance.shader"), + .unwrap(), )?, + environment_map_specular_convolution: RenderPassContainer::from_str(server, include_shader!("prefilter.shader"))?, + environment_map_irradiance_convolution: RenderPassContainer::from_str(server, include_shader!("irradiance.shader"))?, }) } } @@ -309,9 +272,7 @@ impl RendererResources { kind: BufferKind::Uniform, usage: BufferUsage::StaticDraw, })?; - const SIZE: usize = ShaderDefinition::MAX_BONE_MATRICES * size_of::>(); - let zeros = [0.0; SIZE]; - buffer.write_data(array_as_u8_slice(&zeros))?; + buffer.write_data(&vec![0u8; ShaderDefinition::MAX_BONE_MATRICES * size_of::>()])?; buffer }, linear_clamp_sampler: server.create_sampler(GpuSamplerDescriptor { diff --git a/fyrox-impl/src/renderer/shaders/ambient_light.shader b/fyrox-impl/src/renderer/shaders/opengl/ambient_light.shader similarity index 100% rename from fyrox-impl/src/renderer/shaders/ambient_light.shader rename to fyrox-impl/src/renderer/shaders/opengl/ambient_light.shader diff --git a/fyrox-impl/src/renderer/shaders/blit.shader b/fyrox-impl/src/renderer/shaders/opengl/blit.shader similarity index 100% rename from fyrox-impl/src/renderer/shaders/blit.shader rename to fyrox-impl/src/renderer/shaders/opengl/blit.shader diff --git a/fyrox-impl/src/renderer/shaders/bloom.shader b/fyrox-impl/src/renderer/shaders/opengl/bloom.shader similarity index 100% rename from fyrox-impl/src/renderer/shaders/bloom.shader rename to fyrox-impl/src/renderer/shaders/opengl/bloom.shader diff --git a/fyrox-impl/src/renderer/shaders/blur.shader b/fyrox-impl/src/renderer/shaders/opengl/blur.shader similarity index 100% rename from fyrox-impl/src/renderer/shaders/blur.shader rename to fyrox-impl/src/renderer/shaders/opengl/blur.shader diff --git a/fyrox-impl/src/renderer/shaders/debug.shader b/fyrox-impl/src/renderer/shaders/opengl/debug.shader similarity index 100% rename from fyrox-impl/src/renderer/shaders/debug.shader rename to fyrox-impl/src/renderer/shaders/opengl/debug.shader diff --git a/fyrox-impl/src/renderer/shaders/decal.shader b/fyrox-impl/src/renderer/shaders/opengl/decal.shader similarity index 100% rename from fyrox-impl/src/renderer/shaders/decal.shader rename to fyrox-impl/src/renderer/shaders/opengl/decal.shader diff --git a/fyrox-impl/src/renderer/shaders/deferred_directional_light.shader b/fyrox-impl/src/renderer/shaders/opengl/deferred_directional_light.shader similarity index 100% rename from fyrox-impl/src/renderer/shaders/deferred_directional_light.shader rename to fyrox-impl/src/renderer/shaders/opengl/deferred_directional_light.shader diff --git a/fyrox-impl/src/renderer/shaders/deferred_point_light.shader b/fyrox-impl/src/renderer/shaders/opengl/deferred_point_light.shader similarity index 100% rename from fyrox-impl/src/renderer/shaders/deferred_point_light.shader rename to fyrox-impl/src/renderer/shaders/opengl/deferred_point_light.shader diff --git a/fyrox-impl/src/renderer/shaders/deferred_spot_light.shader b/fyrox-impl/src/renderer/shaders/opengl/deferred_spot_light.shader similarity index 100% rename from fyrox-impl/src/renderer/shaders/deferred_spot_light.shader rename to fyrox-impl/src/renderer/shaders/opengl/deferred_spot_light.shader diff --git a/fyrox-impl/src/renderer/shaders/fxaa.shader b/fyrox-impl/src/renderer/shaders/opengl/fxaa.shader similarity index 100% rename from fyrox-impl/src/renderer/shaders/fxaa.shader rename to fyrox-impl/src/renderer/shaders/opengl/fxaa.shader diff --git a/fyrox-impl/src/renderer/shaders/gaussian_blur.shader b/fyrox-impl/src/renderer/shaders/opengl/gaussian_blur.shader similarity index 100% rename from fyrox-impl/src/renderer/shaders/gaussian_blur.shader rename to fyrox-impl/src/renderer/shaders/opengl/gaussian_blur.shader diff --git a/fyrox-impl/src/renderer/shaders/hdr_adaptation.shader b/fyrox-impl/src/renderer/shaders/opengl/hdr_adaptation.shader similarity index 100% rename from fyrox-impl/src/renderer/shaders/hdr_adaptation.shader rename to fyrox-impl/src/renderer/shaders/opengl/hdr_adaptation.shader diff --git a/fyrox-impl/src/renderer/shaders/hdr_downscale.shader b/fyrox-impl/src/renderer/shaders/opengl/hdr_downscale.shader similarity index 100% rename from fyrox-impl/src/renderer/shaders/hdr_downscale.shader rename to fyrox-impl/src/renderer/shaders/opengl/hdr_downscale.shader diff --git a/fyrox-impl/src/renderer/shaders/hdr_luminance.shader b/fyrox-impl/src/renderer/shaders/opengl/hdr_luminance.shader similarity index 100% rename from fyrox-impl/src/renderer/shaders/hdr_luminance.shader rename to fyrox-impl/src/renderer/shaders/opengl/hdr_luminance.shader diff --git a/fyrox-impl/src/renderer/shaders/hdr_map.shader b/fyrox-impl/src/renderer/shaders/opengl/hdr_map.shader similarity index 100% rename from fyrox-impl/src/renderer/shaders/hdr_map.shader rename to fyrox-impl/src/renderer/shaders/opengl/hdr_map.shader diff --git a/fyrox-impl/src/renderer/shaders/irradiance.shader b/fyrox-impl/src/renderer/shaders/opengl/irradiance.shader similarity index 100% rename from fyrox-impl/src/renderer/shaders/irradiance.shader rename to fyrox-impl/src/renderer/shaders/opengl/irradiance.shader diff --git a/fyrox-impl/src/renderer/shaders/pixel_counter.shader b/fyrox-impl/src/renderer/shaders/opengl/pixel_counter.shader similarity index 100% rename from fyrox-impl/src/renderer/shaders/pixel_counter.shader rename to fyrox-impl/src/renderer/shaders/opengl/pixel_counter.shader diff --git a/fyrox-impl/src/renderer/shaders/point_volumetric.shader b/fyrox-impl/src/renderer/shaders/opengl/point_volumetric.shader similarity index 100% rename from fyrox-impl/src/renderer/shaders/point_volumetric.shader rename to fyrox-impl/src/renderer/shaders/opengl/point_volumetric.shader diff --git a/fyrox-impl/src/renderer/shaders/prefilter.shader b/fyrox-impl/src/renderer/shaders/opengl/prefilter.shader similarity index 100% rename from fyrox-impl/src/renderer/shaders/prefilter.shader rename to fyrox-impl/src/renderer/shaders/opengl/prefilter.shader diff --git a/fyrox-impl/src/renderer/shaders/skybox.shader b/fyrox-impl/src/renderer/shaders/opengl/skybox.shader similarity index 100% rename from fyrox-impl/src/renderer/shaders/skybox.shader rename to fyrox-impl/src/renderer/shaders/opengl/skybox.shader diff --git a/fyrox-impl/src/renderer/shaders/spot_volumetric.shader b/fyrox-impl/src/renderer/shaders/opengl/spot_volumetric.shader similarity index 100% rename from fyrox-impl/src/renderer/shaders/spot_volumetric.shader rename to fyrox-impl/src/renderer/shaders/opengl/spot_volumetric.shader diff --git a/fyrox-impl/src/renderer/shaders/ssao.shader b/fyrox-impl/src/renderer/shaders/opengl/ssao.shader similarity index 100% rename from fyrox-impl/src/renderer/shaders/ssao.shader rename to fyrox-impl/src/renderer/shaders/opengl/ssao.shader diff --git a/fyrox-impl/src/renderer/shaders/visibility.shader b/fyrox-impl/src/renderer/shaders/opengl/visibility.shader similarity index 100% rename from fyrox-impl/src/renderer/shaders/visibility.shader rename to fyrox-impl/src/renderer/shaders/opengl/visibility.shader diff --git a/fyrox-impl/src/renderer/shaders/visibility_optimizer.shader b/fyrox-impl/src/renderer/shaders/opengl/visibility_optimizer.shader similarity index 100% rename from fyrox-impl/src/renderer/shaders/visibility_optimizer.shader rename to fyrox-impl/src/renderer/shaders/opengl/visibility_optimizer.shader diff --git a/fyrox-impl/src/renderer/shaders/volume_marker_lit.shader b/fyrox-impl/src/renderer/shaders/opengl/volume_marker_lit.shader similarity index 100% rename from fyrox-impl/src/renderer/shaders/volume_marker_lit.shader rename to fyrox-impl/src/renderer/shaders/opengl/volume_marker_lit.shader diff --git a/fyrox-impl/src/renderer/shaders/volume_marker_vol.shader b/fyrox-impl/src/renderer/shaders/opengl/volume_marker_vol.shader similarity index 100% rename from fyrox-impl/src/renderer/shaders/volume_marker_vol.shader rename to fyrox-impl/src/renderer/shaders/opengl/volume_marker_vol.shader diff --git a/fyrox-impl/src/renderer/shaders/wgpu/ambient_light.shader b/fyrox-impl/src/renderer/shaders/wgpu/ambient_light.shader new file mode 100644 index 0000000000..2f3fbda94a --- /dev/null +++ b/fyrox-impl/src/renderer/shaders/wgpu/ambient_light.shader @@ -0,0 +1,174 @@ +( + name: "AmbientLight", + resources: [ + ( + name: "diffuseTexture", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 0 + ), + ( + name: "aoSampler", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 1 + ), + ( + name: "bakedLightingTexture", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 2 + ), + ( + name: "depthTexture", + kind: Texture(kind: DepthSampler2D, fallback: White), + binding: 3 + ), + ( + name: "normalTexture", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 4 + ), + ( + name: "materialTexture", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 5 + ), + ( + name: "prefilteredSpecularMap", + kind: Texture(kind: SamplerCube, fallback: White), + binding: 6 + ), + ( + name: "irradianceMap", + kind: Texture(kind: SamplerCube, fallback: White), + binding: 7 + ), + ( + name: "brdfLUT", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 8 + ), + ( + name: "properties", + kind: PropertyGroup([ + (name: "worldViewProjection", kind: Matrix4()), + (name: "ambientColor", kind: Vector4()), + (name: "cameraPosition", kind: Vector3()), + (name: "invViewProj", kind: Matrix4()), + (name: "skyboxLighting", kind: Bool()), + (name: "environmentLightingBrightness", kind: Float()), + ]), + binding: 0 + ), + ], + passes: [ + ( + name: "Primary", + + draw_parameters: DrawParameters( + cull_face: None, + color_write: ColorMask( + red: true, + green: true, + blue: true, + alpha: true, + ), + depth_write: false, + stencil_test: None, + depth_test: None, + blend: Some(BlendParameters( + func: BlendFunc( + sfactor: SrcAlpha, + dfactor: OneMinusSrcAlpha, + alpha_sfactor: SrcAlpha, + alpha_dfactor: OneMinusSrcAlpha, + ), + equation: BlendEquation( + rgb: Add, + alpha: Add + ) + )), + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Keep, + write_mask: 0xFFFF_FFFF, + ), + scissor_box: None + ), + + vertex_shader: + r#" + struct VertexInput { + @location(0) vertex_position: vec3f, + @location(1) vertex_tex_coord: vec2f, + } + + struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) tex_coord: vec2f, + } + + @vertex fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + output.tex_coord = input.vertex_tex_coord; + output.position = properties.worldViewProjection * vec4f(input.vertex_position, 1.0); + return output; + } + "#, + + fragment_shader: + r#" + @fragment fn fs_main(@location(0) tex_coord: vec2f) -> @location(0) vec4f { + let depth = textureSample(depthTexture_tex, depthTexture_samp, tex_coord); + let fragment_position = S_UnProject(vec3f(tex_coord, depth), properties.invViewProj); + + let albedo = S_SRGBToLinear(textureSample(diffuseTexture_tex, diffuseTexture_samp, tex_coord)); + + let fragment_normal = normalize(textureSample(normalTexture_tex, normalTexture_samp, tex_coord).xyz * 2.0 - 1.0); + + let material = textureSample(materialTexture_tex, materialTexture_samp, tex_coord).rgb; + let metallic = material.x; + let roughness = material.y; + let material_ao = material.z; + + let view_vector = normalize(properties.cameraPosition - fragment_position); + let reflection_vector = -reflect(view_vector, fragment_normal); + + let clamped_cos_view_angle = max(dot(fragment_normal, view_vector), 0.0); + + let cube_map_size = textureDimensions(prefilteredSpecularMap_tex, 0); + let mip = roughness * (floor(log2(f32(cube_map_size.x))) + 1.0); + + var reflection: vec3f; + if (properties.skyboxLighting != 0u) { + reflection = S_SRGBToLinear(textureSampleLevel(prefilteredSpecularMap_tex, prefilteredSpecularMap_samp, reflection_vector, mip)).rgb; + } else { + reflection = properties.ambientColor.rgb; + } + + let F0 = mix(vec3f(0.04), albedo.rgb, metallic); + let F = S_FresnelSchlickRoughness(clamped_cos_view_angle, F0, roughness); + let kD = (vec3f(1.0) - F) * (1.0 - metallic); + + let envBRDF = textureSample(brdfLUT_tex, brdfLUT_samp, vec2f(clamped_cos_view_angle, roughness)).rg; + let specular = reflection * (F * envBRDF.x + envBRDF.y); + + let ambient_occlusion = textureSample(aoSampler_tex, aoSampler_samp, tex_coord).r * material_ao; + let baked_lighting = textureSample(bakedLightingTexture_tex, bakedLightingTexture_samp, tex_coord); + + let irradiance = S_SRGBToLinear(textureSample(irradianceMap_tex, irradianceMap_samp, fragment_normal)).rgb; + + var ambient_lighting: vec3f; + if (properties.skyboxLighting != 0u) { + ambient_lighting = irradiance; + } else { + ambient_lighting = properties.ambientColor.rgb; + } + let diffuse = (baked_lighting.rgb + properties.environmentLightingBrightness * ambient_lighting) * albedo.rgb; + + let output_rgb = (kD * diffuse + specular) * ambient_occlusion; + return vec4f(output_rgb, baked_lighting.a); + } + "#, + ) + ] +) \ No newline at end of file diff --git a/fyrox-impl/src/renderer/shaders/wgpu/blit.shader b/fyrox-impl/src/renderer/shaders/wgpu/blit.shader new file mode 100644 index 0000000000..5a9da7c5fa --- /dev/null +++ b/fyrox-impl/src/renderer/shaders/wgpu/blit.shader @@ -0,0 +1,70 @@ +( + name: "Blit", + resources: [ + ( + name: "diffuseTexture", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 0 + ), + ( + name: "properties", + kind: PropertyGroup([ + (name: "worldViewProjection", kind: Matrix4()), + ]), + binding: 0 + ), + ], + passes: [ + ( + name: "Primary", + + draw_parameters: DrawParameters( + cull_face: None, + color_write: ColorMask( + red: true, + green: true, + blue: true, + alpha: true, + ), + depth_write: true, + stencil_test: None, + depth_test: None, + blend: None, + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Keep, + write_mask: 0xFFFF_FFFF, + ), + scissor_box: None + ), + + vertex_shader: + r#" + struct VertexInput { + @location(0) vertexPosition: vec3f, + @location(1) vertexTexCoord: vec2f, + }; + + struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) texCoord: vec2f, + }; + + @vertex fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + output.texCoord = input.vertexTexCoord; + output.position = properties.worldViewProjection * vec4f(input.vertexPosition, 1.0); + return output; + } + "#, + + fragment_shader: + r#" + @fragment fn fs_main(@location(0) texCoord: vec2f) -> @location(0) vec4f { + return textureSample(diffuseTexture_tex, diffuseTexture_samp, texCoord); + } + "#, + ) + ] +) diff --git a/fyrox-impl/src/renderer/shaders/wgpu/bloom.shader b/fyrox-impl/src/renderer/shaders/wgpu/bloom.shader new file mode 100644 index 0000000000..75a58f555a --- /dev/null +++ b/fyrox-impl/src/renderer/shaders/wgpu/bloom.shader @@ -0,0 +1,76 @@ +( + name: "Bloom", + resources: [ + ( + name: "hdrSampler", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 0 + ), + ( + name: "properties", + kind: PropertyGroup([ + (name: "worldViewProjection", kind: Matrix4()), + (name: "threshold", kind: Float(value: 1.01)), + ]), + binding: 0 + ), + ], + passes: [ + ( + name: "Primary", + + draw_parameters: DrawParameters( + cull_face: None, + color_write: ColorMask( + red: true, + green: true, + blue: true, + alpha: true, + ), + depth_write: false, + stencil_test: None, + depth_test: None, + blend: None, + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Keep, + write_mask: 0xFFFF_FFFF, + ), + scissor_box: None + ), + + vertex_shader: + r#" + struct VertexInput { + @location(0) vertexPosition: vec3f, + @location(1) vertexTexCoord: vec2f, + }; + + struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) texCoord: vec2f, + }; + + @vertex fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + output.texCoord = input.vertexTexCoord; + output.position = properties.worldViewProjection * vec4f(input.vertexPosition, 1.0); + return output; + } + "#, + + fragment_shader: + r#" + @fragment fn fs_main(@location(0) texCoord: vec2f) -> @location(0) vec4f { + let hdrPixel = textureSample(hdrSampler_tex, hdrSampler_samp, texCoord).rgb; + if (S_Luminance(hdrPixel) > properties.threshold) { + return vec4f(hdrPixel, 0.0); + } else { + return vec4f(0.0); + } + } + "#, + ) + ] +) diff --git a/fyrox-impl/src/renderer/shaders/wgpu/blur.shader b/fyrox-impl/src/renderer/shaders/wgpu/blur.shader new file mode 100644 index 0000000000..c8c3bb749d --- /dev/null +++ b/fyrox-impl/src/renderer/shaders/wgpu/blur.shader @@ -0,0 +1,80 @@ +( + name: "Blur", + resources: [ + ( + name: "inputTexture", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 0 + ), + ( + name: "properties", + kind: PropertyGroup([ + (name: "worldViewProjection", kind: Matrix4()), + ]), + binding: 0 + ), + ], + passes: [ + ( + name: "Primary", + + draw_parameters: DrawParameters( + cull_face: None, + color_write: ColorMask( + red: true, + green: true, + blue: true, + alpha: true, + ), + depth_write: false, + stencil_test: None, + depth_test: None, + blend: None, + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Keep, + write_mask: 0xFFFF_FFFF, + ), + scissor_box: None + ), + + vertex_shader: + r#" + struct VertexInput { + @location(0) vertex_position: vec3f, + @location(1) vertex_tex_coord: vec2f, + } + + struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) tex_coord: vec2f, + } + + @vertex fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + output.tex_coord = input.vertex_tex_coord; + output.position = properties.worldViewProjection * vec4f(input.vertex_position, 1.0); + return output; + } + "#, + + fragment_shader: + r#" + // Simple 4x4 box blur. + + @fragment fn fs_main(@location(0) tex_coord: vec2f) -> @location(0) f32 { + let texel_size = 1.0 / vec2f(textureDimensions(inputTexture_tex, 0)); + var result: f32 = 0.0; + for (var y: i32 = -2; y < 2; y++) { + for (var x: i32 = -2; x < 2; x++) { + let offset = vec2f(f32(x), f32(y)) * texel_size; + result += textureSample(inputTexture_tex, inputTexture_samp, tex_coord + offset).r; + } + } + return result / 16.0; + } + "#, + ) + ] +) \ No newline at end of file diff --git a/fyrox-impl/src/renderer/shaders/wgpu/debug.shader b/fyrox-impl/src/renderer/shaders/wgpu/debug.shader new file mode 100644 index 0000000000..c0005c2dfe --- /dev/null +++ b/fyrox-impl/src/renderer/shaders/wgpu/debug.shader @@ -0,0 +1,65 @@ +( + name: "Debug", + resources: [ + ( + name: "properties", + kind: PropertyGroup([ + (name: "worldViewProjection", kind: Matrix4()), + ]), + binding: 0 + ), + ], + passes: [ + ( + name: "Primary", + + draw_parameters: DrawParameters( + cull_face: None, + color_write: ColorMask( + red: true, + green: true, + blue: true, + alpha: true, + ), + depth_write: false, + stencil_test: None, + depth_test: Some(LessOrEqual), + blend: None, + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Keep, + write_mask: 0xFFFF_FFFF, + ), + scissor_box: None + ), + + vertex_shader: + r#" + struct VertexInput { + @location(0) vertexPosition: vec3f, + @location(1) vertexColor: vec4f, + }; + + struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) color: vec4f, + }; + + @vertex fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + output.color = input.vertexColor; + output.position = properties.worldViewProjection * vec4f(input.vertexPosition, 1.0); + return output; + } + "#, + + fragment_shader: + r#" + @fragment fn fs_main(@location(0) color: vec4f) -> @location(0) vec4f { + return color; + } + "#, + ) + ] +) diff --git a/fyrox-impl/src/renderer/shaders/wgpu/decal.shader b/fyrox-impl/src/renderer/shaders/wgpu/decal.shader new file mode 100644 index 0000000000..7f3cb7c34b --- /dev/null +++ b/fyrox-impl/src/renderer/shaders/wgpu/decal.shader @@ -0,0 +1,150 @@ +( + name: "Decal", + resources: [ + ( + name: "sceneDepth", + kind: Texture(kind: DepthSampler2D, fallback: White), + binding: 0 + ), + ( + name: "diffuseTexture", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 1 + ), + ( + name: "normalTexture", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 2 + ), + ( + name: "decalMask", + kind: Texture(kind: USampler2D, fallback: White), + binding: 3 + ), + ( + name: "properties", + kind: PropertyGroup([ + (name: "worldViewProjection", kind: Matrix4()), + (name: "invViewProj", kind: Matrix4()), + (name: "invWorldDecal", kind: Matrix4()), + (name: "resolution", kind: Vector2()), + (name: "color", kind: Vector4()), + (name: "layerIndex", kind: UInt()), + ]), + binding: 0 + ), + ], + passes: [ + ( + name: "Primary", + + draw_parameters: DrawParameters( + cull_face: None, + color_write: ColorMask( + red: true, + green: true, + blue: true, + alpha: true, + ), + depth_write: false, + stencil_test: None, + depth_test: None, + blend: Some(BlendParameters( + func: BlendFunc( + sfactor: SrcAlpha, + dfactor: OneMinusSrcAlpha, + alpha_sfactor: SrcAlpha, + alpha_dfactor: OneMinusSrcAlpha, + ), + equation: BlendEquation( + rgb: Add, + alpha: Add + ) + )), + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Keep, + write_mask: 0xFFFF_FFFF, + ), + scissor_box: None + ), + + vertex_shader: + r#" + struct VertexInput { + @location(0) vertex_position: vec3f, + } + + struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) clip_space_position: vec4f, + } + + @vertex fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + output.position = properties.worldViewProjection * vec4f(input.vertex_position, 1.0); + output.clip_space_position = output.position; + return output; + } + "#, + + fragment_shader: + r#" + struct FragmentOutput { + @location(0) out_diffuse_map: vec4f, + @location(1) out_normal_map: vec4f, + } + + @fragment fn fs_main(@location(0) clip_space_position: vec4f) -> FragmentOutput { + let screen_pos = clip_space_position.xy / clip_space_position.w; + + let tex_coord = vec2f( + (1.0 + screen_pos.x) / 2.0 + (0.5 / properties.resolution.x), + (1.0 + screen_pos.y) / 2.0 + (0.5 / properties.resolution.y) + ); + + let mask_index = textureLoad(decalMask_tex, vec2i(tex_coord * vec2f(textureDimensions(decalMask_tex))), 0); + + // Masking. + if (mask_index.r != properties.layerIndex) { + discard; + } + + let scene_depth = textureSample(sceneDepth_tex, sceneDepth_samp, tex_coord); + + let scene_world_position = S_UnProject(vec3f(tex_coord, scene_depth), properties.invViewProj); + + let decal_space_position = (properties.invWorldDecal * vec4f(scene_world_position, 1.0)).xyz; + + // Check if scene pixel is not inside decal bounds. + let dpos = vec3f(0.5) - abs(decal_space_position); + if (dpos.x < 0.0 || dpos.y < 0.0 || dpos.z < 0.0) { + discard; + } + + let decal_tex_coord = decal_space_position.xz + 0.5; + + let diffuse = properties.color * textureSample(diffuseTexture_tex, diffuseTexture_samp, decal_tex_coord); + + let fragment_tangent = dpdx(scene_world_position); + let fragment_binormal = dpdy(scene_world_position); + let fragment_normal = cross(fragment_tangent, fragment_binormal); + + var tangent_to_world: mat3x3f; + tangent_to_world[0] = normalize(fragment_tangent); // Tangent + tangent_to_world[1] = normalize(fragment_binormal); // Binormal + tangent_to_world[2] = normalize(fragment_normal); // Normal + + let raw_normal = (textureSample(normalTexture_tex, normalTexture_samp, decal_tex_coord) * 2.0 - 1.0).xyz; + let world_space_normal = tangent_to_world * raw_normal; + + var output: FragmentOutput; + output.out_diffuse_map = diffuse; + output.out_normal_map = vec4f(world_space_normal * 0.5 + 0.5, diffuse.a); + return output; + } + "#, + ) + ] +) \ No newline at end of file diff --git a/fyrox-impl/src/renderer/shaders/wgpu/deferred_directional_light.shader b/fyrox-impl/src/renderer/shaders/wgpu/deferred_directional_light.shader new file mode 100644 index 0000000000..2dec5a6e70 --- /dev/null +++ b/fyrox-impl/src/renderer/shaders/wgpu/deferred_directional_light.shader @@ -0,0 +1,155 @@ +( + name: "DeferredDirectionalLight", + resources: [ + ( + name: "depthTexture", + kind: Texture(kind: DepthSampler2D, fallback: White), + binding: 0 + ), + ( + name: "colorTexture", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 1 + ), + ( + name: "normalTexture", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 2 + ), + ( + name: "materialTexture", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 3 + ), + ( + name: "shadowCascade0", + kind: Texture(kind: DepthSampler2D, fallback: White), + binding: 4 + ), + ( + name: "shadowCascade1", + kind: Texture(kind: DepthSampler2D, fallback: White), + binding: 5 + ), + ( + name: "shadowCascade2", + kind: Texture(kind: DepthSampler2D, fallback: White), + binding: 6 + ), + ( + name: "properties", + kind: PropertyGroup([ + (name: "worldViewProjection", kind: Matrix4()), + (name: "viewMatrix", kind: Matrix4()), + (name: "invViewProj", kind: Matrix4()), + (name: "lightViewProjMatrices", kind: Matrix4Array(max_len: 3, value: [])), + (name: "lightColor", kind: Vector4()), + (name: "lightDirection", kind: Vector3()), + (name: "cameraPosition", kind: Vector3()), + (name: "lightIntensity", kind: Float()), + (name: "shadowsEnabled", kind: Bool()), + (name: "shadowBias", kind: Float()), + (name: "softShadows", kind: Bool()), + (name: "cascadeDistances", kind: FloatArray(max_len: 3, value: [])), + ]), + binding: 0 + ), + ], + passes: [ + ( + name: "Primary", + + draw_parameters: DrawParameters( + cull_face: None, + color_write: ColorMask( + red: true, + green: true, + blue: true, + alpha: true, + ), + depth_write: false, + stencil_test: None, + depth_test: None, + blend: Some(BlendParameters( + func: BlendFunc( + sfactor: One, + dfactor: One, + alpha_sfactor: One, + alpha_dfactor: One, + ), + equation: BlendEquation( + rgb: Add, + alpha: Add + ) + )), + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Keep, + write_mask: 0xFFFF_FFFF, + ), + scissor_box: None + ), + + vertex_shader: + r#" + struct VertexInput { + @location(0) vertex_position: vec3f, + @location(1) vertex_tex_coord: vec2f, + } + + struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) tex_coord: vec2f, + } + + @vertex fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + output.position = properties.worldViewProjection * vec4f(input.vertex_position, 1.0); + output.tex_coord = input.vertex_tex_coord; + return output; + } + "#, + + fragment_shader: + r#" + @fragment fn fs_main(@location(0) tex_coord: vec2f) -> @location(0) vec4f { + let material = textureSample(materialTexture_tex, materialTexture_samp, tex_coord).rgb; + + let fragment_position = S_UnProject(vec3f(tex_coord, textureSample(depthTexture_tex, depthTexture_samp, tex_coord)), properties.invViewProj); + let diffuse_color = textureSample(colorTexture_tex, colorTexture_samp, tex_coord); + + var ctx: TPBRContext; + ctx.albedo = S_SRGBToLinear(diffuse_color).rgb; + ctx.fragmentToLight = properties.lightDirection; + ctx.fragmentNormal = normalize(textureSample(normalTexture_tex, normalTexture_samp, tex_coord).xyz * 2.0 - 1.0); + ctx.lightColor = properties.lightColor.rgb; + ctx.metallic = material.x; + ctx.roughness = material.y; + ctx.viewVector = normalize(properties.cameraPosition - fragment_position); + + let lighting = S_PBR_CalculateLight(ctx); + + let fragment_z_view_space = abs((properties.viewMatrix * vec4f(fragment_position, 1.0)).z); + + var shadow: f32 = 1.0; + if (fragment_z_view_space <= properties.cascadeDistances[0].x) { + let inv_size = 1.0 / f32(textureDimensions(shadowCascade0_tex).x); + shadow = S_SpotShadowFactor_Depth(properties.shadowsEnabled != 0u, properties.softShadows != 0u, + properties.shadowBias, fragment_position, properties.lightViewProjMatrices[0], inv_size, shadowCascade0_tex, shadowCascade0_samp); + } else if (fragment_z_view_space <= properties.cascadeDistances[1].x) { + let inv_size = 1.0 / f32(textureDimensions(shadowCascade1_tex).x); + shadow = S_SpotShadowFactor_Depth(properties.shadowsEnabled != 0u, properties.softShadows != 0u, + properties.shadowBias, fragment_position, properties.lightViewProjMatrices[1], inv_size, shadowCascade1_tex, shadowCascade1_samp); + } else if (fragment_z_view_space <= properties.cascadeDistances[2].x) { + let inv_size = 1.0 / f32(textureDimensions(shadowCascade2_tex).x); + shadow = S_SpotShadowFactor_Depth(properties.shadowsEnabled != 0u, properties.softShadows != 0u, + properties.shadowBias, fragment_position, properties.lightViewProjMatrices[2], inv_size, shadowCascade2_tex, shadowCascade2_samp); + } + + return shadow * vec4f(properties.lightIntensity * lighting, diffuse_color.a); + } + "#, + ) + ] +) \ No newline at end of file diff --git a/fyrox-impl/src/renderer/shaders/wgpu/deferred_point_light.shader b/fyrox-impl/src/renderer/shaders/wgpu/deferred_point_light.shader new file mode 100644 index 0000000000..12d4cbf271 --- /dev/null +++ b/fyrox-impl/src/renderer/shaders/wgpu/deferred_point_light.shader @@ -0,0 +1,140 @@ +( + name: "DeferredPointLight", + resources: [ + ( + name: "depthTexture", + kind: Texture(kind: DepthSampler2D, fallback: White), + binding: 0 + ), + ( + name: "colorTexture", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 1 + ), + ( + name: "normalTexture", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 2 + ), + ( + name: "materialTexture", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 3 + ), + ( + name: "pointShadowTexture", + kind: Texture(kind: SamplerCube, fallback: White), + binding: 4 + ), + ( + name: "properties", + kind: PropertyGroup([ + (name: "worldViewProjection", kind: Matrix4()), + (name: "invViewProj", kind: Matrix4()), + (name: "lightColor", kind: Vector4()), + (name: "lightPos", kind: Vector3()), + (name: "cameraPosition", kind: Vector3()), + (name: "lightRadius", kind: Float()), + (name: "shadowBias", kind: Float()), + (name: "lightIntensity", kind: Float()), + (name: "shadowAlpha", kind: Float()), + (name: "softShadows", kind: Bool()), + (name: "shadowsEnabled", kind: Bool()), + ]), + binding: 0 + ), + ], + passes: [ + ( + name: "Primary", + + draw_parameters: DrawParameters( + cull_face: None, + color_write: ColorMask( + red: true, + green: true, + blue: true, + alpha: true, + ), + depth_write: false, + stencil_test: Some(StencilFunc( + func: NotEqual, + ref_value: 0, + mask: 0xFFFF_FFFF + )), + depth_test: None, + blend: Some(BlendParameters( + func: BlendFunc( + sfactor: One, + dfactor: One, + alpha_sfactor: One, + alpha_dfactor: One, + ), + equation: BlendEquation( + rgb: Add, + alpha: Add + ) + )), + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Zero, + write_mask: 0xFFFF_FFFF, + ), + scissor_box: None + ), + + vertex_shader: + r#" + struct VertexInput { + @location(0) vertex_position: vec3f, + @location(1) vertex_tex_coord: vec2f, + } + + struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) tex_coord: vec2f, + } + + @vertex fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + output.position = properties.worldViewProjection * vec4f(input.vertex_position, 1.0); + output.tex_coord = input.vertex_tex_coord; + return output; + } + "#, + + fragment_shader: + r#" + @fragment fn fs_main(@location(0) tex_coord: vec2f) -> @location(0) vec4f { + let material = textureSample(materialTexture_tex, materialTexture_samp, tex_coord).rgb; + + let fragment_position = S_UnProject(vec3f(tex_coord, textureSample(depthTexture_tex, depthTexture_samp, tex_coord)), properties.invViewProj); + let fragment_to_light = properties.lightPos - fragment_position; + let dist = length(fragment_to_light); + + let diffuse_color = textureSample(colorTexture_tex, colorTexture_samp, tex_coord); + + var ctx: TPBRContext; + ctx.albedo = S_SRGBToLinear(diffuse_color).rgb; + ctx.fragmentToLight = fragment_to_light / dist; + ctx.fragmentNormal = normalize(textureSample(normalTexture_tex, normalTexture_samp, tex_coord).xyz * 2.0 - 1.0); + ctx.lightColor = properties.lightColor.rgb; + ctx.metallic = material.x; + ctx.roughness = material.y; + ctx.viewVector = normalize(properties.cameraPosition - fragment_position); + + let lighting = S_PBR_CalculateLight(ctx); + + let distance_attenuation = S_LightDistanceAttenuation(dist, properties.lightRadius); + + let shadow = S_PointShadow( + properties.shadowsEnabled != 0u, properties.softShadows != 0u, dist, properties.shadowBias, ctx.fragmentToLight, pointShadowTexture_tex, pointShadowTexture_samp); + let final_shadow = mix(1.0, shadow, properties.shadowAlpha); + + return vec4f(properties.lightIntensity * distance_attenuation * final_shadow * lighting, diffuse_color.a); + } + "#, + ) + ] +) \ No newline at end of file diff --git a/fyrox-impl/src/renderer/shaders/wgpu/deferred_spot_light.shader b/fyrox-impl/src/renderer/shaders/wgpu/deferred_spot_light.shader new file mode 100644 index 0000000000..69940bc0e9 --- /dev/null +++ b/fyrox-impl/src/renderer/shaders/wgpu/deferred_spot_light.shader @@ -0,0 +1,160 @@ +( + name: "DeferredSpotLight", + resources: [ + ( + name: "depthTexture", + kind: Texture(kind: DepthSampler2D, fallback: White), + binding: 0 + ), + ( + name: "colorTexture", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 1 + ), + ( + name: "normalTexture", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 2 + ), + ( + name: "materialTexture", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 3 + ), + ( + name: "spotShadowTexture", + kind: Texture(kind: DepthSampler2D, fallback: White), + binding: 4 + ), + ( + name: "cookieTexture", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 5 + ), + ( + name: "properties", + kind: PropertyGroup([ + (name: "worldViewProjection", kind: Matrix4()), + (name: "lightViewProjMatrix", kind: Matrix4()), + (name: "invViewProj", kind: Matrix4()), + (name: "lightPos", kind: Vector3()), + (name: "lightColor", kind: Vector4()), + (name: "cameraPosition", kind: Vector3()), + (name: "lightDirection", kind: Vector3()), + (name: "lightRadius", kind: Float()), + (name: "halfHotspotConeAngleCos", kind: Float()), + (name: "halfConeAngleCos", kind: Float()), + (name: "shadowMapInvSize", kind: Float()), + (name: "shadowBias", kind: Float()), + (name: "lightIntensity", kind: Float()), + (name: "shadowAlpha", kind: Float()), + (name: "cookieEnabled", kind: Bool()), + (name: "shadowsEnabled", kind: Bool()), + (name: "softShadows", kind: Bool()), + ]), + binding: 0 + ), + ], + passes: [ + ( + name: "Primary", + + draw_parameters: DrawParameters( + cull_face: None, + color_write: ColorMask( + red: true, + green: true, + blue: true, + alpha: true, + ), + depth_write: false, + stencil_test: Some(StencilFunc( + func: NotEqual, + ref_value: 0, + mask: 0xFFFF_FFFF + )), + depth_test: None, + blend: Some(BlendParameters( + func: BlendFunc( + sfactor: One, + dfactor: One, + alpha_sfactor: One, + alpha_dfactor: One, + ), + equation: BlendEquation( + rgb: Add, + alpha: Add + ) + )), + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Zero, + write_mask: 0xFFFF_FFFF, + ), + scissor_box: None + ), + + vertex_shader: + r#" + struct VertexInput { + @location(0) vertex_position: vec3f, + @location(1) vertex_tex_coord: vec2f, + } + + struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) tex_coord: vec2f, + } + + @vertex fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + output.position = properties.worldViewProjection * vec4f(input.vertex_position, 1.0); + output.tex_coord = input.vertex_tex_coord; + return output; + } + "#, + + fragment_shader: + r#" + @fragment fn fs_main(@location(0) tex_coord: vec2f) -> @location(0) vec4f { + let material = textureSample(materialTexture_tex, materialTexture_samp, tex_coord).rgb; + + let fragment_position = S_UnProject(vec3f(tex_coord, textureSample(depthTexture_tex, depthTexture_samp, tex_coord)), properties.invViewProj); + let fragment_to_light = properties.lightPos - fragment_position; + let dist = length(fragment_to_light); + let diffuse_color = textureSample(colorTexture_tex, colorTexture_samp, tex_coord); + + var ctx: TPBRContext; + ctx.albedo = S_SRGBToLinear(diffuse_color).rgb; + ctx.fragmentToLight = fragment_to_light / dist; + ctx.fragmentNormal = normalize(textureSample(normalTexture_tex, normalTexture_samp, tex_coord).xyz * 2.0 - 1.0); + ctx.lightColor = properties.lightColor.rgb; + ctx.metallic = material.x; + ctx.roughness = material.y; + ctx.viewVector = normalize(properties.cameraPosition - fragment_position); + + let lighting = S_PBR_CalculateLight(ctx); + + let distance_attenuation = S_LightDistanceAttenuation(dist, properties.lightRadius); + + let spot_angle_cos = dot(properties.lightDirection, ctx.fragmentToLight); + let cone_factor = smoothstep(properties.halfConeAngleCos, properties.halfHotspotConeAngleCos, spot_angle_cos); + + let shadow = S_SpotShadowFactor_Depth( + properties.shadowsEnabled != 0u, properties.softShadows != 0u, properties.shadowBias, fragment_position, + properties.lightViewProjMatrix, properties.shadowMapInvSize, spotShadowTexture_tex, spotShadowTexture_samp); + let final_shadow = mix(1.0, shadow, properties.shadowAlpha); + + var cookie_attenuation = vec4f(1.0); + if (properties.cookieEnabled != 0u) { + let cookie_tex_coords = S_Project(fragment_position, properties.lightViewProjMatrix).xy; + cookie_attenuation = textureSample(cookieTexture_tex, cookieTexture_samp, cookie_tex_coords); + } + + return cookie_attenuation * vec4f(distance_attenuation * properties.lightIntensity * cone_factor * final_shadow * lighting, diffuse_color.a); + } + "#, + ) + ] +) \ No newline at end of file diff --git a/fyrox-impl/src/renderer/shaders/wgpu/fxaa.shader b/fyrox-impl/src/renderer/shaders/wgpu/fxaa.shader new file mode 100644 index 0000000000..a6a6fd29a6 --- /dev/null +++ b/fyrox-impl/src/renderer/shaders/wgpu/fxaa.shader @@ -0,0 +1,209 @@ +( + name: "FXAA", + resources: [ + ( + name: "screenTexture", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 0 + ), + ( + name: "properties", + kind: PropertyGroup([ + (name: "worldViewProjection", kind: Matrix4()), + (name: "inverseScreenSize", kind: Vector2()), + ]), + binding: 0 + ), + ], + passes: [ + ( + name: "Primary", + + draw_parameters: DrawParameters( + cull_face: None, + color_write: ColorMask( + red: true, + green: true, + blue: true, + alpha: true, + ), + depth_write: false, + stencil_test: None, + depth_test: None, + blend: None, + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Keep, + write_mask: 0xFFFF_FFFF, + ), + scissor_box: None + ), + + vertex_shader: + r#" + struct VertexInput { + @location(0) vertexPosition: vec3f, + @location(1) vertexTexCoord: vec2f, + }; + + struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) texCoord: vec2f, + }; + + @vertex fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + output.texCoord = input.vertexTexCoord; + output.position = properties.worldViewProjection * vec4f(input.vertexPosition, 1.0); + return output; + } + "#, + + fragment_shader: + r#" + // NVIDIA FXAA 3.11 + // Original source code by TIMOTHY LOTTES + // WGSL port + + const EDGE_THRESHOLD_MIN: f32 = 0.0312; + const EDGE_THRESHOLD_MAX: f32 = 0.125; + const ITERATIONS: i32 = 12; + const SUBPIXEL_QUALITY: f32 = 0.75; + + fn quality(q: i32) -> f32 { + if (q < 5) { return 1.0; } + if (q > 5) { + if (q < 10) { return 2.0; } + if (q < 11) { return 4.0; } + return 8.0; + } + return 1.5; + } + + fn rgb2luma(rgb: vec3f) -> f32 { + return sqrt(dot(rgb, vec3f(0.299, 0.587, 0.114))); + } + + @fragment fn fs_main(@location(0) texCoord: vec2f) -> @location(0) vec4f { + let colorCenter = textureSample(screenTexture_tex, screenTexture_samp, texCoord); + let lumaCenter = rgb2luma(colorCenter.rgb); + + let lumaDown = rgb2luma(textureSampleLevel(screenTexture_tex, screenTexture_samp, texCoord + vec2f(0.0, -properties.inverseScreenSize.y), 0.0).rgb); + let lumaUp = rgb2luma(textureSampleLevel(screenTexture_tex, screenTexture_samp, texCoord + vec2f(0.0, properties.inverseScreenSize.y), 0.0).rgb); + let lumaLeft = rgb2luma(textureSampleLevel(screenTexture_tex, screenTexture_samp, texCoord + vec2f(-properties.inverseScreenSize.x, 0.0), 0.0).rgb); + let lumaRight = rgb2luma(textureSampleLevel(screenTexture_tex, screenTexture_samp, texCoord + vec2f(properties.inverseScreenSize.x, 0.0), 0.0).rgb); + + let lumaMin = min(lumaCenter, min(min(lumaDown, lumaUp), min(lumaLeft, lumaRight))); + let lumaMax = max(lumaCenter, max(max(lumaDown, lumaUp), max(lumaLeft, lumaRight))); + let lumaRange = lumaMax - lumaMin; + + if (lumaRange < max(EDGE_THRESHOLD_MIN, lumaMax * EDGE_THRESHOLD_MAX)) { + return colorCenter; + } + + let lumaDownLeft = rgb2luma(textureSampleLevel(screenTexture_tex, screenTexture_samp, texCoord + vec2f(-properties.inverseScreenSize.x, -properties.inverseScreenSize.y), 0.0).rgb); + let lumaUpRight = rgb2luma(textureSampleLevel(screenTexture_tex, screenTexture_samp, texCoord + vec2f(properties.inverseScreenSize.x, properties.inverseScreenSize.y), 0.0).rgb); + let lumaUpLeft = rgb2luma(textureSampleLevel(screenTexture_tex, screenTexture_samp, texCoord + vec2f(-properties.inverseScreenSize.x, properties.inverseScreenSize.y), 0.0).rgb); + let lumaDownRight = rgb2luma(textureSampleLevel(screenTexture_tex, screenTexture_samp, texCoord + vec2f(properties.inverseScreenSize.x, -properties.inverseScreenSize.y), 0.0).rgb); + + let lumaDownUp = lumaDown + lumaUp; + let lumaLeftRight = lumaLeft + lumaRight; + let lumaLeftCorners = lumaDownLeft + lumaUpLeft; + let lumaDownCorners = lumaDownLeft + lumaDownRight; + let lumaRightCorners = lumaDownRight + lumaUpRight; + let lumaUpCorners = lumaUpRight + lumaUpLeft; + + let edgeHorizontal = abs(-2.0 * lumaLeft + lumaLeftCorners) + abs(-2.0 * lumaCenter + lumaDownUp) * 2.0 + abs(-2.0 * lumaRight + lumaRightCorners); + let edgeVertical = abs(-2.0 * lumaUp + lumaUpCorners) + abs(-2.0 * lumaCenter + lumaLeftRight) * 2.0 + abs(-2.0 * lumaDown + lumaDownCorners); + + let isHorizontal = (edgeHorizontal >= edgeVertical); + var stepLength = select(properties.inverseScreenSize.x, properties.inverseScreenSize.y, isHorizontal); + + let luma1 = select(lumaLeft, lumaDown, isHorizontal); + let luma2 = select(lumaRight, lumaUp, isHorizontal); + let gradient1 = luma1 - lumaCenter; + let gradient2 = luma2 - lumaCenter; + let is1Steepest = abs(gradient1) >= abs(gradient2); + let gradientScaled = 0.25 * max(abs(gradient1), abs(gradient2)); + + var lumaLocalAverage: f32; + if (is1Steepest) { + stepLength = -stepLength; + lumaLocalAverage = 0.5 * (luma1 + lumaCenter); + } else { + lumaLocalAverage = 0.5 * (luma2 + lumaCenter); + } + + var currentUv = texCoord; + if (isHorizontal) { + currentUv.y += stepLength * 0.5; + } else { + currentUv.x += stepLength * 0.5; + } + + let offset = select(vec2f(0.0, properties.inverseScreenSize.y), vec2f(properties.inverseScreenSize.x, 0.0), isHorizontal); + var uv1 = currentUv - offset * quality(0); + var uv2 = currentUv + offset * quality(0); + + var lumaEnd1 = rgb2luma(textureSampleLevel(screenTexture_tex, screenTexture_samp, uv1, 0.0).rgb) - lumaLocalAverage; + var lumaEnd2 = rgb2luma(textureSampleLevel(screenTexture_tex, screenTexture_samp, uv2, 0.0).rgb) - lumaLocalAverage; + + var reached1 = abs(lumaEnd1) >= gradientScaled; + var reached2 = abs(lumaEnd2) >= gradientScaled; + var reachedBoth = reached1 && reached2; + + if (!reached1) { uv1 -= offset * quality(1); } + if (!reached2) { uv2 += offset * quality(1); } + + if (!reachedBoth) { + for (var i: i32 = 2; i < ITERATIONS; i++) { + if (!reached1) { + lumaEnd1 = rgb2luma(textureSampleLevel(screenTexture_tex, screenTexture_samp, uv1, 0.0).rgb) - lumaLocalAverage; + } + if (!reached2) { + lumaEnd2 = rgb2luma(textureSampleLevel(screenTexture_tex, screenTexture_samp, uv2, 0.0).rgb) - lumaLocalAverage; + } + reached1 = abs(lumaEnd1) >= gradientScaled; + reached2 = abs(lumaEnd2) >= gradientScaled; + reachedBoth = reached1 && reached2; + + if (!reached1) { uv1 -= offset * quality(i); } + if (!reached2) { uv2 += offset * quality(i); } + if (reachedBoth) { break; } + } + } + + let distance1 = select(texCoord.y - uv1.y, texCoord.x - uv1.x, isHorizontal); + let distance2 = select(uv2.y - texCoord.y, uv2.x - texCoord.x, isHorizontal); + let isDirection1 = distance1 < distance2; + let distanceFinal = min(distance1, distance2); + let edgeThickness = (distance1 + distance2); + let isLumaCenterSmaller = lumaCenter < lumaLocalAverage; + let correctVariation1 = (lumaEnd1 < 0.0) != isLumaCenterSmaller; + let correctVariation2 = (lumaEnd2 < 0.0) != isLumaCenterSmaller; + let correctVariation = select(correctVariation2, correctVariation1, isDirection1); + + let pixelOffset = -distanceFinal / edgeThickness + 0.5; + var finalOffset = select(0.0, pixelOffset, correctVariation); + + let lumaAverage = (1.0 / 12.0) * (2.0 * (lumaDownUp + lumaLeftRight) + lumaLeftCorners + lumaRightCorners); + let subPixelOffset1 = clamp(abs(lumaAverage - lumaCenter) / lumaRange, 0.0, 1.0); + let subPixelOffset2 = (-2.0 * subPixelOffset1 + 3.0) * subPixelOffset1 * subPixelOffset1; + let subPixelOffsetFinal = subPixelOffset2 * subPixelOffset2 * SUBPIXEL_QUALITY; + finalOffset = max(finalOffset, subPixelOffsetFinal); + + var finalUv = texCoord; + if (isHorizontal) { + finalUv.y += finalOffset * stepLength; + } else { + finalUv.x += finalOffset * stepLength; + } + + let finalColor = textureSampleLevel(screenTexture_tex, screenTexture_samp, finalUv, 0.0).rgb; + return vec4f(finalColor, 1.0); + } + "#, + ) + ] +) diff --git a/fyrox-impl/src/renderer/shaders/wgpu/gaussian_blur.shader b/fyrox-impl/src/renderer/shaders/wgpu/gaussian_blur.shader new file mode 100644 index 0000000000..ffbdd68bd6 --- /dev/null +++ b/fyrox-impl/src/renderer/shaders/wgpu/gaussian_blur.shader @@ -0,0 +1,94 @@ +( + name: "GaussianBlur", + resources: [ + ( + name: "image", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 0 + ), + ( + name: "properties", + kind: PropertyGroup([ + (name: "worldViewProjection", kind: Matrix4()), + (name: "pixelSize", kind: Vector2()), + (name: "horizontal", kind: Bool()), + ]), + binding: 0 + ), + ], + passes: [ + ( + name: "Primary", + + draw_parameters: DrawParameters( + cull_face: None, + color_write: ColorMask( + red: true, + green: true, + blue: true, + alpha: true, + ), + depth_write: false, + stencil_test: None, + depth_test: None, + blend: None, + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Keep, + write_mask: 0xFFFF_FFFF, + ), + scissor_box: None + ), + + vertex_shader: + r#" + struct VertexInput { + @location(0) vertexPosition: vec3f, + @location(1) vertexTexCoord: vec2f, + }; + + struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) texCoord: vec2f, + }; + + @vertex fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + output.texCoord = input.vertexTexCoord; + output.position = properties.worldViewProjection * vec4f(input.vertexPosition, 1.0); + return output; + } + "#, + + fragment_shader: + r#" + @fragment fn fs_main(@location(0) texCoord: vec2f) -> @location(0) vec4f { + const weights = array(0.227027, 0.1945946, 0.1216216, 0.054054, 0.016216); + + let center = textureSample(image_tex, image_samp, texCoord); + + var result = center.rgb * weights[0]; + + if (properties.horizontal != 0u) { + for (var i: i32 = 1; i < 5; i++) { + let fi = f32(i); + + result += textureSample(image_tex, image_samp, texCoord + vec2f(properties.pixelSize.x * fi, 0.0)).rgb * weights[i]; + result += textureSample(image_tex, image_samp, texCoord - vec2f(properties.pixelSize.x * fi, 0.0)).rgb * weights[i]; + } + } else { + for (var i: i32 = 1; i < 5; i++) { + let fi = f32(i); + + result += textureSample(image_tex, image_samp, texCoord + vec2f(0.0, properties.pixelSize.y * fi)).rgb * weights[i]; + result += textureSample(image_tex, image_samp, texCoord - vec2f(0.0, properties.pixelSize.y * fi)).rgb * weights[i]; + } + } + + return vec4f(result, center.a); + } + "#, + ) + ] +) diff --git a/fyrox-impl/src/renderer/shaders/wgpu/hdr_adaptation.shader b/fyrox-impl/src/renderer/shaders/wgpu/hdr_adaptation.shader new file mode 100644 index 0000000000..12d4a261dc --- /dev/null +++ b/fyrox-impl/src/renderer/shaders/wgpu/hdr_adaptation.shader @@ -0,0 +1,78 @@ +( + name: "HdrAdaptation", + resources: [ + ( + name: "oldLumSampler", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 0 + ), + ( + name: "newLumSampler", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 1 + ), + ( + name: "properties", + kind: PropertyGroup([ + (name: "worldViewProjection", kind: Matrix4()), + (name: "speed", kind: Float()), + ]), + binding: 0 + ), + ], + passes: [ + ( + name: "Primary", + + draw_parameters: DrawParameters( + cull_face: None, + color_write: ColorMask( + red: true, + green: true, + blue: true, + alpha: true, + ), + depth_write: false, + stencil_test: None, + depth_test: None, + blend: None, + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Zero, + write_mask: 0xFFFF_FFFF, + ), + scissor_box: None + ), + + vertex_shader: + r#" + struct VertexInput { + @location(0) vertexPosition: vec3f, + @location(1) vertexTexCoord: vec2f, + }; + + struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) texCoord: vec2f, + }; + + @vertex fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + output.texCoord = input.vertexTexCoord; + output.position = properties.worldViewProjection * vec4f(input.vertexPosition, 1.0); + return output; + } + "#, + + fragment_shader: + r#" + @fragment fn fs_main() -> @location(0) f32 { + let oldLum = textureSample(oldLumSampler_tex, oldLumSampler_samp, vec2f(0.5, 0.5)).r; + let newLum = textureSample(newLumSampler_tex, newLumSampler_samp, vec2f(0.5, 0.5)).r; + return oldLum + (newLum - oldLum) * properties.speed; + } + "#, + ) + ] +) diff --git a/fyrox-impl/src/renderer/shaders/wgpu/hdr_downscale.shader b/fyrox-impl/src/renderer/shaders/wgpu/hdr_downscale.shader new file mode 100644 index 0000000000..e9de91647b --- /dev/null +++ b/fyrox-impl/src/renderer/shaders/wgpu/hdr_downscale.shader @@ -0,0 +1,97 @@ +( + name: "HdrDownscale", + resources: [ + ( + name: "lumSampler", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 0 + ), + ( + name: "properties", + kind: PropertyGroup([ + (name: "worldViewProjection", kind: Matrix4()), + (name: "invSize", kind: Vector2()), + ]), + binding: 0 + ), + ], + passes: [ + ( + name: "Primary", + + draw_parameters: DrawParameters( + cull_face: None, + color_write: ColorMask( + red: true, + green: true, + blue: true, + alpha: true, + ), + depth_write: false, + stencil_test: None, + depth_test: None, + blend: None, + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Keep, + write_mask: 0xFFFF_FFFF, + ), + scissor_box: None + ), + + vertex_shader: + r#" + struct VertexInput { + @location(0) vertexPosition: vec3f, + @location(1) vertexTexCoord: vec2f, + }; + + struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) texCoord: vec2f, + }; + + @vertex fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + output.texCoord = input.vertexTexCoord; + output.position = properties.worldViewProjection * vec4f(input.vertexPosition, 1.0); + return output; + } + "#, + + fragment_shader: + r#" + @fragment fn fs_main(@location(0) texCoord: vec2f) -> @location(0) f32 { + let x = properties.invSize.x; + let y = properties.invSize.y; + let twoX = 2.0 * x; + let twoY = 2.0 * y; + + let a = textureSample(lumSampler_tex, lumSampler_samp, vec2f(texCoord.x - twoX, texCoord.y + twoY)).r; + let b = textureSample(lumSampler_tex, lumSampler_samp, vec2f(texCoord.x, texCoord.y + twoY)).r; + let c = textureSample(lumSampler_tex, lumSampler_samp, vec2f(texCoord.x + twoX, texCoord.y + twoY)).r; + + let d = textureSample(lumSampler_tex, lumSampler_samp, vec2f(texCoord.x - twoX, texCoord.y)).r; + let e = textureSample(lumSampler_tex, lumSampler_samp, vec2f(texCoord.x, texCoord.y)).r; + let f = textureSample(lumSampler_tex, lumSampler_samp, vec2f(texCoord.x + twoX, texCoord.y)).r; + + let g = textureSample(lumSampler_tex, lumSampler_samp, vec2f(texCoord.x - twoX, texCoord.y - twoY)).r; + let h = textureSample(lumSampler_tex, lumSampler_samp, vec2f(texCoord.x, texCoord.y - twoY)).r; + let i = textureSample(lumSampler_tex, lumSampler_samp, vec2f(texCoord.x + twoX, texCoord.y - twoY)).r; + + let j = textureSample(lumSampler_tex, lumSampler_samp, vec2f(texCoord.x - x, texCoord.y + y)).r; + let k = textureSample(lumSampler_tex, lumSampler_samp, vec2f(texCoord.x + x, texCoord.y + y)).r; + let l = textureSample(lumSampler_tex, lumSampler_samp, vec2f(texCoord.x - x, texCoord.y - y)).r; + let m = textureSample(lumSampler_tex, lumSampler_samp, vec2f(texCoord.x + x, texCoord.y - y)).r; + + var outLum = e * 0.125; + outLum += (a + c + g + i) * 0.03125; + outLum += (b + d + f + h) * 0.0625; + outLum += (j + k + l + m) * 0.125; + return outLum; + } + "#, + ) + ] +) diff --git a/fyrox-impl/src/renderer/shaders/wgpu/hdr_luminance.shader b/fyrox-impl/src/renderer/shaders/wgpu/hdr_luminance.shader new file mode 100644 index 0000000000..1d193053a3 --- /dev/null +++ b/fyrox-impl/src/renderer/shaders/wgpu/hdr_luminance.shader @@ -0,0 +1,77 @@ +( + name: "HdrLuminance", + resources: [ + ( + name: "frameSampler", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 0 + ), + ( + name: "properties", + kind: PropertyGroup([ + (name: "worldViewProjection", kind: Matrix4()), + (name: "invSize", kind: Vector2()), + ]), + binding: 0 + ), + ], + passes: [ + ( + name: "Primary", + + draw_parameters: DrawParameters( + cull_face: None, + color_write: ColorMask( + red: true, + green: true, + blue: true, + alpha: true, + ), + depth_write: false, + stencil_test: None, + depth_test: None, + blend: None, + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Keep, + write_mask: 0xFFFF_FFFF, + ), + scissor_box: None + ), + + vertex_shader: + r#" + struct VertexInput { + @location(0) vertexPosition: vec3f, + @location(1) vertexTexCoord: vec2f, + }; + + struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) texCoord: vec2f, + }; + + @vertex fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + output.texCoord = input.vertexTexCoord; + output.position = properties.worldViewProjection * vec4f(input.vertexPosition, 1.0); + return output; + } + "#, + + fragment_shader: + r#" + @fragment fn fs_main(@location(0) texCoord: vec2f) -> @location(0) f32 { + var totalLum: f32 = 0.0; + for (var y: f32 = -0.5; y < 0.5; y += 0.5) { + for (var x: f32 = -0.5; x < 0.5; x += 0.5) { + totalLum += S_Luminance(textureSample(frameSampler_tex, frameSampler_samp, texCoord - vec2f(x, y) * properties.invSize).xyz); + } + } + return totalLum / 9.0; + } + "#, + ) + ] +) diff --git a/fyrox-impl/src/renderer/shaders/wgpu/hdr_map.shader b/fyrox-impl/src/renderer/shaders/wgpu/hdr_map.shader new file mode 100644 index 0000000000..b3854f6905 --- /dev/null +++ b/fyrox-impl/src/renderer/shaders/wgpu/hdr_map.shader @@ -0,0 +1,130 @@ +( + name: "HdrMap", + resources: [ + ( + name: "hdrSampler", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 0 + ), + ( + name: "lumSampler", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 1 + ), + ( + name: "bloomSampler", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 2 + ), + ( + name: "colorMapSampler", + kind: Texture(kind: Sampler3D, fallback: White), + binding: 3 + ), + ( + name: "properties", + kind: PropertyGroup([ + (name: "worldViewProjection", kind: Matrix4()), + (name: "useColorGrading", kind: Bool()), + (name: "minLuminance", kind: Float()), + (name: "maxLuminance", kind: Float()), + (name: "autoExposure", kind: Bool()), + (name: "fixedExposure", kind: Float()), + ]), + binding: 0 + ), + ], + passes: [ + ( + name: "Primary", + + draw_parameters: DrawParameters( + cull_face: None, + color_write: ColorMask( + red: true, + green: true, + blue: true, + alpha: true, + ), + depth_write: false, + stencil_test: None, + depth_test: None, + blend: None, + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Keep, + write_mask: 0xFFFF_FFFF, + ), + scissor_box: None + ), + + vertex_shader: + r#" + struct VertexInput { + @location(0) vertexPosition: vec3f, + @location(1) vertexTexCoord: vec2f, + }; + + struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) texCoord: vec2f, + }; + + @vertex fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + output.texCoord = input.vertexTexCoord; + output.position = properties.worldViewProjection * vec4f(input.vertexPosition, 1.0); + return output; + } + "#, + + fragment_shader: + r#" + fn ColorGrading(color: vec3f) -> vec3f { + const lutSize: f32 = 16.0; + const a: f32 = (lutSize - 1.0) / lutSize; + const b: f32 = 1.0 / (2.0 * lutSize); + let scale = vec3f(a); + let offset = vec3f(b); + return textureSample(colorMapSampler_tex, colorMapSampler_samp, scale * color + offset).rgb; + } + + // Narkowicz 2015, "ACES Filmic Tone Mapping Curve" + fn TonemapACES(x: f32) -> f32 { + const a: f32 = 2.51; + const b: f32 = 0.03; + const c: f32 = 2.43; + const d: f32 = 0.59; + const e: f32 = 0.14; + return (x * (a * x + b)) / (x * (c * x + d) + e); + } + + @fragment fn fs_main(@location(0) texCoord: vec2f) -> @location(0) vec4f { + let hdrColor = textureSample(hdrSampler_tex, hdrSampler_samp, texCoord) + textureSample(bloomSampler_tex, bloomSampler_samp, texCoord); + + var Yxy = S_ConvertRgbToYxy(hdrColor.rgb); + + var lp: f32; + if (properties.autoExposure != 0u) { + let avgLum = textureSample(lumSampler_tex, lumSampler_samp, vec2f(0.5, 0.5)).r; + let clampedAvgLum = clamp(avgLum, properties.minLuminance, properties.maxLuminance); + lp = Yxy.x / (9.6 * clampedAvgLum + 0.0001); + } else { + lp = Yxy.x * properties.fixedExposure; + } + + Yxy.x = TonemapACES(lp); + + let ldrColor = vec4f(S_ConvertYxyToRgb(Yxy), hdrColor.a); + + if (properties.useColorGrading != 0u) { + return vec4f(ColorGrading(S_LinearToSRGB(ldrColor).rgb), ldrColor.a); + } else { + return S_LinearToSRGB(ldrColor); + } + } + "#, + ) + ] +) diff --git a/fyrox-impl/src/renderer/shaders/wgpu/irradiance.shader b/fyrox-impl/src/renderer/shaders/wgpu/irradiance.shader new file mode 100644 index 0000000000..35251b729c --- /dev/null +++ b/fyrox-impl/src/renderer/shaders/wgpu/irradiance.shader @@ -0,0 +1,96 @@ +( + name: "IrradianceShader", + resources: [ + ( + name: "environmentMap", + kind: Texture(kind: SamplerCube, fallback: White), + binding: 0 + ), + ( + name: "properties", + kind: PropertyGroup([ + (name: "worldViewProjection", kind: Matrix4()), + ]), + binding: 0 + ), + ], + passes: [ + ( + name: "Primary", + + draw_parameters: DrawParameters( + cull_face: None, + color_write: ColorMask( + red: true, + green: true, + blue: true, + alpha: true, + ), + depth_write: false, + stencil_test: None, + depth_test: None, + blend: None, + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Keep, + write_mask: 0xFFFF_FFFF, + ), + scissor_box: None + ), + + vertex_shader: + r#" + struct VertexInput { + @location(0) vertexPosition: vec3f, + }; + + struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) localPos: vec3f, + }; + + @vertex fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + output.localPos = input.vertexPosition; + output.position = properties.worldViewProjection * vec4f(input.vertexPosition, 1.0); + return output; + } + "#, + + fragment_shader: + r#" + @fragment fn fs_main(@location(0) localPos: vec3f) -> @location(0) vec4f { + let N = normalize(localPos); + + var irradiance = vec3f(0.0); + + var up = vec3f(0.0, 1.0, 0.0); + let right = normalize(cross(up, N)); + up = normalize(cross(N, right)); + + let sampleDelta: f32 = 0.1; + var nrSamples: f32 = 0.0; + for (var phi: f32 = 0.0; phi < 2.0 * PI; phi += sampleDelta) { + let cosPhi = cos(phi); + let sinPhi = sin(phi); + + for (var theta: f32 = 0.0; theta < 0.5 * PI; theta += sampleDelta) { + let cosTheta = cos(theta); + let sinTheta = sin(theta); + + let tangentSample = vec3f(sinTheta * cosPhi, sinTheta * sinPhi, cosTheta); + let sampleVec = tangentSample.x * right + tangentSample.y * up + tangentSample.z * N; + + irradiance += textureSample(environmentMap_tex, environmentMap_samp, sampleVec).rgb * cosTheta * sinTheta; + nrSamples += 1.0; + } + } + irradiance = PI * irradiance * (1.0 / nrSamples); + + return vec4f(irradiance, 1.0); + } + "#, + ) + ] +) diff --git a/fyrox-impl/src/renderer/shaders/wgpu/pixel_counter.shader b/fyrox-impl/src/renderer/shaders/wgpu/pixel_counter.shader new file mode 100644 index 0000000000..4dcc78aa92 --- /dev/null +++ b/fyrox-impl/src/renderer/shaders/wgpu/pixel_counter.shader @@ -0,0 +1,66 @@ +( + name: "PixelCounter", + resources: [ + ( + name: "properties", + kind: PropertyGroup([ + (name: "worldViewProjection", kind: Matrix4()), + ]), + binding: 0 + ), + ], + passes: [ + ( + name: "Primary", + + draw_parameters: DrawParameters( + cull_face: None, + color_write: ColorMask( + red: false, + green: false, + blue: false, + alpha: false, + ), + depth_write: false, + stencil_test: Some(StencilFunc ( + func: NotEqual, + ref_value: 0, + mask: 0xFFFF_FFFF, + )), + depth_test: None, + blend: None, + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Keep, + write_mask: 0xFFFF_FFFF, + ), + scissor_box: None + ), + + vertex_shader: + r#" + struct VertexInput { + @location(0) vertex_position: vec3f, + } + + struct VertexOutput { + @builtin(position) position: vec4f, + } + + @vertex fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + output.position = properties.worldViewProjection * vec4f(input.vertex_position, 1.0); + return output; + } + "#, + + fragment_shader: + r#" + @fragment fn fs_main() -> @location(0) vec4f { + return vec4f(1.0); + } + "#, + ) + ] +) \ No newline at end of file diff --git a/fyrox-impl/src/renderer/shaders/wgpu/point_volumetric.shader b/fyrox-impl/src/renderer/shaders/wgpu/point_volumetric.shader new file mode 100644 index 0000000000..30375ce7f1 --- /dev/null +++ b/fyrox-impl/src/renderer/shaders/wgpu/point_volumetric.shader @@ -0,0 +1,113 @@ +( + name: "PointVolumetric", + resources: [ + ( + name: "depthSampler", + kind: Texture(kind: DepthSampler2D, fallback: White), + binding: 0 + ), + ( + name: "properties", + kind: PropertyGroup([ + (name: "worldViewProjection", kind: Matrix4()), + (name: "invProj", kind: Matrix4()), + (name: "lightPosition", kind: Vector3()), + (name: "lightColor", kind: Vector3()), + (name: "scatterFactor", kind: Vector3()), + (name: "intensity", kind: Float()), + (name: "lightRadius", kind: Float()), + ]), + binding: 0 + ), + ], + passes: [ + ( + name: "Primary", + + draw_parameters: DrawParameters( + cull_face: None, + color_write: ColorMask( + red: true, + green: true, + blue: true, + alpha: true, + ), + depth_write: false, + stencil_test: Some(StencilFunc( + func: Equal, + ref_value: 0xFF, + mask: 0xFFFF_FFFF + )), + depth_test: None, + blend: Some(BlendParameters( + func: BlendFunc( + sfactor: One, + dfactor: One, + alpha_sfactor: One, + alpha_dfactor: One, + ), + equation: BlendEquation( + rgb: Add, + alpha: Add + ) + )), + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Zero, + write_mask: 0xFFFF_FFFF, + ), + scissor_box: None + ), + + vertex_shader: + r#" + struct VertexInput { + @location(0) vertex_position: vec3f, + @location(1) vertex_tex_coord: vec2f, + } + + struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) tex_coord: vec2f, + } + + @vertex fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + output.tex_coord = input.vertex_tex_coord; + output.position = properties.worldViewProjection * vec4f(input.vertex_position, 1.0); + return output; + } + "#, + + fragment_shader: + r#" + @fragment fn fs_main(@location(0) tex_coord: vec2f) -> @location(0) vec4f { + let fragmentPosition = S_UnProject(vec3f(tex_coord, textureSample(depthSampler_tex, depthSampler_samp, tex_coord)), properties.invProj); + let fragmentDepth = length(fragmentPosition); + let viewDirection = fragmentPosition / fragmentDepth; + + // Find intersection + var scatter = vec3f(0.0); + var minDepth: f32; + var maxDepth: f32; + if (S_RaySphereIntersection(vec3f(0.0), viewDirection, properties.lightPosition, properties.lightRadius, &minDepth, &maxDepth)) + { + // Perform depth test. + if (minDepth > 0.0 || fragmentDepth > minDepth) + { + minDepth = max(minDepth, 0.0); + maxDepth = clamp(maxDepth, 0.0, fragmentDepth); + + let closestPoint = viewDirection * minDepth; + + scatter = properties.scatterFactor * S_InScatter(closestPoint, viewDirection, properties.lightPosition, maxDepth - minDepth); + } + } + + return vec4f(properties.lightColor.xyz * pow(clamp(properties.intensity * scatter, vec3f(0.0), vec3f(1.0)), vec3f(2.2)), 1.0); + } + "#, + ) + ] +) \ No newline at end of file diff --git a/fyrox-impl/src/renderer/shaders/wgpu/prefilter.shader b/fyrox-impl/src/renderer/shaders/wgpu/prefilter.shader new file mode 100644 index 0000000000..c04f6b6e45 --- /dev/null +++ b/fyrox-impl/src/renderer/shaders/wgpu/prefilter.shader @@ -0,0 +1,126 @@ +( + name: "ReflectionCubeMapPrefilter", + resources: [ + ( + name: "environmentMap", + kind: Texture(kind: SamplerCube, fallback: White), + binding: 0 + ), + ( + name: "properties", + kind: PropertyGroup([ + (name: "worldViewProjection", kind: Matrix4()), + (name: "roughness", kind: Float()), + ]), + binding: 0 + ), + ], + passes: [ + ( + name: "Primary", + + draw_parameters: DrawParameters( + cull_face: None, + color_write: ColorMask( + red: true, + green: true, + blue: true, + alpha: true, + ), + depth_write: false, + stencil_test: None, + depth_test: None, + blend: None, + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Keep, + write_mask: 0xFFFF_FFFF, + ), + scissor_box: None + ), + + vertex_shader: + r#" + struct VertexInput { + @location(0) vertex_position: vec3f, + } + + struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) local_pos: vec3f, + } + + @vertex fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + output.local_pos = input.vertex_position; + output.position = properties.worldViewProjection * vec4f(input.vertex_position, 1.0); + return output; + } + "#, + + fragment_shader: + r#" + fn RadicalInverse_VdC(bits_in: u32) -> f32 { + var bits = bits_in; + bits = (bits << 16u) | (bits >> 16u); + bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u); + bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u); + bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u); + bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u); + return f32(bits) * 2.3283064e-10; + } + + fn Hammersley(i: u32, N: u32) -> vec2f { + return vec2f(f32(i) / f32(N), RadicalInverse_VdC(i)); + } + + fn ImportanceSampleGGX(Xi: vec2f, N: vec3f, roughness: f32) -> vec3f { + let a = roughness * roughness; + + let phi = 2.0 * PI * Xi.x; + let cosTheta = sqrt((1.0 - Xi.y) / (1.0 + (a * a - 1.0) * Xi.y)); + let sinTheta = sqrt(1.0 - cosTheta * cosTheta); + + var H: vec3f; + H.x = cos(phi) * sinTheta; + H.y = sin(phi) * sinTheta; + H.z = cosTheta; + + let up = select(vec3f(1.0, 0.0, 0.0), vec3f(0.0, 0.0, 1.0), abs(N.z) < 0.999); + let tangent = normalize(cross(up, N)); + let bitangent = cross(N, tangent); + + let sampleVec = tangent * H.x + bitangent * H.y + N * H.z; + return normalize(sampleVec); + } + + @fragment fn fs_main(@location(0) local_pos: vec3f) -> @location(0) vec4f { + let N = normalize(local_pos); + let R = N; + let V = R; + + const SAMPLE_COUNT: u32 = 64u; + var totalWeight: f32 = 0.0; + var prefilteredColor = vec3f(0.0); + for (var i: u32 = 0u; i < SAMPLE_COUNT; i++) + { + let Xi = Hammersley(i, SAMPLE_COUNT); + let H = ImportanceSampleGGX(Xi, N, properties.roughness); + let L = normalize(2.0 * dot(V, H) * H - V); + + let NdotL = max(dot(N, L), 0.0); + if (NdotL > 0.0) + { + prefilteredColor += textureSample(environmentMap_tex, environmentMap_samp, L).rgb * NdotL; + totalWeight += NdotL; + } + } + prefilteredColor = prefilteredColor / totalWeight; + + return vec4f(prefilteredColor, 1.0); + } + "#, + ) + ] +) \ No newline at end of file diff --git a/fyrox-impl/src/renderer/shaders/wgpu/skybox.shader b/fyrox-impl/src/renderer/shaders/wgpu/skybox.shader new file mode 100644 index 0000000000..b361c3bb2d --- /dev/null +++ b/fyrox-impl/src/renderer/shaders/wgpu/skybox.shader @@ -0,0 +1,69 @@ +( + name: "SkyBox", + resources: [ + ( + name: "cubemapTexture", + kind: Texture(kind: SamplerCube, fallback: White), + binding: 0 + ), + ( + name: "properties", + kind: PropertyGroup([ + (name: "worldViewProjection", kind: Matrix4()), + ]), + binding: 0 + ), + ], + passes: [ + ( + name: "Primary", + + draw_parameters: DrawParameters( + cull_face: None, + color_write: ColorMask( + red: true, + green: true, + blue: true, + alpha: true, + ), + depth_write: false, + stencil_test: None, + depth_test: None, + blend: None, + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Keep, + write_mask: 0xFFFF_FFFF, + ), + scissor_box: None + ), + + vertex_shader: + r#" + struct VertexInput { + @location(0) vertex_position: vec3f, + } + + struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) tex_coord: vec3f, + } + + @vertex fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + output.tex_coord = input.vertex_position; + output.position = properties.worldViewProjection * vec4f(input.vertex_position, 1.0); + return output; + } + "#, + + fragment_shader: + r#" + @fragment fn fs_main(@location(0) tex_coord: vec3f) -> @location(0) vec4f { + return S_SRGBToLinear(textureSample(cubemapTexture_tex, cubemapTexture_samp, tex_coord)); + } + "#, + ) + ] +) \ No newline at end of file diff --git a/fyrox-impl/src/renderer/shaders/wgpu/spot_volumetric.shader b/fyrox-impl/src/renderer/shaders/wgpu/spot_volumetric.shader new file mode 100644 index 0000000000..7599edda28 --- /dev/null +++ b/fyrox-impl/src/renderer/shaders/wgpu/spot_volumetric.shader @@ -0,0 +1,137 @@ +( + name: "SpotVolumetric", + resources: [ + ( + name: "depthSampler", + kind: Texture(kind: DepthSampler2D, fallback: White), + binding: 0 + ), + ( + name: "properties", + kind: PropertyGroup([ + (name: "worldViewProjection", kind: Matrix4()), + (name: "invProj", kind: Matrix4()), + (name: "lightPosition", kind: Vector3()), + (name: "lightDirection", kind: Vector3()), + (name: "lightColor", kind: Vector3()), + (name: "scatterFactor", kind: Vector3()), + (name: "intensity", kind: Float()), + (name: "coneAngleCos", kind: Float()), + ]), + binding: 0 + ), + ], + passes: [ + ( + name: "Primary", + + draw_parameters: DrawParameters( + cull_face: None, + color_write: ColorMask( + red: true, + green: true, + blue: true, + alpha: true, + ), + depth_write: false, + stencil_test: Some(StencilFunc( + func: Equal, + ref_value: 0xFF, + mask: 0xFFFF_FFFF + )), + depth_test: None, + blend: Some(BlendParameters( + func: BlendFunc( + sfactor: One, + dfactor: One, + alpha_sfactor: One, + alpha_dfactor: One, + ), + equation: BlendEquation( + rgb: Add, + alpha: Add + ) + )), + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Zero, + write_mask: 0xFFFF_FFFF, + ), + scissor_box: None + ), + + vertex_shader: + r#" + struct VertexInput { + @location(0) vertex_position: vec3f, + @location(1) vertex_tex_coord: vec2f, + } + + struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) tex_coord: vec2f, + } + + @vertex fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + output.tex_coord = input.vertex_tex_coord; + output.position = properties.worldViewProjection * vec4f(input.vertex_position, 1.0); + return output; + } + "#, + + fragment_shader: + r#" + @fragment fn fs_main(@location(0) tex_coord: vec2f) -> @location(0) vec4f { + let fragmentPosition = S_UnProject(vec3f(tex_coord, textureSample(depthSampler_tex, depthSampler_samp, tex_coord)), properties.invProj); + let fragmentDepth = length(fragmentPosition); + let viewDirection = fragmentPosition / fragmentDepth; + + // Ray-cone intersection + let sqrConeAngleCos = properties.coneAngleCos * properties.coneAngleCos; + let CO = -properties.lightPosition; + let DdotV = dot(viewDirection, properties.lightDirection); + let COdotV = dot(CO, properties.lightDirection); + let a = DdotV * DdotV - sqrConeAngleCos; + let b = 2.0 * (DdotV * COdotV - dot(viewDirection, CO) * sqrConeAngleCos); + let c = COdotV * COdotV - dot(CO, CO) * sqrConeAngleCos; + + // Find intersection + var scatter = vec3f(0.0); + var minDepth: f32; + var maxDepth: f32; + if (S_SolveQuadraticEq(a, b, c, &minDepth, &maxDepth)) + { + let dt1 = dot((minDepth * viewDirection) - properties.lightPosition, properties.lightDirection); + let dt2 = dot((maxDepth * viewDirection) - properties.lightPosition, properties.lightDirection); + + // Discard points on reflected cylinder and perform depth test. + if ((dt1 > 0.0 || dt2 > 0.0) && (minDepth > 0.0 || fragmentDepth > minDepth)) + { + if (dt1 > 0.0 && dt2 < 0.0) + { + // Closest point is on cylinder, farthest on reflected. + maxDepth = minDepth; + minDepth = 0.0; + } + else if (dt1 < 0.0 && dt2 > 0.0) + { + // Farthest point is on cylinder, closest on reflected. + minDepth = maxDepth; + maxDepth = fragmentDepth; + } + + minDepth = max(minDepth, 0.0); + maxDepth = clamp(maxDepth, 0.0, fragmentDepth); + + scatter = properties.scatterFactor * S_InScatter(viewDirection * minDepth, viewDirection, properties.lightPosition, maxDepth - minDepth); + } + } + + return vec4f(properties.lightColor.xyz * pow(clamp(properties.intensity * scatter, vec3f(0.0), vec3f(1.0)), vec3f(2.2)), 1.0); + } + "#, + ) + ] +) \ No newline at end of file diff --git a/fyrox-impl/src/renderer/shaders/wgpu/ssao.shader b/fyrox-impl/src/renderer/shaders/wgpu/ssao.shader new file mode 100644 index 0000000000..25c17e771b --- /dev/null +++ b/fyrox-impl/src/renderer/shaders/wgpu/ssao.shader @@ -0,0 +1,113 @@ +( + name: "SSAO", + resources: [ + ( + name: "depthSampler", + kind: Texture(kind: DepthSampler2D, fallback: White), + binding: 0 + ), + ( + name: "normalSampler", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 1 + ), + ( + name: "noiseSampler", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 2 + ), + ( + name: "properties", + kind: PropertyGroup([ + (name: "worldViewProjection", kind: Matrix4()), + (name: "inverseProjectionMatrix", kind: Matrix4()), + (name: "projectionMatrix", kind: Matrix4()), + (name: "kernel", kind: Vector3Array(max_len: 32, value: [])), + (name: "noiseScale", kind: Vector2()), + (name: "viewMatrix", kind: Matrix3()), + (name: "radius", kind: Float()), + ]), + binding: 0 + ), + ], + passes: [ + ( + name: "Primary", + + draw_parameters: DrawParameters( + cull_face: None, + color_write: ColorMask( + red: true, + green: true, + blue: true, + alpha: true, + ), + depth_write: false, + stencil_test: None, + depth_test: None, + blend: None, + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Keep, + write_mask: 0xFFFF_FFFF, + ), + scissor_box: None + ), + + vertex_shader: + r#" + struct VertexInput { + @location(0) vertex_position: vec3f, + @location(1) vertex_tex_coord: vec2f, + } + + struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) tex_coord: vec2f, + } + + @vertex fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + output.tex_coord = input.vertex_tex_coord; + output.position = properties.worldViewProjection * vec4f(input.vertex_position, 1.0); + return output; + } + "#, + + fragment_shader: + r#" + fn GetViewSpacePosition(screenCoord: vec2f) -> vec3f { + return S_UnProject(vec3f(screenCoord, textureSample(depthSampler_tex, depthSampler_samp, screenCoord)), properties.inverseProjectionMatrix); + } + + @fragment fn fs_main(@location(0) tex_coord: vec2f) -> @location(0) f32 { + let fragPos = GetViewSpacePosition(tex_coord); + let worldSpaceNormal = textureSample(normalSampler_tex, normalSampler_samp, tex_coord).xyz * 2.0 - 1.0; + let viewSpaceNormal = normalize(properties.viewMatrix * worldSpaceNormal); + let randomVec = normalize(textureSample(noiseSampler_tex, noiseSampler_samp, tex_coord * properties.noiseScale).xyz * 2.0 - 1.0); + + let tangent = normalize(randomVec - viewSpaceNormal * dot(randomVec, viewSpaceNormal)); + let bitangent = normalize(cross(viewSpaceNormal, tangent)); + let TBN = mat3x3f(tangent, bitangent, viewSpaceNormal); + + var occlusion: f32 = 0.0; + const kernelSize: i32 = 32; + for (var i: i32 = 0; i < kernelSize; i++) { + let samplePoint = fragPos + TBN * properties.kernel[i] * properties.radius; + + let offset = properties.projectionMatrix * vec4f(samplePoint, 1.0); + let screenUv = (offset.xy / offset.w) * 0.5 + 0.5; + + let position = GetViewSpacePosition(screenUv); + + let rangeCheck = smoothstep(0.0, 1.0, properties.radius / abs(fragPos.z - position.z)); + occlusion += rangeCheck * select(0.0, 1.0, position.z > samplePoint.z + 0.04); + } + + return 1.0 - occlusion / f32(kernelSize); + } + "#, + ) + ] +) \ No newline at end of file diff --git a/fyrox-impl/src/renderer/shaders/wgpu/visibility.shader b/fyrox-impl/src/renderer/shaders/wgpu/visibility.shader new file mode 100644 index 0000000000..e4e749d64c --- /dev/null +++ b/fyrox-impl/src/renderer/shaders/wgpu/visibility.shader @@ -0,0 +1,113 @@ +( + name: "Visibility", + resources: [ + ( + name: "matrices", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 0 + ), + ( + name: "tileBuffer", + kind: Texture(kind: USampler2D, fallback: White), + binding: 1 + ), + ( + name: "properties", + kind: PropertyGroup([ + (name: "viewProjection", kind: Matrix4()), + (name: "tileSize", kind: Int()), + (name: "frameBufferHeight", kind: Float()), + ]), + binding: 0 + ), + ], + passes: [ + ( + name: "Primary", + + draw_parameters: DrawParameters( + cull_face: Some(Back), + color_write: ColorMask( + red: true, + green: true, + blue: true, + alpha: true, + ), + depth_write: false, + stencil_test: None, + depth_test: Some(LessOrEqual), + blend: Some(BlendParameters( + func: BlendFunc( + sfactor: One, + dfactor: One, + alpha_sfactor: One, + alpha_dfactor: One, + ), + equation: BlendEquation( + rgb: Add, + alpha: Add + ) + )), + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Zero, + write_mask: 0xFFFF_FFFF, + ), + scissor_box: None + ), + + vertex_shader: + r#" + struct VertexInput { + @location(0) vertexPosition: vec3f, + }; + + struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) @interpolate(flat) objectIndex: u32, + }; + + @vertex + fn vs_main(input: VertexInput, @builtin(instance_index) instance_index: u32) -> VertexOutput { + var output: VertexOutput; + output.objectIndex = instance_index; + output.position = (properties.viewProjection * S_FetchMatrix(matrices_tex, matrices_samp, i32(instance_index))) * vec4f(input.vertexPosition, 1.0); + return output; + } + "#, + + fragment_shader: + r#" + @fragment + fn fs_main(@location(0) @interpolate(flat) objectIndex: u32, @builtin(position) fragCoord: vec4f) -> @location(0) vec4f { + var x = i32(fragCoord.x) / properties.tileSize; + var y = i32(properties.frameBufferHeight - fragCoord.y) / properties.tileSize; + + var bitIndex: i32 = -1; + var tileDataIndex = x * 33; + var count = i32(textureLoad(tileBuffer_tex, vec2i(tileDataIndex, y), 0).x); + var objectsListStartIndex = tileDataIndex + 1; + for (var i: i32 = 0; i < count; i++) { + var pixelObjectIndex = u32(textureLoad(tileBuffer_tex, vec2i(objectsListStartIndex + i, y), 0).x); + if (pixelObjectIndex == objectIndex) { + bitIndex = i; + break; + } + } + + if (bitIndex < 0) { + return vec4f(0.0, 0.0, 0.0, 0.0); + } else { + var outMask = 1u << u32(bitIndex); + var r = f32(outMask & 255u) / 255.0; + var g = f32((outMask & 65280u) >> 8) / 255.0; + var b = f32((outMask & 16711680u) >> 16) / 255.0; + var a = f32((outMask & 4278190080u) >> 24) / 255.0; + return vec4f(r, g, b, a); + } + } + "#, + ) + ] +) diff --git a/fyrox-impl/src/renderer/shaders/wgpu/visibility_optimizer.shader b/fyrox-impl/src/renderer/shaders/wgpu/visibility_optimizer.shader new file mode 100644 index 0000000000..da1b7c4dde --- /dev/null +++ b/fyrox-impl/src/renderer/shaders/wgpu/visibility_optimizer.shader @@ -0,0 +1,86 @@ +( + name: "VisibilityOptimizer", + resources: [ + ( + name: "visibilityBuffer", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 0 + ), + ( + name: "properties", + kind: PropertyGroup([ + (name: "viewProjection", kind: Matrix4()), + (name: "tileSize", kind: Int()), + ]), + binding: 0 + ), + ], + passes: [ + ( + name: "Primary", + + draw_parameters: DrawParameters( + cull_face: None, + color_write: ColorMask( + red: true, + green: true, + blue: true, + alpha: true, + ), + depth_write: false, + stencil_test: None, + depth_test: Some(LessOrEqual), + blend: None, + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Zero, + write_mask: 0xFFFF_FFFF, + ), + scissor_box: None + ), + + vertex_shader: + r#" + struct VertexInput { + @location(0) vertexPosition: vec3f, + }; + + struct VertexOutput { + @builtin(position) position: vec4f, + }; + + @vertex + fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + output.position = properties.viewProjection * vec4f(input.vertexPosition, 1.0); + return output; + } + "#, + + fragment_shader: + r#" + @fragment + fn fs_main(@builtin(position) fragCoord: vec4f) -> @location(0) u32 { + var tileX = i32(fragCoord.x); + var tileY = i32(fragCoord.y); + + var beginX = tileX * properties.tileSize; + var beginY = tileY * properties.tileSize; + + var endX = (tileX + 1) * properties.tileSize; + var endY = (tileY + 1) * properties.tileSize; + + var visibilityMask: i32 = 0; + for (var y: i32 = beginY; y < endY; y++) { + for (var x: i32 = beginX; x < endX; x++) { + var mask = vec4i(textureLoad(visibilityBuffer_tex, vec2i(x, y), 0) * 255.0); + visibilityMask |= (mask.w << 24) | (mask.z << 16) | (mask.y << 8) | mask.x; + } + } + return u32(visibilityMask); + } + "#, + ) + ] +) diff --git a/fyrox-impl/src/renderer/shaders/wgpu/volume_marker_lit.shader b/fyrox-impl/src/renderer/shaders/wgpu/volume_marker_lit.shader new file mode 100644 index 0000000000..f8fcb8dc2d --- /dev/null +++ b/fyrox-impl/src/renderer/shaders/wgpu/volume_marker_lit.shader @@ -0,0 +1,45 @@ +( + name: "VolumeMarkerLighting", + resources: [ + ( + name: "properties", + kind: PropertyGroup([ + (name: "worldViewProjection", kind: Matrix4()), + ]), + binding: 0 + ), + ], + passes: [ + ( + name: "Primary", + + // Drawing params are dynamic. + + vertex_shader: + r#" + struct VertexInput { + @location(0) vertexPosition: vec3f, + }; + + struct VertexOutput { + @builtin(position) position: vec4f, + }; + + @vertex + fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + output.position = properties.worldViewProjection * vec4f(input.vertexPosition, 1.0); + return output; + } + "#, + + fragment_shader: + r#" + @fragment + fn fs_main() -> @location(0) vec4f { + return vec4f(1.0); + } + "#, + ) + ] +) diff --git a/fyrox-impl/src/renderer/shaders/wgpu/volume_marker_vol.shader b/fyrox-impl/src/renderer/shaders/wgpu/volume_marker_vol.shader new file mode 100644 index 0000000000..9095f03210 --- /dev/null +++ b/fyrox-impl/src/renderer/shaders/wgpu/volume_marker_vol.shader @@ -0,0 +1,68 @@ +( + name: "VolumeMarkerVolume", + resources: [ + ( + name: "properties", + kind: PropertyGroup([ + (name: "worldViewProjection", kind: Matrix4()), + ]), + binding: 0 + ), + ], + passes: [ + ( + name: "Primary", + + draw_parameters: DrawParameters( + cull_face: None, + color_write: ColorMask( + red: false, + green: false, + blue: false, + alpha: false, + ), + depth_write: false, + stencil_test: Some(StencilFunc ( + func: Equal, + ref_value: 0xFF, + mask: 0xFFFF_FFFF + )), + depth_test: Some(Less), + blend: None, + stencil_op: StencilOp( + fail: Replace, + zfail: Keep, + zpass: Replace, + write_mask: 0xFFFF_FFFF, + ), + scissor_box: None + ), + + vertex_shader: + r#" + struct VertexInput { + @location(0) vertexPosition: vec3f, + }; + + struct VertexOutput { + @builtin(position) position: vec4f, + }; + + @vertex + fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + output.position = properties.worldViewProjection * vec4f(input.vertexPosition, 1.0); + return output; + } + "#, + + fragment_shader: + r#" + @fragment + fn fs_main() -> @location(0) vec4f { + return vec4f(1.0); + } + "#, + ) + ] +) diff --git a/fyrox-impl/src/resource/gltf/material.rs b/fyrox-impl/src/resource/gltf/material.rs index 6b9ae417a5..56e032428f 100644 --- a/fyrox-impl/src/resource/gltf/material.rs +++ b/fyrox-impl/src/resource/gltf/material.rs @@ -49,10 +49,24 @@ use crate::resource::texture::TextureMinificationFilter as FyroxMinFilter; use gltf::texture::MagFilter as GltfMagFilter; use gltf::texture::MinFilter as GltfMinFilter; +#[cfg(feature = "backend_wgpu")] +macro_rules! embedded_gltf_shader { + ($file:literal) => { + embedded_data_source!(concat!("wgpu/", $file)) + }; +} + +#[cfg(feature = "backend_opengl")] +macro_rules! embedded_gltf_shader { + ($file:literal) => { + embedded_data_source!(concat!("opengl/", $file)) + }; +} + pub static GLTF_SHADER: LazyLock> = LazyLock::new(|| { BuiltInResource::new( "GltfShader", - embedded_data_source!("gltf_standard.shader"), + embedded_gltf_shader!("gltf_standard.shader"), |data| { ShaderResource::new_ok( uuid!("33ee0142-f345-4c0a-9aca-d1f684a3485b"), diff --git a/fyrox-impl/src/resource/gltf/gltf_standard.shader b/fyrox-impl/src/resource/gltf/opengl/gltf_standard.shader similarity index 100% rename from fyrox-impl/src/resource/gltf/gltf_standard.shader rename to fyrox-impl/src/resource/gltf/opengl/gltf_standard.shader diff --git a/fyrox-impl/src/resource/gltf/wgpu/gltf_standard.shader b/fyrox-impl/src/resource/gltf/wgpu/gltf_standard.shader new file mode 100644 index 0000000000..fd6aa11cbf --- /dev/null +++ b/fyrox-impl/src/resource/gltf/wgpu/gltf_standard.shader @@ -0,0 +1,673 @@ +( + name: "GLTFShader", + + resources: [ + ( + name: "diffuseTexture", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 0 + ), + ( + name: "normalTexture", + kind: Texture(kind: Sampler2D, fallback: Normal), + binding: 1 + ), + ( + name: "metallicRoughnessTexture", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 2 + ), + ( + name: "heightTexture", + kind: Texture(kind: Sampler2D, fallback: Black), + binding: 3 + ), + ( + name: "emissionTexture", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 4 + ), + ( + name: "lightmapTexture", + kind: Texture(kind: Sampler2D, fallback: Black), + binding: 5 + ), + ( + name: "aoTexture", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 6 + ), + ( + name: "blendShapesStorage", + kind: Texture(kind: Sampler3D, fallback: Volume), + binding: 7 + ), + ( + name: "properties", + kind: PropertyGroup([ + ( + name: "texCoordScale", + kind: Vector2(value: (1.0, 1.0)), + ), + ( + name: "layerIndex", + kind: UInt(value: 0), + ), + ( + name: "emissionStrength", + kind: Vector3(value: (2.0, 2.0, 2.0)), + ), + ( + name: "diffuseColor", + kind: Color(r: 255, g: 255, b: 255, a: 255), + ), + ( + name: "parallaxCenter", + kind: Float(value: 0.0), + ), + ( + name: "parallaxScale", + kind: Float(value: 0.08), + ), + ( + name: "metallicFactor", + kind: Float(value: 1.0) + ), + ( + name: "roughnessFactor", + kind: Float(value: 1.0) + ) + ]), + binding: 0 + ), + ( + name: "fyrox_instanceData", + kind: PropertyGroup([ + // Autogenerated + ]), + binding: 1 + ), + ( + name: "fyrox_boneMatrices", + kind: PropertyGroup([ + // Autogenerated + ]), + binding: 2 + ), + ( + name: "fyrox_graphicsSettings", + kind: PropertyGroup([ + // Autogenerated + ]), + binding: 3 + ), + ( + name: "fyrox_cameraData", + kind: PropertyGroup([ + // Autogenerated + ]), + binding: 4 + ), + ( + name: "fyrox_lightData", + kind: PropertyGroup([ + // Autogenerated + ]), + binding: 5 + ), + ], + + passes: [ + ( + name: "GBuffer", + draw_parameters: DrawParameters( + cull_face: Some(Back), + color_write: ColorMask( + red: true, + green: true, + blue: true, + alpha: true, + ), + depth_write: true, + stencil_test: None, + depth_test: Some(Less), + blend: None, + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Keep, + write_mask: 0xFFFF_FFFF, + ), + scissor_box: None + ), + vertex_shader: + r#" + struct VertexInput { + @builtin(vertex_index) vertex_index: u32, + @location(0) vertexPosition: vec3f, + @location(1) vertexTexCoord: vec2f, + @location(2) vertexNormal: vec3f, + @location(3) vertexTangent: vec4f, + @location(4) boneWeights: vec4f, + @location(5) boneIndices: vec4f, + @location(6) vertexSecondTexCoord: vec2f, + }; + + struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) outPosition: vec3f, + @location(1) normal: vec3f, + @location(2) texCoord: vec2f, + @location(3) tangent: vec3f, + @location(4) binormal: vec3f, + @location(5) secondTexCoord: vec2f, + }; + + @vertex + fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + + var localPosition = vec4f(0.0); + var localNormal = vec3f(0.0); + var localTangent = vec3f(0.0); + + var inputPosition = vec4f(input.vertexPosition, 1.0); + var inputNormal = input.vertexNormal; + var inputTangent = input.vertexTangent.xyz; + + for (var i: i32 = 0; i < fyrox_instanceData.blendShapesCount; i++) { + var offsets = S_FetchBlendShapeOffsets(blendShapesStorage_tex, blendShapesStorage_samp, i32(input.vertex_index), i); + var weight = fyrox_instanceData.blendShapesWeights[i / 4][i % 4]; + inputPosition = vec4f(inputPosition.xyz + offsets.position * weight, inputPosition.w); + inputNormal += offsets.normal * weight; + inputTangent += offsets.tangent * weight; + } + + if (fyrox_instanceData.useSkeletalAnimation != 0u) + { + var i0 = i32(input.boneIndices.x); + var i1 = i32(input.boneIndices.y); + var i2 = i32(input.boneIndices.z); + var i3 = i32(input.boneIndices.w); + + var m0 = fyrox_boneMatrices.matrices[i0]; + var m1 = fyrox_boneMatrices.matrices[i1]; + var m2 = fyrox_boneMatrices.matrices[i2]; + var m3 = fyrox_boneMatrices.matrices[i3]; + + localPosition += m0 * inputPosition * input.boneWeights.x; + localPosition += m1 * inputPosition * input.boneWeights.y; + localPosition += m2 * inputPosition * input.boneWeights.z; + localPosition += m3 * inputPosition * input.boneWeights.w; + + localNormal += mat3x3f(m0[0].xyz, m0[1].xyz, m0[2].xyz) * inputNormal * input.boneWeights.x; + localNormal += mat3x3f(m1[0].xyz, m1[1].xyz, m1[2].xyz) * inputNormal * input.boneWeights.y; + localNormal += mat3x3f(m2[0].xyz, m2[1].xyz, m2[2].xyz) * inputNormal * input.boneWeights.z; + localNormal += mat3x3f(m3[0].xyz, m3[1].xyz, m3[2].xyz) * inputNormal * input.boneWeights.w; + + localTangent += mat3x3f(m0[0].xyz, m0[1].xyz, m0[2].xyz) * inputTangent * input.boneWeights.x; + localTangent += mat3x3f(m1[0].xyz, m1[1].xyz, m1[2].xyz) * inputTangent * input.boneWeights.y; + localTangent += mat3x3f(m2[0].xyz, m2[1].xyz, m2[2].xyz) * inputTangent * input.boneWeights.z; + localTangent += mat3x3f(m3[0].xyz, m3[1].xyz, m3[2].xyz) * inputTangent * input.boneWeights.w; + } + else + { + localPosition = inputPosition; + localNormal = inputNormal; + localTangent = inputTangent; + } + + var nm = mat3x3f(fyrox_instanceData.worldMatrix[0].xyz, fyrox_instanceData.worldMatrix[1].xyz, fyrox_instanceData.worldMatrix[2].xyz); + output.normal = normalize(nm * localNormal); + output.tangent = normalize(nm * localTangent); + output.binormal = normalize(input.vertexTangent.w * cross(output.normal, output.tangent)); + output.texCoord = input.vertexTexCoord; + output.outPosition = (fyrox_instanceData.worldMatrix * localPosition).xyz; + output.secondTexCoord = input.vertexSecondTexCoord; + + output.position = fyrox_instanceData.worldViewProjection * localPosition; + return output; + } + "#, + fragment_shader: + r#" + struct GBufferOutput { + @location(0) outColor: vec4f, + @location(1) outNormal: vec4f, + @location(2) outAmbient: vec4f, + @location(3) outMaterial: vec4f, + @location(4) outDecalMask: u32, + }; + + @fragment + fn fs_main( + @location(0) position: vec3f, + @location(1) normal: vec3f, + @location(2) texCoord: vec2f, + @location(3) tangent: vec3f, + @location(4) binormal: vec3f, + @location(5) secondTexCoord: vec2f + ) -> GBufferOutput { + var tangentSpace = mat3x3f(tangent, binormal, normal); + var toFragment = normalize(position - fyrox_cameraData.position); + + var tc: vec2f; + if (fyrox_graphicsSettings.usePOM != 0u) { + var toFragmentTangentSpace = normalize(transpose(tangentSpace) * toFragment); + tc = S_ComputeParallaxTextureCoordinates( + heightTexture_tex, + heightTexture_samp, + toFragmentTangentSpace, + texCoord * properties.texCoordScale, + properties.parallaxCenter, + properties.parallaxScale + ); + } else { + tc = texCoord * properties.texCoordScale; + } + + var output: GBufferOutput; + output.outColor = properties.diffuseColor * textureSample(diffuseTexture_tex, diffuseTexture_samp, tc); + + // Alpha test. + if (output.outColor.a < 0.5) { + discard; + } + output.outColor.a = 1.0; + + var n = normalize(textureSample(normalTexture_tex, normalTexture_samp, tc) * 2.0 - 1.0); + output.outNormal = vec4f(normalize(tangentSpace * n.xyz) * 0.5 + 0.5, 1.0); + + output.outMaterial.x = properties.metallicFactor * textureSample(metallicRoughnessTexture_tex, metallicRoughnessTexture_samp, tc).b; // Metallic + output.outMaterial.y = properties.roughnessFactor * textureSample(metallicRoughnessTexture_tex, metallicRoughnessTexture_samp, tc).g; // Roughness + output.outMaterial.z = textureSample(aoTexture_tex, aoTexture_samp, tc).r; + output.outMaterial.a = 1.0; + + output.outAmbient = vec4f(properties.emissionStrength * textureSample(emissionTexture_tex, emissionTexture_samp, tc).rgb + textureSample(lightmapTexture_tex, lightmapTexture_samp, secondTexCoord).rgb, 1.0); + + output.outDecalMask = properties.layerIndex; + + return output; + } + "#, + ), + ( + name: "Forward", + draw_parameters: DrawParameters( + cull_face: Some(Back), + color_write: ColorMask( + red: true, + green: true, + blue: true, + alpha: true, + ), + depth_write: true, + stencil_test: None, + depth_test: Some(Less), + blend: Some(BlendParameters( + func: BlendFunc( + sfactor: SrcAlpha, + dfactor: OneMinusSrcAlpha, + alpha_sfactor: SrcAlpha, + alpha_dfactor: OneMinusSrcAlpha, + ), + equation: BlendEquation( + rgb: Add, + alpha: Add + ) + )), + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Keep, + write_mask: 0xFFFF_FFFF, + ), + scissor_box: None + ), + vertex_shader: + r#" + struct VertexInput { + @builtin(vertex_index) vertex_index: u32, + @location(0) vertexPosition: vec3f, + @location(1) vertexTexCoord: vec2f, + @location(2) vertexNormal: vec3f, + @location(3) vertexTangent: vec4f, + @location(4) boneWeights: vec4f, + @location(5) boneIndices: vec4f, + @location(6) vertexSecondTexCoord: vec2f, + }; + + struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) outPosition: vec3f, + @location(1) texCoord: vec2f, + }; + + @vertex + fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + + var localPosition = vec4f(0.0); + + var inputPosition = vec4f(input.vertexPosition, 1.0); + + for (var i: i32 = 0; i < fyrox_instanceData.blendShapesCount; i++) { + var offsets = S_FetchBlendShapeOffsets(blendShapesStorage_tex, blendShapesStorage_samp, i32(input.vertex_index), i); + var weight = fyrox_instanceData.blendShapesWeights[i / 4][i % 4]; + inputPosition = vec4f(inputPosition.xyz + offsets.position * weight, inputPosition.w); + } + + if (fyrox_instanceData.useSkeletalAnimation != 0u) + { + var i0 = i32(input.boneIndices.x); + var i1 = i32(input.boneIndices.y); + var i2 = i32(input.boneIndices.z); + var i3 = i32(input.boneIndices.w); + + var m0 = fyrox_boneMatrices.matrices[i0]; + var m1 = fyrox_boneMatrices.matrices[i1]; + var m2 = fyrox_boneMatrices.matrices[i2]; + var m3 = fyrox_boneMatrices.matrices[i3]; + + localPosition += m0 * inputPosition * input.boneWeights.x; + localPosition += m1 * inputPosition * input.boneWeights.y; + localPosition += m2 * inputPosition * input.boneWeights.z; + localPosition += m3 * inputPosition * input.boneWeights.w; + } + else + { + localPosition = inputPosition; + } + output.position = fyrox_instanceData.worldViewProjection * localPosition; + output.texCoord = input.vertexTexCoord; + output.outPosition = (fyrox_instanceData.worldMatrix * localPosition).xyz; + return output; + } + "#, + + fragment_shader: + r#" + @fragment + fn fs_main(@location(0) position: vec3f, @location(1) texCoord: vec2f) -> @location(0) vec4f { + return properties.diffuseColor * textureSample(diffuseTexture_tex, diffuseTexture_samp, texCoord); + } + "#, + ), + ( + name: "DirectionalShadow", + + draw_parameters: DrawParameters ( + cull_face: Some(Back), + color_write: ColorMask( + red: false, + green: false, + blue: false, + alpha: false, + ), + depth_write: true, + stencil_test: None, + depth_test: Some(Less), + blend: None, + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Keep, + write_mask: 0xFFFF_FFFF, + ), + scissor_box: None + ), + + vertex_shader: + r#" + struct VertexInput { + @builtin(vertex_index) vertex_index: u32, + @location(0) vertexPosition: vec3f, + @location(1) vertexTexCoord: vec2f, + @location(2) vertexNormal: vec3f, + @location(3) vertexTangent: vec4f, + @location(4) boneWeights: vec4f, + @location(5) boneIndices: vec4f, + @location(6) vertexSecondTexCoord: vec2f, + }; + + struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) texCoord: vec2f, + }; + + @vertex + fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + + var localPosition = vec4f(0.0); + + var inputPosition = vec4f(input.vertexPosition, 1.0); + + for (var i: i32 = 0; i < fyrox_instanceData.blendShapesCount; i++) { + var offsets = S_FetchBlendShapeOffsets(blendShapesStorage_tex, blendShapesStorage_samp, i32(input.vertex_index), i); + var weight = fyrox_instanceData.blendShapesWeights[i / 4][i % 4]; + inputPosition = vec4f(inputPosition.xyz + offsets.position * weight, inputPosition.w); + } + + if (fyrox_instanceData.useSkeletalAnimation != 0u) + { + var vertex = vec4f(input.vertexPosition, 1.0); + + var m0 = fyrox_boneMatrices.matrices[i32(input.boneIndices.x)]; + var m1 = fyrox_boneMatrices.matrices[i32(input.boneIndices.y)]; + var m2 = fyrox_boneMatrices.matrices[i32(input.boneIndices.z)]; + var m3 = fyrox_boneMatrices.matrices[i32(input.boneIndices.w)]; + + localPosition += m0 * inputPosition * input.boneWeights.x; + localPosition += m1 * inputPosition * input.boneWeights.y; + localPosition += m2 * inputPosition * input.boneWeights.z; + localPosition += m3 * inputPosition * input.boneWeights.w; + } + else + { + localPosition = inputPosition; + } + + output.position = fyrox_instanceData.worldViewProjection * localPosition; + output.texCoord = input.vertexTexCoord; + return output; + } + "#, + + fragment_shader: + r#" + @fragment + fn fs_main(@location(0) texCoord: vec2f) { + if (textureSample(diffuseTexture_tex, diffuseTexture_samp, texCoord).a < 0.2) { discard; } + } + "#, + ), + ( + name: "SpotShadow", + + draw_parameters: DrawParameters ( + cull_face: Some(Back), + color_write: ColorMask( + red: false, + green: false, + blue: false, + alpha: false, + ), + depth_write: true, + stencil_test: None, + depth_test: Some(Less), + blend: None, + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Keep, + write_mask: 0xFFFF_FFFF, + ), + scissor_box: None + ), + + vertex_shader: + r#" + struct VertexInput { + @builtin(vertex_index) vertex_index: u32, + @location(0) vertexPosition: vec3f, + @location(1) vertexTexCoord: vec2f, + @location(2) vertexNormal: vec3f, + @location(3) vertexTangent: vec4f, + @location(4) boneWeights: vec4f, + @location(5) boneIndices: vec4f, + @location(6) vertexSecondTexCoord: vec2f, + }; + + struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) texCoord: vec2f, + }; + + @vertex + fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + + var localPosition = vec4f(0.0); + + var inputPosition = vec4f(input.vertexPosition, 1.0); + + for (var i: i32 = 0; i < fyrox_instanceData.blendShapesCount; i++) { + var offsets = S_FetchBlendShapeOffsets(blendShapesStorage_tex, blendShapesStorage_samp, i32(input.vertex_index), i); + var weight = fyrox_instanceData.blendShapesWeights[i / 4][i % 4]; + inputPosition = vec4f(inputPosition.xyz + offsets.position * weight, inputPosition.w); + } + + if (fyrox_instanceData.useSkeletalAnimation != 0u) + { + var vertex = vec4f(input.vertexPosition, 1.0); + + var m0 = fyrox_boneMatrices.matrices[i32(input.boneIndices.x)]; + var m1 = fyrox_boneMatrices.matrices[i32(input.boneIndices.y)]; + var m2 = fyrox_boneMatrices.matrices[i32(input.boneIndices.z)]; + var m3 = fyrox_boneMatrices.matrices[i32(input.boneIndices.w)]; + + localPosition += m0 * inputPosition * input.boneWeights.x; + localPosition += m1 * inputPosition * input.boneWeights.y; + localPosition += m2 * inputPosition * input.boneWeights.z; + localPosition += m3 * inputPosition * input.boneWeights.w; + } + else + { + localPosition = inputPosition; + } + + output.position = fyrox_instanceData.worldViewProjection * localPosition; + output.texCoord = input.vertexTexCoord; + return output; + } + "#, + + fragment_shader: + r#" + @fragment + fn fs_main(@location(0) texCoord: vec2f) { + if (textureSample(diffuseTexture_tex, diffuseTexture_samp, texCoord).a < 0.2) { discard; } + } + "#, + ), + ( + name: "PointShadow", + + draw_parameters: DrawParameters ( + cull_face: Some(Back), + color_write: ColorMask( + red: true, + green: true, + blue: true, + alpha: true, + ), + depth_write: true, + stencil_test: None, + depth_test: Some(Less), + blend: None, + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Keep, + write_mask: 0xFFFF_FFFF, + ), + scissor_box: None + ), + + vertex_shader: + r#" + struct VertexInput { + @builtin(vertex_index) vertex_index: u32, + @location(0) vertexPosition: vec3f, + @location(1) vertexTexCoord: vec2f, + @location(2) vertexNormal: vec3f, + @location(3) vertexTangent: vec4f, + @location(4) boneWeights: vec4f, + @location(5) boneIndices: vec4f, + @location(6) vertexSecondTexCoord: vec2f, + }; + + struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) texCoord: vec2f, + @location(1) worldPosition: vec3f, + }; + + @vertex + fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + + var localPosition = vec4f(0.0); + + var inputPosition = vec4f(input.vertexPosition, 1.0); + + for (var i: i32 = 0; i < fyrox_instanceData.blendShapesCount; i++) { + var offsets = S_FetchBlendShapeOffsets(blendShapesStorage_tex, blendShapesStorage_samp, i32(input.vertex_index), i); + var weight = fyrox_instanceData.blendShapesWeights[i / 4][i % 4]; + inputPosition = vec4f(inputPosition.xyz + offsets.position * weight, inputPosition.w); + } + + if (fyrox_instanceData.useSkeletalAnimation != 0u) + { + var vertex = vec4f(input.vertexPosition, 1.0); + + var m0 = fyrox_boneMatrices.matrices[i32(input.boneIndices.x)]; + var m1 = fyrox_boneMatrices.matrices[i32(input.boneIndices.y)]; + var m2 = fyrox_boneMatrices.matrices[i32(input.boneIndices.z)]; + var m3 = fyrox_boneMatrices.matrices[i32(input.boneIndices.w)]; + + localPosition += m0 * inputPosition * input.boneWeights.x; + localPosition += m1 * inputPosition * input.boneWeights.y; + localPosition += m2 * inputPosition * input.boneWeights.z; + localPosition += m3 * inputPosition * input.boneWeights.w; + } + else + { + localPosition = inputPosition; + } + + output.position = fyrox_instanceData.worldViewProjection * localPosition; + output.worldPosition = (fyrox_instanceData.worldMatrix * localPosition).xyz; + output.texCoord = input.vertexTexCoord; + return output; + } + "#, + + fragment_shader: + r#" + struct PointShadowOutput { + @location(0) depth: f32, + }; + + @fragment + fn fs_main(@location(0) texCoord: vec2f, @location(1) worldPosition: vec3f) -> PointShadowOutput { + if (textureSample(diffuseTexture_tex, diffuseTexture_samp, texCoord).a < 0.2) { discard; } + var output: PointShadowOutput; + output.depth = length(fyrox_lightData.lightPosition - worldPosition); + return output; + } + "#, + ) + ], +) diff --git a/fyrox-material/Cargo.toml b/fyrox-material/Cargo.toml index 7067c95511..758ac93619 100644 --- a/fyrox-material/Cargo.toml +++ b/fyrox-material/Cargo.toml @@ -28,3 +28,7 @@ regex = "1" [dev-dependencies] fyrox-texture = { version = "2.0.0-rc.1", path = "../fyrox-texture" } + +[features] +backend_opengl = [] +backend_wgpu = [] diff --git a/fyrox-material/src/shader/mod.rs b/fyrox-material/src/shader/mod.rs index a6c530584f..ad94031832 100644 --- a/fyrox-material/src/shader/mod.rs +++ b/fyrox-material/src/shader/mod.rs @@ -966,11 +966,25 @@ impl ShaderResourceExtension for ShaderResource { } } +#[cfg(all(feature = "backend_wgpu", not(feature = "backend_opengl")))] +macro_rules! embedded_shader { + ($file:literal) => { + embedded_data_source!(concat!("standard/wgpu/", $file)) + }; +} + +#[cfg(feature = "backend_opengl")] +macro_rules! embedded_shader { + ($file:literal) => { + embedded_data_source!(concat!("standard/opengl/", $file)) + }; +} + /// Standard shader. pub static STANDARD: LazyLock> = LazyLock::new(|| { BuiltInResource::new( STANDARD_SHADER_NAME, - embedded_data_source!("standard/standard.shader"), + embedded_shader!("standard.shader"), |data| { ShaderResource::new_ok( uuid!("87195f6e-cba4-4c27-9f89-d0bf726db965"), @@ -985,7 +999,7 @@ pub static STANDARD: LazyLock> = LazyLock::new(|| { pub static STANDARD_2D: LazyLock> = LazyLock::new(|| { BuiltInResource::new( STANDARD_2D_SHADER_NAME, - embedded_data_source!("standard/standard2d.shader"), + embedded_shader!("standard2d.shader"), |data| { ShaderResource::new_ok( uuid!("55fa05b0-3c25-4e46-bae7-65f093185b75"), @@ -1000,7 +1014,7 @@ pub static STANDARD_2D: LazyLock> = LazyLock::new(|| { pub static STANDARD_PARTICLE_SYSTEM: LazyLock> = LazyLock::new(|| { BuiltInResource::new( STANDARD_PARTICLE_SYSTEM_SHADER_NAME, - embedded_data_source!("standard/standard_particle_system.shader"), + embedded_shader!("standard_particle_system.shader"), |data| { ShaderResource::new_ok( uuid!("eb474445-6a25-4481-bca9-f919699c300f"), @@ -1015,7 +1029,7 @@ pub static STANDARD_PARTICLE_SYSTEM: LazyLock> = LazyLoc pub static STANDARD_SPRITE: LazyLock> = LazyLock::new(|| { BuiltInResource::new( STANDARD_SPRITE_SHADER_NAME, - embedded_data_source!("standard/standard_sprite.shader"), + embedded_shader!("standard_sprite.shader"), |data| { ShaderResource::new_ok( uuid!("a135826a-4c1b-46d5-ba1f-0c9a226aa52c"), @@ -1030,7 +1044,7 @@ pub static STANDARD_SPRITE: LazyLock> = LazyLock::new(|| pub static STANDARD_TERRAIN: LazyLock> = LazyLock::new(|| { BuiltInResource::new( STANDARD_TERRAIN_SHADER_NAME, - embedded_data_source!("standard/terrain.shader"), + embedded_shader!("terrain.shader"), |data| { ShaderResource::new_ok( uuid!("4911aafe-9bb1-4115-a958-25b57b87b51e"), @@ -1045,7 +1059,7 @@ pub static STANDARD_TERRAIN: LazyLock> = LazyLock::new(| pub static STANDARD_TILE: LazyLock> = LazyLock::new(|| { BuiltInResource::new( STANDARD_TILE_SHADER_NAME, - embedded_data_source!("standard/tile.shader"), + embedded_shader!("tile.shader"), |data| { ShaderResource::new_ok( uuid!("5f29dd3a-ea99-480c-bb02-d2c6420843b1"), @@ -1060,7 +1074,7 @@ pub static STANDARD_TILE: LazyLock> = LazyLock::new(|| { pub static STANDARD_TWOSIDES: LazyLock> = LazyLock::new(|| { BuiltInResource::new( STANDARD_TWOSIDES_SHADER_NAME, - embedded_data_source!("standard/standard-two-sides.shader"), + embedded_shader!("standard-two-sides.shader"), |data| { ShaderResource::new_ok( uuid!("f7979409-5185-4e1c-a644-d53cea64af8f"), @@ -1075,7 +1089,7 @@ pub static STANDARD_TWOSIDES: LazyLock> = LazyLock::new( pub static STANDARD_WIDGET: LazyLock> = LazyLock::new(|| { BuiltInResource::new( STANDARD_WIDGET_SHADER_NAME, - embedded_data_source!("standard/widget.shader"), + embedded_shader!("widget.shader"), |data| { ShaderResource::new_ok( uuid!("f5908aa4-e187-42a8-95d2-dc6577f6def4"), diff --git a/fyrox-material/src/shader/standard/standard-two-sides.shader b/fyrox-material/src/shader/standard/opengl/standard-two-sides.shader similarity index 100% rename from fyrox-material/src/shader/standard/standard-two-sides.shader rename to fyrox-material/src/shader/standard/opengl/standard-two-sides.shader diff --git a/fyrox-material/src/shader/standard/standard.shader b/fyrox-material/src/shader/standard/opengl/standard.shader similarity index 100% rename from fyrox-material/src/shader/standard/standard.shader rename to fyrox-material/src/shader/standard/opengl/standard.shader diff --git a/fyrox-material/src/shader/standard/standard2d.shader b/fyrox-material/src/shader/standard/opengl/standard2d.shader similarity index 100% rename from fyrox-material/src/shader/standard/standard2d.shader rename to fyrox-material/src/shader/standard/opengl/standard2d.shader diff --git a/fyrox-material/src/shader/standard/standard_particle_system.shader b/fyrox-material/src/shader/standard/opengl/standard_particle_system.shader similarity index 100% rename from fyrox-material/src/shader/standard/standard_particle_system.shader rename to fyrox-material/src/shader/standard/opengl/standard_particle_system.shader diff --git a/fyrox-material/src/shader/standard/standard_sprite.shader b/fyrox-material/src/shader/standard/opengl/standard_sprite.shader similarity index 100% rename from fyrox-material/src/shader/standard/standard_sprite.shader rename to fyrox-material/src/shader/standard/opengl/standard_sprite.shader diff --git a/fyrox-material/src/shader/standard/terrain.shader b/fyrox-material/src/shader/standard/opengl/terrain.shader similarity index 100% rename from fyrox-material/src/shader/standard/terrain.shader rename to fyrox-material/src/shader/standard/opengl/terrain.shader diff --git a/fyrox-material/src/shader/standard/tile.shader b/fyrox-material/src/shader/standard/opengl/tile.shader similarity index 100% rename from fyrox-material/src/shader/standard/tile.shader rename to fyrox-material/src/shader/standard/opengl/tile.shader diff --git a/fyrox-material/src/shader/standard/widget.shader b/fyrox-material/src/shader/standard/opengl/widget.shader similarity index 100% rename from fyrox-material/src/shader/standard/widget.shader rename to fyrox-material/src/shader/standard/opengl/widget.shader diff --git a/fyrox-material/src/shader/standard/wgpu/standard-two-sides.shader b/fyrox-material/src/shader/standard/wgpu/standard-two-sides.shader new file mode 100644 index 0000000000..c7cc4dc3b6 --- /dev/null +++ b/fyrox-material/src/shader/standard/wgpu/standard-two-sides.shader @@ -0,0 +1,194 @@ +( + name: "StandardTwoSidesShader", + + resources: [ + ( + name: "diffuseTexture", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 0 + ), + ( + name: "normalTexture", + kind: Texture(kind: Sampler2D, fallback: Normal), + binding: 1 + ), + ( + name: "metallicTexture", + kind: Texture(kind: Sampler2D, fallback: Black), + binding: 2 + ), + ( + name: "roughnessTexture", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 3 + ), + ( + name: "heightTexture", + kind: Texture(kind: Sampler2D, fallback: Black), + binding: 4 + ), + ( + name: "emissionTexture", + kind: Texture(kind: Sampler2D, fallback: Black), + binding: 5 + ), + ( + name: "lightmapTexture", + kind: Texture(kind: Sampler2D, fallback: Black), + binding: 6 + ), + ( + name: "aoTexture", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 7 + ), + ( + name: "blendShapesStorage", + kind: Texture(kind: Sampler3D, fallback: Volume), + binding: 8 + ), + ( + name: "properties", + kind: PropertyGroup([ + (name: "texCoordScale", kind: Vector2(value: (1.0, 1.0))), + (name: "layerIndex", kind: UInt(value: 0)), + (name: "emissionStrength", kind: Vector3(value: (2.0, 2.0, 2.0))), + (name: "diffuseColor", kind: Color(r: 255, g: 255, b: 255, a: 255)), + (name: "parallaxCenter", kind: Float(value: 0.0)), + (name: "parallaxScale", kind: Float(value: 0.08)), + ]), + binding: 0 + ), + (name: "fyrox_instanceData", kind: PropertyGroup([]), binding: 1), + (name: "fyrox_boneMatrices", kind: PropertyGroup([]), binding: 2), + (name: "fyrox_graphicsSettings", kind: PropertyGroup([]), binding: 3), + (name: "fyrox_cameraData", kind: PropertyGroup([]), binding: 4), + (name: "fyrox_lightData", kind: PropertyGroup([]), binding: 5), + ], + + passes: [ + ( + name: "GBuffer", + draw_parameters: DrawParameters(cull_face: None, color_write: ColorMask(red: true, green: true, blue: true, alpha: true), depth_write: true, stencil_test: None, depth_test: Some(Less), blend: None, stencil_op: StencilOp(fail: Keep, zfail: Keep, zpass: Keep, write_mask: 0xFFFF_FFFF), scissor_box: None), + vertex_shader: + r#" + struct VertexInput { + @location(0) vertexPosition: vec3f, @location(1) vertexTexCoord: vec2f, @location(2) vertexNormal: vec3f, + @location(3) vertexTangent: vec4f, @location(4) boneWeights: vec4f, @location(5) boneIndices: vec4f, + @location(6) vertexSecondTexCoord: vec2f, @builtin(vertex_index) vertex_index: u32, + }; + struct VertexOutput { + @builtin(position) position: vec4f, @location(0) outPosition: vec3f, @location(1) outNormal: vec3f, + @location(2) texCoord: vec2f, @location(3) outTangent: vec3f, @location(4) outBinormal: vec3f, @location(5) secondTexCoord: vec2f, + }; + @vertex fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + var localPosition = vec4f(0.0); var localNormal = vec3f(0.0); var localTangent = vec3f(0.0); + var inputPosition = vec4f(input.vertexPosition, 1.0); var inputNormal = input.vertexNormal; var inputTangent = input.vertexTangent.xyz; + for (var i: i32 = 0; i < i32(fyrox_instanceData.blendShapesCount); i++) { + let offsets = S_FetchBlendShapeOffsets(blendShapesStorage_tex, blendShapesStorage_samp, i32(input.vertex_index), i); + let weight = fyrox_instanceData.blendShapesWeights[i / 4][i % 4]; + inputPosition += vec4f(offsets.position * weight, 0.0); inputNormal += offsets.normal * weight; inputTangent += offsets.tangent * weight; + } + if (fyrox_instanceData.useSkeletalAnimation != 0u) { + let m0 = fyrox_boneMatrices.matrices[i32(input.boneIndices.x)]; let m1 = fyrox_boneMatrices.matrices[i32(input.boneIndices.y)]; + let m2 = fyrox_boneMatrices.matrices[i32(input.boneIndices.z)]; let m3 = fyrox_boneMatrices.matrices[i32(input.boneIndices.w)]; + localPosition += m0 * inputPosition * input.boneWeights.x + m1 * inputPosition * input.boneWeights.y + m2 * inputPosition * input.boneWeights.z + m3 * inputPosition * input.boneWeights.w; + localNormal += mat3x3f(m0[0].xyz, m0[1].xyz, m0[2].xyz) * inputNormal * input.boneWeights.x + mat3x3f(m1[0].xyz, m1[1].xyz, m1[2].xyz) * inputNormal * input.boneWeights.y + mat3x3f(m2[0].xyz, m2[1].xyz, m2[2].xyz) * inputNormal * input.boneWeights.z + mat3x3f(m3[0].xyz, m3[1].xyz, m3[2].xyz) * inputNormal * input.boneWeights.w; + localTangent += mat3x3f(m0[0].xyz, m0[1].xyz, m0[2].xyz) * inputTangent * input.boneWeights.x + mat3x3f(m1[0].xyz, m1[1].xyz, m1[2].xyz) * inputTangent * input.boneWeights.y + mat3x3f(m2[0].xyz, m2[1].xyz, m2[2].xyz) * inputTangent * input.boneWeights.z + mat3x3f(m3[0].xyz, m3[1].xyz, m3[2].xyz) * inputTangent * input.boneWeights.w; + } else { localPosition = inputPosition; localNormal = inputNormal; localTangent = inputTangent; } + let nm = mat3x3f(fyrox_instanceData.worldMatrix[0].xyz, fyrox_instanceData.worldMatrix[1].xyz, fyrox_instanceData.worldMatrix[2].xyz); + output.outNormal = normalize(nm * localNormal); output.outTangent = normalize(nm * localTangent); + output.outBinormal = normalize(input.vertexTangent.w * cross(output.outNormal, output.outTangent)); + output.texCoord = input.vertexTexCoord; output.outPosition = (fyrox_instanceData.worldMatrix * localPosition).xyz; + output.secondTexCoord = input.vertexSecondTexCoord; output.position = fyrox_instanceData.worldViewProjection * localPosition; + return output; + } + "#, + fragment_shader: + r#" + struct FragmentOutput { @location(0) outColor: vec4f, @location(1) outNormal: vec4f, @location(2) outAmbient: vec4f, @location(3) outMaterial: vec4f, @location(4) outDecalMask: u32 }; + @fragment fn fs_main(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) texCoord: vec2f, @location(3) tangent: vec3f, @location(4) binormal: vec3f, @location(5) secondTexCoord: vec2f) -> FragmentOutput { + var output: FragmentOutput; + let tangentSpace = mat3x3f(tangent, binormal, normal); + let toFragment = normalize(position - fyrox_cameraData.position); + var tc: vec2f; + if (fyrox_graphicsSettings.usePOM != 0u) { tc = S_ComputeParallaxTextureCoordinates(heightTexture_tex, heightTexture_samp, normalize(transpose(tangentSpace) * toFragment), texCoord * properties.texCoordScale, properties.parallaxCenter, properties.parallaxScale); } else { tc = texCoord * properties.texCoordScale; } + output.outColor = properties.diffuseColor * textureSample(diffuseTexture_tex, diffuseTexture_samp, tc); + if (output.outColor.a < 0.5) { discard; } output.outColor.a = 1.0; + let n = normalize(textureSample(normalTexture_tex, normalTexture_samp, tc) * 2.0 - 1.0); + output.outNormal = vec4f(normalize(tangentSpace * n.xyz) * 0.5 + 0.5, 1.0); + output.outMaterial.x = textureSample(metallicTexture_tex, metallicTexture_samp, tc).r; + output.outMaterial.y = textureSample(roughnessTexture_tex, roughnessTexture_samp, tc).r; + output.outMaterial.z = textureSample(aoTexture_tex, aoTexture_samp, tc).r; output.outMaterial.a = 1.0; + output.outAmbient = vec4f(properties.emissionStrength * textureSample(emissionTexture_tex, emissionTexture_samp, tc).rgb + textureSample(lightmapTexture_tex, lightmapTexture_samp, secondTexCoord).rgb, 1.0); output.outDecalMask = properties.layerIndex; + return output; + } + "#, + ), + ( + name: "Forward", + draw_parameters: DrawParameters(cull_face: None, color_write: ColorMask(red: true, green: true, blue: true, alpha: true), depth_write: true, stencil_test: None, depth_test: Some(Less), blend: Some(BlendParameters(func: BlendFunc(sfactor: SrcAlpha, dfactor: OneMinusSrcAlpha, alpha_sfactor: SrcAlpha, alpha_dfactor: OneMinusSrcAlpha), equation: BlendEquation(rgb: Add, alpha: Add))), stencil_op: StencilOp(fail: Keep, zfail: Keep, zpass: Keep, write_mask: 0xFFFF_FFFF), scissor_box: None), + vertex_shader: + r#" + struct VertexInput { @location(0) vertexPosition: vec3f, @location(1) vertexTexCoord: vec2f, @location(4) boneWeights: vec4f, @location(5) boneIndices: vec4f, @builtin(vertex_index) vertex_index: u32 }; + struct VertexOutput { @builtin(position) position: vec4f, @location(0) outPosition: vec3f, @location(1) texCoord: vec2f }; + @vertex fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; var localPosition = vec4f(0.0); var inputPosition = vec4f(input.vertexPosition, 1.0); + for (var i: i32 = 0; i < i32(fyrox_instanceData.blendShapesCount); i++) { let offsets = S_FetchBlendShapeOffsets(blendShapesStorage_tex, blendShapesStorage_samp, i32(input.vertex_index), i); let weight = fyrox_instanceData.blendShapesWeights[i / 4][i % 4]; inputPosition += vec4f(offsets.position * weight, 0.0); } + if (fyrox_instanceData.useSkeletalAnimation != 0u) { let m0 = fyrox_boneMatrices.matrices[i32(input.boneIndices.x)]; let m1 = fyrox_boneMatrices.matrices[i32(input.boneIndices.y)]; let m2 = fyrox_boneMatrices.matrices[i32(input.boneIndices.z)]; let m3 = fyrox_boneMatrices.matrices[i32(input.boneIndices.w)]; localPosition += m0 * inputPosition * input.boneWeights.x + m1 * inputPosition * input.boneWeights.y + m2 * inputPosition * input.boneWeights.z + m3 * inputPosition * input.boneWeights.w; } else { localPosition = inputPosition; } + output.position = fyrox_instanceData.worldViewProjection * localPosition; output.texCoord = input.vertexTexCoord; return output; + } + "#, + fragment_shader: r#"@fragment fn fs_main(@location(1) texCoord: vec2f) -> @location(0) vec4f { return properties.diffuseColor * S_SRGBToLinear(textureSample(diffuseTexture_tex, diffuseTexture_samp, texCoord)); }"#, + ), + ( + name: "DirectionalShadow", + draw_parameters: DrawParameters(cull_face: None, color_write: ColorMask(red: false, green: false, blue: false, alpha: false), depth_write: true, stencil_test: None, depth_test: Some(Less), blend: None, stencil_op: StencilOp(fail: Keep, zfail: Keep, zpass: Keep, write_mask: 0xFFFF_FFFF), scissor_box: None), + vertex_shader: + r#" + struct VertexInput { @location(0) vertexPosition: vec3f, @location(1) vertexTexCoord: vec2f, @location(4) boneWeights: vec4f, @location(5) boneIndices: vec4f, @builtin(vertex_index) vertex_index: u32 }; + struct VertexOutput { @builtin(position) position: vec4f, @location(0) texCoord: vec2f }; + @vertex fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; var localPosition = vec4f(0.0); var inputPosition = vec4f(input.vertexPosition, 1.0); + for (var i: i32 = 0; i < i32(fyrox_instanceData.blendShapesCount); i++) { let offsets = S_FetchBlendShapeOffsets(blendShapesStorage_tex, blendShapesStorage_samp, i32(input.vertex_index), i); let weight = fyrox_instanceData.blendShapesWeights[i / 4][i % 4]; inputPosition += vec4f(offsets.position * weight, 0.0); } + if (fyrox_instanceData.useSkeletalAnimation != 0u) { let m0 = fyrox_boneMatrices.matrices[i32(input.boneIndices.x)]; let m1 = fyrox_boneMatrices.matrices[i32(input.boneIndices.y)]; let m2 = fyrox_boneMatrices.matrices[i32(input.boneIndices.z)]; let m3 = fyrox_boneMatrices.matrices[i32(input.boneIndices.w)]; localPosition += m0 * inputPosition * input.boneWeights.x + m1 * inputPosition * input.boneWeights.y + m2 * inputPosition * input.boneWeights.z + m3 * inputPosition * input.boneWeights.w; } else { localPosition = inputPosition; } + output.position = fyrox_instanceData.worldViewProjection * localPosition; output.texCoord = input.vertexTexCoord; return output; + } + "#, + fragment_shader: r#"@fragment fn fs_main(@location(0) texCoord: vec2f) { if (textureSample(diffuseTexture_tex, diffuseTexture_samp, texCoord).a < 0.2) { discard; } }"#, + ), + ( + name: "SpotShadow", + draw_parameters: DrawParameters(cull_face: None, color_write: ColorMask(red: false, green: false, blue: false, alpha: false), depth_write: true, stencil_test: None, depth_test: Some(Less), blend: None, stencil_op: StencilOp(fail: Keep, zfail: Keep, zpass: Keep, write_mask: 0xFFFF_FFFF), scissor_box: None), + vertex_shader: + r#" + struct VertexInput { @location(0) vertexPosition: vec3f, @location(1) vertexTexCoord: vec2f, @location(4) boneWeights: vec4f, @location(5) boneIndices: vec4f, @builtin(vertex_index) vertex_index: u32 }; + struct VertexOutput { @builtin(position) position: vec4f, @location(0) texCoord: vec2f }; + @vertex fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; var localPosition = vec4f(0.0); var inputPosition = vec4f(input.vertexPosition, 1.0); + for (var i: i32 = 0; i < i32(fyrox_instanceData.blendShapesCount); i++) { let offsets = S_FetchBlendShapeOffsets(blendShapesStorage_tex, blendShapesStorage_samp, i32(input.vertex_index), i); let weight = fyrox_instanceData.blendShapesWeights[i / 4][i % 4]; inputPosition += vec4f(offsets.position * weight, 0.0); } + if (fyrox_instanceData.useSkeletalAnimation != 0u) { let m0 = fyrox_boneMatrices.matrices[i32(input.boneIndices.x)]; let m1 = fyrox_boneMatrices.matrices[i32(input.boneIndices.y)]; let m2 = fyrox_boneMatrices.matrices[i32(input.boneIndices.z)]; let m3 = fyrox_boneMatrices.matrices[i32(input.boneIndices.w)]; localPosition += m0 * inputPosition * input.boneWeights.x + m1 * inputPosition * input.boneWeights.y + m2 * inputPosition * input.boneWeights.z + m3 * inputPosition * input.boneWeights.w; } else { localPosition = inputPosition; } + output.position = fyrox_instanceData.worldViewProjection * localPosition; output.texCoord = input.vertexTexCoord; return output; + } + "#, + fragment_shader: r#"@fragment fn fs_main(@location(0) texCoord: vec2f) { if (textureSample(diffuseTexture_tex, diffuseTexture_samp, texCoord).a < 0.2) { discard; } }"#, + ), + ( + name: "PointShadow", + draw_parameters: DrawParameters(cull_face: None, color_write: ColorMask(red: true, green: true, blue: true, alpha: true), depth_write: true, stencil_test: None, depth_test: Some(Less), blend: None, stencil_op: StencilOp(fail: Keep, zfail: Keep, zpass: Keep, write_mask: 0xFFFF_FFFF), scissor_box: None), + vertex_shader: + r#" + struct VertexInput { @location(0) vertexPosition: vec3f, @location(1) vertexTexCoord: vec2f, @location(4) boneWeights: vec4f, @location(5) boneIndices: vec4f, @builtin(vertex_index) vertex_index: u32 }; + struct VertexOutput { @builtin(position) position: vec4f, @location(0) texCoord: vec2f, @location(1) worldPosition: vec3f }; + @vertex fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; var localPosition = vec4f(0.0); var inputPosition = vec4f(input.vertexPosition, 1.0); + for (var i: i32 = 0; i < i32(fyrox_instanceData.blendShapesCount); i++) { let offsets = S_FetchBlendShapeOffsets(blendShapesStorage_tex, blendShapesStorage_samp, i32(input.vertex_index), i); let weight = fyrox_instanceData.blendShapesWeights[i / 4][i % 4]; inputPosition += vec4f(offsets.position * weight, 0.0); } + if (fyrox_instanceData.useSkeletalAnimation != 0u) { let m0 = fyrox_boneMatrices.matrices[i32(input.boneIndices.x)]; let m1 = fyrox_boneMatrices.matrices[i32(input.boneIndices.y)]; let m2 = fyrox_boneMatrices.matrices[i32(input.boneIndices.z)]; let m3 = fyrox_boneMatrices.matrices[i32(input.boneIndices.w)]; localPosition += m0 * inputPosition * input.boneWeights.x + m1 * inputPosition * input.boneWeights.y + m2 * inputPosition * input.boneWeights.z + m3 * inputPosition * input.boneWeights.w; } else { localPosition = inputPosition; } + output.position = fyrox_instanceData.worldViewProjection * localPosition; output.worldPosition = (fyrox_instanceData.worldMatrix * localPosition).xyz; output.texCoord = input.vertexTexCoord; return output; + } + "#, + fragment_shader: r#"@fragment fn fs_main(@location(0) texCoord: vec2f, @location(1) worldPosition: vec3f) -> @location(0) f32 { if (textureSample(diffuseTexture_tex, diffuseTexture_samp, texCoord).a < 0.2) { discard; } return length(fyrox_lightData.lightPosition - worldPosition); }"#, + ) + ], +) diff --git a/fyrox-material/src/shader/standard/wgpu/standard.shader b/fyrox-material/src/shader/standard/wgpu/standard.shader new file mode 100644 index 0000000000..889518618f --- /dev/null +++ b/fyrox-material/src/shader/standard/wgpu/standard.shader @@ -0,0 +1,608 @@ +( + name: "StandardShader", + + resources: [ + ( + name: "diffuseTexture", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 0 + ), + ( + name: "normalTexture", + kind: Texture(kind: Sampler2D, fallback: Normal), + binding: 1 + ), + ( + name: "metallicTexture", + kind: Texture(kind: Sampler2D, fallback: Black), + binding: 2 + ), + ( + name: "roughnessTexture", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 3 + ), + ( + name: "heightTexture", + kind: Texture(kind: Sampler2D, fallback: Black), + binding: 4 + ), + ( + name: "emissionTexture", + kind: Texture(kind: Sampler2D, fallback: Black), + binding: 5 + ), + ( + name: "lightmapTexture", + kind: Texture(kind: Sampler2D, fallback: Black), + binding: 6 + ), + ( + name: "aoTexture", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 7 + ), + ( + name: "blendShapesStorage", + kind: Texture(kind: Sampler3D, fallback: Volume), + binding: 8 + ), + ( + name: "properties", + kind: PropertyGroup([ + ( + name: "texCoordScale", + kind: Vector2(value: (1.0, 1.0)), + ), + ( + name: "layerIndex", + kind: UInt(value: 0), + ), + ( + name: "emissionStrength", + kind: Vector3(value: (2.0, 2.0, 2.0)), + ), + ( + name: "diffuseColor", + kind: Color(r: 255, g: 255, b: 255, a: 255), + ), + ( + name: "parallaxCenter", + kind: Float(value: 0.0), + ), + ( + name: "parallaxScale", + kind: Float(value: 0.08), + ), + ]), + binding: 0 + ), + ( + name: "fyrox_instanceData", + kind: PropertyGroup([ + // Autogenerated + ]), + binding: 1 + ), + ( + name: "fyrox_boneMatrices", + kind: PropertyGroup([ + // Autogenerated + ]), + binding: 2 + ), + ( + name: "fyrox_graphicsSettings", + kind: PropertyGroup([ + // Autogenerated + ]), + binding: 3 + ), + ( + name: "fyrox_cameraData", + kind: PropertyGroup([ + // Autogenerated + ]), + binding: 4 + ), + ( + name: "fyrox_lightData", + kind: PropertyGroup([ + // Autogenerated + ]), + binding: 5 + ), + ], + + passes: [ + ( + name: "GBuffer", + draw_parameters: DrawParameters( + cull_face: Some(Back), + color_write: ColorMask( + red: true, + green: true, + blue: true, + alpha: true, + ), + depth_write: true, + stencil_test: None, + depth_test: Some(Less), + blend: None, + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Keep, + write_mask: 0xFFFF_FFFF, + ), + scissor_box: None + ), + vertex_shader: + r#" + struct VertexInput { + @location(0) vertexPosition: vec3f, + @location(1) vertexTexCoord: vec2f, + @location(2) vertexNormal: vec3f, + @location(3) vertexTangent: vec4f, + @location(4) boneWeights: vec4f, + @location(5) boneIndices: vec4f, + @location(6) vertexSecondTexCoord: vec2f, + @builtin(vertex_index) vertex_index: u32, + }; + + struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) outPosition: vec3f, + @location(1) outNormal: vec3f, + @location(2) texCoord: vec2f, + @location(3) outTangent: vec3f, + @location(4) outBinormal: vec3f, + @location(5) secondTexCoord: vec2f, + }; + + @vertex fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + var localPosition = vec4f(0.0); + var localNormal = vec3f(0.0); + var localTangent = vec3f(0.0); + + var inputPosition = vec4f(input.vertexPosition, 1.0); + var inputNormal = input.vertexNormal; + var inputTangent = input.vertexTangent.xyz; + + for (var i: i32 = 0; i < i32(fyrox_instanceData.blendShapesCount); i++) { + let offsets = S_FetchBlendShapeOffsets(blendShapesStorage_tex, blendShapesStorage_samp, i32(input.vertex_index), i); + let weight = fyrox_instanceData.blendShapesWeights[i / 4][i % 4]; + inputPosition += vec4f(offsets.position * weight, 0.0); + inputNormal += offsets.normal * weight; + inputTangent += offsets.tangent * weight; + } + + if (fyrox_instanceData.useSkeletalAnimation != 0u) { + let i0 = i32(input.boneIndices.x); + let i1 = i32(input.boneIndices.y); + let i2 = i32(input.boneIndices.z); + let i3 = i32(input.boneIndices.w); + + let m0 = fyrox_boneMatrices.matrices[i0]; + let m1 = fyrox_boneMatrices.matrices[i1]; + let m2 = fyrox_boneMatrices.matrices[i2]; + let m3 = fyrox_boneMatrices.matrices[i3]; + + localPosition += m0 * inputPosition * input.boneWeights.x; + localPosition += m1 * inputPosition * input.boneWeights.y; + localPosition += m2 * inputPosition * input.boneWeights.z; + localPosition += m3 * inputPosition * input.boneWeights.w; + + localNormal += mat3x3f(m0[0].xyz, m0[1].xyz, m0[2].xyz) * inputNormal * input.boneWeights.x; + localNormal += mat3x3f(m1[0].xyz, m1[1].xyz, m1[2].xyz) * inputNormal * input.boneWeights.y; + localNormal += mat3x3f(m2[0].xyz, m2[1].xyz, m2[2].xyz) * inputNormal * input.boneWeights.z; + localNormal += mat3x3f(m3[0].xyz, m3[1].xyz, m3[2].xyz) * inputNormal * input.boneWeights.w; + + localTangent += mat3x3f(m0[0].xyz, m0[1].xyz, m0[2].xyz) * inputTangent * input.boneWeights.x; + localTangent += mat3x3f(m1[0].xyz, m1[1].xyz, m1[2].xyz) * inputTangent * input.boneWeights.y; + localTangent += mat3x3f(m2[0].xyz, m2[1].xyz, m2[2].xyz) * inputTangent * input.boneWeights.z; + localTangent += mat3x3f(m3[0].xyz, m3[1].xyz, m3[2].xyz) * inputTangent * input.boneWeights.w; + } else { + localPosition = inputPosition; + localNormal = inputNormal; + localTangent = inputTangent; + } + + let nm = mat3x3f(fyrox_instanceData.worldMatrix[0].xyz, fyrox_instanceData.worldMatrix[1].xyz, fyrox_instanceData.worldMatrix[2].xyz); + output.outNormal = normalize(nm * localNormal); + output.outTangent = normalize(nm * localTangent); + output.outBinormal = normalize(input.vertexTangent.w * cross(output.outNormal, output.outTangent)); + output.texCoord = input.vertexTexCoord; + output.outPosition = (fyrox_instanceData.worldMatrix * localPosition).xyz; + output.secondTexCoord = input.vertexSecondTexCoord; + output.position = fyrox_instanceData.worldViewProjection * localPosition; + return output; + } + "#, + fragment_shader: + r#" + struct FragmentOutput { + @location(0) outColor: vec4f, + @location(1) outNormal: vec4f, + @location(2) outAmbient: vec4f, + @location(3) outMaterial: vec4f, + @location(4) outDecalMask: u32, + }; + + @fragment fn fs_main( + @location(0) position: vec3f, + @location(1) normal: vec3f, + @location(2) texCoord: vec2f, + @location(3) tangent: vec3f, + @location(4) binormal: vec3f, + @location(5) secondTexCoord: vec2f + ) -> FragmentOutput { + var output: FragmentOutput; + let tangentSpace = mat3x3f(tangent, binormal, normal); + let toFragment = normalize(position - fyrox_cameraData.position); + + var tc: vec2f; + if (fyrox_graphicsSettings.usePOM != 0u) { + let toFragmentTangentSpace = normalize(transpose(tangentSpace) * toFragment); + tc = S_ComputeParallaxTextureCoordinates( + heightTexture_tex, heightTexture_samp, + toFragmentTangentSpace, + texCoord * properties.texCoordScale, + properties.parallaxCenter, + properties.parallaxScale + ); + } else { + tc = texCoord * properties.texCoordScale; + } + + output.outColor = properties.diffuseColor * textureSample(diffuseTexture_tex, diffuseTexture_samp, tc); + + // Alpha test. + if (output.outColor.a < 0.5) { + discard; + } + output.outColor.a = 1.0; + + let n = normalize(textureSample(normalTexture_tex, normalTexture_samp, tc) * 2.0 - 1.0); + output.outNormal = vec4f(normalize(tangentSpace * n.xyz) * 0.5 + 0.5, 1.0); + + output.outMaterial.x = textureSample(metallicTexture_tex, metallicTexture_samp, tc).r; + output.outMaterial.y = textureSample(roughnessTexture_tex, roughnessTexture_samp, tc).r; + output.outMaterial.z = textureSample(aoTexture_tex, aoTexture_samp, tc).r; + output.outMaterial.a = 1.0; + + output.outAmbient = vec4f(properties.emissionStrength * textureSample(emissionTexture_tex, emissionTexture_samp, tc).rgb + textureSample(lightmapTexture_tex, lightmapTexture_samp, secondTexCoord).rgb, 1.0); + + output.outDecalMask = properties.layerIndex; + return output; + } + "#, + ), + ( + name: "Forward", + draw_parameters: DrawParameters( + cull_face: Some(Back), + color_write: ColorMask( + red: true, + green: true, + blue: true, + alpha: true, + ), + depth_write: true, + stencil_test: None, + depth_test: Some(Less), + blend: Some(BlendParameters( + func: BlendFunc( + sfactor: SrcAlpha, + dfactor: OneMinusSrcAlpha, + alpha_sfactor: SrcAlpha, + alpha_dfactor: OneMinusSrcAlpha, + ), + equation: BlendEquation( + rgb: Add, + alpha: Add + ) + )), + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Keep, + write_mask: 0xFFFF_FFFF, + ), + scissor_box: None + ), + vertex_shader: + r#" + struct VertexInput { + @location(0) vertexPosition: vec3f, + @location(1) vertexTexCoord: vec2f, + @location(4) boneWeights: vec4f, + @location(5) boneIndices: vec4f, + @builtin(vertex_index) vertex_index: u32, + }; + + struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) outPosition: vec3f, + @location(1) texCoord: vec2f, + }; + + @vertex fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + var localPosition = vec4f(0.0); + var inputPosition = vec4f(input.vertexPosition, 1.0); + + for (var i: i32 = 0; i < i32(fyrox_instanceData.blendShapesCount); i++) { + let offsets = S_FetchBlendShapeOffsets(blendShapesStorage_tex, blendShapesStorage_samp, i32(input.vertex_index), i); + let weight = fyrox_instanceData.blendShapesWeights[i / 4][i % 4]; + inputPosition += vec4f(offsets.position * weight, 0.0); + } + + if (fyrox_instanceData.useSkeletalAnimation != 0u) { + let i0 = i32(input.boneIndices.x); + let i1 = i32(input.boneIndices.y); + let i2 = i32(input.boneIndices.z); + let i3 = i32(input.boneIndices.w); + + let m0 = fyrox_boneMatrices.matrices[i0]; + let m1 = fyrox_boneMatrices.matrices[i1]; + let m2 = fyrox_boneMatrices.matrices[i2]; + let m3 = fyrox_boneMatrices.matrices[i3]; + + localPosition += m0 * inputPosition * input.boneWeights.x; + localPosition += m1 * inputPosition * input.boneWeights.y; + localPosition += m2 * inputPosition * input.boneWeights.z; + localPosition += m3 * inputPosition * input.boneWeights.w; + } else { + localPosition = inputPosition; + } + output.position = fyrox_instanceData.worldViewProjection * localPosition; + output.texCoord = input.vertexTexCoord; + return output; + } + "#, + + fragment_shader: + r#" + @fragment fn fs_main(@location(1) texCoord: vec2f) -> @location(0) vec4f { + return properties.diffuseColor * S_SRGBToLinear(textureSample(diffuseTexture_tex, diffuseTexture_samp, texCoord)); + } + "#, + ), + ( + name: "DirectionalShadow", + + draw_parameters: DrawParameters ( + cull_face: Some(Back), + color_write: ColorMask( + red: false, + green: false, + blue: false, + alpha: false, + ), + depth_write: true, + stencil_test: None, + depth_test: Some(Less), + blend: None, + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Keep, + write_mask: 0xFFFF_FFFF, + ), + scissor_box: None + ), + + vertex_shader: + r#" + struct VertexInput { + @location(0) vertexPosition: vec3f, + @location(1) vertexTexCoord: vec2f, + @location(4) boneWeights: vec4f, + @location(5) boneIndices: vec4f, + @builtin(vertex_index) vertex_index: u32, + }; + + struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) texCoord: vec2f, + }; + + @vertex fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + var localPosition = vec4f(0.0); + var inputPosition = vec4f(input.vertexPosition, 1.0); + + for (var i: i32 = 0; i < i32(fyrox_instanceData.blendShapesCount); i++) { + let offsets = S_FetchBlendShapeOffsets(blendShapesStorage_tex, blendShapesStorage_samp, i32(input.vertex_index), i); + let weight = fyrox_instanceData.blendShapesWeights[i / 4][i % 4]; + inputPosition += vec4f(offsets.position * weight, 0.0); + } + + if (fyrox_instanceData.useSkeletalAnimation != 0u) { + let m0 = fyrox_boneMatrices.matrices[i32(input.boneIndices.x)]; + let m1 = fyrox_boneMatrices.matrices[i32(input.boneIndices.y)]; + let m2 = fyrox_boneMatrices.matrices[i32(input.boneIndices.z)]; + let m3 = fyrox_boneMatrices.matrices[i32(input.boneIndices.w)]; + + localPosition += m0 * inputPosition * input.boneWeights.x; + localPosition += m1 * inputPosition * input.boneWeights.y; + localPosition += m2 * inputPosition * input.boneWeights.z; + localPosition += m3 * inputPosition * input.boneWeights.w; + } else { + localPosition = inputPosition; + } + + output.position = fyrox_instanceData.worldViewProjection * localPosition; + output.texCoord = input.vertexTexCoord; + return output; + } + "#, + + fragment_shader: + r#" + @fragment fn fs_main(@location(0) texCoord: vec2f) { + if (textureSample(diffuseTexture_tex, diffuseTexture_samp, texCoord).a < 0.2) { discard; } + } + "#, + ), + ( + name: "SpotShadow", + + draw_parameters: DrawParameters ( + cull_face: Some(Back), + color_write: ColorMask( + red: false, + green: false, + blue: false, + alpha: false, + ), + depth_write: true, + stencil_test: None, + depth_test: Some(Less), + blend: None, + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Keep, + write_mask: 0xFFFF_FFFF, + ), + scissor_box: None + ), + + vertex_shader: + r#" + struct VertexInput { + @location(0) vertexPosition: vec3f, + @location(1) vertexTexCoord: vec2f, + @location(4) boneWeights: vec4f, + @location(5) boneIndices: vec4f, + @builtin(vertex_index) vertex_index: u32, + }; + + struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) texCoord: vec2f, + }; + + @vertex fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + var localPosition = vec4f(0.0); + var inputPosition = vec4f(input.vertexPosition, 1.0); + + for (var i: i32 = 0; i < i32(fyrox_instanceData.blendShapesCount); i++) { + let offsets = S_FetchBlendShapeOffsets(blendShapesStorage_tex, blendShapesStorage_samp, i32(input.vertex_index), i); + let weight = fyrox_instanceData.blendShapesWeights[i / 4][i % 4]; + inputPosition += vec4f(offsets.position * weight, 0.0); + } + + if (fyrox_instanceData.useSkeletalAnimation != 0u) { + let m0 = fyrox_boneMatrices.matrices[i32(input.boneIndices.x)]; + let m1 = fyrox_boneMatrices.matrices[i32(input.boneIndices.y)]; + let m2 = fyrox_boneMatrices.matrices[i32(input.boneIndices.z)]; + let m3 = fyrox_boneMatrices.matrices[i32(input.boneIndices.w)]; + + localPosition += m0 * inputPosition * input.boneWeights.x; + localPosition += m1 * inputPosition * input.boneWeights.y; + localPosition += m2 * inputPosition * input.boneWeights.z; + localPosition += m3 * inputPosition * input.boneWeights.w; + } else { + localPosition = inputPosition; + } + + output.position = fyrox_instanceData.worldViewProjection * localPosition; + output.texCoord = input.vertexTexCoord; + return output; + } + "#, + + fragment_shader: + r#" + @fragment fn fs_main(@location(0) texCoord: vec2f) { + if (textureSample(diffuseTexture_tex, diffuseTexture_samp, texCoord).a < 0.2) { discard; } + } + "#, + ), + ( + name: "PointShadow", + + draw_parameters: DrawParameters ( + cull_face: Some(Back), + color_write: ColorMask( + red: true, + green: true, + blue: true, + alpha: true, + ), + depth_write: true, + stencil_test: None, + depth_test: Some(Less), + blend: None, + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Keep, + write_mask: 0xFFFF_FFFF, + ), + scissor_box: None + ), + + vertex_shader: + r#" + struct VertexInput { + @location(0) vertexPosition: vec3f, + @location(1) vertexTexCoord: vec2f, + @location(4) boneWeights: vec4f, + @location(5) boneIndices: vec4f, + @builtin(vertex_index) vertex_index: u32, + }; + + struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) texCoord: vec2f, + @location(1) worldPosition: vec3f, + }; + + @vertex fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + var localPosition = vec4f(0.0); + var inputPosition = vec4f(input.vertexPosition, 1.0); + + for (var i: i32 = 0; i < i32(fyrox_instanceData.blendShapesCount); i++) { + let offsets = S_FetchBlendShapeOffsets(blendShapesStorage_tex, blendShapesStorage_samp, i32(input.vertex_index), i); + let weight = fyrox_instanceData.blendShapesWeights[i / 4][i % 4]; + inputPosition += vec4f(offsets.position * weight, 0.0); + } + + if (fyrox_instanceData.useSkeletalAnimation != 0u) { + let m0 = fyrox_boneMatrices.matrices[i32(input.boneIndices.x)]; + let m1 = fyrox_boneMatrices.matrices[i32(input.boneIndices.y)]; + let m2 = fyrox_boneMatrices.matrices[i32(input.boneIndices.z)]; + let m3 = fyrox_boneMatrices.matrices[i32(input.boneIndices.w)]; + + localPosition += m0 * inputPosition * input.boneWeights.x; + localPosition += m1 * inputPosition * input.boneWeights.y; + localPosition += m2 * inputPosition * input.boneWeights.z; + localPosition += m3 * inputPosition * input.boneWeights.w; + } else { + localPosition = inputPosition; + } + + output.position = fyrox_instanceData.worldViewProjection * localPosition; + output.worldPosition = (fyrox_instanceData.worldMatrix * localPosition).xyz; + output.texCoord = input.vertexTexCoord; + return output; + } + "#, + + fragment_shader: + r#" + @fragment fn fs_main(@location(0) texCoord: vec2f, @location(1) worldPosition: vec3f) -> @location(0) f32 { + if (textureSample(diffuseTexture_tex, diffuseTexture_samp, texCoord).a < 0.2) { discard; } + return length(fyrox_lightData.lightPosition - worldPosition); + } + "#, + ) + ], +) diff --git a/fyrox-material/src/shader/standard/wgpu/standard2d.shader b/fyrox-material/src/shader/standard/wgpu/standard2d.shader new file mode 100644 index 0000000000..d82f3cd1ac --- /dev/null +++ b/fyrox-material/src/shader/standard/wgpu/standard2d.shader @@ -0,0 +1,124 @@ +( + name: "Standard2DShader", + + resources: [ + ( + name: "diffuseTexture", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 0 + ), + ( + name: "fyrox_instanceData", + kind: PropertyGroup([ + // Autogenerated + ]), + binding: 0 + ), + ( + name: "fyrox_lightData", + kind: PropertyGroup([ + // Autogenerated + ]), + binding: 1 + ), + ( + name: "fyrox_lightsBlock", + kind: PropertyGroup([ + // Autogenerated + ]), + binding: 2 + ), + ], + + disabled_passes: ["GBuffer", "DirectionalShadow", "PointShadow", "SpotShadow"], + + passes: [ + ( + name: "Forward", + draw_parameters: DrawParameters( + cull_face: None, + color_write: ColorMask( + red: true, + green: true, + blue: true, + alpha: true, + ), + depth_write: true, + stencil_test: None, + depth_test: Some(LessOrEqual), + blend: Some(BlendParameters( + func: BlendFunc( + sfactor: SrcAlpha, + dfactor: OneMinusSrcAlpha, + alpha_sfactor: SrcAlpha, + alpha_dfactor: OneMinusSrcAlpha, + ), + equation: BlendEquation( + rgb: Add, + alpha: Add + ) + )), + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Keep, + write_mask: 0xFFFF_FFFF, + ), + scissor_box: None + ), + vertex_shader: + r#" + struct VertexInput { + @location(0) vertexPosition: vec3f, + @location(1) vertexTexCoord: vec2f, + @location(2) vertexColor: vec4f, + }; + + struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) texCoord: vec2f, + @location(1) color: vec4f, + @location(2) fragmentPosition: vec3f, + }; + + @vertex fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + output.texCoord = input.vertexTexCoord; + output.fragmentPosition = (fyrox_instanceData.worldMatrix * vec4f(input.vertexPosition, 1.0)).xyz; + output.position = fyrox_instanceData.worldViewProjection * vec4f(input.vertexPosition, 1.0); + output.color = input.vertexColor; + return output; + } + "#, + + fragment_shader: + r#" + @fragment fn fs_main( + @location(0) texCoord: vec2f, + @location(1) color: vec4f, + @location(2) fragmentPosition: vec3f + ) -> @location(0) vec4f { + var lighting = fyrox_lightData.ambientLightColor.xyz; + for (var i: i32 = 0; i < min(i32(fyrox_lightsBlock.lightCount), 16); i++) { + let halfHotspotAngleCos = fyrox_lightsBlock.lightsParameters[i].x; + let halfConeAngleCos = fyrox_lightsBlock.lightsParameters[i].y; + let lightColor = fyrox_lightsBlock.lightsColorRadius[i].xyz; + let radius = fyrox_lightsBlock.lightsColorRadius[i].w; + let lightPosition = fyrox_lightsBlock.lightsPosition[i]; + let direction = fyrox_lightsBlock.lightsDirection[i]; + + let toFragment = fragmentPosition - lightPosition; + let distance = length(toFragment); + let toFragmentNormalized = toFragment / distance; + let distanceAttenuation = S_LightDistanceAttenuation(distance, radius); + let spotAngleCos = dot(toFragmentNormalized, direction); + let directionalAttenuation = smoothstep(halfConeAngleCos, halfHotspotAngleCos, spotAngleCos); + lighting += lightColor * (distanceAttenuation * directionalAttenuation); + } + + return vec4f(lighting, 1.0) * color * S_SRGBToLinear(textureSample(diffuseTexture_tex, diffuseTexture_samp, texCoord)); + } + "#, + ) + ], +) diff --git a/fyrox-material/src/shader/standard/wgpu/standard_particle_system.shader b/fyrox-material/src/shader/standard/wgpu/standard_particle_system.shader new file mode 100644 index 0000000000..26bb2d2960 --- /dev/null +++ b/fyrox-material/src/shader/standard/wgpu/standard_particle_system.shader @@ -0,0 +1,174 @@ +( + name: "StandardParticleSystemShader", + + resources: [ + ( + name: "diffuseTexture", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 0 + ), + ( + name: "fyrox_sceneDepth", + kind: Texture(kind: DepthSampler2D, fallback: White), + binding: 1 + ), + ( + name: "properties", + kind: PropertyGroup([ + ( + name: "softBoundarySharpnessFactor", + kind: Float(value: 100.0), + ), + ( + name: "useLighting", + kind: Bool(value: false), + ), + ]), + binding: 0 + ), + ( + name: "fyrox_instanceData", + kind: PropertyGroup([ + // Autogenerated + ]), + binding: 1 + ), + ( + name: "fyrox_cameraData", + kind: PropertyGroup([ + // Autogenerated + ]), + binding: 2 + ), + ( + name: "fyrox_lightsBlock", + kind: PropertyGroup([ + // Autogenerated + ]), + binding: 3 + ), + ( + name: "fyrox_lightData", + kind: PropertyGroup([ + // Autogenerated + ]), + binding: 4 + ), + ], + + disabled_passes: ["GBuffer", "DirectionalShadow", "PointShadow", "SpotShadow"], + + passes: [ + ( + name: "Forward", + draw_parameters: DrawParameters( + cull_face: None, + color_write: ColorMask( + red: true, + green: true, + blue: true, + alpha: true, + ), + depth_write: false, + stencil_test: None, + depth_test: Some(Less), + blend: Some(BlendParameters( + func: BlendFunc( + sfactor: SrcAlpha, + dfactor: OneMinusSrcAlpha, + alpha_sfactor: SrcAlpha, + alpha_dfactor: OneMinusSrcAlpha, + ), + equation: BlendEquation( + rgb: Add, + alpha: Add + ) + )), + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Keep, + write_mask: 0xFFFF_FFFF, + ), + scissor_box: None + ), + vertex_shader: + r#" + struct VertexInput { + @location(0) vertexPosition: vec3f, + @location(1) vertexTexCoord: vec2f, + @location(2) particleSize: f32, + @location(3) particleRotation: f32, + @location(4) vertexColor: vec4f, + }; + + struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) texCoord: vec2f, + @location(1) color: vec4f, + @location(2) fragmentPosition: vec3f, + }; + + @vertex fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + output.color = S_SRGBToLinear(input.vertexColor); + output.texCoord = input.vertexTexCoord; + let vertexOffset = S_RotateVec2(input.vertexTexCoord * 2.0 - 1.0, input.particleRotation); + let worldPosition = fyrox_instanceData.worldMatrix * vec4f(input.vertexPosition, 1.0); + let offset = (vertexOffset.x * fyrox_cameraData.sideVector + vertexOffset.y * fyrox_cameraData.upVector) * input.particleSize; + let finalPosition = worldPosition + vec4f(offset.x, offset.y, offset.z, 0.0); + output.fragmentPosition = finalPosition.xyz; + output.position = fyrox_cameraData.viewProjectionMatrix * finalPosition; + return output; + } + "#, + + fragment_shader: + r#" + fn toProjSpace(z: f32) -> f32 { + return (fyrox_cameraData.zFar * fyrox_cameraData.zNear) / (fyrox_cameraData.zFar - z * fyrox_cameraData.zRange); + } + + @fragment fn fs_main( + @location(0) texCoord: vec2f, + @location(1) color: vec4f, + @location(2) fragmentPosition: vec3f, + @builtin(position) fragCoord: vec4f + ) -> @location(0) vec4f { + let depthTextureSize = textureDimensions(fyrox_sceneDepth_tex, 0); + let pixelSize = vec2f(1.0 / f32(depthTextureSize.x), 1.0 / f32(depthTextureSize.y)); + let sceneDepth = toProjSpace(textureSample(fyrox_sceneDepth_tex, fyrox_sceneDepth_samp, fragCoord.xy * pixelSize)); + let fragmentDepth = toProjSpace(fragCoord.z); + let depthOpacity = smoothstep((sceneDepth - fragmentDepth) * properties.softBoundarySharpnessFactor, 0.0, 1.0); + + var lighting: vec3f; + if (properties.useLighting != 0u) { + lighting = fyrox_lightData.ambientLightColor.xyz; + for (var i: i32 = 0; i < min(i32(fyrox_lightsBlock.lightCount), 16); i++) { + let halfHotspotAngleCos = fyrox_lightsBlock.lightsParameters[i].x; + let halfConeAngleCos = fyrox_lightsBlock.lightsParameters[i].y; + let lightColor = fyrox_lightsBlock.lightsColorRadius[i].xyz; + let radius = fyrox_lightsBlock.lightsColorRadius[i].w; + let lightPosition = fyrox_lightsBlock.lightsPosition[i]; + let direction = fyrox_lightsBlock.lightsDirection[i]; + + let toFragment = fragmentPosition - lightPosition; + let dist = length(toFragment); + let toFragmentNormalized = toFragment / dist; + let distanceAttenuation = S_LightDistanceAttenuation(dist, radius); + let spotAngleCos = dot(toFragmentNormalized, direction); + let directionalAttenuation = smoothstep(halfConeAngleCos, halfHotspotAngleCos, spotAngleCos); + lighting += lightColor * (distanceAttenuation * directionalAttenuation); + } + } else { + lighting = vec3f(1.0); + } + + var fragColor = vec4f(lighting, 1.0) * color * S_SRGBToLinear(textureSample(diffuseTexture_tex, diffuseTexture_samp, texCoord)).r; + fragColor.a *= depthOpacity; + return fragColor; + } + "#, + ) + ], +) diff --git a/fyrox-material/src/shader/standard/wgpu/standard_sprite.shader b/fyrox-material/src/shader/standard/wgpu/standard_sprite.shader new file mode 100644 index 0000000000..3b40e3bf55 --- /dev/null +++ b/fyrox-material/src/shader/standard/wgpu/standard_sprite.shader @@ -0,0 +1,102 @@ +( + name: "StandardSpriteShader", + + resources: [ + ( + name: "diffuseTexture", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 0 + ), + ( + name: "fyrox_instanceData", + kind: PropertyGroup([ + // Autogenerated + ]), + binding: 0 + ), + ( + name: "fyrox_cameraData", + kind: PropertyGroup([ + // Autogenerated + ]), + binding: 1 + ), + ], + + disabled_passes: ["GBuffer", "DirectionalShadow", "PointShadow", "SpotShadow"], + + passes: [ + ( + name: "Forward", + draw_parameters: DrawParameters( + cull_face: None, + color_write: ColorMask( + red: true, + green: true, + blue: true, + alpha: true, + ), + depth_write: false, + stencil_test: None, + depth_test: Some(Less), + blend: Some(BlendParameters( + func: BlendFunc( + sfactor: SrcAlpha, + dfactor: OneMinusSrcAlpha, + alpha_sfactor: SrcAlpha, + alpha_dfactor: OneMinusSrcAlpha, + ), + equation: BlendEquation( + rgb: Add, + alpha: Add + ) + )), + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Keep, + write_mask: 0xFFFF_FFFF, + ), + scissor_box: None + ), + vertex_shader: + r#" + struct VertexInput { + @location(0) vertexPosition: vec3f, + @location(1) vertexTexCoord: vec2f, + @location(2) vertexParams: vec4f, + @location(3) vertexColor: vec4f, + }; + + struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) texCoord: vec2f, + @location(1) color: vec4f, + }; + + @vertex fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + let size = input.vertexParams.x; + let rotation = input.vertexParams.y; + let dx = input.vertexParams.z; + let dy = input.vertexParams.w; + + output.texCoord = input.vertexTexCoord; + output.color = input.vertexColor; + let vertexOffset = S_RotateVec2(vec2f(dx, dy), rotation); + let worldPosition = fyrox_instanceData.worldMatrix * vec4f(input.vertexPosition, 1.0); + let offset = (vertexOffset.x * fyrox_cameraData.sideVector + vertexOffset.y * fyrox_cameraData.upVector) * size; + output.position = fyrox_cameraData.viewProjectionMatrix * (worldPosition + vec4f(offset.x, offset.y, offset.z, 0.0)); + return output; + } + "#, + + fragment_shader: + r#" + @fragment fn fs_main(@location(0) texCoord: vec2f, @location(1) color: vec4f) -> @location(0) vec4f { + return color * S_SRGBToLinear(textureSample(diffuseTexture_tex, diffuseTexture_samp, texCoord)); + } + "#, + ) + ], +) diff --git a/fyrox-material/src/shader/standard/wgpu/terrain.shader b/fyrox-material/src/shader/standard/wgpu/terrain.shader new file mode 100644 index 0000000000..22905f9668 --- /dev/null +++ b/fyrox-material/src/shader/standard/wgpu/terrain.shader @@ -0,0 +1,222 @@ +( + name: "StandardTerrainShader", + + resources: [ + (name: "diffuseTexture", kind: Texture(kind: Sampler2D, fallback: White), binding: 0), + (name: "normalTexture", kind: Texture(kind: Sampler2D, fallback: Normal), binding: 1), + (name: "metallicTexture", kind: Texture(kind: Sampler2D, fallback: Black), binding: 2), + (name: "roughnessTexture", kind: Texture(kind: Sampler2D, fallback: White), binding: 3), + (name: "heightTexture", kind: Texture(kind: Sampler2D, fallback: Black), binding: 4), + (name: "emissionTexture", kind: Texture(kind: Sampler2D, fallback: Black), binding: 5), + (name: "lightmapTexture", kind: Texture(kind: Sampler2D, fallback: Black), binding: 6), + (name: "aoTexture", kind: Texture(kind: Sampler2D, fallback: White), binding: 7), + (name: "maskTexture", kind: Texture(kind: Sampler2D, fallback: White), binding: 8), + (name: "heightMapTexture", kind: Texture(kind: Sampler2D, fallback: White), binding: 9), + (name: "holeMaskTexture", kind: Texture(kind: Sampler2D, fallback: White), binding: 10), + ( + name: "properties", + kind: PropertyGroup([ + (name: "nodeUvOffsets", kind: Vector4(value: (0.0, 0.0, 0.0, 0.0))), + (name: "texCoordScale", kind: Vector2(value: (1.0, 1.0))), + (name: "layerIndex", kind: UInt(value: 0)), + (name: "emissionStrength", kind: Vector3(value: (2.0, 2.0, 2.0))), + (name: "diffuseColor", kind: Color(r: 255, g: 255, b: 255, a: 255)), + (name: "parallaxCenter", kind: Float(value: 0.0)), + (name: "parallaxScale", kind: Float(value: 0.08)), + ]), + binding: 0 + ), + (name: "fyrox_instanceData", kind: PropertyGroup([]), binding: 1), + (name: "fyrox_graphicsSettings", kind: PropertyGroup([]), binding: 2), + (name: "fyrox_cameraData", kind: PropertyGroup([]), binding: 3), + (name: "fyrox_lightData", kind: PropertyGroup([]), binding: 4), + ], + + passes: [ + ( + name: "GBuffer", + draw_parameters: DrawParameters(cull_face: Some(Back), color_write: ColorMask(red: true, green: true, blue: true, alpha: true), depth_write: true, stencil_test: None, depth_test: Some(LessOrEqual), blend: Some(BlendParameters(func: BlendFunc(sfactor: SrcAlpha, dfactor: OneMinusSrcAlpha, alpha_sfactor: SrcAlpha, alpha_dfactor: OneMinusSrcAlpha), equation: BlendEquation(rgb: Add, alpha: Max))), stencil_op: StencilOp(fail: Keep, zfail: Keep, zpass: Keep, write_mask: 0xFFFF_FFFF), scissor_box: None), + vertex_shader: + r#" + struct VertexInput { + @location(0) vertexPosition: vec3f, @location(1) vertexTexCoord: vec2f, @location(2) vertexNormal: vec3f, + @location(3) vertexTangent: vec4f, @location(6) vertexSecondTexCoord: vec2f, + }; + struct VertexOutput { + @builtin(position) position: vec4f, @location(0) outPosition: vec3f, @location(1) outNormal: vec3f, + @location(2) texCoord: vec2f, @location(3) outTangent: vec3f, @location(4) outBinormal: vec3f, @location(5) secondTexCoord: vec2f, + }; + @vertex fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + let actualTexCoords = input.vertexTexCoord * properties.nodeUvOffsets.zw + properties.nodeUvOffsets.xy; + let heightSize = vec2f(textureDimensions(heightMapTexture_tex, 0)); + let innerSize = heightSize - 3.0; + let pixelSize = 1.0 / heightSize; + let heightCoords = (actualTexCoords * innerSize + 1.5) * pixelSize; + let heightDim = textureDimensions(heightMapTexture_tex, 0); + let height = textureLoad(heightMapTexture_tex, vec2i(heightCoords * vec2f(heightDim)), 0).r; + let finalVertexPosition = vec4f(input.vertexPosition.x, height, input.vertexPosition.z, 1.0); + let hx0 = textureLoad(heightMapTexture_tex, vec2i((heightCoords + vec2f(-1.0, 0.0) * pixelSize) * vec2f(heightDim)), 0).r; + let hx1 = textureLoad(heightMapTexture_tex, vec2i((heightCoords + vec2f(1.0, 0.0) * pixelSize) * vec2f(heightDim)), 0).r; + let hy0 = textureLoad(heightMapTexture_tex, vec2i((heightCoords + vec2f(0.0, -1.0) * pixelSize) * vec2f(heightDim)), 0).r; + let hy1 = textureLoad(heightMapTexture_tex, vec2i((heightCoords + vec2f(0.0, 1.0) * pixelSize) * vec2f(heightDim)), 0).r; + let n = vec3f((hx0 - hx1) / 2.0, 1.0, (hy0 - hy1) / 2.0); + let tan = vec3f(n.y, -n.x, 0.0); + let nm = mat3x3f(fyrox_instanceData.worldMatrix[0].xyz, fyrox_instanceData.worldMatrix[1].xyz, fyrox_instanceData.worldMatrix[2].xyz); + output.outNormal = normalize(nm * n); + output.outTangent = normalize(nm * tan); + output.outBinormal = normalize(-1.0 * cross(output.outNormal, output.outTangent)); + output.texCoord = actualTexCoords; + output.outPosition = (fyrox_instanceData.worldMatrix * finalVertexPosition).xyz; + output.secondTexCoord = input.vertexSecondTexCoord; + output.position = fyrox_instanceData.worldViewProjection * finalVertexPosition; + return output; + } + "#, + fragment_shader: + r#" + struct FragmentOutput { @location(0) outColor: vec4f, @location(1) outNormal: vec4f, @location(2) outAmbient: vec4f, @location(3) outMaterial: vec4f, @location(4) outDecalMask: u32 }; + @fragment fn fs_main(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) texCoord: vec2f, @location(3) tangent: vec3f, @location(4) binormal: vec3f, @location(5) secondTexCoord: vec2f) -> FragmentOutput { + var output: FragmentOutput; + if (textureSample(holeMaskTexture_tex, holeMaskTexture_samp, texCoord).r < 0.5) { discard; } + let tangentSpace = mat3x3f(tangent, binormal, normal); + let toFragment = normalize(position - fyrox_cameraData.position); + var tc: vec2f; + if (fyrox_graphicsSettings.usePOM != 0u) { tc = S_ComputeParallaxTextureCoordinates(heightTexture_tex, heightTexture_samp, normalize(transpose(tangentSpace) * toFragment), texCoord * properties.texCoordScale, properties.parallaxCenter, properties.parallaxScale); } else { tc = texCoord * properties.texCoordScale; } + output.outColor = properties.diffuseColor * textureSample(diffuseTexture_tex, diffuseTexture_samp, tc); + let n = normalize(textureSample(normalTexture_tex, normalTexture_samp, tc).xyz * 2.0 - 1.0); + output.outNormal = vec4f(normalize(tangentSpace * n) * 0.5 + 0.5, 1.0); + output.outMaterial.x = textureSample(metallicTexture_tex, metallicTexture_samp, tc).r; + output.outMaterial.y = textureSample(roughnessTexture_tex, roughnessTexture_samp, tc).r; + output.outMaterial.z = textureSample(aoTexture_tex, aoTexture_samp, tc).r; output.outMaterial.a = 1.0; + output.outAmbient = vec4f(properties.emissionStrength * textureSample(emissionTexture_tex, emissionTexture_samp, tc).rgb + textureSample(lightmapTexture_tex, lightmapTexture_samp, secondTexCoord).rgb, 1.0); output.outDecalMask = properties.layerIndex; + let mask = textureSample(maskTexture_tex, maskTexture_samp, texCoord).r; + output.outColor.a = mask; output.outAmbient.a = mask; output.outNormal.a = mask; output.outMaterial.a = mask; + return output; + } + "#, + ), + ( + name: "Forward", + draw_parameters: DrawParameters(cull_face: Some(Back), color_write: ColorMask(red: true, green: true, blue: true, alpha: true), depth_write: true, stencil_test: None, depth_test: Some(Less), blend: Some(BlendParameters(func: BlendFunc(sfactor: SrcAlpha, dfactor: OneMinusSrcAlpha, alpha_sfactor: SrcAlpha, alpha_dfactor: OneMinusSrcAlpha), equation: BlendEquation(rgb: Add, alpha: Max))), stencil_op: StencilOp(fail: Keep, zfail: Keep, zpass: Keep, write_mask: 0xFFFF_FFFF), scissor_box: None), + vertex_shader: + r#" + struct VertexInput { @location(0) vertexPosition: vec3f, @location(1) vertexTexCoord: vec2f }; + struct VertexOutput { @builtin(position) position: vec4f, @location(0) outPosition: vec3f, @location(1) texCoord: vec2f }; + @vertex fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + let actualTexCoords = input.vertexTexCoord * properties.nodeUvOffsets.zw + properties.nodeUvOffsets.xy; + let heightSize = vec2f(textureDimensions(heightMapTexture_tex, 0)); + let innerSize = heightSize - 3.0; + let pixelSize = 1.0 / heightSize; + let heightCoords = (actualTexCoords * innerSize + 1.5) * pixelSize; + let heightDim = textureDimensions(heightMapTexture_tex, 0); + let height = textureLoad(heightMapTexture_tex, vec2i(heightCoords * vec2f(heightDim)), 0).r; + let finalVertexPosition = vec4f(input.vertexPosition.x, height, input.vertexPosition.z, 1.0); + output.position = fyrox_instanceData.worldViewProjection * finalVertexPosition; + output.texCoord = actualTexCoords; + return output; + } + "#, + fragment_shader: + r#" + @fragment fn fs_main(@location(1) texCoord: vec2f) -> @location(0) vec4f { + if (textureSample(holeMaskTexture_tex, holeMaskTexture_samp, texCoord).r < 0.5) { discard; } + return properties.diffuseColor * S_SRGBToLinear(textureSample(diffuseTexture_tex, diffuseTexture_samp, texCoord)); + } + "#, + ), + ( + name: "SpotShadow", + draw_parameters: DrawParameters(cull_face: Some(Back), color_write: ColorMask(red: false, green: false, blue: false, alpha: false), depth_write: true, stencil_test: None, depth_test: Some(Less), blend: None, stencil_op: StencilOp(fail: Keep, zfail: Keep, zpass: Keep, write_mask: 0xFFFF_FFFF), scissor_box: None), + vertex_shader: + r#" + struct VertexInput { @location(0) vertexPosition: vec3f, @location(1) vertexTexCoord: vec2f }; + struct VertexOutput { @builtin(position) position: vec4f, @location(0) texCoord: vec2f }; + @vertex fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + let actualTexCoords = input.vertexTexCoord * properties.nodeUvOffsets.zw + properties.nodeUvOffsets.xy; + let heightSize = vec2f(textureDimensions(heightMapTexture_tex, 0)); + let innerSize = heightSize - 3.0; + let pixelSize = 1.0 / heightSize; + let heightCoords = (actualTexCoords * innerSize + 1.5) * pixelSize; + let heightDim = textureDimensions(heightMapTexture_tex, 0); + let height = textureLoad(heightMapTexture_tex, vec2i(heightCoords * vec2f(heightDim)), 0).r; + let finalVertexPosition = vec4f(input.vertexPosition.x, height, input.vertexPosition.z, 1.0); + output.position = fyrox_instanceData.worldViewProjection * finalVertexPosition; + output.texCoord = actualTexCoords; + return output; + } + "#, + fragment_shader: + r#" + @fragment fn fs_main(@location(0) texCoord: vec2f) { + if (textureSample(holeMaskTexture_tex, holeMaskTexture_samp, texCoord).r < 0.5) { discard; } + if (textureSample(diffuseTexture_tex, diffuseTexture_samp, texCoord).a < 0.2) { discard; } + } + "#, + ), + ( + name: "DirectionalShadow", + draw_parameters: DrawParameters(cull_face: Some(Back), color_write: ColorMask(red: false, green: false, blue: false, alpha: false), depth_write: true, stencil_test: None, depth_test: Some(Less), blend: None, stencil_op: StencilOp(fail: Keep, zfail: Keep, zpass: Keep, write_mask: 0xFFFF_FFFF), scissor_box: None), + vertex_shader: + r#" + struct VertexInput { @location(0) vertexPosition: vec3f, @location(1) vertexTexCoord: vec2f }; + struct VertexOutput { @builtin(position) position: vec4f, @location(0) texCoord: vec2f }; + @vertex fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + let actualTexCoords = input.vertexTexCoord * properties.nodeUvOffsets.zw + properties.nodeUvOffsets.xy; + let heightSize = vec2f(textureDimensions(heightMapTexture_tex, 0)); + let innerSize = heightSize - 3.0; + let pixelSize = 1.0 / heightSize; + let heightCoords = (actualTexCoords * innerSize + 1.5) * pixelSize; + let heightDim = textureDimensions(heightMapTexture_tex, 0); + let height = textureLoad(heightMapTexture_tex, vec2i(heightCoords * vec2f(heightDim)), 0).r; + let finalVertexPosition = vec4f(input.vertexPosition.x, height, input.vertexPosition.z, 1.0); + output.position = fyrox_instanceData.worldViewProjection * finalVertexPosition; + output.texCoord = actualTexCoords; + return output; + } + "#, + fragment_shader: + r#" + @fragment fn fs_main(@location(0) texCoord: vec2f) { + if (textureSample(holeMaskTexture_tex, holeMaskTexture_samp, texCoord).r < 0.5) { discard; } + if (textureSample(diffuseTexture_tex, diffuseTexture_samp, texCoord).a < 0.2) { discard; } + } + "#, + ), + ( + name: "PointShadow", + draw_parameters: DrawParameters(cull_face: Some(Back), color_write: ColorMask(red: true, green: true, blue: true, alpha: true), depth_write: true, stencil_test: None, depth_test: Some(Less), blend: None, stencil_op: StencilOp(fail: Keep, zfail: Keep, zpass: Keep, write_mask: 0xFFFF_FFFF), scissor_box: None), + vertex_shader: + r#" + struct VertexInput { @location(0) vertexPosition: vec3f, @location(1) vertexTexCoord: vec2f }; + struct VertexOutput { @builtin(position) position: vec4f, @location(0) texCoord: vec2f, @location(1) worldPosition: vec3f }; + @vertex fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + let actualTexCoords = input.vertexTexCoord * properties.nodeUvOffsets.zw + properties.nodeUvOffsets.xy; + let heightSize = vec2f(textureDimensions(heightMapTexture_tex, 0)); + let innerSize = heightSize - 3.0; + let pixelSize = 1.0 / heightSize; + let heightCoords = (actualTexCoords * innerSize + 1.5) * pixelSize; + let heightDim = textureDimensions(heightMapTexture_tex, 0); + let height = textureLoad(heightMapTexture_tex, vec2i(heightCoords * vec2f(heightDim)), 0).r; + let finalVertexPosition = vec4f(input.vertexPosition.x, height, input.vertexPosition.z, 1.0); + output.position = fyrox_instanceData.worldViewProjection * finalVertexPosition; + output.worldPosition = (fyrox_instanceData.worldMatrix * finalVertexPosition).xyz; + output.texCoord = actualTexCoords; + return output; + } + "#, + fragment_shader: + r#" + @fragment fn fs_main(@location(0) texCoord: vec2f, @location(1) worldPosition: vec3f) -> @location(0) f32 { + if (textureSample(holeMaskTexture_tex, holeMaskTexture_samp, texCoord).r < 0.5) { discard; } + if (textureSample(diffuseTexture_tex, diffuseTexture_samp, texCoord).a < 0.2) { discard; } + return length(fyrox_lightData.lightPosition - worldPosition); + } + "#, + ) + ], +) diff --git a/fyrox-material/src/shader/standard/wgpu/tile.shader b/fyrox-material/src/shader/standard/wgpu/tile.shader new file mode 100644 index 0000000000..aa42f29ad0 --- /dev/null +++ b/fyrox-material/src/shader/standard/wgpu/tile.shader @@ -0,0 +1,124 @@ +( + name: "Standard2DShader", + + resources: [ + ( + name: "diffuseTexture", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 0 + ), + ( + name: "fyrox_instanceData", + kind: PropertyGroup([ + // Autogenerated + ]), + binding: 0 + ), + ( + name: "fyrox_lightData", + kind: PropertyGroup([ + // Autogenerated + ]), + binding: 1 + ), + ( + name: "fyrox_lightsBlock", + kind: PropertyGroup([ + // Autogenerated + ]), + binding: 2 + ), + ], + + disabled_passes: ["GBuffer", "DirectionalShadow", "PointShadow", "SpotShadow"], + + passes: [ + ( + name: "Forward", + draw_parameters: DrawParameters( + cull_face: None, + color_write: ColorMask( + red: true, + green: true, + blue: true, + alpha: true, + ), + depth_write: true, + stencil_test: None, + depth_test: Some(LessOrEqual), + blend: Some(BlendParameters( + func: BlendFunc( + sfactor: SrcAlpha, + dfactor: OneMinusSrcAlpha, + alpha_sfactor: SrcAlpha, + alpha_dfactor: OneMinusSrcAlpha, + ), + equation: BlendEquation( + rgb: Add, + alpha: Add + ) + )), + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Keep, + write_mask: 0xFFFF_FFFF, + ), + scissor_box: None + ), + vertex_shader: + r#" + struct VertexInput { + @location(0) vertexPosition: vec3f, + @location(1) vertexTexCoord: vec2f, + @location(2) vertexColor: vec4f, + }; + + struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) texCoord: vec2f, + @location(1) color: vec4f, + @location(2) fragmentPosition: vec3f, + }; + + @vertex fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + output.texCoord = input.vertexTexCoord / vec2f(textureDimensions(diffuseTexture_tex, 0)); + output.fragmentPosition = (fyrox_instanceData.worldMatrix * vec4f(input.vertexPosition, 1.0)).xyz; + output.position = fyrox_instanceData.worldViewProjection * vec4f(input.vertexPosition, 1.0); + output.color = input.vertexColor; + return output; + } + "#, + + fragment_shader: + r#" + @fragment fn fs_main( + @location(0) texCoord: vec2f, + @location(1) color: vec4f, + @location(2) fragmentPosition: vec3f + ) -> @location(0) vec4f { + var lighting = fyrox_lightData.ambientLightColor.xyz; + for (var i: i32 = 0; i < min(i32(fyrox_lightsBlock.lightCount), 16); i++) { + let halfHotspotAngleCos = fyrox_lightsBlock.lightsParameters[i].x; + let halfConeAngleCos = fyrox_lightsBlock.lightsParameters[i].y; + let lightColor = fyrox_lightsBlock.lightsColorRadius[i].xyz; + let radius = fyrox_lightsBlock.lightsColorRadius[i].w; + let lightPosition = fyrox_lightsBlock.lightsPosition[i]; + let direction = fyrox_lightsBlock.lightsDirection[i]; + + let toFragment = fragmentPosition - lightPosition; + let distance = length(toFragment); + let toFragmentNormalized = toFragment / distance; + let distanceAttenuation = S_LightDistanceAttenuation(distance, radius); + let spotAngleCos = dot(toFragmentNormalized, direction); + let directionalAttenuation = smoothstep(halfConeAngleCos, halfHotspotAngleCos, spotAngleCos); + lighting += lightColor * (distanceAttenuation * directionalAttenuation); + } + + return vec4f(lighting, 1.0) * color * S_SRGBToLinear(textureSample(diffuseTexture_tex, diffuseTexture_samp, texCoord)); + } + "#, + ) + ], +) diff --git a/fyrox-material/src/shader/standard/wgpu/widget.shader b/fyrox-material/src/shader/standard/wgpu/widget.shader new file mode 100644 index 0000000000..153b4341f5 --- /dev/null +++ b/fyrox-material/src/shader/standard/wgpu/widget.shader @@ -0,0 +1,155 @@ +( + name: "Widget", + resources: [ + ( + name: "fyrox_widgetTexture", + kind: Texture(kind: Sampler2D, fallback: White), + binding: 0 + ), + ( + name: "fyrox_widgetData", + kind: PropertyGroup([ + // Autogenerated + ]), + binding: 0 + ), + ], + disabled_passes: ["GBuffer", "DirectionalShadow", "PointShadow", "SpotShadow"], + passes: [ + ( + name: "Forward", + + vertex_shader: + r#" + struct VertexInput { + @location(0) vertexPosition: vec2f, + @location(1) vertexTexCoord: vec2f, + @location(2) vertexColor: vec4f, + }; + + struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) texCoord: vec2f, + @location(1) color: vec4f, + @location(2) localPosition: vec2f, + }; + + @vertex fn vs_main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + output.texCoord = input.vertexTexCoord; + output.color = input.vertexColor; + output.localPosition = input.vertexPosition; + + // Умножаем mat3x3 на vec3f (получаем vec3f) + let worldSpaceVertex = fyrox_widgetData.worldMatrix * vec3f(input.vertexPosition, 1.0); + + // Умножаем mat4x4 на vec4f (получаем vec4f) + var clip_pos = fyrox_widgetData.projectionMatrix * vec4f(worldSpaceVertex, 1.0); + + // WebGPU Z-range: конвертируем OpenGL [-1, 1] в WebGPU [0, 1] + clip_pos.z = (clip_pos.z + clip_pos.w) * 0.5; + + output.position = clip_pos; + return output; + } + "#, + + fragment_shader: + r#" + fn project_point(a: vec2f, b: vec2f, p: vec2f) -> f32 { + let ab = b - a; + return clamp(dot(p - a, ab) / dot(ab, ab), 0.0, 1.0); + } + + fn find_stop_index(t: f32) -> i32 { + var idx: i32 = 0; + for (var i: i32 = 0; i < i32(fyrox_widgetData.gradientPointCount); i++) { + if (t > fyrox_widgetData.gradientStops[i].x) { + idx = i; + } + } + return idx; + } + + @fragment fn fs_main( + @location(0) texCoord: vec2f, + @location(1) color: vec4f, + @location(2) localPosition: vec2f + ) -> @location(0) vec4f { + let size = vec2f(fyrox_widgetData.boundsMax.x - fyrox_widgetData.boundsMin.x, fyrox_widgetData.boundsMax.y - fyrox_widgetData.boundsMin.y); + let normalizedPosition = (localPosition - fyrox_widgetData.boundsMin) / size; + + var fragColor: vec4f; + if (fyrox_widgetData.brushType == 0) { + fragColor = fyrox_widgetData.solidColor; + } else { + var t: f32 = 0.0; + if (fyrox_widgetData.brushType == 1) { + t = project_point(fyrox_widgetData.gradientOrigin, fyrox_widgetData.gradientEnd, normalizedPosition); + } else if (fyrox_widgetData.brushType == 2) { + t = clamp(length(normalizedPosition - fyrox_widgetData.gradientOrigin), 0.0, 1.0); + } + let current = find_stop_index(t); + let next = min(current + 1, i32(fyrox_widgetData.gradientPointCount)); + let delta = fyrox_widgetData.gradientStops[next] - fyrox_widgetData.gradientStops[current]; + let mix_factor = (t - fyrox_widgetData.gradientStops[current]) / delta; + fragColor = mix(fyrox_widgetData.gradientColors[current], fyrox_widgetData.gradientColors[next], mix_factor); + } + + let diffuseColor = textureSample(fyrox_widgetTexture_tex, fyrox_widgetTexture_samp, texCoord); + + if (fyrox_widgetData.isFont != 0u) { + fragColor.a *= diffuseColor.r; + } else { + fragColor *= diffuseColor; + } + + fragColor.a *= fyrox_widgetData.opacity; + fragColor *= color; + return fragColor; + } + "#, + ), + ( + name: "Clip", + + draw_parameters: DrawParameters( + cull_face: None, + color_write: ColorMask( + red: false, + green: false, + blue: false, + alpha: false, + ), + depth_write: false, + stencil_test: None, + depth_test: None, + blend: None, + stencil_op: StencilOp( + fail: Keep, + zfail: Keep, + zpass: Incr, + write_mask: 0xFFFF_FFFF, + ), + scissor_box: None + ), + + vertex_shader: + r#" + @vertex fn vs_main(@location(0) vertexPosition: vec2f) -> @builtin(position) vec4f { + let worldSpaceVertex = fyrox_widgetData.worldMatrix * vec3f(vertexPosition, 1.0); + var clip_pos = fyrox_widgetData.projectionMatrix * vec4f(worldSpaceVertex, 1.0); + clip_pos.z = (clip_pos.z + clip_pos.w) * 0.5; + return clip_pos; + } + "#, + + fragment_shader: + r#" + @fragment fn fs_main() -> @location(0) vec4f { + return vec4f(1.0); + } + "#, + ) + ] +) diff --git a/fyrox/Cargo.toml b/fyrox/Cargo.toml index 884d8f6bf5..1a3f9738c7 100644 --- a/fyrox/Cargo.toml +++ b/fyrox/Cargo.toml @@ -18,6 +18,8 @@ rust-version = "1.94" default = ["fyrox-impl"] dylib = ["fyrox-dylib"] mesh_analysis = ["fyrox-impl/mesh_analysis", "fyrox-dylib/mesh_analysis"] +backend_opengl = ["fyrox-impl/backend_opengl"] +backend_wgpu = ["fyrox-impl/backend_wgpu"] [dependencies] fyrox-impl = { version = "2.0.0-rc.1", path = "../fyrox-impl", optional = true } diff --git a/project-manager/Cargo.toml b/project-manager/Cargo.toml index afb71e925f..d7d211e547 100644 --- a/project-manager/Cargo.toml +++ b/project-manager/Cargo.toml @@ -24,4 +24,9 @@ serde_json = "1.0.133" directories = "5.0.1" [build-dependencies] -toml_edit = "0.23.5" \ No newline at end of file +toml_edit = "0.23.5" + +[features] +default = ["backend_opengl"] +backend_opengl = ["fyrox/backend_opengl"] +backend_wgpu = ["fyrox/backend_wgpu"] \ No newline at end of file