Merge pull request #927 from nice0hack/master

Add wgpu graphics server
This commit is contained in:
nicehack
2026-08-17 21:52:40 +03:00
committed by GitHub
parent d30f42867f
commit d1e3469ca5
118 changed files with 10868 additions and 91 deletions
+2 -1
View File
@@ -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]
+7 -2
View File
@@ -13,6 +13,11 @@ readme = "README.md"
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" }
fyrox = { version = "2.0.0-rc.1", path = "../fyrox", default-features = false }
fyroxed_base = { version = "2.0.0-rc.1", path = "../editor", default-features = false }
clap = { version = "4", features = ["derive"] }
[features]
default = ["backend_opengl"]
backend_opengl = ["fyrox/backend_opengl", "fyroxed_base/backend_opengl"]
backend_wgpu = ["fyrox/backend_wgpu", "fyroxed_base/backend_wgpu"]
+3 -1
View File
@@ -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"]
@@ -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;
}
"#,
),
],
)
+215
View File
@@ -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;
}
"#,
),
],
)
@@ -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<f32, 9>;
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);
}
"#,
)
]
)
@@ -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);
}
"#,
)
]
)
@@ -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;
}
"#,
),
],
)
+15 -5
View File
@@ -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(not(feature = "backend_opengl"))]
const HIGHLIGHT_SHADER_SRC: &str = include_str!("../resources/shaders/wgpu/highlight.shader");
pub struct HighlightRenderPass {
framebuffer: GpuFrameBuffer,
edge_detect_shader: RenderPassContainer,
@@ -77,11 +83,8 @@ impl HighlightRenderPass {
pub fn new_raw(server: &dyn GraphicsServer, width: usize, height: usize) -> Self {
Self {
framebuffer: Self::create_frame_buffer(server, width, height),
edge_detect_shader: RenderPassContainer::from_str(
server,
include_str!("../resources/shaders/highlight.shader"),
)
.unwrap(),
edge_detect_shader: RenderPassContainer::from_str(server, HIGHLIGHT_SHADER_SRC)
.unwrap(),
scene_handle: Default::default(),
nodes_to_highlight: Default::default(),
}
@@ -107,6 +110,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");
+12 -1
View File
@@ -273,10 +273,21 @@ macro_rules! load_image {
};
}
#[cfg(feature = "backend_opengl")]
static GIZMO_SHADER: LazyLock<ShaderResource> = 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(not(feature = "backend_opengl"))]
static GIZMO_SHADER: LazyLock<ShaderResource> = LazyLock::new(|| {
ShaderResource::from_str(
Uuid::new_v4(),
include_str!("../resources/shaders/wgpu/gizmo.shader"),
Default::default(),
)
.unwrap()
+7 -5
View File
@@ -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(not(feature = "backend_opengl"))]
const OVERLAY_SHADER_SRC: &str = include_str!("../resources/shaders/wgpu/overlay.shader");
pub struct OverlayRenderPass {
quad: GpuGeometryBuffer,
shader: RenderPassContainer,
@@ -63,11 +69,7 @@ impl OverlayRenderPass {
server,
)
.unwrap(),
shader: RenderPassContainer::from_str(
server,
include_str!("../resources/shaders/overlay.shader"),
)
.unwrap(),
shader: RenderPassContainer::from_str(server, OVERLAY_SHADER_SRC).unwrap(),
sound_icon: TextureResource::load_from_memory(
Uuid::new_v4(),
ResourceKind::Embedded,
+12 -1
View File
@@ -270,10 +270,21 @@ fn make_shape_gizmo(
}
}
#[cfg(feature = "backend_opengl")]
static GIZMO_SHADER: LazyLock<ShaderResource> = 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(not(feature = "backend_opengl"))]
static GIZMO_SHADER: LazyLock<ShaderResource> = LazyLock::new(|| {
ShaderResource::from_str(
Uuid::new_v4(),
include_str!("../../../resources/shaders/wgpu/sprite_gizmo.shader"),
Default::default(),
)
.unwrap()
+12 -1
View File
@@ -152,10 +152,21 @@ pub struct GameScene {
pub settings_receiver: Receiver<SettingsMessage>,
}
#[cfg(feature = "backend_opengl")]
static GRID_SHADER: LazyLock<ShaderResource> = 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(not(feature = "backend_opengl"))]
static GRID_SHADER: LazyLock<ShaderResource> = LazyLock::new(|| {
ShaderResource::from_str(
Uuid::new_v4(),
include_str!("../../resources/shaders/wgpu/grid.shader"),
Default::default(),
)
.unwrap()
+2
View File
@@ -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",
}
}
+2 -1
View File
@@ -1494,7 +1494,8 @@ 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,
+22
View File
@@ -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"
+196
View File
@@ -0,0 +1,196 @@
// 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`].
///
/// 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<WgpuGraphicsServer>,
buffer: RefCell<wgpu::Buffer>,
size: Cell<usize>,
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<Self, FrameworkError> {
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(())
}
}
+141
View File
@@ -0,0 +1,141 @@
// 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 },
}
}
File diff suppressed because it is too large Load Diff
+241
View File
@@ -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`].
///
/// 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<WgpuGraphicsServer>,
vertex_buffers: RefCell<Vec<wgpu::Buffer>>,
vertex_buffer_layouts: Vec<wgpu::VertexBufferLayout<'static>>,
element_buffer: RefCell<wgpu::Buffer>,
element_count: Cell<usize>,
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<Self, FrameworkError> {
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<VertexAttribute>` 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<wgpu::Buffer>> {
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));
}
}
+90
View File
@@ -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;
+601
View File
@@ -0,0 +1,601 @@
// 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<f32>",
SamplerKind::Sampler2D => "texture_2d<f32>",
SamplerKind::Sampler3D => "texture_3d<f32>",
SamplerKind::SamplerCube => "texture_cube<f32>",
SamplerKind::USampler1D => "texture_1d<u32>",
SamplerKind::USampler2D => "texture_2d<u32>",
SamplerKind::USampler3D => "texture_3d<u32>",
SamplerKind::USamplerCube => "texture_cube<u32>",
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<vec4f, {max_len}>"),
ShaderPropertyKind::Int { .. } => "i32".into(),
ShaderPropertyKind::IntArray { max_len, .. } => format!("array<vec4i, {max_len}>"),
ShaderPropertyKind::UInt { .. } => "u32".into(),
ShaderPropertyKind::UIntArray { max_len, .. } => format!("array<vec4u, {max_len}>"),
ShaderPropertyKind::Bool { .. } => "u32".into(),
ShaderPropertyKind::Vector2 { .. } => "vec2f".into(),
ShaderPropertyKind::Vector2Array { max_len, .. } => format!("array<vec4f, {max_len}>"),
ShaderPropertyKind::Vector3 { .. } => "vec3f".into(),
ShaderPropertyKind::Vector3Array { max_len, .. } => format!("array<vec3f, {max_len}>"),
ShaderPropertyKind::Vector4 { .. } => "vec4f".into(),
ShaderPropertyKind::Vector4Array { max_len, .. } => format!("array<vec4f, {max_len}>"),
ShaderPropertyKind::Matrix2 { .. } => "mat2x2f".into(),
ShaderPropertyKind::Matrix2Array { max_len, .. } => format!("array<mat2x2f, {max_len}>"),
ShaderPropertyKind::Matrix3 { .. } => "mat3x3f".into(),
ShaderPropertyKind::Matrix3Array { max_len, .. } => format!("array<mat3x3f, {max_len}>"),
ShaderPropertyKind::Matrix4 { .. } => "mat4x4f".into(),
ShaderPropertyKind::Matrix4Array { max_len, .. } => format!("array<mat4x4f, {max_len}>"),
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<uniform> {}: 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<wgpu::ShaderModule, FrameworkError> {
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<usize, wgpu::TextureFormat> =
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`].
///
/// Wraps a compiled [`wgpu::ShaderModule`]. Shaders are compiled from WGSL source
/// with automatically generated resource binding declarations prepended.
pub struct WgpuShader {
_server: Weak<WgpuGraphicsServer>,
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<Self, FrameworkError> {
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`].
///
/// 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<WgpuGraphicsServer>,
name: String,
vertex_module: wgpu::ShaderModule,
fragment_module: wgpu::ShaderModule,
resources: Vec<ShaderResourceDefinition>,
#[allow(clippy::type_complexity)]
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<Self, FrameworkError> {
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<Self, FrameworkError> {
let vert = vs
.as_any()
.downcast_ref::<WgpuShader>()
.ok_or_else(|| FrameworkError::Custom("Expected WgpuShader".into()))?;
let frag = fs
.as_any()
.downcast_ref::<WgpuShader>()
.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<Self, FrameworkError> {
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<f32>;"));
assert!(decls.contains("@group(0) @binding(100) var diffuseTexture_samp: sampler;"));
assert!(decls.contains("@group(0) @binding(1) var shadowMap_tex: texture_cube<f32>;"));
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<uniform> properties: Tproperties;"));
}
#[test]
fn test_wgsl_texture_type_mapping() {
assert_eq!(wgsl_texture_type(SamplerKind::Sampler2D), "texture_2d<f32>");
assert_eq!(wgsl_texture_type(SamplerKind::Sampler3D), "texture_3d<f32>");
assert_eq!(
wgsl_texture_type(SamplerKind::SamplerCube),
"texture_cube<f32>"
);
assert_eq!(
wgsl_texture_type(SamplerKind::USampler2D),
"texture_2d<u32>"
);
}
#[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<vec4f, 32>"
);
}
}
+74
View File
@@ -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`].
///
/// **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<WgpuGraphicsServer>,
active: Cell<bool>,
}
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<Self, FrameworkError> {
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<QueryResult> {
Some(QueryResult::SamplesPassed(u32::MAX))
}
}
+180
View File
@@ -0,0 +1,180 @@
// 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`].
///
/// 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<WgpuGraphicsServer>,
buffer: wgpu::Buffer,
_pixel_count: usize,
pixel_size: usize,
request_pending: Cell<bool>,
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<Self, FrameworkError> {
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<Rect<i32>>,
) -> 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::<crate::texture::WgpuTexture>()
.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<Vec<u8>> {
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
}
}
}
}
+144
View File
@@ -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`].
///
/// 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<WgpuGraphicsServer>,
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<Self, FrameworkError> {
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
}
}
+893
View File
@@ -0,0 +1,893 @@
// 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<f32> {
var pos = array<vec2<f32>, 3>(
vec2<f32>(-1.0, -1.0),
vec2<f32>( 3.0, -1.0),
vec2<f32>(-1.0, 3.0)
);
return vec4<f32>(pos[idx], 0.0, 1.0);
}
@group(0) @binding(0) var src_tex: texture_2d<f32>;
@group(0) @binding(1) var src_sampler: sampler;
@fragment
fn fs_main(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
return textureLoad(src_tex, vec2<i32>(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<wgpu::BindGroup>,
/// Vertex buffers bound for this draw.
pub vertex_buffers: Vec<wgpu::Buffer>,
/// 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<i32>,
/// Stencil reference value, if stencil testing is enabled.
pub stencil_ref: Option<u32>,
/// Optional scissor clipping rectangle.
pub scissor_box: Option<ScissorBox>,
/// 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<wgpu::TextureView>,
/// Depth-stencil attachment view, if present.
pub depth_view: Option<wgpu::TextureView>,
/// Load operation for color attachments.
pub color_load: wgpu::LoadOp<wgpu::Color>,
/// Load operation for the depth attachment.
pub depth_load: wgpu::LoadOp<f32>,
/// Load operation for the stencil attachment, if present.
pub stencil_load: Option<wgpu::LoadOp<u32>>,
/// Accumulated draw commands to replay when the pass is flushed.
pub commands: Vec<DrawCommand>,
}
/// 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`] 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<HashMap>`]
/// 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<WgpuState>,
/// The rendering surface (window or canvas).
pub surface: wgpu::Surface<'static>,
/// Current surface configuration (format, size, present mode).
pub surface_config: RwLock<wgpu::SurfaceConfiguration>,
/// 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<HashMap<PipelineKey, wgpu::RenderPipeline>>,
/// Hash-based cache of bind groups, keyed by resource pointers and texture formats.
pub bind_group_cache: RefCell<HashMap<u64, wgpu::BindGroup>>,
weak_self: RefCell<Option<Weak<WgpuGraphicsServer>>>,
/// Tracked GPU memory usage (buffers + textures).
pub memory_usage: RefCell<ServerMemoryUsage>,
pipeline_statistics: RefCell<PipelineStatistics>,
/// 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<Option<wgpu::SurfaceTexture>>,
/// Whether the backbuffer needs clearing at the start of the next frame.
pub backbuffer_needs_clear: Cell<bool>,
/// Cached depth-stencil texture for the backbuffer, with its (width, height).
backbuffer_depth_stencil: RefCell<Option<(u32, u32, GpuTexture)>>,
/// 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<Option<wgpu::CommandEncoder>>,
/// Currently accumulating render pass. Draw commands are batched here and
/// flushed when the target changes or the frame ends.
pub active_pass: RefCell<Option<ActivePass>>,
/// 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<HashMap<wgpu::TextureFormat, wgpu::RenderPipeline>>,
/// Shared shader module for mipmap generation (created once).
mipmap_shader: OnceCell<wgpu::ShaderModule>,
/// Shared pipeline layout for mipmap generation (created once).
mipmap_pipeline_layout: OnceCell<wgpu::PipelineLayout>,
/// Current polygon fill mode (Fill, Line, or Point). Baked into new pipelines.
polygon_fill_mode: Cell<PolygonFillMode>,
}
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
#[allow(clippy::new_ret_no_self)]
pub fn new(
vsync: bool,
_msaa_sample_count: Option<u8>,
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<WgpuGraphicsServer> {
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<GpuBuffer, FrameworkError> {
Ok(GpuBuffer(Rc::new(WgpuBuffer::new(self, desc)?)))
}
fn create_texture(&self, desc: GpuTextureDescriptor) -> Result<GpuTexture, FrameworkError> {
Ok(GpuTexture(Rc::new(WgpuTexture::new(self, desc)?)))
}
fn create_sampler(&self, desc: GpuSamplerDescriptor) -> Result<GpuSampler, FrameworkError> {
Ok(GpuSampler(Rc::new(WgpuSampler::new(self, desc)?)))
}
fn create_frame_buffer(
&self,
depth: Option<Attachment>,
colors: Vec<Attachment>,
) -> Result<GpuFrameBuffer, FrameworkError> {
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<GpuQuery, FrameworkError> {
Ok(GpuQuery(Rc::new(WgpuQuery::new(self)?)))
}
fn create_shader(
&self,
name: String,
kind: ShaderKind,
source: String,
resources: &[ShaderResourceDefinition],
line_offset: isize,
) -> Result<GpuShader, FrameworkError> {
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<GpuProgram, FrameworkError> {
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<GpuProgram, FrameworkError> {
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<GpuAsyncReadBuffer, FrameworkError> {
Ok(GpuAsyncReadBuffer(Rc::new(WgpuAsyncReadBuffer::new(
self,
name,
pixel_size,
pixel_count,
)?)))
}
fn create_geometry_buffer(
&self,
desc: GpuGeometryBufferDescriptor,
) -> Result<GpuGeometryBuffer, FrameworkError> {
Ok(GpuGeometryBuffer(Rc::new(WgpuGeometryBuffer::new(
self, desc,
)?)))
}
fn weak(&self) -> Weak<dyn GraphicsServer> {
self.weak_ref() as Weak<dyn GraphicsServer>
}
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::<WgpuTexture>() 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) {}
}
+454
View File
@@ -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<function, f32>, maxT: ptr<function, f32>) -> 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<function, f32>, maxT: ptr<function, f32>) -> 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<f32>,
shadowMap_samp: sampler) -> f32
{
if (shadowsEnabled) {
let biasedFragmentDistance = fragmentDistance - shadowBias;
if (softShadows) {
const directions = array<vec3f, 20>(
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<f32>,
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, 20>(
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<f32>, 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<f32>, 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<f32>, 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<f32>, 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));
}
+526
View File
@@ -0,0 +1,526 @@
// 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<u8> {
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.
#[allow(clippy::too_many_arguments)]
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`].
///
/// 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<WgpuGraphicsServer>,
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<GpuTextureKind>,
pixel_kind: Cell<PixelKind>,
size_bytes: Cell<usize>,
}
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<Self, FrameworkError> {
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<usize, FrameworkError> {
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()
}
}
+4
View File
@@ -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.
+4 -2
View File
@@ -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
+4 -1
View File
@@ -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]
+20 -2
View File
@@ -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<GraphicsServerConstructorCallback>);
#[cfg(feature = "backend_opengl")]
impl Default for GraphicsServerConstructor {
fn default() -> Self {
Self(Rc::new(
|params, window_target, window_builder, named_objects| {
GlGraphicsServer::new(
fyrox_graphics_gl::server::GlGraphicsServer::new(
params.vsync,
params.msaa_sample_count,
window_target,
window_builder,
named_objects,
)
},
))
}
}
#[cfg(not(feature = "backend_opengl"))]
impl Default for GraphicsServerConstructor {
fn default() -> Self {
Self(Rc::new(
|params, window_target, window_builder, named_objects| {
fyrox_graphics_wgpu::server::WgpuGraphicsServer::new(
params.vsync,
params.msaa_sample_count,
window_target,
-3
View File
@@ -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 {
+20 -9
View File
@@ -88,7 +88,15 @@ impl UniformBufferCache {
pub fn get_or_create(&self, size: usize) -> Result<GpuBuffer, FrameworkError> {
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<GpuBuffer>,
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<Page>,
blocks: Vec<UniformBlockLocation>,
}
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;
}
+3 -2
View File
@@ -18,6 +18,7 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use crate::renderer::make_deferred_viewport_matrix;
use crate::{
asset::manager::ResourceManager,
core::{
@@ -48,7 +49,6 @@ use crate::{
framework::GeometryBufferExt,
gbuffer::GBuffer,
light_volume::LightVolumeRenderer,
make_viewport_matrix,
observer::Observer,
resources::RendererResources,
shadow::{
@@ -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
+51 -10
View File
@@ -168,16 +168,21 @@ fn recreate_render_data_if_needed<T: Any>(
frame_size: Vector2<f32>,
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::<T>(),
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<i32>) -> Matrix4<f32> {
Matrix4::new_orthographic(
0.0,
@@ -374,6 +380,36 @@ pub fn make_viewport_matrix(viewport: Rect<i32>) -> Matrix4<f32> {
))
}
/// 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(not(feature = "backend_opengl"))]
pub fn make_deferred_viewport_matrix(viewport: Rect<i32>) -> Matrix4<f32> {
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(feature = "backend_opengl")]
pub fn make_deferred_viewport_matrix(viewport: Rect<i32>) -> Matrix4<f32> {
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));
+47 -31
View File
@@ -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,93 @@ 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(not(feature = "backend_opengl"))]
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<Self, FrameworkError> {
Ok(Self {
decal: RenderPassContainer::from_str(server, include_str!("shaders/decal.shader"))?,
decal: RenderPassContainer::from_str(server, include_shader!("decal.shader"))?,
spot_light: RenderPassContainer::from_str(
server,
include_str!("shaders/deferred_spot_light.shader"),
include_shader!("deferred_spot_light.shader"),
)?,
point_light: RenderPassContainer::from_str(
server,
include_str!("shaders/deferred_point_light.shader"),
include_shader!("deferred_point_light.shader"),
)?,
directional_light: RenderPassContainer::from_str(
server,
include_str!("shaders/deferred_directional_light.shader"),
include_shader!("deferred_directional_light.shader"),
)?,
ambient_light: RenderPassContainer::from_str(
server,
include_str!("shaders/ambient_light.shader"),
include_shader!("ambient_light.shader"),
)?,
volume_marker_lit: RenderPassContainer::from_str(
server,
include_str!("shaders/volume_marker_lit.shader"),
include_shader!("volume_marker_lit.shader"),
)?,
pixel_counter: RenderPassContainer::from_str(
server,
include_str!("shaders/pixel_counter.shader"),
include_shader!("pixel_counter.shader"),
)?,
debug: RenderPassContainer::from_str(server, include_str!("shaders/debug.shader"))?,
fxaa: RenderPassContainer::from_str(server, include_str!("shaders/fxaa.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_str!("shaders/spot_volumetric.shader"),
include_shader!("spot_volumetric.shader"),
)?,
point_light_volume: RenderPassContainer::from_str(
server,
include_str!("shaders/point_volumetric.shader"),
include_shader!("point_volumetric.shader"),
)?,
volume_marker_vol: RenderPassContainer::from_str(
server,
include_str!("shaders/volume_marker_vol.shader"),
include_shader!("volume_marker_vol.shader"),
)?,
visibility_optimizer: RenderPassContainer::from_str(
server,
include_str!("shaders/visibility_optimizer.shader"),
include_shader!("visibility_optimizer.shader"),
)?,
ssao: RenderPassContainer::from_str(server, include_str!("shaders/ssao.shader"))?,
ssao: RenderPassContainer::from_str(server, include_shader!("ssao.shader"))?,
visibility: RenderPassContainer::from_str(
server,
include_str!("shaders/visibility.shader"),
include_shader!("visibility.shader"),
)?,
blit: RenderPassContainer::from_str(server, include_str!("shaders/blit.shader"))?,
blit: RenderPassContainer::from_str(server, include_shader!("blit.shader"))?,
hdr_adaptation: RenderPassContainer::from_str(
server,
include_str!("shaders/hdr_adaptation.shader"),
include_shader!("hdr_adaptation.shader"),
)?,
hdr_luminance: RenderPassContainer::from_str(
server,
include_str!("shaders/hdr_luminance.shader"),
include_shader!("hdr_luminance.shader"),
)?,
hdr_downscale: RenderPassContainer::from_str(
server,
include_str!("shaders/hdr_downscale.shader"),
include_shader!("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"))?,
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_str!("shaders/gaussian_blur.shader"),
include_shader!("gaussian_blur.shader"),
)?,
box_blur: RenderPassContainer::from_str(server, include_str!("shaders/blur.shader"))?,
box_blur: RenderPassContainer::from_str(server, include_shader!("blur.shader"))?,
ui: RenderPassContainer::from_str(
server,
str::from_utf8(
@@ -185,11 +199,11 @@ impl ShadersContainer {
)?,
environment_map_specular_convolution: RenderPassContainer::from_str(
server,
include_str!("shaders/prefilter.shader"),
include_shader!("prefilter.shader"),
)?,
environment_map_irradiance_convolution: RenderPassContainer::from_str(
server,
include_str!("shaders/irradiance.shader"),
include_shader!("irradiance.shader"),
)?,
})
}
@@ -309,9 +323,11 @@ impl RendererResources {
kind: BufferKind::Uniform,
usage: BufferUsage::StaticDraw,
})?;
const SIZE: usize = ShaderDefinition::MAX_BONE_MATRICES * size_of::<Matrix4<f32>>();
let zeros = [0.0; SIZE];
buffer.write_data(array_as_u8_slice(&zeros))?;
buffer.write_data(&vec![
0u8;
ShaderDefinition::MAX_BONE_MATRICES
* size_of::<Matrix4<f32>>()
])?;
buffer
},
linear_clamp_sampler: server.create_sampler(GpuSamplerDescriptor {
@@ -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);
}
"#,
)
]
)
@@ -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);
}
"#,
)
]
)
@@ -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);
}
}
"#,
)
]
)
@@ -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;
}
"#,
)
]
)
@@ -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;
}
"#,
)
]
)
@@ -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;
}
"#,
)
]
)
@@ -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);
}
"#,
)
]
)
@@ -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);
}
"#,
)
]
)
@@ -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);
}
"#,
)
]
)
@@ -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);
}
"#,
)
]
)
@@ -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<f32, 5>(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);
}
"#,
)
]
)
@@ -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;
}
"#,
)
]
)
@@ -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;
}
"#,
)
]
)
@@ -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;
}
"#,
)
]
)
@@ -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);
}
}
"#,
)
]
)
@@ -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);
}
"#,
)
]
)
@@ -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);
}
"#,
)
]
)
@@ -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);
}
"#,
)
]
)
@@ -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);
}
"#,
)
]
)
@@ -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));
}
"#,
)
]
)
@@ -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);
}
"#,
)
]
)
@@ -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);
}
"#,
)
]
)
@@ -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);
}
}
"#,
)
]
)
@@ -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);
}
"#,
)
]
)
@@ -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);
}
"#,
)
]
)
@@ -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);
}
"#,
)
]
)
+15 -1
View File
@@ -49,10 +49,24 @@ use crate::resource::texture::TextureMinificationFilter as FyroxMinFilter;
use gltf::texture::MagFilter as GltfMagFilter;
use gltf::texture::MinFilter as GltfMinFilter;
#[cfg(not(feature = "backend_opengl"))]
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<BuiltInResource<Shader>> = 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"),
@@ -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;
}
"#,
)
],
)
+4
View File
@@ -28,3 +28,7 @@ regex = "1"
[dev-dependencies]
fyrox-texture = { version = "2.0.0-rc.1", path = "../fyrox-texture" }
[features]
backend_opengl = []
backend_wgpu = []
+22 -8
View File
@@ -966,11 +966,25 @@ impl ShaderResourceExtension for ShaderResource {
}
}
#[cfg(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<BuiltInResource<Shader>> = 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<BuiltInResource<Shader>> = LazyLock::new(|| {
pub static STANDARD_2D: LazyLock<BuiltInResource<Shader>> = 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<BuiltInResource<Shader>> = LazyLock::new(|| {
pub static STANDARD_PARTICLE_SYSTEM: LazyLock<BuiltInResource<Shader>> = 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<BuiltInResource<Shader>> = LazyLoc
pub static STANDARD_SPRITE: LazyLock<BuiltInResource<Shader>> = 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<BuiltInResource<Shader>> = LazyLock::new(||
pub static STANDARD_TERRAIN: LazyLock<BuiltInResource<Shader>> = 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<BuiltInResource<Shader>> = LazyLock::new(|
pub static STANDARD_TILE: LazyLock<BuiltInResource<Shader>> = 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<BuiltInResource<Shader>> = LazyLock::new(|| {
pub static STANDARD_TWOSIDES: LazyLock<BuiltInResource<Shader>> = 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<BuiltInResource<Shader>> = LazyLock::new(
pub static STANDARD_WIDGET: LazyLock<BuiltInResource<Shader>> = 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"),

Some files were not shown because too many files have changed in this diff Show More