diff --git a/Content/Editor/MaterialTemplates/VolumeParticle.shader b/Content/Editor/MaterialTemplates/VolumeParticle.shader new file mode 100644 index 000000000..c0fc57e54 --- /dev/null +++ b/Content/Editor/MaterialTemplates/VolumeParticle.shader @@ -0,0 +1,246 @@ +// File generated by Flax Materials Editor +// Version: @0 + +#define MATERIAL 1 +@3 + +#include "./Flax/Common.hlsl" +#include "./Flax/MaterialCommon.hlsl" +#include "./Flax/GBufferCommon.hlsl" +@7 + +// Primary constant buffer (with additional material parameters) +META_CB_BEGIN(0, Data) +float4x4 ViewProjectionMatrix; +float4x4 InverseViewProjectionMatrix; +float4x4 ViewMatrix; +float4x4 WorldMatrix; +float4x4 WorldMatrixInverseTransposed; +float3 ViewPos; +float ViewFar; +float3 ViewDir; +float TimeParam; +float4 ViewInfo; +float4 ScreenSize; +float3 GridSize; +float PerInstanceRandom; +float Dummy0; +float VolumetricFogMaxDistance; +int ParticleStride; +int ParticleIndex; +@1META_CB_END + +// Particles attributes buffer +ByteAddressBuffer ParticlesData : register(t0); + +// Shader resources +@2 +// Material properties generation input +struct MaterialInput +{ + float3 WorldPosition; + float TwoSidedSign; + float2 TexCoord; + uint ParticleIndex; +#if USE_VERTEX_COLOR + half4 VertexColor; +#endif + float3x3 TBN; + float4 SvPosition; + float3 PreSkinnedPosition; + float3 PreSkinnedNormal; + float3 InstanceOrigin; + float InstanceParams; +#if USE_CUSTOM_VERTEX_INTERPOLATORS + float4 CustomVSToPS[CUSTOM_VERTEX_INTERPOLATORS_COUNT]; +#endif +}; + +#define GetInstanceTransform(input) WorldMatrix; + +// Removes the scale vector from the local to world transformation matrix (supports instancing) +float3x3 RemoveScaleFromLocalToWorld(float3x3 localToWorld) +{ + // Extract per axis scales from localToWorld transform + float scaleX = length(localToWorld[0]); + float scaleY = length(localToWorld[1]); + float scaleZ = length(localToWorld[2]); + float3 invScale = float3( + scaleX > 0.00001f ? 1.0f / scaleX : 0.0f, + scaleY > 0.00001f ? 1.0f / scaleY : 0.0f, + scaleZ > 0.00001f ? 1.0f / scaleZ : 0.0f); + localToWorld[0] *= invScale.x; + localToWorld[1] *= invScale.y; + localToWorld[2] *= invScale.z; + return localToWorld; +} + +// Transforms a vector from tangent space to world space +float3 TransformTangentVectorToWorld(MaterialInput input, float3 tangentVector) +{ + return mul(tangentVector, input.TBN); +} + +// Transforms a vector from world space to tangent space +float3 TransformWorldVectorToTangent(MaterialInput input, float3 worldVector) +{ + return mul(input.TBN, worldVector); +} + +// Transforms a vector from world space to view space +float3 TransformWorldVectorToView(MaterialInput input, float3 worldVector) +{ + return mul(worldVector, (float3x3)ViewMatrix); +} + +// Transforms a vector from view space to world space +float3 TransformViewVectorToWorld(MaterialInput input, float3 viewVector) +{ + return mul((float3x3)ViewMatrix, viewVector); +} + +// Transforms a vector from local space to world space +float3 TransformLocalVectorToWorld(MaterialInput input, float3 localVector) +{ + float3x3 localToWorld = (float3x3)GetInstanceTransform(input); + //localToWorld = RemoveScaleFromLocalToWorld(localToWorld); + return mul(localVector, localToWorld); +} + +// Transforms a vector from local space to world space +float3 TransformWorldVectorToLocal(MaterialInput input, float3 worldVector) +{ + float3x3 localToWorld = (float3x3)GetInstanceTransform(input); + //localToWorld = RemoveScaleFromLocalToWorld(localToWorld); + return mul(localToWorld, worldVector); +} + +// Gets the current object position (supports instancing) +float3 GetObjectPosition(MaterialInput input) +{ + return input.InstanceOrigin.xyz; +} + +// Gets the current object size +float3 GetObjectSize(MaterialInput input) +{ + return float3(1, 1, 1); +} + +// Get the current object random value (supports instancing) +float GetPerInstanceRandom(MaterialInput input) +{ + return input.InstanceParams; +} + +// Get the current object LOD transition dither factor (supports instancing) +float GetLODDitherFactor(MaterialInput input) +{ + return 0; +} + +// Gets the interpolated vertex color (in linear space) +float4 GetVertexColor(MaterialInput input) +{ + return 1; +} + +uint GetParticleUint(uint particleIndex, int offset) +{ + return ParticlesData.Load(particleIndex * ParticleStride + offset); +} + +int GetParticleInt(uint particleIndex, int offset) +{ + return asint(ParticlesData.Load(particleIndex * ParticleStride + offset)); +} + +float GetParticleFloat(uint particleIndex, int offset) +{ + return asfloat(ParticlesData.Load(particleIndex * ParticleStride + offset)); +} + +float2 GetParticleVec2(uint particleIndex, int offset) +{ + return asfloat(ParticlesData.Load2(particleIndex * ParticleStride + offset)); +} + +float3 GetParticleVec3(uint particleIndex, int offset) +{ + return asfloat(ParticlesData.Load3(particleIndex * ParticleStride + offset)); +} + +float4 GetParticleVec4(uint particleIndex, int offset) +{ + return asfloat(ParticlesData.Load4(particleIndex * ParticleStride + offset)); +} + +float3 TransformParticlePosition(float3 input) +{ + return mul(float4(input, 1.0f), WorldMatrix).xyz; +} + +float3 TransformParticleVector(float3 input) +{ + return mul(float4(input, 0.0f), WorldMatrixInverseTransposed).xyz; +} + +@8 + +// Get material properties function (for vertex shader) +Material GetMaterialVS(MaterialInput input) +{ +@5 +} + +// Get material properties function (for domain shader) +Material GetMaterialDS(MaterialInput input) +{ +@6 +} + +// Get material properties function (for pixel shader) +Material GetMaterialPS(MaterialInput input) +{ +@4 +} + +// Pixel Shader function for Volumetric Fog material injection (local fog) +META_PS(true, FEATURE_LEVEL_SM5) +void PS_VolumetricFog(Quad_GS2PS input, out float4 VBufferA : SV_Target0, out float4 VBufferB : SV_Target1) +{ + uint3 gridCoordinate = uint3(input.Vertex.Position.xy, input.LayerIndex); + float3 cellOffset = 0.5f; + float2 volumeUV = (gridCoordinate.xy + cellOffset.xy) / GridSize.xy; + float zSlice = gridCoordinate.z + cellOffset.z; + float sceneDepth = (zSlice / GridSize.z) * VolumetricFogMaxDistance / ViewFar; + float deviceDepth = (ViewInfo.w / sceneDepth) + ViewInfo.z; + float4 clipPos = float4(volumeUV * float2(2.0, -2.0) + float2(-1.0, 1.0), deviceDepth, 1.0); + float4 wsPos = mul(clipPos, InverseViewProjectionMatrix); + float3 positionWS = wsPos.xyz / wsPos.w; + + // Get material parameters + MaterialInput materialInput = (MaterialInput)0; + materialInput.WorldPosition = positionWS; + materialInput.TexCoord = input.Vertex.TexCoord; + materialInput.ParticleIndex = ParticleIndex; + materialInput.TBN = float3x3(float3(1, 0, 0), float3(0, 1, 0), float3(0, 0, 1)); + materialInput.TwoSidedSign = 1.0f; + materialInput.InstanceOrigin = WorldMatrix[3].xyz; + materialInput.InstanceParams = PerInstanceRandom; + materialInput.SvPosition = clipPos; + Material material = GetMaterialPS(materialInput); + + // Compute fog properties + float3 albedo = material.Color; + float extinction = material.Opacity * material.Mask * 0.001f; + float3 emission = material.Emissive; + float3 scattering = albedo * extinction; + float absorption = max(0.0f, extinction - Luminance(scattering)); + + // Write fog properties + VBufferA = float4(scattering, absorption); + VBufferB = float4(emission, 0); +} + +@9 diff --git a/Source/Editor/CustomEditors/Editors/ListEditor.cs b/Source/Editor/CustomEditors/Editors/ListEditor.cs index 07963a33f..0c6efaea4 100644 --- a/Source/Editor/CustomEditors/Editors/ListEditor.cs +++ b/Source/Editor/CustomEditors/Editors/ListEditor.cs @@ -1,6 +1,5 @@ // Copyright (c) 2012-2021 Wojciech Figat. All rights reserved. -using System; using System.Collections; using System.Collections.Generic; using FlaxEngine; diff --git a/Source/Editor/Surface/Archetypes/Material.cs b/Source/Editor/Surface/Archetypes/Material.cs index a89f62274..38ce19df6 100644 --- a/Source/Editor/Surface/Archetypes/Material.cs +++ b/Source/Editor/Surface/Archetypes/Material.cs @@ -234,7 +234,25 @@ namespace FlaxEditor.Surface.Archetypes GetBox(MaterialNodeBoxes.SubsurfaceColor).Enabled = false; break; } - default: throw new ArgumentOutOfRangeException(); + case MaterialDomain.VolumeParticle: + { + GetBox(MaterialNodeBoxes.Color).Enabled = true; + GetBox(MaterialNodeBoxes.Mask).Enabled = true; + GetBox(MaterialNodeBoxes.Emissive).Enabled = true; + GetBox(MaterialNodeBoxes.Metalness).Enabled = false; + GetBox(MaterialNodeBoxes.Specular).Enabled = false; + GetBox(MaterialNodeBoxes.Roughness).Enabled = false; + GetBox(MaterialNodeBoxes.AmbientOcclusion).Enabled = false; + GetBox(MaterialNodeBoxes.Normal).Enabled = false; + GetBox(MaterialNodeBoxes.Opacity).Enabled = true; + GetBox(MaterialNodeBoxes.Refraction).Enabled = false; + GetBox(MaterialNodeBoxes.PositionOffset).Enabled = false; + GetBox(MaterialNodeBoxes.TessellationMultiplier).Enabled = false; + GetBox(MaterialNodeBoxes.WorldDisplacement).Enabled = false; + GetBox(MaterialNodeBoxes.SubsurfaceColor).Enabled = false; + break; + } + default: throw new ArgumentOutOfRangeException(); } } diff --git a/Source/Editor/Surface/Archetypes/ParticleModules.cs b/Source/Editor/Surface/Archetypes/ParticleModules.cs index 64adc828d..49a5d031d 100644 --- a/Source/Editor/Surface/Archetypes/ParticleModules.cs +++ b/Source/Editor/Surface/Archetypes/ParticleModules.cs @@ -1483,7 +1483,7 @@ namespace FlaxEditor.Surface.Archetypes TypeID = 404, Create = CreateParticleModuleNode, Title = "Ribbon Rendering", - Description = "Draws a a ribbon connecting all particles in order by particle age.", + Description = "Draws a ribbon connecting all particles in order by particle age.", Flags = DefaultModuleFlags, Size = new Vector2(200, 170), DefaultValues = new object[] @@ -1517,6 +1517,27 @@ namespace FlaxEditor.Surface.Archetypes NodeElementArchetype.Factory.Enum(100.0f, 6.0f * Surface.Constants.LayoutOffsetY, 140, 6, typeof(DrawPass)), }, }, + new NodeArchetype + { + TypeID = 405, + Create = CreateParticleModuleNode, + Title = "Volumetric Fog Rendering", + Description = "Draws the particle into the volumetric fog (material color, opacity and emission are used for local fog properties).", + Flags = DefaultModuleFlags, + Size = new Vector2(200, 70), + DefaultValues = new object[] + { + true, + (int)ModuleType.Render, + Guid.Empty, // Material + }, + Elements = new[] + { + // Material + NodeElementArchetype.Factory.Text(0, -10, "Material", 80.0f, 16.0f, "The material used for volumetric fog rendering. It must have Domain set to Particle."), + NodeElementArchetype.Factory.Asset(80, -10, 2, typeof(MaterialBase)), + }, + }, }; } } diff --git a/Source/Editor/Viewport/Previews/MaterialPreview.cs b/Source/Editor/Viewport/Previews/MaterialPreview.cs index 4d03e51f8..c4f2c8131 100644 --- a/Source/Editor/Viewport/Previews/MaterialPreview.cs +++ b/Source/Editor/Viewport/Previews/MaterialPreview.cs @@ -179,6 +179,9 @@ namespace FlaxEditor.Viewport.Previews usePreviewActor = false; deformableMaterial = _material; break; + case MaterialDomain.VolumeParticle: + usePreviewActor = false; + break; default: throw new ArgumentOutOfRangeException(); } } diff --git a/Source/Engine/Graphics/Materials/MaterialInfo.h b/Source/Engine/Graphics/Materials/MaterialInfo.h index 862f8f221..7edf7da54 100644 --- a/Source/Engine/Graphics/Materials/MaterialInfo.h +++ b/Source/Engine/Graphics/Materials/MaterialInfo.h @@ -44,6 +44,11 @@ API_ENUM() enum class MaterialDomain : byte /// The deformable shader. Can be used only with objects that can be deformed (spline models). /// Deformable = 6, + + /// + /// The particle shader used for volumetric effects rendering such as Volumetric Fog. + /// + VolumeParticle = 7, }; /// diff --git a/Source/Engine/Graphics/Materials/MaterialShader.cpp b/Source/Engine/Graphics/Materials/MaterialShader.cpp index caac3b5f2..95a6f556e 100644 --- a/Source/Engine/Graphics/Materials/MaterialShader.cpp +++ b/Source/Engine/Graphics/Materials/MaterialShader.cpp @@ -15,6 +15,7 @@ #include "TerrainMaterialShader.h" #include "ParticleMaterialShader.h" #include "DeformableMaterialShader.h" +#include "VolumeParticleMaterialShader.h" GPUPipelineState* MaterialShader::PipelineStateCache::InitPS(CullMode mode, bool wireframe) { @@ -65,8 +66,11 @@ MaterialShader* MaterialShader::Create(const String& name, MemoryReadStream& sha case MaterialDomain::Deformable: material = New(name); break; + case MaterialDomain::VolumeParticle: + material = New(name); + break; default: - LOG(Fatal, "Unknown material type."); + LOG(Error, "Unknown material type."); return nullptr; } if (material->Load(shaderCacheStream, info)) diff --git a/Source/Engine/Graphics/Materials/ParticleMaterialShader.cpp b/Source/Engine/Graphics/Materials/ParticleMaterialShader.cpp index 7a718ceb7..23c7f81a4 100644 --- a/Source/Engine/Graphics/Materials/ParticleMaterialShader.cpp +++ b/Source/Engine/Graphics/Materials/ParticleMaterialShader.cpp @@ -15,8 +15,6 @@ #include "Engine/Graphics/Shaders/GPUConstantBuffer.h" #include "Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.h" -#define MAX_LOCAL_LIGHTS 4 - PACK_STRUCT(struct ParticleMaterialShaderData { Matrix ViewProjectionMatrix; Matrix WorldMatrix; @@ -98,53 +96,6 @@ void ParticleMaterialShader::Bind(BindParameters& params) } } - // Select pipeline state based on current pass and render mode - const bool wireframe = (_info.FeaturesFlags & MaterialFeaturesFlags::Wireframe) != 0 || view.Mode == ViewMode::Wireframe; - CullMode cullMode = view.Pass == DrawPass::Depth ? CullMode::TwoSided : _info.CullMode; - PipelineStateCache* psCache = nullptr; - switch (drawCall.Particle.Module->TypeID) - { - // Sprite Rendering - case 400: - { - psCache = (PipelineStateCache*)_cacheSprite.GetPS(view.Pass); - break; - } - // Model Rendering - case 403: - { - psCache = (PipelineStateCache*)_cacheModel.GetPS(view.Pass); - break; - } - // Ribbon Rendering - case 404: - { - psCache = (PipelineStateCache*)_cacheRibbon.GetPS(view.Pass); - - static StringView ParticleRibbonWidth(TEXT("RibbonWidth")); - static StringView ParticleRibbonTwist(TEXT("RibbonTwist")); - static StringView ParticleRibbonFacingVector(TEXT("RibbonFacingVector")); - - materialData->RibbonWidthOffset = drawCall.Particle.Particles->Layout->FindAttributeOffset(ParticleRibbonWidth, ParticleAttribute::ValueTypes::Float, -1); - materialData->RibbonTwistOffset = drawCall.Particle.Particles->Layout->FindAttributeOffset(ParticleRibbonTwist, ParticleAttribute::ValueTypes::Float, -1); - materialData->RibbonFacingVectorOffset = drawCall.Particle.Particles->Layout->FindAttributeOffset(ParticleRibbonFacingVector, ParticleAttribute::ValueTypes::Vector3, -1); - - materialData->RibbonUVTilingDistance = drawCall.Particle.Ribbon.UVTilingDistance; - materialData->RibbonUVScale.X = drawCall.Particle.Ribbon.UVScaleX; - materialData->RibbonUVScale.Y = drawCall.Particle.Ribbon.UVScaleY; - materialData->RibbonUVOffset.X = drawCall.Particle.Ribbon.UVOffsetX; - materialData->RibbonUVOffset.Y = drawCall.Particle.Ribbon.UVOffsetY; - materialData->RibbonSegmentCount = drawCall.Particle.Ribbon.SegmentCount; - - if (drawCall.Particle.Ribbon.SegmentDistances) - context->BindSR(1, drawCall.Particle.Ribbon.SegmentDistances->View()); - - break; - } - } - ASSERT(psCache); - GPUPipelineState* state = psCache->GetPS(cullMode, wireframe); - // Setup material constants { static StringView ParticlePosition(TEXT("Position")); @@ -179,6 +130,53 @@ void ParticleMaterialShader::Bind(BindParameters& params) Matrix::Invert(drawCall.World, materialData->WorldMatrixInverseTransposed); } + // Select pipeline state based on current pass and render mode + bool wireframe = (_info.FeaturesFlags & MaterialFeaturesFlags::Wireframe) != 0 || view.Mode == ViewMode::Wireframe; + CullMode cullMode = view.Pass == DrawPass::Depth ? CullMode::TwoSided : _info.CullMode; + PipelineStateCache* psCache = nullptr; + switch (drawCall.Particle.Module->TypeID) + { + // Sprite Rendering + case 400: + { + psCache = _cacheSprite.GetPS(view.Pass); + break; + } + // Model Rendering + case 403: + { + psCache = _cacheModel.GetPS(view.Pass); + break; + } + // Ribbon Rendering + case 404: + { + psCache = _cacheRibbon.GetPS(view.Pass); + + static StringView ParticleRibbonWidth(TEXT("RibbonWidth")); + static StringView ParticleRibbonTwist(TEXT("RibbonTwist")); + static StringView ParticleRibbonFacingVector(TEXT("RibbonFacingVector")); + + materialData->RibbonWidthOffset = drawCall.Particle.Particles->Layout->FindAttributeOffset(ParticleRibbonWidth, ParticleAttribute::ValueTypes::Float, -1); + materialData->RibbonTwistOffset = drawCall.Particle.Particles->Layout->FindAttributeOffset(ParticleRibbonTwist, ParticleAttribute::ValueTypes::Float, -1); + materialData->RibbonFacingVectorOffset = drawCall.Particle.Particles->Layout->FindAttributeOffset(ParticleRibbonFacingVector, ParticleAttribute::ValueTypes::Vector3, -1); + + materialData->RibbonUVTilingDistance = drawCall.Particle.Ribbon.UVTilingDistance; + materialData->RibbonUVScale.X = drawCall.Particle.Ribbon.UVScaleX; + materialData->RibbonUVScale.Y = drawCall.Particle.Ribbon.UVScaleY; + materialData->RibbonUVOffset.X = drawCall.Particle.Ribbon.UVOffsetX; + materialData->RibbonUVOffset.Y = drawCall.Particle.Ribbon.UVOffsetY; + materialData->RibbonSegmentCount = drawCall.Particle.Ribbon.SegmentCount; + + if (drawCall.Particle.Ribbon.SegmentDistances) + context->BindSR(1, drawCall.Particle.Ribbon.SegmentDistances->View()); + + break; + } + } + ASSERT(psCache); + GPUPipelineState* state = psCache->GetPS(cullMode, wireframe); + // Bind constants if (_cb) { @@ -198,6 +196,7 @@ void ParticleMaterialShader::Unload() _cacheSprite.Release(); _cacheModel.Release(); _cacheRibbon.Release(); + _cacheVolumetricFog.Release(); } bool ParticleMaterialShader::Load() @@ -263,5 +262,8 @@ bool ParticleMaterialShader::Load() psDesc.VS = vsRibbon; _cacheRibbon.Depth.Init(psDesc); + // Lazy initialization + _cacheVolumetricFog.Desc.PS = nullptr; + return false; } diff --git a/Source/Engine/Graphics/Materials/ParticleMaterialShader.h b/Source/Engine/Graphics/Materials/ParticleMaterialShader.h index 3a8794183..a38bf56f0 100644 --- a/Source/Engine/Graphics/Materials/ParticleMaterialShader.h +++ b/Source/Engine/Graphics/Materials/ParticleMaterialShader.h @@ -45,6 +45,7 @@ private: Cache _cacheSprite; Cache _cacheModel; Cache _cacheRibbon; + PipelineStateCache _cacheVolumetricFog; DrawPass _drawModes = DrawPass::None; public: diff --git a/Source/Engine/Graphics/Materials/VolumeParticleMaterialShader.cpp b/Source/Engine/Graphics/Materials/VolumeParticleMaterialShader.cpp new file mode 100644 index 000000000..5a536e926 --- /dev/null +++ b/Source/Engine/Graphics/Materials/VolumeParticleMaterialShader.cpp @@ -0,0 +1,135 @@ +// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved. + +#include "VolumeParticleMaterialShader.h" +#include "MaterialShaderFeatures.h" +#include "MaterialParams.h" +#include "Engine/Engine/Time.h" +#include "Engine/Renderer/DrawCall.h" +#include "Engine/Renderer/VolumetricFogPass.h" +#include "Engine/Renderer/RenderList.h" +#include "Engine/Graphics/RenderView.h" +#include "Engine/Graphics/GPUContext.h" +#include "Engine/Graphics/GPUDevice.h" +#include "Engine/Graphics/RenderTask.h" +#include "Engine/Graphics/Shaders/GPUShader.h" +#include "Engine/Graphics/Shaders/GPUConstantBuffer.h" +#include "Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.h" + +PACK_STRUCT(struct VolumeParticleMaterialShaderData { + Matrix ViewProjectionMatrix; + Matrix InverseViewProjectionMatrix; + Matrix ViewMatrix; + Matrix WorldMatrix; + Matrix WorldMatrixInverseTransposed; + Vector3 ViewPos; + float ViewFar; + Vector3 ViewDir; + float TimeParam; + Vector4 ViewInfo; + Vector4 ScreenSize; + Vector3 GridSize; + float PerInstanceRandom; + float Dummy0; + float VolumetricFogMaxDistance; + int ParticleStride; + int ParticleIndex; + }); + +DrawPass VolumeParticleMaterialShader::GetDrawModes() const +{ + return DrawPass::None; +} + +void VolumeParticleMaterialShader::Bind(BindParameters& params) +{ + // Prepare + auto context = params.GPUContext; + auto& view = params.RenderContext.View; + auto& drawCall = *params.FirstDrawCall; + byte* cb = _cbData.Get(); + auto materialData = reinterpret_cast(cb); + cb += sizeof(VolumeParticleMaterialShaderData); + int32 srv = 1; + auto customData = (VolumetricFogPass::CustomData*)params.CustomData; + + // Setup parameters + MaterialParameter::BindMeta bindMeta; + bindMeta.Context = context; + bindMeta.Constants = cb; + bindMeta.Input = nullptr; + bindMeta.Buffers = params.RenderContext.Buffers; + bindMeta.CanSampleDepth = true; + bindMeta.CanSampleGBuffer = true; + MaterialParams::Bind(params.ParamsLink, bindMeta); + + // Setup particles data + context->BindSR(0, drawCall.Particle.Particles->GPU.Buffer->View()); + + // Setup particles attributes binding info + { + const auto& p = *params.ParamsLink->This; + for (int32 i = 0; i < p.Count(); i++) + { + const auto& param = p.At(i); + if (param.GetParameterType() == MaterialParameterType::Integer && param.GetName().StartsWith(TEXT("Particle."))) + { + const StringView name(param.GetName().Get() + 9, param.GetName().Length() - 9); + const int32 offset = drawCall.Particle.Particles->Layout->FindAttributeOffset(name); + *((int32*)(bindMeta.Constants + param.GetBindOffset())) = offset; + } + } + } + + // Setup material constants + { + Matrix::Transpose(view.Frustum.GetMatrix(), materialData->ViewProjectionMatrix); + Matrix::Transpose(view.IVP, materialData->InverseViewProjectionMatrix); + Matrix::Transpose(view.View, materialData->ViewMatrix); + Matrix::Transpose(drawCall.World, materialData->WorldMatrix); + Matrix::Invert(drawCall.World, materialData->WorldMatrixInverseTransposed); + materialData->ViewPos = view.Position; + materialData->ViewFar = view.Far; + materialData->ViewDir = view.Direction; + materialData->TimeParam = Time::Draw.UnscaledTime.GetTotalSeconds(); + materialData->ViewInfo = view.ViewInfo; + materialData->ScreenSize = view.ScreenSize; + materialData->GridSize = customData->GridSize; + materialData->PerInstanceRandom = drawCall.PerInstanceRandom; + materialData->VolumetricFogMaxDistance = customData->VolumetricFogMaxDistance; + materialData->ParticleStride = drawCall.Particle.Particles->Stride; + materialData->ParticleIndex = customData->ParticleIndex; + } + + // Bind constants + if (_cb) + { + context->UpdateCB(_cb, _cbData.Get()); + context->BindCB(0, _cb); + } + + // Bind pipeline + if (_psVolumetricFog == nullptr) + { + auto psDesc = GPUPipelineState::Description::DefaultFullscreenTriangle; + psDesc.BlendMode = BlendingMode::Add; + psDesc.VS = customData->Shader->GetVS("VS_WriteToSlice"); + psDesc.GS = customData->Shader->GetGS("GS_WriteToSlice"); + psDesc.PS = _shader->GetPS("PS_VolumetricFog"); + _psVolumetricFog = GPUDevice::Instance->CreatePipelineState(); + _psVolumetricFog->Init(psDesc); + } + context->SetState(_psVolumetricFog); +} + +void VolumeParticleMaterialShader::Unload() +{ + // Base + MaterialShader::Unload(); + + SAFE_DELETE_GPU_RESOURCE(_psVolumetricFog); +} + +bool VolumeParticleMaterialShader::Load() +{ + return false; +} diff --git a/Source/Engine/Graphics/Materials/VolumeParticleMaterialShader.h b/Source/Engine/Graphics/Materials/VolumeParticleMaterialShader.h new file mode 100644 index 000000000..e38944342 --- /dev/null +++ b/Source/Engine/Graphics/Materials/VolumeParticleMaterialShader.h @@ -0,0 +1,38 @@ +// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved. + +#pragma once + +#include "MaterialShader.h" + +/// +/// Represents material that can be used to render particles. +/// +class VolumeParticleMaterialShader : public MaterialShader +{ +private: + + GPUPipelineState* _psVolumetricFog = nullptr; + +public: + + /// + /// Init + /// + /// Material resource name + VolumeParticleMaterialShader(const String& name) + : MaterialShader(name) + { + } + +public: + + // [MaterialShader] + DrawPass GetDrawModes() const override; + void Bind(BindParameters& params) override; + void Unload() override; + +protected: + + // [MaterialShader] + bool Load() override; +}; diff --git a/Source/Engine/Particles/Graph/ParticleEmitterGraph.h b/Source/Engine/Particles/Graph/ParticleEmitterGraph.h index 76af84011..d65d8339a 100644 --- a/Source/Engine/Particles/Graph/ParticleEmitterGraph.h +++ b/Source/Engine/Particles/Graph/ParticleEmitterGraph.h @@ -99,6 +99,7 @@ protected: int32 _attrMass; int32 _attrRibbonWidth; int32 _attrColor; + int32 _attrRadius; public: @@ -169,6 +170,8 @@ public: /// Array> RibbonRenderingModules; + bool UsesVolumetricFogRendering = false; + protected: virtual void InitializeNode(NodeType* node) @@ -505,6 +508,14 @@ protected: // TODO: add support for custom sorting key - not only by age USE_ATTRIBUTE(Age, Float, 1); break; + } + // Volumetric Fog Rendering + case GRAPH_NODE_MAKE_TYPE(15, 405): + { + node->Assets[0] = Content::LoadAsync((Guid)node->Values[2]); + USE_ATTRIBUTE(Position, Vector3, 0); + USE_ATTRIBUTE(Radius, Float, 1); + break; } } @@ -544,6 +555,7 @@ public: LightModules.Clear(); SortModules.Clear(); RibbonRenderingModules.Clear(); + UsesVolumetricFogRendering = false; // Base Base::Clear(); @@ -607,6 +619,7 @@ public: SETUP_ATTRIBUTE(Mass, Float, Variant(1.0f)); SETUP_ATTRIBUTE(RibbonWidth, Float, Variant(10.0f)); SETUP_ATTRIBUTE(Color, Vector4, Variant(Vector4(0.0f, 0.0f, 0.0f, 1.0f))); + SETUP_ATTRIBUTE(Radius, Float, Variant(100.0f)); #undef SETUP_ATTRIBUTE return false; @@ -649,6 +662,9 @@ public: case 404: RibbonRenderingModules.Add(n); break; + case 405: + UsesVolumetricFogRendering = true; + break; } break; } diff --git a/Source/Engine/Particles/ParticleEmitter.cpp b/Source/Engine/Particles/ParticleEmitter.cpp index 716fdccea..777c2f0f5 100644 --- a/Source/Engine/Particles/ParticleEmitter.cpp +++ b/Source/Engine/Particles/ParticleEmitter.cpp @@ -112,12 +112,8 @@ Asset::LoadResult ParticleEmitter::load() } if (SimulationMode == ParticlesSimulationMode::GPU) { - if (IsUsingLights) - { - // Downgrade to CPU simulation for light particles (no GPU support for that) - SimulationMode = ParticlesSimulationMode::CPU; - } - else if (Graph.RibbonRenderingModules.HasItems()) + // Downgrade to CPU simulation if no GPU support for that + if (IsUsingLights || Graph.RibbonRenderingModules.HasItems() || Graph.UsesVolumetricFogRendering) { // Downgrade to CPU simulation for ribbons (no GPU support for that) SimulationMode = ParticlesSimulationMode::CPU; diff --git a/Source/Engine/Particles/ParticleManager.cpp b/Source/Engine/Particles/ParticleManager.cpp index 0ad96c74a..99ce64b73 100644 --- a/Source/Engine/Particles/ParticleManager.cpp +++ b/Source/Engine/Particles/ParticleManager.cpp @@ -524,6 +524,34 @@ void DrawEmitterCPU(RenderContext& renderContext, ParticleBuffer* buffer, DrawCa break; } + // Volumetric Fog Rendering + case 405: + { + const auto material = (MaterialBase*)module->Assets[0].Get(); + drawCall.Material = material; + drawCall.InstanceCount = 1; + + auto positionOffset = emitter->Graph.Layout.GetAttributeOffset(module->Attributes[0]); + int32 count = buffer->CPU.Count; + if (positionOffset == -1 || count < 0) + break; + auto radiusOffset = emitter->Graph.Layout.GetAttributeOffset(module->Attributes[1]); + ParticleBufferCPUDataAccessor positionData(buffer, positionOffset); + ParticleBufferCPUDataAccessor radiusData(buffer, radiusOffset); + const bool hasRadius = radiusOffset != -1; + for (int32 i = 0; i < count; i++) + { + // Submit draw call + // TODO: use instancing for volumetric fog particles (combine it with instanced circle rasterization into 3d texture) + drawCall.Particle.VolumetricFog.Position = positionData[i]; + if (emitter->SimulationSpace == ParticlesSimulationSpace::Local) + Vector3::Transform(drawCall.Particle.VolumetricFog.Position, drawCall.World, drawCall.Particle.VolumetricFog.Position); + drawCall.Particle.VolumetricFog.Radius = hasRadius ? radiusData[i] : 100.0f; + drawCall.Particle.VolumetricFog.ParticleIndex = i; + renderContext.List->VolumetricFogParticles.Add(drawCall); + } + break; + } } } } @@ -713,7 +741,12 @@ void DrawEmitterGPU(RenderContext& renderContext, ParticleBuffer* buffer, DrawCa case 404: { // Not supported - CRASH; + break; + } + // Volumetric Fog Rendering + case 405: + { + // Not supported break; } } @@ -775,7 +808,12 @@ void DrawEmitterGPU(RenderContext& renderContext, ParticleBuffer* buffer, DrawCa case 404: { // Not supported - CRASH; + break; + } + // Volumetric Fog Rendering + case 405: + { + // Not supported break; } } @@ -841,7 +879,12 @@ void DrawEmitterGPU(RenderContext& renderContext, ParticleBuffer* buffer, DrawCa case 404: { // Not supported - CRASH; + break; + } + // Volumetric Fog Rendering + case 405: + { + // Not supported break; } } @@ -909,7 +952,6 @@ void ParticleManager::DrawParticles(RenderContext& renderContext, ParticleEffect (view.Pass & material->GetDrawModes() & moduleDrawModes) == 0 ) break; - renderModulesIndices.Add(moduleIndex); break; } @@ -929,7 +971,6 @@ void ParticleManager::DrawParticles(RenderContext& renderContext, ParticleEffect (view.Pass & material->GetDrawModes() & moduleDrawModes) == 0 ) break; - renderModulesIndices.Add(moduleIndex); break; } @@ -944,7 +985,19 @@ void ParticleManager::DrawParticles(RenderContext& renderContext, ParticleEffect (view.Pass & material->GetDrawModes() & moduleDrawModes) == 0 ) break; - + renderModulesIndices.Add(moduleIndex); + break; + } + // Volumetric Fog Rendering + case 405: + { + const auto material = (MaterialBase*)module->Assets[0].Get(); + if (!material || + !material->IsReady() || + material->GetInfo().Domain != MaterialDomain::VolumeParticle || + (view.Flags & ViewFlags::Fog) == 0 + ) + break; renderModulesIndices.Add(moduleIndex); break; } diff --git a/Source/Engine/Renderer/DrawCall.h b/Source/Engine/Renderer/DrawCall.h index 036ba26bd..28acd72ff 100644 --- a/Source/Engine/Renderer/DrawCall.h +++ b/Source/Engine/Renderer/DrawCall.h @@ -218,6 +218,13 @@ struct DrawCall uint32 SegmentCount; GPUBuffer* SegmentDistances; } Ribbon; + + struct + { + Vector3 Position; + float Radius; + int32 ParticleIndex; + } VolumetricFog; } Particle; struct diff --git a/Source/Engine/Renderer/RenderList.cpp b/Source/Engine/Renderer/RenderList.cpp index 1dbf1b663..8f9be5e74 100644 --- a/Source/Engine/Renderer/RenderList.cpp +++ b/Source/Engine/Renderer/RenderList.cpp @@ -350,6 +350,7 @@ void RenderList::Clear() DirectionalLights.Clear(); EnvironmentProbes.Clear(); Decals.Clear(); + VolumetricFogParticles.Clear(); Sky = nullptr; AtmosphericFog = nullptr; Fog = nullptr; diff --git a/Source/Engine/Renderer/RenderList.h b/Source/Engine/Renderer/RenderList.h index 69ef0c33d..bf0f25d0e 100644 --- a/Source/Engine/Renderer/RenderList.h +++ b/Source/Engine/Renderer/RenderList.h @@ -303,6 +303,11 @@ public: /// Array Decals; + /// + /// Local volumetric fog particles registered for the rendering. + /// + Array VolumetricFogParticles; + /// /// Sky/skybox renderer proxy to use (only one per frame) /// diff --git a/Source/Engine/Renderer/VolumetricFogPass.cpp b/Source/Engine/Renderer/VolumetricFogPass.cpp index ff8f469ae..6dbc6e5ac 100644 --- a/Source/Engine/Renderer/VolumetricFogPass.cpp +++ b/Source/Engine/Renderer/VolumetricFogPass.cpp @@ -253,13 +253,9 @@ GPUTextureView* VolumetricFogPass::GetLocalShadowedLightScattering(RenderContext if (renderContext.Buffers->LocalShadowedLightScattering == nullptr) { ASSERT(renderContext.Buffers->LastFrameVolumetricFog == Engine::FrameCount); - const GPUTextureDescription volumeDescRGB = GPUTextureDescription::New3D(_cache.GridSize, PixelFormat::R11G11B10_Float, GPUTextureFlags::RenderTarget | GPUTextureFlags::ShaderResource | GPUTextureFlags::UnorderedAccess); - const auto texture = RenderTargetPool::Get(volumeDescRGB); - renderContext.Buffers->LocalShadowedLightScattering = texture; - context->Clear(texture->ViewVolume(), Color::Transparent); } @@ -299,7 +295,7 @@ void VolumetricFogPass::RenderRadialLight(RenderContext& renderContext, GPUConte // Bind the output context->SetRenderTarget(localShadowedLightScattering); - context->SetViewportAndScissors((float)_cache.Data.GridSizeIntX, (float)_cache.Data.GridSizeIntY); + context->SetViewportAndScissors(_cache.Data.GridSize.X, _cache.Data.GridSize.Y); // Setup data perLight.MinZ = volumeZBoundsMin; @@ -481,8 +477,8 @@ void VolumetricFogPass::Render(RenderContext& renderContext) && Vector3::NearEqual(renderContext.Buffers->VolumetricFogHistory->Size3(), cache.GridSize); // Allocate buffers - const GPUTextureDescription volumeDesc = GPUTextureDescription::New3D(cache.GridSize, PixelFormat::R16G16B16A16_Float, GPUTextureFlags::ShaderResource | GPUTextureFlags::UnorderedAccess); - const GPUTextureDescription volumeDescRGB = GPUTextureDescription::New3D(cache.GridSize, PixelFormat::R11G11B10_Float, GPUTextureFlags::ShaderResource | GPUTextureFlags::UnorderedAccess); + const GPUTextureDescription volumeDesc = GPUTextureDescription::New3D(cache.GridSize, PixelFormat::R16G16B16A16_Float, GPUTextureFlags::RenderTarget | GPUTextureFlags::ShaderResource | GPUTextureFlags::UnorderedAccess); + const GPUTextureDescription volumeDescRGB = GPUTextureDescription::New3D(cache.GridSize, PixelFormat::R11G11B10_Float, GPUTextureFlags::RenderTarget | GPUTextureFlags::ShaderResource | GPUTextureFlags::UnorderedAccess); auto vBufferA = RenderTargetPool::Get(volumeDesc); auto vBufferB = RenderTargetPool::Get(volumeDescRGB); const auto lightScattering = RenderTargetPool::Get(volumeDesc); @@ -500,15 +496,78 @@ void VolumetricFogPass::Render(RenderContext& renderContext) context->BindUA(1, vBufferB->ViewVolume()); context->Dispatch(_csInitialize, groupCountX, groupCountY, groupCountZ); + + context->UnBindUA(0); + context->UnBindUA(1); + context->FlushState(); } - // Voxelize local fog particles - // TODO: support rendering volume particles with custom Albedo/Extinction/Emissive + // Render local fog particles + if (renderContext.List->VolumetricFogParticles.HasItems()) + { + PROFILE_GPU_CPU("Local Fog"); - // Unbind fog attributes tables - context->UnBindUA(0); - context->UnBindUA(1); - context->FlushState(); + // Bind the output + GPUTextureView* rt[] = { vBufferA->ViewVolume(), vBufferB->ViewVolume() }; + context->SetRenderTarget(nullptr, Span(rt, 2)); + context->SetViewportAndScissors((float)volumeDesc.Width, (float)volumeDesc.Height); + + // Ensure to have valid buffers created + if (_vbCircleRasterize == nullptr || _ibCircleRasterize == nullptr) + InitCircleBuffer(); + + MaterialBase::BindParameters bindParams(context, renderContext); + bindParams.DrawCallsCount = 1; + CustomData customData; + customData.Shader = _shader->GetShader(); + customData.GridSize = cache.GridSize; + customData.VolumetricFogMaxDistance = cache.Data.VolumetricFogMaxDistance; + bindParams.CustomData = &customData; + + for (auto& drawCall : renderContext.List->VolumetricFogParticles) + { + const BoundingSphere bounds(drawCall.Particle.VolumetricFog.Position, drawCall.Particle.VolumetricFog.Radius); + ASSERT(!bounds.Center.IsNanOrInfinity() && !isnan(bounds.Radius) && !isinf(bounds.Radius)); + + // Calculate light volume bounds in camera frustum depth range (min and max) + const Vector3 viewSpaceLightBoundsOrigin = Vector3::Transform(bounds.Center, view.View); + const float furthestSliceIndexUnclamped = ComputeZSliceFromDepth(viewSpaceLightBoundsOrigin.Z + bounds.Radius, options, cache.GridSizeZ); + const float closestSliceIndexUnclamped = ComputeZSliceFromDepth(viewSpaceLightBoundsOrigin.Z - bounds.Radius, options, cache.GridSizeZ); + const int32 volumeZBoundsMin = (int32)Math::Clamp(closestSliceIndexUnclamped, 0.0f, cache.GridSize.Z - 1.0f); + const int32 volumeZBoundsMax = (int32)Math::Clamp(furthestSliceIndexUnclamped, 0.0f, cache.GridSize.Z - 1.0f); + + // Culling + if ((view.Position - bounds.Center).LengthSquared() >= (options.Distance + bounds.Radius) * (options.Distance + bounds.Radius) || volumeZBoundsMin >= volumeZBoundsMax) + continue; + + // Setup material shader data + customData.ParticleIndex = drawCall.Particle.VolumetricFog.ParticleIndex; + bindParams.FirstDrawCall = &drawCall; + drawCall.Material->Bind(bindParams); + + // Setup volumetric shader data + PerLight perLight; + auto cb1 = _shader->GetShader()->GetCB(1); + perLight.SliceToDepth.X = cache.Data.GridSize.Z; + perLight.SliceToDepth.Y = cache.Data.VolumetricFogMaxDistance; + perLight.MinZ = volumeZBoundsMin; + perLight.ViewSpaceBoundingSphere = Vector4(viewSpaceLightBoundsOrigin, bounds.Radius); + Matrix::Transpose(renderContext.View.Projection, perLight.ViewToVolumeClip); + + // Upload data + context->UpdateCB(cb1, &perLight); + context->BindCB(1, cb1); + + // Call rendering to the volume + const int32 instanceCount = volumeZBoundsMax - volumeZBoundsMin; + const int32 indexCount = _ibCircleRasterize->GetElementsCount(); + context->BindVB(ToSpan(&_vbCircleRasterize, 1)); + context->BindIB(_ibCircleRasterize); + context->DrawIndexedInstanced(indexCount, instanceCount, 0); + } + + context->ResetRenderTarget(); + } // Render Lights GPUTextureView* localShadowedLightScattering = nullptr; @@ -542,13 +601,15 @@ void VolumetricFogPass::Render(RenderContext& renderContext) // Skip if no lights to render if (pointLights.Count() + spotLights.Count()) { - PROFILE_GPU("Render Lights"); + PROFILE_GPU_CPU("Lights Injection"); // Allocate temporary buffer for light scattering injection localShadowedLightScattering = GetLocalShadowedLightScattering(renderContext, context, options); // Prepare PerLight perLight; + perLight.SliceToDepth.X = cache.Data.GridSize.Z; + perLight.SliceToDepth.Y = cache.Data.VolumetricFogMaxDistance; auto cb1 = _shader->GetShader()->GetCB(1); // Bind the output diff --git a/Source/Engine/Renderer/VolumetricFogPass.h b/Source/Engine/Renderer/VolumetricFogPass.h index 8a1f42730..52fc84a77 100644 --- a/Source/Engine/Renderer/VolumetricFogPass.h +++ b/Source/Engine/Renderer/VolumetricFogPass.h @@ -11,6 +11,16 @@ /// class VolumetricFogPass : public RendererPass { +public: + + struct CustomData + { + GPUShader* Shader; + Vector3 GridSize; + float VolumetricFogMaxDistance; + int32 ParticleIndex; + }; + private: PACK_STRUCT(struct SkyLightData { @@ -53,7 +63,7 @@ private: }); PACK_STRUCT(struct PerLight { - Vector2 Dummy1; + Vector2 SliceToDepth; int32 MinZ; float LocalLightScatteringIntensity; diff --git a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Particles.cpp b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Particles.cpp index f000cb015..2f3394932 100644 --- a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Particles.cpp +++ b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Particles.cpp @@ -75,7 +75,7 @@ MaterialValue MaterialGenerator::AccessParticleAttribute(Node* caller, const Str void MaterialGenerator::ProcessGroupParticles(Box* box, Node* node, Value& value) { // Only particle shaders can access particles data - if (GetRootLayer()->Domain != MaterialDomain::Particle) + if (GetRootLayer()->Domain != MaterialDomain::Particle && GetRootLayer()->Domain != MaterialDomain::VolumeParticle) { value = MaterialValue::Zero; return; diff --git a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.cpp b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.cpp index 23779e3e1..b6b5306e2 100644 --- a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.cpp +++ b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.cpp @@ -296,6 +296,21 @@ bool MaterialGenerator::Generate(WriteStream& source, MaterialInfo& materialInfo eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::Refraction); eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::SubsurfaceColor); } + else if (baseLayer->Domain == MaterialDomain::VolumeParticle) + { + eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Emissive); + eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Opacity); + eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Mask); + eatMaterialGraphBox(baseLayer, MaterialGraphBoxes::Color); + + eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::Normal); + eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::Metalness); + eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::Specular); + eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::AmbientOcclusion); + eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::Roughness); + eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::Refraction); + eatMaterialGraphBoxWithDefault(baseLayer, MaterialGraphBoxes::SubsurfaceColor); + } else { CRASH; @@ -422,6 +437,9 @@ bool MaterialGenerator::Generate(WriteStream& source, MaterialInfo& materialInfo case MaterialDomain::Deformable: srv = 1; // Mesh deformation buffer break; + case MaterialDomain::VolumeParticle: + srv = 1; // Particles data + break; } for (auto f : features) { @@ -504,6 +522,9 @@ bool MaterialGenerator::Generate(WriteStream& source, MaterialInfo& materialInfo case MaterialDomain::Deformable: path /= TEXT("Deformable.shader"); break; + case MaterialDomain::VolumeParticle: + path /= TEXT("VolumeParticle.shader"); + break; default: LOG(Warning, "Unknown material domain."); return true; diff --git a/Source/Shaders/VolumetricFog.shader b/Source/Shaders/VolumetricFog.shader index 44aa71254..79b055732 100644 --- a/Source/Shaders/VolumetricFog.shader +++ b/Source/Shaders/VolumetricFog.shader @@ -60,7 +60,7 @@ META_CB_END META_CB_BEGIN(1, PerLight) -float2 Dummy1; +float2 SliceToDepth; int MinZ; float LocalLightScatteringIntensity; @@ -119,7 +119,7 @@ Quad_VS2GS VS_WriteToSlice(float2 TexCoord : TEXCOORD0, uint LayerIndex : SV_Ins Quad_VS2GS output; uint slice = LayerIndex + MinZ; - float depth = GetSliceDepth(slice); + float depth = (slice / SliceToDepth.x) * SliceToDepth.y; float depthOffset = abs(depth - ViewSpaceBoundingSphere.z); if (depthOffset < ViewSpaceBoundingSphere.w) @@ -133,7 +133,7 @@ Quad_VS2GS VS_WriteToSlice(float2 TexCoord : TEXCOORD0, uint LayerIndex : SV_Ins output.Vertex.Position = float4(0, 0, 0, 0); } - output.Vertex.TexCoord = 0; + output.Vertex.TexCoord = TexCoord; output.LayerIndex = slice; return output;