Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
450a9d1
feat(graphic): implement GBuffer pass and related systems
Miou-zora Jan 16, 2026
273a584
style: apply linter
github-actions[bot] Jan 16, 2026
f64ecf1
Merge branch 'main' into gbuffer-pass
Miou-zora Jan 21, 2026
a0a6864
feat(graphic): add support for RGBA8UnormSrgb texture format in Textu…
Miou-zora Jan 21, 2026
b98669f
refactor(texture): optimize pixel retrieval using std::bit_cast for R…
Miou-zora Jan 21, 2026
e47f68c
feat(gbuffer): implement texture descriptor creation and resize handl…
Miou-zora Jan 21, 2026
5659ff7
feat(gbuffer): update TransformGPUData structure and enhance GBuffer …
Miou-zora Jan 21, 2026
76fbb0e
style: apply linter
github-actions[bot] Jan 21, 2026
736e4b9
fix(gbuffer): correct normal calculation in vertex shader and update …
Miou-zora Jan 21, 2026
510b438
style: apply linter
github-actions[bot] Jan 21, 2026
1521c55
ci: enable verbose output for xmake test commands
Miou-zora Jan 23, 2026
1c3be86
ci: simplify xmake test command by removing debug flag
Miou-zora Jan 23, 2026
73af0cc
test(gbuffer): enable texture saving in ExtractTextures function
Miou-zora Jan 23, 2026
7ec59c8
Merge branch 'main' into gbuffer-pass
Miou-zora Jan 23, 2026
506ed74
fix(default-pipeline): resolve merge with main
Miou-zora Jan 23, 2026
2ec0949
style: apply linter
github-actions[bot] Jan 23, 2026
6d8b755
refactor(graphic): make code sonar compliant
Miou-zora Jan 23, 2026
1f115b8
📝 Add docstrings to `gbuffer-pass` (#430)
coderabbitai[bot] Jan 23, 2026
1964d04
fix(gbuffer): update model buffer size and use window dimensions for …
Miou-zora Jan 23, 2026
0ced421
style: apply linter
github-actions[bot] Jan 23, 2026
dbb2823
fix(gbuffer): use default window dimensions for G-buffer texture size…
Miou-zora Jan 23, 2026
171a6fc
style: apply linter
github-actions[bot] Jan 23, 2026
82fcefa
Merge branch 'main' into gbuffer-pass
Miou-zora Jan 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -255,20 +255,20 @@ jobs:
- name: Run tests (Linux)
if: contains(runner.os, 'linux')
run: |
xmake test -y -vD
xmake test -y -v
timeout-minutes: 120

- name: Run tests (Windows)
if: contains(runner.os, 'windows')
shell: pwsh
run: |
xmake test -y -vD
xmake test -y -v
timeout-minutes: 120

- name: Run tests (macOS)
if: contains(runner.os, 'macos')
run: |
xmake test -y -vD
xmake test -y -v
timeout-minutes: 120


Expand Down
3 changes: 3 additions & 0 deletions src/plugin/default-pipeline/src/DefaultPipeline.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
#include "resource/buffer/PointLightsBuffer.hpp"
#include "resource/buffer/TransformGPUBuffer.hpp"

#include "resource/pass/GBuffer.hpp"

#include "system/initialization/Create3DGraph.hpp"
#include "system/initialization/CreateAmbientLight.hpp"
#include "system/initialization/CreateDefaultMaterial.hpp"
#include "system/initialization/CreateDefaultRenderPipeline.hpp"
Expand Down
18 changes: 16 additions & 2 deletions src/plugin/default-pipeline/src/plugin/PluginDefaultPipeline.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,19 @@ static void SetupGPUComponent(Engine::Core &core)
registry.on_destroy<GPUComponent>().template connect<DestructionFunction>(core);
}

