diff --git a/assets/shaders/decal.hlsl b/assets/shaders/decal.hlsl new file mode 100644 index 00000000..32fe02c9 --- /dev/null +++ b/assets/shaders/decal.hlsl @@ -0,0 +1,107 @@ +// Screen-space decal pass. +// +// Reads the scene depth buffer to reconstruct world-space positions, then for +// each active decal tests whether the position lies inside the decal OBB +// (represented as a unit cube in decal-local space). Matching pixels receive +// the decal colour blended over the existing scene colour via src-alpha blending +// in the pipeline blend state. + +#define MAX_DECALS 16 + +struct DecalData { + float4x4 WorldToDecal; // Inverse world transform of the decal OBB + float4 DecalColor; // RGB tint + alpha + float4 Params; // x: blend factor, yzw: unused +}; + +[[vk::binding(0, 0)]] cbuffer DecalPassInfo : register(b0, space0) +{ + uint DecalCount; + uint3 _pad; + DecalData Decals[MAX_DECALS]; +} + +// ViewInfo bound separately so this pass is independent of the default pass layout. +// Mirrors the SceneViewInfo C++ struct (world / view / viewProject / lastViewProject / invViewProject). +struct ViewInfo { + float4x4 World; + float4x4 View; + float4x4 ViewProj; + float4x4 LastViewProj; + float4x4 InvViewProj; +}; + +[[vk::binding(1, 0)]] cbuffer view : register(b1, space0) +{ + ViewInfo View; +} + +[[vk::binding(2, 0)]] Texture2D DepthBuffer : register(t0, space0); +[[vk::binding(3, 0)]] SamplerState DepthSampler : register(s0, space0); + +// Full-screen triangle +const float2 FT_POSITIONS[3] = { + float2(-1.0, 3.0), + float2(-1.0, -1.0), + float2( 3.0, -1.0) +}; + +const float2 FT_UVS[3] = { + float2(0.0, 2.0), + float2(0.0, 0.0), + float2(2.0, 0.0) +}; + +struct VSOutput { + float4 Pos : SV_POSITION; + float2 UV : TEXCOORD; +}; + +VSOutput VSMain(uint vid : SV_VertexID) +{ + VSOutput res; + res.Pos = float4(FT_POSITIONS[vid], 0.0, 1.0); + res.UV = FT_UVS[vid]; + return res; +} + +float4 FSMain(VSOutput input) : SV_TARGET +{ + float depth = DepthBuffer.Sample(DepthSampler, input.UV).r; + + // Discard sky / far-plane pixels (depth == 1 in non-reversed-z) + if (depth >= 1.0) { + discard; + } + + // Reconstruct clip-space position and unproject to world space. + // NDC XY maps linearly from UV: (0,0) -> (-1,-1), (1,1) -> (1,1) + float2 ndcXY = input.UV * 2.0 - 1.0; + float4 clipPos = float4(ndcXY, depth, 1.0); + + float4 worldPos4 = mul(View.InvViewProj, clipPos); + float3 worldPos = worldPos4.xyz / worldPos4.w; + + float4 outColor = float4(0.0, 0.0, 0.0, 0.0); + + for (uint i = 0; i < DecalCount; ++i) + { + // Transform world position into decal local space + float4 localPos = mul(Decals[i].WorldToDecal, float4(worldPos, 1.0)); + + // The decal OBB is a unit cube [-0.5, 0.5]^3 in local space + if (all(abs(localPos.xyz) <= 0.5)) + { + // Map local XZ to UV [0,1] + float2 decalUV = localPos.xz + 0.5; + + float4 decalColor = Decals[i].DecalColor; + float blend = decalColor.a * Decals[i].Params.x; + + // Accumulate: later decals paint over earlier ones (painter's algorithm) + outColor = lerp(outColor, float4(decalColor.rgb, 1.0), blend); + } + } + + return outColor; +} diff --git a/assets/shaders/layout/default_global_view.hlslh b/assets/shaders/layout/default_global_view.hlslh index 19e8e390..13f9e8f7 100644 --- a/assets/shaders/layout/default_global_view.hlslh +++ b/assets/shaders/layout/default_global_view.hlslh @@ -3,6 +3,7 @@ struct ViewInfo { float4x4 View; float4x4 ViewProj; float4x4 LastViewProj; + float4x4 InvViewProj; }; [[vk::binding(0, 0)]] cbuffer global : register(b0, space0) diff --git a/assets/techniques/decal.tech b/assets/techniques/decal.tech new file mode 100644 index 00000000..64c059df --- /dev/null +++ b/assets/techniques/decal.tech @@ -0,0 +1,27 @@ +{ + "type": "graphics", + "shader": { + "path": "decal.hlsl", + "vertex": "VSMain", + "fragment": "FSMain" + }, + "pass": { + "tag": "Decal" + }, + "blend_state": [ + { + "blendEnable": true, + "srcColor": "SRC_ALPHA", + "dstColor": "ONE_MINUS_SRC_ALPHA", + "srcAlpha": "ONE", + "dstAlpha": "ONE_MINUS_SRC_ALPHA" + } + ], + "depth_stencil": { + "depthTestEnable": false, + "depthWriteEnable": false + }, + "raster_state": { + "cullMode": "NONE" + } +} diff --git a/engine/render/adaptor/include/render/adaptor/components/DecalComponent.h b/engine/render/adaptor/include/render/adaptor/components/DecalComponent.h new file mode 100644 index 00000000..d5164063 --- /dev/null +++ b/engine/render/adaptor/include/render/adaptor/components/DecalComponent.h @@ -0,0 +1,46 @@ +// +// Created by blues on 2024/12/8. +// + +#pragma once + +#include +#include +#include +#include + +namespace sky { + + struct DecalComponentData { + Vector4 color = {1.f, 1.f, 1.f, 1.f}; + float blendFactor = 1.f; + }; + + /** + * Actor component that places a screen-space decal in the world. + * The decal is an oriented bounding box derived from the actor transform; + * scaling the actor controls the decal's extents in world space. + */ + class DecalComponent + : public ComponentAdaptor + , public ITransformEvent { + public: + DecalComponent() = default; + ~DecalComponent() override = default; + + COMPONENT_RUNTIME_INFO(DecalComponent) + + static void Reflect(SerializationContext *context); + + void OnAttachToWorld() override; + void OnDetachFromWorld() override; + + private: + void OnTransformChanged(const Transform &global, const Transform &local) override; + void UpdateDecal(const Transform &transform); + + EventBinder transformEvent; + DecalInstance *decal = nullptr; + }; + +} // namespace sky diff --git a/engine/render/adaptor/include/render/adaptor/pipeline/DecalPass.h b/engine/render/adaptor/include/render/adaptor/pipeline/DecalPass.h new file mode 100644 index 00000000..dc452797 --- /dev/null +++ b/engine/render/adaptor/include/render/adaptor/pipeline/DecalPass.h @@ -0,0 +1,39 @@ +// +// Created by blues on 2024/12/8. +// + +#pragma once + +#include +#include + +namespace sky { + + /** + * Screen-space decal pass. Decoupled from forward/deferred pipelines: + * it only requires a depth buffer resource name and a color render target + * resource name which can be provided by any opaque pass. + * + * Algorithm: + * 1. Full-screen triangle reconstructs world position from depth using InvViewProj. + * 2. For each active decal the world position is transformed into the decal's + * OBB local space (WorldToDecal matrix). If the local position lies inside + * the unit cube [-0.5, 0.5]³ the decal colour is alpha-blended onto the output. + */ + class DecalPass : public FullScreenPass { + public: + /** + * @param tech Pre-loaded decal technique (decal.tech). + * @param colorName Name of the colour render target to write into (LoadOp::LOAD). + * @param depthName Name of the scene depth texture to read from (SRV). + */ + DecalPass(const RDGfxTechPtr &tech, const Name &colorName, const Name &depthName); + ~DecalPass() override = default; + + void Setup(rdg::RenderGraph &rdg, RenderScene &scene) override; + + private: + RDResourceLayoutPtr decalLayout; + }; + +} // namespace sky diff --git a/engine/render/adaptor/include/render/adaptor/pipeline/DefaultForwardPipeline.h b/engine/render/adaptor/include/render/adaptor/pipeline/DefaultForwardPipeline.h index 16ff4b52..0ea5c0a1 100644 --- a/engine/render/adaptor/include/render/adaptor/pipeline/DefaultForwardPipeline.h +++ b/engine/render/adaptor/include/render/adaptor/pipeline/DefaultForwardPipeline.h @@ -12,6 +12,7 @@ #include #include #include +#include #include namespace sky { @@ -45,6 +46,7 @@ namespace sky { std::unique_ptr hiz; std::unique_ptr shadowMap; std::unique_ptr forward; + std::unique_ptr decal; std::unique_ptr postProcess; std::unique_ptr present; std::unique_ptr brdfLut; diff --git a/engine/render/adaptor/include/render/adaptor/pipeline/DefaultPassConstants.h b/engine/render/adaptor/include/render/adaptor/pipeline/DefaultPassConstants.h index 2b1db4b4..c63bb873 100644 --- a/engine/render/adaptor/include/render/adaptor/pipeline/DefaultPassConstants.h +++ b/engine/render/adaptor/include/render/adaptor/pipeline/DefaultPassConstants.h @@ -18,6 +18,8 @@ namespace sky { static constexpr std::string_view FWD_DS_RESOLVE = "ForwardDepthResolve"; + static constexpr std::string_view DECAL_PASS_INFO = "DECAL_PASS_INFO"; + static constexpr std::string_view SWAP_CHAIN = "SwapChain"; } // namespace sky \ No newline at end of file diff --git a/engine/render/adaptor/src/RenderModule.cpp b/engine/render/adaptor/src/RenderModule.cpp index 93c6b45e..8ab0ec36 100644 --- a/engine/render/adaptor/src/RenderModule.cpp +++ b/engine/render/adaptor/src/RenderModule.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -31,6 +32,7 @@ #include #include #include +#include #include #include @@ -50,6 +52,7 @@ namespace sky { SkeletalMeshComponent::Reflect(context); SkyBoxComponent::Reflect(context); PrefabComponent::Reflect(context); + DecalComponent::Reflect(context); SkeletonDisplayComponent::Reflect(context); AnimationPreviewComponent::Reflect(context); @@ -63,6 +66,7 @@ namespace sky { ComponentFactory::Get()->RegisterComponent(GROUP); ComponentFactory::Get()->RegisterComponent(GROUP); ComponentFactory::Get()->RegisterComponent(GROUP); + ComponentFactory::Get()->RegisterComponent(GROUP); } { @@ -147,6 +151,8 @@ namespace sky { LightFeature::Get()->Init(); EnvFeature::Get()->Init(); + Renderer::Get()->RegisterRenderFeature(); + auto *am = AssetManager::Get(); { auto guiAsset = am->LoadAssetFromPath("techniques/gui.tech"); diff --git a/engine/render/adaptor/src/components/DecalComponent.cpp b/engine/render/adaptor/src/components/DecalComponent.cpp new file mode 100644 index 00000000..26800b10 --- /dev/null +++ b/engine/render/adaptor/src/components/DecalComponent.cpp @@ -0,0 +1,70 @@ +// +// Created by blues on 2024/12/8. +// + +#include + +#include +#include +#include +#include + +#include +#include + +namespace sky { + + void DecalComponent::Reflect(SerializationContext *context) + { + context->Register("DecalComponentData") + .Member<&DecalComponentData::color>("Color") + .Member<&DecalComponentData::blendFactor>("BlendFactor"); + + REGISTER_BEGIN(DecalComponent, context) + } + + void DecalComponent::OnAttachToWorld() + { + auto *df = GetFeatureProcessor(actor); + if (df == nullptr) { + return; + } + + decal = df->CreateDecal(); + decal->SetColor(data.color); + decal->SetBlendFactor(data.blendFactor); + + transformEvent.Bind(this, actor); + + auto *transform = actor->GetComponent(); + if (transform != nullptr) { + UpdateDecal(transform->GetWorldTransform()); + } + } + + void DecalComponent::OnDetachFromWorld() + { + auto *df = GetFeatureProcessor(actor); + if (df != nullptr && decal != nullptr) { + df->RemoveDecal(decal); + } + decal = nullptr; + transformEvent.Reset(); + } + + void DecalComponent::OnTransformChanged(const Transform &global, const Transform &/*local*/) + { + UpdateDecal(global); + } + + void DecalComponent::UpdateDecal(const Transform &transform) + { + if (decal == nullptr) { + return; + } + decal->SetWorldMatrix(transform.ToMatrix()); + decal->SetColor(data.color); + decal->SetBlendFactor(data.blendFactor); + } + +} // namespace sky diff --git a/engine/render/adaptor/src/pipeline/DecalPass.cpp b/engine/render/adaptor/src/pipeline/DecalPass.cpp new file mode 100644 index 00000000..9d1a1999 --- /dev/null +++ b/engine/render/adaptor/src/pipeline/DecalPass.cpp @@ -0,0 +1,72 @@ +// +// Created by blues on 2024/12/8. +// + +#include +#include +#include +#include +#include +#include + +namespace sky { + + DecalPass::DecalPass(const RDGfxTechPtr &tech, const Name &colorName, const Name &depthName) + : FullScreenPass(Name("DecalPass"), tech) + { + auto stageFlags = rhi::ShaderStageFlagBit::VS | rhi::ShaderStageFlagBit::FS; + + // Output: write into the opaque colour target (LoadOp::LOAD keeps existing pixels) + colors.emplace_back(Attachment{ + rdg::RasterAttachment{colorName, rhi::LoadOp::LOAD, rhi::StoreOp::STORE}, + rhi::ClearValue(0.f, 0.f, 0.f, 0.f) + }); + + // Input: decal UBO (count + per-decal OBB + colour) + computeResources.emplace_back(ComputeResource{ + Name(DECAL_PASS_INFO.data()), + rdg::ComputeView{Name("DecalPassInfo"), rdg::ComputeType::CBV, stageFlags} + }); + + // Input: scene view UBO (for InvViewProj) + computeResources.emplace_back(ComputeResource{ + Name("SCENE_VIEW"), + rdg::ComputeView{Name("viewInfo"), rdg::ComputeType::CBV, stageFlags} + }); + + // Input: scene depth texture (to reconstruct world position) + computeResources.emplace_back(ComputeResource{ + depthName, + rdg::ComputeView{Name("DepthBuffer"), rdg::ComputeType::SRV, stageFlags} + }); + + // Sampler for depth texture (reuse the global PointSampler imported by the pipeline) + samplers.emplace_back(SamplerResource{Name("PointSampler"), Name("DepthSampler")}); + + // Descriptor set layout for this pass + rhi::DescriptorSetLayout::Descriptor desc = {}; + desc.bindings.emplace_back(rhi::DescriptorType::UNIFORM_BUFFER, 1, 0, stageFlags, "DecalPassInfo"); + desc.bindings.emplace_back(rhi::DescriptorType::UNIFORM_BUFFER, 1, 1, stageFlags, "viewInfo"); + desc.bindings.emplace_back(rhi::DescriptorType::SAMPLED_IMAGE, 1, 2, stageFlags, "DepthBuffer"); + desc.bindings.emplace_back(rhi::DescriptorType::SAMPLER, 1, 3, stageFlags, "DepthSampler"); + + decalLayout = new ResourceGroupLayout(); + decalLayout->SetRHILayout(RHI::Get()->GetDevice()->CreateDescriptorSetLayout(desc)); + decalLayout->AddNameHandler(Name("DecalPassInfo"), {0, sizeof(DecalPassInfo)}); + decalLayout->AddNameHandler(Name("viewInfo"), {1, sizeof(SceneViewInfo)}); + decalLayout->AddNameHandler(Name("DepthBuffer"), {2}); + decalLayout->AddNameHandler(Name("DepthSampler"), {3}); + } + + void DecalPass::Setup(rdg::RenderGraph &rdg, RenderScene &scene) + { + // Skip the pass when no decals are active + auto *decalFP = scene.GetFeature(); + if (decalFP == nullptr) { + return; + } + + FullScreenPass::Setup(rdg, scene); + } + +} // namespace sky diff --git a/engine/render/adaptor/src/pipeline/DefaultForwardPipeline.cpp b/engine/render/adaptor/src/pipeline/DefaultForwardPipeline.cpp index cccfcfc7..b7011e84 100644 --- a/engine/render/adaptor/src/pipeline/DefaultForwardPipeline.cpp +++ b/engine/render/adaptor/src/pipeline/DefaultForwardPipeline.cpp @@ -24,10 +24,11 @@ namespace sky { void DefaultForwardPipeline::InitPass() { - auto postTech = LoadGfxTech("techniques/post_processing.tech"); - auto brdfTech = LoadGfxTech("techniques/brdf_lut.tech"); - auto depthResolveTech = LoadGfxTech("techniques/depth_resolve.tech"); + auto postTech = LoadGfxTech("techniques/post_processing.tech"); + auto brdfTech = LoadGfxTech("techniques/brdf_lut.tech"); + auto depthResolveTech = LoadGfxTech("techniques/depth_resolve.tech"); auto depthDownSampleTech = LoadGfxTech("techniques/depth_downsample.tech"); + auto decalTech = LoadGfxTech("techniques/decal.tech"); rhi::DescriptorSetLayout::Descriptor desc = {}; auto stageFlags = rhi::ShaderStageFlagBit::VS | rhi::ShaderStageFlagBit::FS | rhi::ShaderStageFlagBit::TAS | rhi::ShaderStageFlagBit::MS; @@ -87,6 +88,11 @@ namespace sky { postProcess = std::make_unique(postTech); present = std::make_unique(output->GetSwapChain()); + decal = std::make_unique( + decalTech, + Name(FWD_CL.data()), + Name(FWD_DS.data())); + hiz = std::make_unique(depthResolveTech, depthDownSampleTech); empty = std::make_unique(); @@ -173,6 +179,9 @@ namespace sky { forward->Resize(renderWidth, renderHeight); AddPass(forward.get()); + decal->Resize(renderWidth, renderHeight); + AddPass(decal.get()); + // AddPass(empty.get()); postProcess->Resize(renderWidth, renderHeight); diff --git a/engine/render/core/include/render/RenderBuiltinLayout.h b/engine/render/core/include/render/RenderBuiltinLayout.h index 75a2e07c..313336ad 100644 --- a/engine/render/core/include/render/RenderBuiltinLayout.h +++ b/engine/render/core/include/render/RenderBuiltinLayout.h @@ -12,6 +12,7 @@ namespace sky { Matrix4 view; Matrix4 viewProject; Matrix4 lastViewProject; + Matrix4 invViewProject; }; struct InstanceLocal { diff --git a/engine/render/core/include/render/decal/DecalFeatureProcessor.h b/engine/render/core/include/render/decal/DecalFeatureProcessor.h new file mode 100644 index 00000000..c8a14cba --- /dev/null +++ b/engine/render/core/include/render/decal/DecalFeatureProcessor.h @@ -0,0 +1,64 @@ +// +// Created by blues on 2024/12/8. +// + +#pragma once + +#include +#include +#include +#include +#include + +namespace sky { + + static constexpr uint32_t MAX_DECALS = 16; + + struct DecalData { + Matrix4 worldToDecal; + Vector4 decalColor; + Vector4 params; // x: blend factor, yzw: unused + }; + + struct DecalPassInfo { + uint32_t decalCount; + uint32_t pad[3]; + DecalData decals[MAX_DECALS]; + }; + + class DecalInstance { + public: + DecalInstance() = default; + ~DecalInstance() = default; + + void SetWorldMatrix(const Matrix4 &worldMatrix); + void SetColor(const Vector4 &color); + void SetBlendFactor(float blend); + + const DecalData &GetDecalData() const { return data; } + + private: + DecalData data; + }; + + class DecalFeatureProcessor : public IFeatureProcessor { + public: + explicit DecalFeatureProcessor(RenderScene *scn); + ~DecalFeatureProcessor() override = default; + + void Tick(float time) override; + void Render(rdg::RenderGraph &rdg) override; + + DecalInstance *CreateDecal(); + void RemoveDecal(DecalInstance *decal); + + const RDUniformBufferPtr &GetDecalUBO() const { return decalUBO; } + + private: + void UpdateDecalUBO(); + + std::list> decals; + RDUniformBufferPtr decalUBO; + }; + +} // namespace sky diff --git a/engine/render/core/src/SceneView.cpp b/engine/render/core/src/SceneView.cpp index f702b7de..8152ec88 100644 --- a/engine/render/core/src/SceneView.cpp +++ b/engine/render/core/src/SceneView.cpp @@ -68,6 +68,7 @@ namespace sky { p[1][1] = RHI::Get()->GetDevice()->GetConstants().flipY && flipY ? -1.f : 1.f; viewInfo[i].viewProject = p * projects[i] * viewInfo[i].view; + viewInfo[i].invViewProject = viewInfo[i].viewProject.Inverse(); frustums[i] = CreateFrustumByViewProjectMatrix(viewInfo[i].viewProject); viewUbo->WriteT(i * sizeof(SceneViewInfo), viewInfo[i]); diff --git a/engine/render/core/src/decal/DecalFeatureProcessor.cpp b/engine/render/core/src/decal/DecalFeatureProcessor.cpp new file mode 100644 index 00000000..3bf8bd97 --- /dev/null +++ b/engine/render/core/src/decal/DecalFeatureProcessor.cpp @@ -0,0 +1,70 @@ +// +// Created by blues on 2024/12/8. +// + +#include +#include + +namespace sky { + + void DecalInstance::SetWorldMatrix(const Matrix4 &worldMatrix) + { + data.worldToDecal = worldMatrix.Inverse(); + } + + void DecalInstance::SetColor(const Vector4 &color) + { + data.decalColor = color; + } + + void DecalInstance::SetBlendFactor(float blend) + { + data.params.x = blend; + } + + DecalFeatureProcessor::DecalFeatureProcessor(RenderScene *scn) : IFeatureProcessor(scn) + { + decalUBO = new UniformBuffer(); + decalUBO->Init(sizeof(DecalPassInfo)); + } + + DecalInstance *DecalFeatureProcessor::CreateDecal() + { + auto *decal = decals.emplace_back(std::make_unique()).get(); + return decal; + } + + void DecalFeatureProcessor::RemoveDecal(DecalInstance *decal) + { + decals.remove_if([decal](const std::unique_ptr &d) { + return d.get() == decal; + }); + } + + void DecalFeatureProcessor::UpdateDecalUBO() + { + DecalPassInfo info = {}; + info.decalCount = 0; + + for (const auto &decal : decals) { + if (info.decalCount >= MAX_DECALS) { + break; + } + info.decals[info.decalCount++] = decal->GetDecalData(); + } + + decalUBO->WriteT(0, info); + } + + void DecalFeatureProcessor::Tick(float /*time*/) + { + UpdateDecalUBO(); + } + + void DecalFeatureProcessor::Render(rdg::RenderGraph &rdg) + { + auto &rg = rdg.resourceGraph; + rg.ImportUBO(Name("DECAL_PASS_INFO"), decalUBO); + } + +} // namespace sky