/**
* @brief Configure and register the default rendering pipeline for the plugin.
*
* Registers required plugin dependencies and runtime resources, wires CPU component
* lifecycle events to GPU creation/destruction handlers for camera, mesh, transform,
* and material components, and registers the rendering setup and preparation systems.
*
* The setup systems registered include: CreateDefaultRenderPipeline, Create3DGraph,
* CreateDefaultMaterial, CreateAmbientLight, and CreatePointLights.
*
* The preparation systems registered include: UpdateGPUTransforms, UpdateGPUCameras,
* UpdateGPUMaterials, UpdateGPUMeshes, UpdateAmbientLight, and UpdatePointLights.
*/
void DefaultPipeline::Plugin::Bind()
{
RequirePlugins<RenderingPipeline::Plugin, Graphic::Plugin>();
Expand All @@ -29,8 +42,9 @@ void DefaultPipeline::Plugin::Bind()
SetupGPUComponent<Object::Component::Material, Component::GPUMaterial, &System::OnMaterialCreation,
&System::OnMaterialDestruction>(this->GetCore());

RegisterSystems<RenderingPipeline::Setup>(System::CreateDefaultRenderPipeline, System::CreateDefaultMaterial,
System::CreateAmbientLight, System::CreatePointLights);
RegisterSystems<RenderingPipeline::Setup>(System::CreateDefaultRenderPipeline, System::Create3DGraph,
System::CreateDefaultMaterial, System::CreateAmbientLight,
System::CreatePointLights);
Comment thread
Miou-zora marked this conversation as resolved.

RegisterSystems<RenderingPipeline::Preparation>(System::UpdateGPUTransforms, System::UpdateGPUCameras,
System::UpdateGPUMaterials, System::UpdateGPUMeshes,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,12 @@ namespace DefaultPipeline::Resource {
*
* Layout (WGSL std140 alignment):
* - modelMatrix: mat4x4<f32> (64 bytes, offset 0)
* - normalMatrix: mat3x3<f32> (48 bytes, offset 64) - each column is 16-byte aligned
* Total: 112 bytes
* - normalMatrix: mat4x4<f32> (64 bytes, offset 64)
* Total: 128 bytes
*/
struct TransformGPUData {
glm::mat4 modelMatrix;
// mat3x3 in WGSL has each column aligned to 16 bytes, so we use vec4 for each column
glm::vec4 normalMatrixCol0;
glm::vec4 normalMatrixCol1;
glm::vec4 normalMatrixCol2;
glm::mat4 normalMatrix;
};

class TransformGPUBuffer : public Graphic::Resource::AGPUBuffer {
Expand Down Expand Up @@ -81,18 +78,27 @@ class TransformGPUBuffer : public Graphic::Resource::AGPUBuffer {
return context.GetDevice()->createBuffer(bufferDesc);
}

/**
* @brief Update the GPU buffer with the entity's current model and normal matrices.
*
* Computes the model matrix from the provided Transform component, derives the normal
* matrix as the transpose of the inverse of the model matrix, packs both into a
* TransformGPUData instance, and writes the data to the GPU buffer at offset 0 using
* the provided graphics context queue.
*
* @param transformComponent Component used to compute the model transformation matrix.
* @param context Graphics context containing the queue used to write to the GPU buffer.
*/
void _UpdateBuffer(const Object::Component::Transform &transformComponent,
const Graphic::Resource::Context &context)
{
const glm::mat4 &modelMatrix = transformComponent.ComputeTransformationMatrix();

const glm::mat3 normalMatrix = glm::transpose(glm::inverse(glm::mat3(modelMatrix)));
const glm::mat4 normalMatrix = glm::transpose(glm::inverse(modelMatrix));

TransformGPUData gpuData;
gpuData.modelMatrix = modelMatrix;
gpuData.normalMatrixCol0 = glm::vec4(normalMatrix[0], 0.0f);
gpuData.normalMatrixCol1 = glm::vec4(normalMatrix[1], 0.0f);
gpuData.normalMatrixCol2 = glm::vec4(normalMatrix[2], 0.0f);
gpuData.normalMatrix = normalMatrix;

context.queue->writeBuffer(_buffer, 0, &gpuData, sizeof(TransformGPUData));
}
Expand Down
260 changes: 260 additions & 0 deletions src/plugin/default-pipeline/src/resource/pass/GBuffer.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
#pragma once

#include "Logger.hpp"
#include "component/GPUCamera.hpp"
#include "component/GPUMaterial.hpp"
#include "component/GPUMesh.hpp"
#include "component/GPUTransform.hpp"
#include "core/Core.hpp"
#include "entity/Entity.hpp"
#include "resource/ASingleExecutionRenderPass.hpp"
#include "utils/DefaultMaterial.hpp"
#include "utils/shader/BufferBindGroupLayoutEntry.hpp"
#include "utils/shader/SamplerBindGroupLayoutEntry.hpp"
#include "utils/shader/TextureBindGroupLayoutEntry.hpp"
#include <entt/core/hashed_string.hpp>
#include <string_view>

namespace DefaultPipeline::Resource {
static inline constexpr std::string_view GBUFFER_PASS_OUTPUT_NORMAL = "GBUFFER_PASS_OUTPUT_NORMAL";
static inline const entt::hashed_string GBUFFER_PASS_OUTPUT_NORMAL_ID{GBUFFER_PASS_OUTPUT_NORMAL.data(),
GBUFFER_PASS_OUTPUT_NORMAL.size()};
static inline constexpr std::string_view GBUFFER_PASS_OUTPUT_ALBEDO = "GBUFFER_PASS_OUTPUT_ALBEDO";
static inline const entt::hashed_string GBUFFER_PASS_OUTPUT_ALBEDO_ID{GBUFFER_PASS_OUTPUT_ALBEDO.data(),
GBUFFER_PASS_OUTPUT_ALBEDO.size()};
static inline constexpr std::string_view GBUFFER_PASS_OUTPUT_DEPTH = "GBUFFER_PASS_OUTPUT_DEPTH";
static inline const entt::hashed_string GBUFFER_PASS_OUTPUT_DEPTH_ID{GBUFFER_PASS_OUTPUT_DEPTH.data(),
GBUFFER_PASS_OUTPUT_DEPTH.size()};
static inline constexpr std::string_view GBUFFER_PASS_NAME = "GBUFFER_PASS_NAME";
static inline const entt::hashed_string GBUFFER_PASS_ID{GBUFFER_PASS_NAME.data(), GBUFFER_PASS_NAME.size()};
static inline constexpr std::string_view GBUFFER_SHADER_NAME = "GBUFFER_SHADER_NAME";
static inline const entt::hashed_string GBUFFER_SHADER_ID =
entt::hashed_string{GBUFFER_SHADER_NAME.data(), GBUFFER_SHADER_NAME.size()};
static inline constexpr std::string_view GBUFFER_SHADE_CONTENT = R"(
struct Camera {
viewProjectionMatrix : mat4x4<f32>,
}

struct Object {
model : mat4x4<f32>,
normal : mat4x4<f32>,
}

struct Material {
emission: vec3f,
padding: f32,
};

struct VertexToFragment {
@builtin(position) Position : vec4f,
@location(0) fragNormal: vec3f,
@location(1) fragUV: vec2f,
}

struct GBufferOutput {
@location(0) normal : vec4f,
@location(1) albedo : vec4f,
}

@group(0) @binding(0) var<uniform> camera: Camera;

@group(1) @binding(0) var<uniform> object: Object;

@group(2) @binding(0) var<uniform> material : Material;
@group(2) @binding(1) var texture : texture_2d<f32>;
@group(2) @binding(2) var textureSampler : sampler;

@vertex
fn vs_main(
@location(0) position: vec3f,
@location(1) normal: vec3f,
@location(2) uv: vec2f,
) -> VertexToFragment {
var output : VertexToFragment;
let worldPosition = (object.model * vec4(position, 1.0)).xyz;
output.Position = camera.viewProjectionMatrix * vec4(worldPosition, 1.0);
output.fragNormal = normalize((object.normal * vec4(normal, 0.0)).xyz);
output.fragUV = uv;
Comment thread
Miou-zora marked this conversation as resolved.
return output;
}

@fragment
fn fs_main(
@location(0) fragNormal: vec3f,
@location(1) fragUV : vec2f
) -> GBufferOutput {
var output : GBufferOutput;
output.normal = vec4(normalize(fragNormal), 1.0);
output.albedo = vec4(textureSample(texture, textureSampler, fragUV).rgb, 1.0);

return output;
}

)";

class GBuffer : public Graphic::Resource::ASingleExecutionRenderPass<GBuffer> {
public:
/**
* @brief Constructs a GBuffer render pass with an optional name.
*
* @param name Human-readable name for the render pass; defaults to GBUFFER_PASS_NAME.
*/
explicit GBuffer(std::string_view name = GBUFFER_PASS_NAME) : ASingleExecutionRenderPass<GBuffer>(name) {}

/**
* @brief Render all entities with GPUTransform and GPUMesh into the G-buffer using the active camera.
*
* Binds the first available camera's bind group, then for each entity with GPUTransform and GPUMesh:
* binds the entity's transform and material bind groups (falls back to the default material if none),
* binds the vertex and index buffers, and issues an indexed draw call to populate the G-buffer outputs.
*
* If no entity exposes a GPUCamera component, logs an error and returns without drawing.
*
* @param renderPass The render pass encoder used to record bind-group bindings, buffer bindings, and draw commands.
* @param core Engine core providing access to resources and the entity registry.
*/
void UniqueRenderCallback(wgpu::RenderPassEncoder &renderPass, Engine::Core &core) override
{
const auto &bindGroupManager = core.GetResource<Graphic::Resource::BindGroupManager>();
const auto &bufferContainer = core.GetResource<Graphic::Resource::GPUBufferContainer>();

auto cameraView = core.GetRegistry().view<Component::GPUCamera>();
if (cameraView.empty())
{
Log::Error("GBuffer::UniqueRenderCallback: No camera with GPUCamera component found.");
return;
}
Engine::Entity camera{core, cameraView.front()};
const auto &cameraGPUComponent = camera.GetComponents<Component::GPUCamera>();

const auto &cameraBindGroup = bindGroupManager.Get(cameraGPUComponent.bindGroup);
renderPass.setBindGroup(0, cameraBindGroup.GetBindGroup(), 0, nullptr);

auto view = core.GetRegistry().view<Component::GPUTransform, Component::GPUMesh>();

for (auto &&[e, transform, gpuMesh] : view.each())
{
Engine::Entity entity{core, e};

const auto &transformBindGroup = bindGroupManager.Get(transform.bindGroup);
renderPass.setBindGroup(transformBindGroup.GetLayoutIndex(), transformBindGroup.GetBindGroup(), 0, nullptr);

entt::hashed_string gpuMaterialId{};
if (entity.HasComponents<Component::GPUMaterial>())
{
const auto &materialComponent = entity.GetComponents<Component::GPUMaterial>();
gpuMaterialId = materialComponent.bindGroup;
}
else
{
gpuMaterialId = Utils::DEFAULT_MATERIAL_BIND_GROUP_ID;
}
const auto &materialBindGroup = bindGroupManager.Get(gpuMaterialId);
renderPass.setBindGroup(materialBindGroup.GetLayoutIndex(), materialBindGroup.GetBindGroup(), 0, nullptr);

const auto &pointBuffer = bufferContainer.Get(gpuMesh.pointBufferId);
const auto &pointBufferSize = pointBuffer->GetBuffer().getSize();
renderPass.setVertexBuffer(0, pointBuffer->GetBuffer(), 0, pointBufferSize);
const auto &indexBuffer = bufferContainer.Get(gpuMesh.indexBufferId);
const auto &indexBufferSize = indexBuffer->GetBuffer().getSize();
renderPass.setIndexBuffer(indexBuffer->GetBuffer(), wgpu::IndexFormat::Uint32, 0, indexBufferSize);

renderPass.drawIndexed(indexBufferSize / sizeof(uint32_t), 1, 0, 0, 0);
}
}

/**
* @brief Constructs and returns a shader configured for the G-buffer pass.
*
* The shader includes vertex and fragment entry points ("vs_main", "fs_main"),
* bind-group layouts for camera, model, and material, a vertex buffer layout
* with position/normal/uv attributes, two color outputs (normal as RGBA16Float
* and albedo as BGRA8Unorm), and a depth output (Depth32Float).
*
* @param graphicContext Graphics resource context used to create the shader.
* @return Graphic::Resource::Shader A shader instance configured for the G-buffer rendering pass.
*/
static Graphic::Resource::Shader CreateShader(Graphic::Resource::Context &graphicContext)
{
Graphic::Resource::ShaderDescriptor shaderDescriptor;

auto cameraLayout =
Graphic::Utils::BindGroupLayout("Camera").addEntry(Graphic::Utils::BufferBindGroupLayoutEntry("camera")
.setType(wgpu::BufferBindingType::Uniform)
.setMinBindingSize(sizeof(glm::mat4))
.setVisibility(wgpu::ShaderStage::Vertex)
.setBinding(0));
// Model buffer contains: mat4 modelMatrix (64 bytes) + mat4 normalMatrix (64 bytes) = 128 bytes
auto modelLayout = Graphic::Utils::BindGroupLayout("Model").addEntry(
Graphic::Utils::BufferBindGroupLayoutEntry("model")
.setType(wgpu::BufferBindingType::Uniform)
.setMinBindingSize(sizeof(glm::mat4) + sizeof(glm::mat4))
.setVisibility(wgpu::ShaderStage::Vertex)
.setBinding(0));
auto materialLayout = Graphic::Utils::BindGroupLayout("Material")
.addEntry(Graphic::Utils::BufferBindGroupLayoutEntry("material")
.setType(wgpu::BufferBindingType::Uniform)
.setMinBindingSize(sizeof(glm::vec3) + sizeof(float) /*padding*/)
.setVisibility(wgpu::ShaderStage::Fragment)
.setBinding(0))
.addEntry(Graphic::Utils::TextureBindGroupLayoutEntry("materialTexture")
.setSampleType(wgpu::TextureSampleType::Float)
.setViewDimension(wgpu::TextureViewDimension::_2D)
.setVisibility(wgpu::ShaderStage::Fragment)
.setBinding(1))
.addEntry(Graphic::Utils::SamplerBindGroupLayoutEntry("materialSampler")
.setType(wgpu::SamplerBindingType::Filtering)
.setVisibility(wgpu::ShaderStage::Fragment)
.setBinding(2));

auto vertexLayout = Graphic::Utils::VertexBufferLayout()
.addVertexAttribute(wgpu::VertexFormat::Float32x3, 0, 0)
.addVertexAttribute(wgpu::VertexFormat::Float32x3, 3 * sizeof(float), 1)
.addVertexAttribute(wgpu::VertexFormat::Float32x2, 6 * sizeof(float), 2)
.setArrayStride(8 * sizeof(float))
.setStepMode(wgpu::VertexStepMode::Vertex);

auto normalOutput =
Graphic::Utils::ColorTargetState("GBUFFER_NORMAL").setFormat(wgpu::TextureFormat::RGBA16Float);

auto albedoOutput =
Graphic::Utils::ColorTargetState("GBUFFER_ALBEDO").setFormat(wgpu::TextureFormat::BGRA8Unorm);

auto depthOutput = Graphic::Utils::DepthStencilState("GBUFFER_DEPTH")
.setFormat(wgpu::TextureFormat::Depth32Float)
.setCompareFunction(wgpu::CompareFunction::Less)
.setDepthWriteEnabled(wgpu::OptionalBool::True);

shaderDescriptor.setShader(GBUFFER_SHADE_CONTENT)
.setName(GBUFFER_SHADER_NAME)
.setVertexEntryPoint("vs_main")
.setFragmentEntryPoint("fs_main")
.addBindGroupLayout(cameraLayout)
.addBindGroupLayout(modelLayout)
.addBindGroupLayout(materialLayout)
.addVertexBufferLayout(vertexLayout)
.addOutputColorFormat(normalOutput)
.addOutputColorFormat(albedoOutput)
.setOutputDepthFormat(depthOutput);
const auto validations = shaderDescriptor.validate();
if (!validations.empty())
{
for (const auto &validation : validations)
{
if (validation.severity == Graphic::Utils::ValidationError::Severity::Error)
{
Log::Error(fmt::format("Shader Descriptor Validation Error: {} at {}", validation.message,
validation.location));
}
else if (validation.severity == Graphic::Utils::ValidationError::Severity::Warning)
{
Log::Warn(fmt::format("Shader Descriptor Validation Warning: {} at {}", validation.message,
validation.location));
}
}
}
return Graphic::Resource::Shader::Create(shaderDescriptor, graphicContext);
}
};

} // namespace DefaultPipeline::Resource
Loading
Loading