From bee1d115673b4f22fb6ee8616b29a87e8ba8dc79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tur=C3=A1nszki=20J=C3=A1nos?= Date: Sat, 15 Feb 2025 18:34:21 +0100 Subject: [PATCH] hairparticle simulation rewrite and multisegment fix; added indirect debug draw features; added custom lod selection for shadow maps; --- .github/workflows/build-pr.yml | 1 - Content/terrain/grass.wiscene | Bin 8753820 -> 8754852 bytes Content/terrain/props.wiscene | Bin 7622469 -> 7649898 bytes Editor/Editor.cpp | 7 +- Editor/HairParticleWindow.cpp | 60 ++-- Editor/HairParticleWindow.h | 1 + .../shaders/ShaderInterop_HairParticle.h | 10 +- WickedEngine/shaders/ShaderInterop_Renderer.h | 2 +- WickedEngine/shaders/globals.hlsli | 29 ++ WickedEngine/shaders/hairparticleVS.hlsl | 2 +- .../shaders/hairparticle_simulateCS.hlsl | 300 ++++++++++-------- WickedEngine/wiEnums.h | 2 + WickedEngine/wiHairParticle.cpp | 71 ++++- WickedEngine/wiHairParticle.h | 3 +- WickedEngine/wiRenderer.cpp | 125 +++++++- WickedEngine/wiScene.cpp | 20 +- WickedEngine/wiScene.h | 5 +- WickedEngine/wiTerrain.cpp | 1 - WickedEngine/wiVersion.cpp | 2 +- 19 files changed, 446 insertions(+), 195 deletions(-) diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index 2c2ea249e..3db7aca51 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -5,7 +5,6 @@ on: concurrency: group: ${{ github.ref }} - cancel-in-progress: true env: UPX_VERSION: 4.2.4 diff --git a/Content/terrain/grass.wiscene b/Content/terrain/grass.wiscene index 15013f708e08ac6565d99485e2fec314c87f4688..d7759bed25cf9dc58528622ddf2713a380c78179 100644 GIT binary patch delta 16230 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 F{|8wr`h5TZ delta 15289 zcmaL82|QHa`@nxw+9a(MLLrr9N+ek(WJ?l-tdlHRVv6j`6`_)pZA6($wk%^G`y`4i ziLqoI`@WBDn7Q*m`h32h@8|db{$GEu*PDCKJ@=gFJkL4LbC&BWt2t&}cSN_c9Zj(rp}B z3qi$V`J!mJ*kYe=eG22FX7~ZbqOQt$oe5p?sV9wc`UdqV*p|!&-5cczNu`gM%m!l6 z)iNBqwC0po4xrK>zxK4j+3hCU~QB2W|GxYKLy{nj4p!%0&28V>W+B=Oca$g04^ zDJImO1cVL8HE56h(ru`U_Iy^f-ZE-AqHMJuXXyL2KKowlFK&ryxsWuN9we7FwQ#tM zbhdX6fwwYFkFOIV*?fX}RRfxeKIBbZtt32j=T}+Ry;C|H=$=DQ%{HTLA3$7?L$^^Q zYoT!ld+#)I?zJ90`mwU1>|5=7ccs^t&9n|Js6863^YNBI4yL?a+X-e%w z%Bwf;B?6^bJ08@&mnaQZg||F9X?wUM)+UtWjc2fJwhQWPBPW;4tN0LF)7s7KxskQ^ z;xnp2?!!Fc9~h^y%~qA2)|>Bi6ge>=c{q%dvz!T?O9H=|M(rjT=YGq0BLQLPbS7l% zI@2mXtXgDn!5Z@;cwkC37PFpReCRf)n+f%=PBGww$B!g@nJ{E5v9U6Rn(CNmmnT6# zmJ{D(k=p1?XdrWxQz=sW$Lbt$nK;xV7Sv-q=8GFbEUb9Kc;|BIaD!>8Szpy+;v0m| zmkHG){O_GxLGXD7z5(@9yOO_xWg zT)FMxlYB`q|oT8qX-E#l@jYr3!bAk@=g&)sG#`8;Gyx; zE-RRD`ZnH*`#?%r(sd?;o}$8gF&9qi(CBGY5~_x@%&?Uc=&^G_qiWVW5MBditLBD{ z6*?&$;oe&->8mYquScwOJ7}&kN@7}S@t_o3IoIY#nqO!AWu?3NXpZ590DA&$lTeCG zs3W@UD?)nUbjXBEfTzd{zfi-q_FqmzH@Dc52ILvu#?j=++smknbqJSRonojX`MXENxeHKW#qX$ zHNxQ0Hg779!D%z%E<2+FlDBq1(kh_K14rlw7tB_6SN!oEUQX@zsTSh zVR*f@6_#$NbPz6gkotL=Xf-<94TS^;iNbjOP820yNkRQrMTAgWoYBhysf0kzqpkMZ3k|gu7UfYZyHD3}!-9 zL>uIf{cqA=qBL^CO0L}tz&K23JvEmti}Ex+JX7vZGp1BuVSen2R1JYI0|#SsRBHEh zpyG0LXvrdbO~-NyA=VD~gf#ki>1>q_>WeA*XTsS&UsBVM`uWAZ`lCEeL>k?8)K96dtm>~fd7Elw4P&v2tzhg- zjs8y|;5DSe;(u~e#^8m-H?$pRK>zHfBT_DCfe~}ijbQxum;WOQs2d>>aP(v9b%76) zHk(!Vq^FNm_wpWPkxinq2=w|hK24-yPBYM-lK$x8HyE>AzLY*rvsdx|)s>_#Y%EO$ z?rzKZIaovDsXLqif5*#vql;PnWoS(3Bo17E5of>}W=v>|pW!8oySK)MTPM+>KHJf7 z5b`WX*js6d6AYiKC>i)IygWV{4mZ(*>KPaW0-pI|Vw?g{=i=TQy|ZXRbt&(lGJL

3GLzZgT2_1q2-SYY-n`(_W$O?!~ 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 BSMLA- diff --git a/Content/terrain/props.wiscene b/Content/terrain/props.wiscene index a20dddf8ecaab15f8d83210b389f39b2e7b88757..764c4eaae36448070ed79ca5b98b7c59a730fcc7 100644 GIT binary patch delta 28651 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 Y1bq?QH41_TVL({WV+soLq)_<(0DGRu%m4rY delta 1498 zcmY+^drVtZ90zdjxgGQYrG<{ig|;w2#ElA*Ie{`(U=O(3DQdN*}a!>6k#5 zC6@Tl3@7d|O`J%U;3MkRnETI?X@<#+57b0SObjd}K4an^Qxo|Ezqb#kO+LSS?l0&3 zdS1D|-2a;Y__eS2qW-l1|E6Q@@x+m?5Y@TM3Vg#8&XG{?E|=)WCwudh^XAjFaV6OF zpMTcTl$Lh9_1{vgq%|v_b^W1H(q30h6$VF||4Y5TY)wnM`(QwFB>l*BS}9lC*_oST zU8Q0HTi@2`=-@;-Q4o5=BhbwmLGARoLLp}$*h1(r9a^E zjs3N$iVMm|u`>yvBbPenZQlE+MHcg35Z zdMd~p>FnNpO&)y@OV)XAgd2pnkWC58ATnz3?G&RTnEgZ|BH2y7=~7o-QGJJ-spAL`L=OL`0Rx zx*{qy)m^ZV*OB)mN@NCoXG6ojQI|8gsZXv~$natlgVnjQzb|xf#Fa~z!t%}t^!XVT zS;>uei%b^LTQf@XV{Y;^_K*q#HdH=^QZj6yw%McPEknHTC)D0M$NnZ8OiJL&VfWD^-Sga+=;x=oOq`4&%KyhXB;V{^sMQZG6J@Xq|KHOHo?#N+F z4!jMaOb)Gb7?HyzIV{WJjvTgZZFNy|OB)=4qi_t4!;_M^rG4eQ_~(Fa6pQt|c(a{O zxpuP}qgc*IAG|2o`$n;hx5V1n-~w-yH<)c3#d+EhES3*L$!co51x_0kYrEluB-Yx0 z{fV0V<(xX|w~m4n&cYasg9|3W4U>}J>fr|HOhB&DYj?mI7=mG`*FN&s&qhPyrli?@ zBW_2nqtg|3jyh~z7O}T)zdE{V5}`v{HFc`P_|LT~o$!>jcBO0R70yV_{zBF*^k`6i zKaK&=1eR06YtW(tzOj{vbUSFlp4uDJ&K#*cT?TGb&v3!xRJ{C<)g> X-9>ciTpngHzvuxk_@wzoe{uRhk`dq# diff --git a/Editor/Editor.cpp b/Editor/Editor.cpp index 6df09d7eb..8136759cc 100644 --- a/Editor/Editor.cpp +++ b/Editor/Editor.cpp @@ -2024,8 +2024,9 @@ void EditorComponent::Update(float dt) for (size_t i = 0; i < scene.hairs.GetCount(); ++i) { Entity entity = scene.hairs.GetEntity(i); + const wi::HairParticleSystem& hair = scene.hairs[i]; HierarchyComponent* hier = scene.hierarchy.GetComponent(entity); - if (hier != nullptr && hier->parentID == hovered.entity) + if (hair.meshID == hovered.entity || (hier != nullptr && hier->parentID == hovered.entity)) { XMVECTOR P = XMLoadFloat3(&hovered.position); P += XMLoadFloat3(&hovered.normal) * 2; @@ -2036,8 +2037,8 @@ void EditorComponent::Update(float dt) ForceFieldComponent& force = scene.forces.Create(grass_interaction_entity); TransformComponent& transform = scene.transforms.Create(grass_interaction_entity); force.type = ForceFieldComponent::Type::Point; - force.gravity = -80; - force.range = 3; + force.gravity = -40; + force.range = 2; transform.Translate(P); break; } diff --git a/Editor/HairParticleWindow.cpp b/Editor/HairParticleWindow.cpp index 08e0a1fb0..bb9e267b0 100644 --- a/Editor/HairParticleWindow.cpp +++ b/Editor/HairParticleWindow.cpp @@ -114,7 +114,7 @@ void HairParticleWindow::Create(EditorComponent* _editor) widthSlider.SetTooltip("Set hair strand width multiplier. This only scales the hair particles horizontally."); AddWidget(&widthSlider); - stiffnessSlider.Create(0, 20, 5, 1000, "Stiffness: "); + stiffnessSlider.Create(0, 10, 0.5f, 100, "Stiffness: "); stiffnessSlider.SetSize(XMFLOAT2(wid, hei)); stiffnessSlider.SetPos(XMFLOAT2(x, y += step)); stiffnessSlider.OnSlide([&](wi::gui::EventArgs args) { @@ -127,11 +127,29 @@ void HairParticleWindow::Create(EditorComponent* _editor) hair->stiffness = args.fValue; hair->SetDirty(); } - }); + }); stiffnessSlider.SetEnabled(false); stiffnessSlider.SetTooltip("Set hair strand stiffness, how much it tries to get back to rest position."); AddWidget(&stiffnessSlider); + dragSlider.Create(0, 1, 0.5f, 100, "Drag: "); + dragSlider.SetSize(XMFLOAT2(wid, hei)); + dragSlider.SetPos(XMFLOAT2(x, y += step)); + dragSlider.OnSlide([&](wi::gui::EventArgs args) { + wi::scene::Scene& scene = editor->GetCurrentScene(); + for (auto& x : editor->translator.selected) + { + wi::HairParticleSystem* hair = scene.hairs.GetComponent(x.entity); + if (hair == nullptr) + continue; + hair->drag = args.fValue; + hair->SetDirty(); + } + }); + dragSlider.SetEnabled(false); + dragSlider.SetTooltip("Set hair strand drag, how much its movement slows down over time."); + AddWidget(&dragSlider); + randomnessSlider.Create(0, 1, 0.2f, 1000, "Randomness: "); randomnessSlider.SetSize(XMFLOAT2(wid, hei)); randomnessSlider.SetPos(XMFLOAT2(x, y += step)); @@ -150,22 +168,23 @@ void HairParticleWindow::Create(EditorComponent* _editor) randomnessSlider.SetTooltip("Set hair length randomization factor. This will affect randomness of hair lengths."); AddWidget(&randomnessSlider); - //segmentcountSlider.Create(1, 10, 1, 9, "Segment Count: "); - //segmentcountSlider.SetSize(XMFLOAT2(wid, hei)); - //segmentcountSlider.SetPos(XMFLOAT2(x, y += step)); - //segmentcountSlider.OnSlide([&](wi::gui::EventArgs args) { - // wi::scene::Scene& scene = editor->GetCurrentScene(); - // for (auto& x : editor->translator.selected) - // { - // wi::HairParticleSystem* hair = scene.hairs.GetComponent(x.entity); - // if (hair == nullptr) - // continue; - // hair->segmentCount = (uint32_t)args.iValue; - // } - //}); - //segmentcountSlider.SetEnabled(false); - //segmentcountSlider.SetTooltip("Set hair strand segment count. This will affect simulation quality and performance."); - //AddWidget(&segmentcountSlider); + segmentcountSlider.Create(1, 10, 1, 9, "Segments: "); + segmentcountSlider.SetSize(XMFLOAT2(wid, hei)); + segmentcountSlider.SetPos(XMFLOAT2(x, y += step)); + segmentcountSlider.OnSlide([&](wi::gui::EventArgs args) { + wi::scene::Scene& scene = editor->GetCurrentScene(); + for (auto& x : editor->translator.selected) + { + wi::HairParticleSystem* hair = scene.hairs.GetComponent(x.entity); + if (hair == nullptr) + continue; + hair->segmentCount = (uint32_t)args.iValue; + hair->SetDirty(); + } + }); + segmentcountSlider.SetEnabled(false); + segmentcountSlider.SetTooltip("Set the number of segments that make up one strand."); + AddWidget(&segmentcountSlider); randomSeedSlider.Create(1, 12345, 1, 12344, "Random seed: "); randomSeedSlider.SetSize(XMFLOAT2(wid, hei)); @@ -384,9 +403,10 @@ void HairParticleWindow::SetEntity(Entity entity) lengthSlider.SetValue(hair->length); widthSlider.SetValue(hair->width); stiffnessSlider.SetValue(hair->stiffness); + dragSlider.SetValue(hair->drag); randomnessSlider.SetValue(hair->randomness); countSlider.SetValue((float)hair->strandCount); - //segmentcountSlider.SetValue((float)hair->segmentCount); + segmentcountSlider.SetValue((float)hair->segmentCount); randomSeedSlider.SetValue((float)hair->randomSeed); viewDistanceSlider.SetValue(hair->viewDistance); uniformitySlider.SetValue(hair->uniformity); @@ -503,9 +523,11 @@ void HairParticleWindow::ResizeLayout() add_fullwidth(infoLabel); add(meshComboBox); add(countSlider); + add(segmentcountSlider); add(lengthSlider); add(widthSlider); add(stiffnessSlider); + add(dragSlider); add(randomnessSlider); add(randomSeedSlider); add(viewDistanceSlider); diff --git a/Editor/HairParticleWindow.h b/Editor/HairParticleWindow.h index b7e77a42a..8dfcea053 100644 --- a/Editor/HairParticleWindow.h +++ b/Editor/HairParticleWindow.h @@ -23,6 +23,7 @@ public: wi::gui::Slider lengthSlider; wi::gui::Slider widthSlider; wi::gui::Slider stiffnessSlider; + wi::gui::Slider dragSlider; wi::gui::Slider randomnessSlider; wi::gui::Slider countSlider; wi::gui::Slider segmentcountSlider; diff --git a/WickedEngine/shaders/ShaderInterop_HairParticle.h b/WickedEngine/shaders/ShaderInterop_HairParticle.h index 9fd72aecf..6708e49c8 100644 --- a/WickedEngine/shaders/ShaderInterop_HairParticle.h +++ b/WickedEngine/shaders/ShaderInterop_HairParticle.h @@ -8,10 +8,8 @@ struct PatchSimulationData { - float3 position; - uint tangent_random; - uint3 normal_velocity; // packed fp16 - uint binormal_length; + float3 prevTail; + float3 currentTail; }; enum HAIR_FLAGS @@ -37,7 +35,7 @@ CBUFFER(HairParticleCB, CBSLOT_OTHER_HAIRPARTICLE) uint xHairFlags; float xHairLength; float xHairStiffness; - float xHairRandomness; + float xHairDrag; uint xHairParticleCount; uint xHairStrandCount; @@ -49,8 +47,8 @@ CBUFFER(HairParticleCB, CBSLOT_OTHER_HAIRPARTICLE) uint xHairInstanceIndex; uint xHairLayerMask; + float xHairRandomness; float xHairAspect; - float xHairPadding1; float xHairUniformity; uint xHairAtlasRectCount; diff --git a/WickedEngine/shaders/ShaderInterop_Renderer.h b/WickedEngine/shaders/ShaderInterop_Renderer.h index a8af72ba9..50ca00350 100644 --- a/WickedEngine/shaders/ShaderInterop_Renderer.h +++ b/WickedEngine/shaders/ShaderInterop_Renderer.h @@ -1193,7 +1193,7 @@ struct alignas(16) FrameCB uint lights; uint decals; uint forces; - uint padding; + int indirect_debugbufferindex; ShaderEntity entityArray[SHADER_ENTITY_COUNT]; float4x4 matrixArray[SHADER_ENTITY_COUNT]; diff --git a/WickedEngine/shaders/globals.hlsli b/WickedEngine/shaders/globals.hlsli index 89da6005f..e051883fc 100644 --- a/WickedEngine/shaders/globals.hlsli +++ b/WickedEngine/shaders/globals.hlsli @@ -884,6 +884,35 @@ inline min16uint2 GetTemporalAASampleRotation() { return uint2(GetFrame().tempor inline bool IsStaticSky() { return GetScene().globalenvmap >= 0; } inline half GetGIBoost() { return unpack_half2(GetFrame().giboost_packed).x; } +// Indirect debug drawing functionality, useable from any shader: +inline void draw_line(float3 a, float3 b, float4 color = 1) +{ + [branch] + if (GetFrame().indirect_debugbufferindex < 0) + return; + RWByteAddressBuffer indirect_debug_buffer = bindless_rwbuffers[descriptor_index(GetFrame().indirect_debugbufferindex)]; + uint prevCount; + indirect_debug_buffer.InterlockedAdd(0, 2, prevCount); // VertexCountPerInstance += 2 + struct DebugLine + { + float4 posA; + float4 colorA; + float4 posB; + float4 colorB; + } debugline; + debugline.posA = float4(a, 1); + debugline.posB = float4(b, 1); + debugline.colorA = color; + debugline.colorB = color; + indirect_debug_buffer.Store(sizeof(IndirectDrawArgsInstanced) + sizeof(DebugLine) * prevCount / 2, debugline); +} +inline void draw_point(float3 pos, float size = 1, float4 color = 1) +{ + draw_line(pos - float3(size, 0, 0), pos + float3(size, 0, 0), color); + draw_line(pos - float3(0, size, 0), pos + float3(0, size, 0), color); + draw_line(pos - float3(0, 0, size), pos + float3(0, 0, size), color); +} + // Mie scaterring approximated with Henyey-Greenstein phase function. // https://www.alexandre-pestana.com/volumetric-lights/ #define G_SCATTERING 0.66 diff --git a/WickedEngine/shaders/hairparticleVS.hlsl b/WickedEngine/shaders/hairparticleVS.hlsl index 685997e4b..169b6ebf9 100644 --- a/WickedEngine/shaders/hairparticleVS.hlsl +++ b/WickedEngine/shaders/hairparticleVS.hlsl @@ -4,7 +4,7 @@ Buffer primitiveBuffer : register(t0); -VertexToPixel main(uint vid : SV_VERTEXID) +VertexToPixel main(uint vid : SV_VertexID) { ShaderMeshInstance inst = HairGetInstance(); ShaderGeometry geometry = HairGetGeometry(); diff --git a/WickedEngine/shaders/hairparticle_simulateCS.hlsl b/WickedEngine/shaders/hairparticle_simulateCS.hlsl index fd09df25f..fee7364ea 100644 --- a/WickedEngine/shaders/hairparticle_simulateCS.hlsl +++ b/WickedEngine/shaders/hairparticle_simulateCS.hlsl @@ -3,8 +3,11 @@ #include "ShaderInterop_HairParticle.h" static const float3 HAIRPATCH[] = { + // root (for every strand): float3(-1, -1, 0), float3(1, -1, 0), + + // cap (for every segment of every strand): float3(-1, 1, 0), float3(1, 1, 0), }; @@ -25,7 +28,7 @@ RWBuffer vertexBuffer_NOR : register(u6); [numthreads(THREADCOUNT_SIMULATEHAIR, 1, 1)] void main(uint3 DTid : SV_DispatchThreadID, uint3 Gid : SV_GroupID, uint groupIndex : SV_GroupIndex) { - if (DTid.x >= xHairParticleCount) + if (DTid.x >= xHairStrandCount) return; ShaderGeometry geometry = HairGetGeometry(); @@ -63,76 +66,118 @@ void main(uint3 DTid : SV_DispatchThreadID, uint3 Gid : SV_GroupID, uint groupIn g = 1 - g; } float2 bary = float2(f, g); + + const float4x4 worldMatrix = xHairTransform.GetMatrix(); // compute final surface position on triangle from barycentric coords: float3 position = attribute_at_bary(pos0, pos1, pos2, bary); position = mul(xHairBaseMeshUnormRemap.GetMatrix(), float4(position, 1)).xyz; half3 target = normalize(attribute_at_bary(nor0, nor1, nor2, bary)); + target = normalize(mul((half3x3)worldMatrix, target)); half3 tangent = normalize(mul(half3(hemispherepoint_cos(rng.next_float(), rng.next_float()).xy, 0), get_tangentspace(target))); half3 binormal = cross(target, tangent); half strand_length = attribute_at_bary(length0, length1, length2, bary); - - uint tangent_random = 0; - tangent_random |= (uint)((uint)(tangent.x * 127.5 + 127.5) << 0); - tangent_random |= (uint)((uint)(tangent.y * 127.5 + 127.5) << 8); - tangent_random |= (uint)((uint)(tangent.z * 127.5 + 127.5) << 16); - tangent_random |= (uint)(rng.next_float() * 255) << 24; const uint currentFrame = uint(noise_gradient_3D(position * xHairUniformity) * 1000) % xHairAtlasRectCount; const HairParticleAtlasRect atlas_rect = xHairAtlasRects[currentFrame]; - - uint binormal_length = 0; - binormal_length |= (uint)((uint)(binormal.x * 127.5 + 127.5) << 0); - binormal_length |= (uint)((uint)(binormal.y * 127.5 + 127.5) << 8); - binormal_length |= (uint)((uint)(binormal.z * 127.5 + 127.5) << 16); - binormal_length |= (uint)(lerp(1, rng.next_float(), saturate(xHairRandomness)) * strand_length * 255) << 24; - + // Identifies the hair strand root particle: const uint strandID = DTid.x * xHairSegmentCount; // Transform particle by the emitter object matrix: - const float4x4 worldMatrix = xHairTransform.GetMatrix(); float3 base = mul(worldMatrix, float4(position.xyz, 1)).xyz; - target = normalize(mul((half3x3)worldMatrix, target)); - const float3 root = base; - - float3 diff = GetCamera().position - root; + + float3 diff = GetCamera().position - base; const float distsq = dot(diff, diff); const bool distance_culled = distsq > sqr(xHairViewDistance); + + half len = lerp(1, rng.next_float(), saturate(xHairRandomness)) * strand_length; + len *= xHairLength; + len *= atlas_rect.size; + len /= (half)xHairSegmentCount; + const float2 frame = float2(atlas_rect.aspect * xHairAspect * xHairSegmentCount, 1) * len * 0.5; + + const uint gfx_vertexcount_per_strand = xHairSegmentCount * 2 + 2; + uint v0 = DTid.x * gfx_vertexcount_per_strand; + + //draw_line(base, base + tangent, float4(1, 0, 0, 1)); + //draw_line(base, base + target, float4(0, 1, 0, 1)); + //draw_line(base, base + binormal, float4(0, 0, 1, 1)); + + // Bottom vertices: + half3x3 TBN = half3x3(tangent, target, binormal); + for (uint vertexID = 0; vertexID < 2; ++vertexID) + { + float3 patchPos = HAIRPATCH[vertexID]; + float2 uv = patchPos.xy; + uv = uv * float2(0.5, 0.5) + 0.5; + uv.y = 1 - uv.y; + patchPos.y += 1; + + // Sprite sheet UV transform: + uv.xy = mad(uv.xy, atlas_rect.texMulAdd.xy, atlas_rect.texMulAdd.zw); + + // scale the billboard by the texture aspect: + patchPos.xyz *= frame.xyx; + + // rotate the patch into the tangent space of the emitting triangle: + patchPos = mul(patchPos, TBN); + + float3 position = base + patchPos; + + if (xHairFlags & HAIR_FLAG_UNORM_POS) + { + position = inverse_lerp(geometry.aabb_min, geometry.aabb_max, position); // remap to UNORM + } + + vertexBuffer_POS[v0] = float4(position, 0); + vertexBuffer_NOR[v0] = half4(target, 0); + vertexBuffer_UVS[v0] = uv.xyxy; // a second uv set could be used here + + if (distance_culled) + { + position = 0; // We can only zero out for raytracing geometry to keep correct prevpos swapping motion vectors! + } + vertexBuffer_POS_RT[v0] = float4(position, 0); + + v0++; + } // Bend down to camera up vector to avoid seeing flat planes from above const float3 bend = GetCamera().up * (1 - saturate(dot(target, GetCamera().up))) * 0.8; + + const float dt = clamp(GetFrame().delta_time, 0, 1.0 / 30.0); // clamp delta time to avoid simulation blowing up - half3 normal = 0; - - const float delta_time = clamp(GetFrame().delta_time, 0, 1.0 / 30.0); // clamp delta time to avoid simulation blowing up + const float stiffnessForce = xHairStiffness; + const float dragForce = xHairDrag; + const float3 boneAxis = target; + const float boneLength = len; for (uint segmentID = 0; segmentID < xHairSegmentCount; ++segmentID) { // Identifies the hair strand segment particle: const uint particleID = strandID + segmentID; - simulationBuffer[particleID].tangent_random = tangent_random; - simulationBuffer[particleID].binormal_length = binormal_length; if (xHairFlags & HAIR_FLAG_REGENERATE_FRAME) { - simulationBuffer[particleID].position = base; - simulationBuffer[particleID].normal_velocity = f32tof16(target); + float3 tail = base + boneAxis * boneLength; + simulationBuffer[particleID].prevTail = tail; + simulationBuffer[particleID].currentTail = tail; } - - normal += f16tof32(simulationBuffer[particleID].normal_velocity); - normal = normalize(normal); - - float len = (binormal_length >> 24) & 0x000000FF; - len /= 255.0; - len *= xHairLength; - len *= atlas_rect.size; - - float3 tip = base + normal * len; - float3 midpoint = lerp(base, tip, 0.5); + + float3 tail_current = simulationBuffer[particleID].currentTail; + float3 tail_prev = simulationBuffer[particleID].prevTail; + float3 inertia = (tail_current - tail_prev) * (1 - dragForce); + float3 stiffness = boneAxis * stiffnessForce; + float3 external = 0; + float3 wind = sample_wind(tail_current, ((float)segmentID + 1) / (float)xHairSegmentCount); + external += wind; + + float3 tail_next = tail_current + inertia + dt * (stiffness + external); + float3 to_tail = normalize(tail_next - base); + tail_next = base + to_tail * boneLength; // Accumulate forces, apply colliders: - half3 force = 0; for (uint i = forces().first_item(); i < forces().end_item(); ++i) { ShaderEntity entity = load_entity(i); @@ -150,153 +195,148 @@ void main(uint3 DTid : SV_DispatchThreadID, uint3 Gid : SV_GroupID, uint groupIn float3 N = normalize(A - B); A -= N * range; B += N * range; - float3 C = closest_point_on_segment(A, B, midpoint); - float3 dir = C - midpoint; + float3 C = closest_point_on_segment(A, B, tail_next); + float3 dir = C - tail_next; float dist = length(dir); dir /= dist; dist = dist - range - len * 0.5; if (dist < 0) { - tip = tip - dir * dist; + tail_next += dir * dist; + to_tail = normalize(tail_next - base); + tail_next = base + to_tail * boneLength; } } else { - float3 closest_point = closest_point_on_segment(base, tip, entity.position); + float3 closest_point = closest_point_on_segment(base, tail_next, entity.position); float3 dir = entity.position - closest_point; float dist = length(dir); dir /= dist; switch (type) { - case ENTITY_TYPE_FORCEFIELD_POINT: - force += dir * entity.GetGravity() * (1 - saturate(dist / range)); - break; - case ENTITY_TYPE_FORCEFIELD_PLANE: - force += entity.GetDirection() * entity.GetGravity() * (1 - saturate(dist / range)); - break; - case ENTITY_TYPE_COLLIDER_SPHERE: - dist = dist - range - len; - if (dist < 0) - { - tip = tip - dir * dist; - } - break; - case ENTITY_TYPE_COLLIDER_PLANE: - dir = normalize(entity.GetDirection()); - dist = plane_point_distance(entity.position, dir, closest_point); - if (dist < 0) - { - dir *= -1; - dist = abs(dist); - } - dist = dist - len; - if (dist < 0) - { - float4x4 planeProjection = load_entitymatrix(entity.GetMatrixIndex()); - const float3 clipSpacePos = mul(planeProjection, float4(closest_point, 1)).xyz; - const float3 uvw = clipspace_to_uv(clipSpacePos.xyz); - [branch] - if (is_saturated(uvw)) + case ENTITY_TYPE_FORCEFIELD_POINT: + tail_next += dt * dir * entity.GetGravity() * (1 - saturate(dist / range)); + to_tail = normalize(tail_next - base); + tail_next = base + to_tail * boneLength; + break; + case ENTITY_TYPE_FORCEFIELD_PLANE: + tail_next += dt * entity.GetDirection() * entity.GetGravity() * (1 - saturate(dist / range)); + to_tail = normalize(tail_next - base); + tail_next = base + to_tail * boneLength; + break; + case ENTITY_TYPE_COLLIDER_SPHERE: + dist = dist - range - len; + if (dist < 0) { - tip = tip + dir * dist; + tail_next += dir * dist; + to_tail = normalize(tail_next - base); + tail_next = base + to_tail * boneLength; } - } - break; - default: - break; + break; + case ENTITY_TYPE_COLLIDER_PLANE: + dir = normalize(entity.GetDirection()); + dist = plane_point_distance(entity.position, dir, closest_point); + if (dist < 0) + { + dir *= -1; + dist = abs(dist); + } + dist = dist - len; + if (dist < 0) + { + float4x4 planeProjection = load_entitymatrix(entity.GetMatrixIndex()); + const float3 clipSpacePos = mul(planeProjection, float4(closest_point, 1)).xyz; + const float3 uvw = clipspace_to_uv(clipSpacePos.xyz); + [branch] + if (is_saturated(uvw)) + { + tail_next -= dir * dist; + to_tail = normalize(tail_next - base); + tail_next = base + to_tail * boneLength; + } + } + break; + default: + break; } } } } - // Pull back to rest position: - force += (target - normal) * xHairStiffness; - - force *= delta_time; - - // Simulation buffer load: - half3 velocity = f16tof32(simulationBuffer[particleID].normal_velocity >> 16u); - - // Apply surface-movement-based velocity: - const float3 old_base = simulationBuffer[particleID].position; - const float3 old_normal = f16tof32(simulationBuffer[particleID].normal_velocity); - const float3 old_tip = old_base + old_normal * len; - const float3 surface_velocity = old_tip - tip; - velocity += surface_velocity; - - // Apply forces: - half3 newVelocity = velocity + force; - half3 newNormal = normal + newVelocity * delta_time; - newNormal = normalize(newNormal); - if (dot(target, newNormal) > 0.5) // clamp the offset + // Don't allow tail to go below the axis plane: + float below_plane = plane_point_distance(base, boneAxis, tail_next); + if (below_plane < 0) { - normal = newNormal; - velocity = newVelocity; + tail_next -= boneAxis * below_plane; } - // Drag: - velocity *= 0.98; - - // Store simulation data: - simulationBuffer[particleID].position = base; - simulationBuffer[particleID].normal_velocity = f32tof16(normal) | (f32tof16(velocity) << 16u); + // Store simulation: + simulationBuffer[particleID].prevTail = tail_current; + simulationBuffer[particleID].currentTail = tail_next; + //draw_point(tail_next, 0.1, float4(0,1,0,1)); + //draw_line(base, tail_next, float4(1,0,0,1)); + + half3 normal = to_tail; + // Write out render buffers: // These must be persistent, not culled (raytracing, surfels...) - uint v0 = particleID * 4; - uint i0 = particleID * 6; - - half3x3 TBN = half3x3(tangent, normalize(normal + bend), binormal); // don't derive binormal, because we want the shear! - float3 rootposition = base - normal * 0.1 * len; // inset to the emitter a bit, to avoid disconnect: - const float2 frame = float2(atlas_rect.aspect * xHairAspect, 1) * len * 0.5; + half3 normal_bend = normalize(normal + bend); + binormal = cross(normal_bend, tangent); + tangent = cross(binormal, normal_bend); + TBN = half3x3(tangent, normal_bend, binormal); - for (uint vertexID = 0; vertexID < 4; ++vertexID) + //draw_line(base, base + tangent, float4(1, 0, 0, 1)); + //draw_line(base, base + normal, float4(0, 1, 0, 1)); + //draw_line(base, base + binormal, float4(0, 0, 1, 1)); + + for (uint vertexID = 2; vertexID < 4; ++vertexID) { - // expand the particle into a billboard cross section, the patch: float3 patchPos = HAIRPATCH[vertexID]; - float2 uv = vertexID < 6 ? patchPos.xy : patchPos.zy; + float2 uv = patchPos.xy; uv = uv * float2(0.5, 0.5) + 0.5; uv.y = lerp((float)segmentID / (float)xHairSegmentCount, ((float)segmentID + 1) / (float)xHairSegmentCount, uv.y); uv.y = 1 - uv.y; patchPos.y += 1; - + // Sprite sheet UV transform: uv.xy = mad(uv.xy, atlas_rect.texMulAdd.xy, atlas_rect.texMulAdd.zw); - + // scale the billboard by the texture aspect: patchPos.xyz *= frame.xyx; - + // rotate the patch into the tangent space of the emitting triangle: patchPos = mul(patchPos, TBN); - - // simplistic wind effect only affects the top, but leaves the base as is: - const float3 wind = sample_wind(rootposition, segmentID + patchPos.y); - - float3 position = rootposition + patchPos + wind; - + + float3 position = base + patchPos; + if (xHairFlags & HAIR_FLAG_UNORM_POS) { position = inverse_lerp(geometry.aabb_min, geometry.aabb_max, position); // remap to UNORM } - vertexBuffer_POS[v0 + vertexID] = float4(position, 0); - vertexBuffer_NOR[v0 + vertexID] = half4(normalize(normal + wind), 0); - vertexBuffer_UVS[v0 + vertexID] = uv.xyxy; // a second uv set could be used here + vertexBuffer_POS[v0] = float4(position, 0); + vertexBuffer_NOR[v0] = half4(normal, 0); + vertexBuffer_UVS[v0] = uv.xyxy; // a second uv set could be used here if (distance_culled) { position = 0; // We can only zero out for raytracing geometry to keep correct prevpos swapping motion vectors! } - vertexBuffer_POS_RT[v0 + vertexID] = float4(position, 0); + vertexBuffer_POS_RT[v0] = float4(position, 0); + + v0++; } // Frustum culling: ShaderSphere sphere; - sphere.center = (base + tip) * 0.5; + sphere.center = (base + tail_next) * 0.5; sphere.radius = len; - const bool visible = !distance_culled && GetCamera().frustum.intersects(sphere); + bool visible = !distance_culled && GetCamera().frustum.intersects(sphere); + visible = true; // Optimization: reduce to 1 atomic operation per wave const uint waveAppendCount = WaveActiveCountBits(visible); @@ -310,6 +350,7 @@ void main(uint3 DTid : SV_DispatchThreadID, uint3 Gid : SV_GroupID, uint groupIn if (visible) { uint prevCount = waveOffset + WavePrefixSum(6u); + uint i0 = particleID * 6; uint ii0 = prevCount; culledIndexBuffer[ii0 + 0] = i0 + 0; culledIndexBuffer[ii0 + 1] = i0 + 1; @@ -320,6 +361,7 @@ void main(uint3 DTid : SV_DispatchThreadID, uint3 Gid : SV_GroupID, uint groupIn } // Offset next segment root to current tip: - base = tip; + base = tail_next; + target = normal; } } diff --git a/WickedEngine/wiEnums.h b/WickedEngine/wiEnums.h index 48e306282..aef11b060 100644 --- a/WickedEngine/wiEnums.h +++ b/WickedEngine/wiEnums.h @@ -82,6 +82,8 @@ namespace wi::enums enum BUFFERTYPES { BUFFERTYPE_FRAMECB, + BUFFERTYPE_INDIRECT_DEBUG_0, + BUFFERTYPE_INDIRECT_DEBUG_1, BUFFERTYPE_COUNT }; diff --git a/WickedEngine/wiHairParticle.cpp b/WickedEngine/wiHairParticle.cpp index eb15d5da6..5fca3bb5b 100644 --- a/WickedEngine/wiHairParticle.cpp +++ b/WickedEngine/wiHairParticle.cpp @@ -109,8 +109,12 @@ namespace wi bd.misc_flags |= ResourceMiscFlag::RAY_TRACING; } + const uint32_t gfx_vertexcount_per_strand = segmentCount * 2 + 2; + const uint32_t gfx_vertexcount = strandCount * gfx_vertexcount_per_strand; + const uint32_t gfx_indexcount = 6 * particleCount; + const size_t position_stride = GetFormatStride(position_format); - const Format ib_format = GetIndexBufferFormatRaw(particleCount * 4); + const Format ib_format = GetIndexBufferFormatRaw(gfx_vertexcount); const uint64_t alignment = device->GetMinOffsetAlignment(&bd) * sizeof(IndirectDrawArgsIndexedInstanced) * // additional alignment @@ -118,14 +122,14 @@ namespace wi ; simulation_view.size = sizeof(PatchSimulationData) * particleCount; - vb_pos[0].size = position_stride * 4 * particleCount; - vb_pos[1].size = position_stride * 4 * particleCount; - vb_nor.size = sizeof(MeshComponent::Vertex_NOR) * 4 * particleCount; - vb_uvs.size = sizeof(MeshComponent::Vertex_UVS) * 4 * particleCount; - wetmap.size = sizeof(uint16_t) * 4 * particleCount; - ib_culled.size = GetFormatStride(ib_format) * 6 * particleCount; + vb_pos[0].size = position_stride * gfx_vertexcount; + vb_pos[1].size = position_stride * gfx_vertexcount; + vb_nor.size = sizeof(MeshComponent::Vertex_NOR) * gfx_vertexcount; + vb_uvs.size = sizeof(MeshComponent::Vertex_UVS) * gfx_vertexcount; + wetmap.size = sizeof(uint16_t) * gfx_vertexcount; + ib_culled.size = GetFormatStride(ib_format) * gfx_indexcount; indirect_view.size = sizeof(IndirectDrawArgsIndexedInstanced); - vb_pos_raytracing.size = position_stride * 4 * particleCount; + vb_pos_raytracing.size = position_stride * gfx_vertexcount; bd.size = AlignTo(indirect_view.size, alignment) + @@ -217,7 +221,40 @@ namespace wi vb_pos_raytracing.descriptor_uav = device->GetDescriptorIndex(&generalBuffer, SubresourceType::UAV, vb_pos_raytracing.subresource_uav); buffer_offset += vb_pos_raytracing.size; - primitiveBuffer = wi::renderer::GetIndexBufferForQuads(particleCount); + + bd = {}; + bd.bind_flags = BindFlag::SHADER_RESOURCE | BindFlag::INDEX_BUFFER; + if (device->CheckCapability(GraphicsDeviceCapability::RAYTRACING)) + { + bd.misc_flags |= ResourceMiscFlag::RAY_TRACING; + } + bd.format = Format::R32_UINT; + bd.stride = GetFormatStride(bd.format); + bd.size = bd.stride * gfx_indexcount; + auto fill_ib = [&](void* dst) + { + uint32_t* primitiveData = (uint32_t*)dst; + uint32_t i = 0; + uint32_t v0 = 0; + for (uint32_t strandID = 0; strandID < strandCount; ++strandID) + { + for (uint32_t segmentID = 0; segmentID < segmentCount; ++segmentID) + { + primitiveData[i++] = v0 + 0; + primitiveData[i++] = v0 + 1; + primitiveData[i++] = v0 + 2; + primitiveData[i++] = v0 + 2; + primitiveData[i++] = v0 + 1; + primitiveData[i++] = v0 + 3; + v0 += 2; + } + v0 += 2; + } + assert(v0 == gfx_vertexcount); + assert(i == gfx_indexcount); + }; + device->CreateBuffer2(&bd, fill_ib, &primitiveBuffer); + device->SetName(&primitiveBuffer, "HairParticleSystem::primitiveBuffer"); } @@ -376,6 +413,7 @@ namespace wi hcb.xHairAspect = hair.width * (float)std::max(1u, desc.width) / (float)std::max(1u, desc.height); hcb.xHairLength = hair.length; hcb.xHairStiffness = hair.stiffness; + hcb.xHairDrag = hair.drag; hcb.xHairRandomness = hair.randomness; hcb.xHairStrandCount = hair.strandCount; hcb.xHairSegmentCount = std::max(hair.segmentCount, 1u); @@ -590,6 +628,16 @@ namespace wi archive >> atlas_rects[i].size; } } + + if (seri.GetVersion() >= 2) + { + archive >> drag; + } + else + { + // Old stiffness remap to new: + stiffness *= 0.05f; + } } else { @@ -618,6 +666,11 @@ namespace wi archive << atlas_rects[i].size; } } + + if (seri.GetVersion() >= 2) + { + archive << drag; + } } } diff --git a/WickedEngine/wiHairParticle.h b/WickedEngine/wiHairParticle.h index 1e340283b..6f6db2ebc 100644 --- a/WickedEngine/wiHairParticle.h +++ b/WickedEngine/wiHairParticle.h @@ -83,7 +83,8 @@ namespace wi uint32_t segmentCount = 1; uint32_t randomSeed = 1; float length = 1.0f; - float stiffness = 10.0f; + float stiffness = 0.5f; + float drag = 0.1f; float randomness = 0.2f; float viewDistance = 200; wi::vector vertex_lengths; diff --git a/WickedEngine/wiRenderer.cpp b/WickedEngine/wiRenderer.cpp index 46c5500b0..fa0ca5a83 100644 --- a/WickedEngine/wiRenderer.cpp +++ b/WickedEngine/wiRenderer.cpp @@ -152,6 +152,9 @@ Texture shadowMapAtlas_Transparent; int max_shadow_resolution_2D = 1024; int max_shadow_resolution_cube = 256; +GPUBuffer indirectDebugStatsReadback[GraphicsDevice::GetBufferCount()]; +bool indirectDebugStatsReadback_available[GraphicsDevice::GetBufferCount()] = {}; + wi::vector> renderableBoxes; wi::vector> renderableBoxes_depth; wi::vector> renderableSpheres; @@ -196,16 +199,18 @@ struct RenderBatch uint32_t meshIndex; uint32_t instanceIndex; uint16_t distance; - uint16_t camera_mask; + uint8_t camera_mask; + uint8_t lod_override; // if overriding the base object LOD is needed, specify less than 0xFF in this uint32_t sort_bits; // an additional bitmask for sorting only, it should be used to reduce pipeline changes - inline void Create(uint32_t meshIndex, uint32_t instanceIndex, float distance, uint32_t sort_bits, uint16_t camera_mask = 0xFFFF) + inline void Create(uint32_t meshIndex, uint32_t instanceIndex, float distance, uint32_t sort_bits, uint8_t camera_mask = 0xFF, uint8_t lod_override = 0xFF) { this->meshIndex = meshIndex; this->instanceIndex = instanceIndex; this->distance = XMConvertFloatToHalf(distance); this->sort_bits = sort_bits; this->camera_mask = camera_mask; + this->lod_override = lod_override; } inline float GetDistance() const @@ -287,9 +292,9 @@ struct RenderQueue { batches.clear(); } - inline void add(uint32_t meshIndex, uint32_t instanceIndex, float distance, uint32_t sort_bits, uint16_t camera_mask = 0xFFFF) + inline void add(uint32_t meshIndex, uint32_t instanceIndex, float distance, uint32_t sort_bits, uint8_t camera_mask = 0xFF, uint8_t lod_override = 0xFF) { - batches.emplace_back().Create(meshIndex, instanceIndex, distance, sort_bits, camera_mask); + batches.emplace_back().Create(meshIndex, instanceIndex, distance, sort_bits, camera_mask, lod_override); } inline void sort_transparent() { @@ -2067,6 +2072,24 @@ void LoadBuffers() device->CreateBuffer(&bd, nullptr, &buffers[BUFFERTYPE_FRAMECB]); device->SetName(&buffers[BUFFERTYPE_FRAMECB], "buffers[BUFFERTYPE_FRAMECB]"); + bd.size = sizeof(IndirectDrawArgsInstanced) + (sizeof(XMFLOAT4) + sizeof(XMFLOAT4)) * 1000; + bd.bind_flags = BindFlag::VERTEX_BUFFER | BindFlag::UNORDERED_ACCESS; + bd.misc_flags = ResourceMiscFlag::BUFFER_RAW | ResourceMiscFlag::INDIRECT_ARGS; + device->CreateBuffer(&bd, nullptr, &buffers[BUFFERTYPE_INDIRECT_DEBUG_0]); + device->SetName(&buffers[BUFFERTYPE_INDIRECT_DEBUG_0], "buffers[BUFFERTYPE_INDIRECT_DEBUG_0]"); + device->CreateBuffer(&bd, nullptr, &buffers[BUFFERTYPE_INDIRECT_DEBUG_1]); + device->SetName(&buffers[BUFFERTYPE_INDIRECT_DEBUG_1], "buffers[BUFFERTYPE_INDIRECT_DEBUG_1]"); + + bd.size = sizeof(IndirectDrawArgsInstanced); + bd.usage = Usage::READBACK; + bd.bind_flags = {}; + bd.misc_flags = {}; + for (auto& buf : indirectDebugStatsReadback) + { + device->CreateBuffer(&bd, nullptr, &buf); + device->SetName(&buf, "indirectDebugStatsReadback"); + } + { TextureDesc desc; desc.bind_flags = BindFlag::SHADER_RESOURCE; @@ -2897,8 +2920,8 @@ void RenderMeshes( uint32_t dataOffset = 0; uint8_t userStencilRefOverride = 0; bool forceAlphatestForDithering = false; + uint8_t lod = 0; AABB aabb; - uint32_t lod = 0; } instancedBatch = {}; uint32_t prev_stencilref = STENCILREF_DEFAULT; @@ -3095,11 +3118,12 @@ void RenderMeshes( const ObjectComponent& instance = vis.scene->objects[instanceIndex]; const AABB& instanceAABB = vis.scene->aabb_objects[instanceIndex]; const uint8_t userStencilRefOverride = instance.userStencilRef; + const uint8_t lod = batch.lod_override == 0xFF ? (uint8_t)instance.lod : batch.lod_override; // When we encounter a new mesh inside the global instance array, we begin a new RenderBatch: if (meshIndex != instancedBatch.meshIndex || userStencilRefOverride != instancedBatch.userStencilRefOverride || - instance.lod != instancedBatch.lod + lod != instancedBatch.lod ) { batch_flush(); @@ -3111,7 +3135,7 @@ void RenderMeshes( instancedBatch.userStencilRefOverride = userStencilRefOverride; instancedBatch.forceAlphatestForDithering = 0; instancedBatch.aabb = AABB(); - instancedBatch.lod = instance.lod; + instancedBatch.lod = lod; } const float dither = std::max(instance.GetTransparency(), std::max(0.0f, batch.GetDistance() - instance.fadeDistance) / instance.radius); @@ -3963,6 +3987,28 @@ void UpdatePerFrameData( frameCB.texture_wind_prev_index = device->GetDescriptorIndex(&textures[TEXTYPE_3D_WIND_PREV], SubresourceType::SRV); frameCB.texture_caustics_index = device->GetDescriptorIndex(&textures[TEXTYPE_2D_CAUSTICS], SubresourceType::SRV); + // See if indirect debug buffer needs to be resized: + if (indirectDebugStatsReadback_available[device->GetBufferIndex()] && indirectDebugStatsReadback[device->GetBufferIndex()].mapped_data != nullptr) + { + const IndirectDrawArgsInstanced* indirectDebugStats = (const IndirectDrawArgsInstanced*)indirectDebugStatsReadback[device->GetBufferIndex()].mapped_data; + const uint64_t required_debug_buffer_size = sizeof(IndirectDrawArgsInstanced) + (sizeof(XMFLOAT4) + sizeof(XMFLOAT4)) * clamp(indirectDebugStats->VertexCountPerInstance, 0u, 4000000u); + if (buffers[BUFFERTYPE_INDIRECT_DEBUG_0].desc.size < required_debug_buffer_size) + { + GPUBufferDesc bd; + bd.size = required_debug_buffer_size; + bd.bind_flags = BindFlag::VERTEX_BUFFER | BindFlag::UNORDERED_ACCESS; + bd.misc_flags = ResourceMiscFlag::BUFFER_RAW | ResourceMiscFlag::INDIRECT_ARGS; + device->CreateBuffer(&bd, nullptr, &buffers[BUFFERTYPE_INDIRECT_DEBUG_0]); + device->SetName(&buffers[BUFFERTYPE_INDIRECT_DEBUG_0], "buffers[BUFFERTYPE_INDIRECT_DEBUG_0]"); + device->CreateBuffer(&bd, nullptr, &buffers[BUFFERTYPE_INDIRECT_DEBUG_1]); + device->SetName(&buffers[BUFFERTYPE_INDIRECT_DEBUG_1], "buffers[BUFFERTYPE_INDIRECT_DEBUG_1]"); + } + } + + // Indirect debug buffers: 0 is always WRITE, 1 is always READ + std::swap(buffers[BUFFERTYPE_INDIRECT_DEBUG_0], buffers[BUFFERTYPE_INDIRECT_DEBUG_1]); + frameCB.indirect_debugbufferindex = device->GetDescriptorIndex(&buffers[BUFFERTYPE_INDIRECT_DEBUG_0], SubresourceType::UAV); + // Note: shadow maps always assumed to be valid to avoid shader branching logic const Texture& shadowMap = shadowMapAtlas.IsValid() ? shadowMapAtlas : *wi::texturehelper::getBlack(); const Texture& shadowMapTransparent = shadowMapAtlas_Transparent.IsValid() ? shadowMapAtlas_Transparent : *wi::texturehelper::getWhite(); @@ -4539,6 +4585,10 @@ void UpdateRenderData( auto prof_updatebuffer_cpu = wi::profiler::BeginRangeCPU("Update Buffers (CPU)"); auto prof_updatebuffer_gpu = wi::profiler::BeginRangeGPU("Update Buffers (GPU)", cmd); + device->CopyBuffer(&indirectDebugStatsReadback[device->GetBufferIndex()], 0, &buffers[BUFFERTYPE_INDIRECT_DEBUG_0], 0, sizeof(IndirectDrawArgsInstanced), cmd); + indirectDebugStatsReadback_available[device->GetBufferIndex()] = true; + barrier_stack.push_back(GPUBarrier::Buffer(&buffers[BUFFERTYPE_INDIRECT_DEBUG_0], ResourceState::COPY_SRC, ResourceState::COPY_DST)); + barrier_stack.push_back(GPUBarrier::Buffer(&vis.scene->meshletBuffer, ResourceState::SHADER_RESOURCE, ResourceState::UNORDERED_ACCESS)); barrier_stack.push_back(GPUBarrier::Buffer(&buffers[BUFFERTYPE_FRAMECB], ResourceState::CONSTANT_BUFFER, ResourceState::COPY_DST)); @@ -4626,6 +4676,16 @@ void UpdateRenderData( barrier_stack.push_back(GPUBarrier::Image(&textures[TEXTYPE_3D_WIND_PREV], textures[TEXTYPE_3D_WIND_PREV].desc.layout, ResourceState::UNORDERED_ACCESS)); barrier_stack.push_back(GPUBarrier::Image(&textures[TEXTYPE_2D_CAUSTICS], textures[TEXTYPE_2D_CAUSTICS].desc.layout, ResourceState::UNORDERED_ACCESS)); + // Indirect debug buffer - clear indirect args: + IndirectDrawArgsInstanced debug_indirect = {}; + debug_indirect.VertexCountPerInstance = 0; + debug_indirect.InstanceCount = 1; + debug_indirect.StartVertexLocation = 0; + debug_indirect.StartInstanceLocation = 0; + device->UpdateBuffer(&buffers[BUFFERTYPE_INDIRECT_DEBUG_0], &debug_indirect, cmd, sizeof(debug_indirect)); + barrier_stack.push_back(GPUBarrier::Buffer(&buffers[BUFFERTYPE_INDIRECT_DEBUG_0], ResourceState::COPY_DST, ResourceState::UNORDERED_ACCESS)); + barrier_stack.push_back(GPUBarrier::Buffer(&buffers[BUFFERTYPE_INDIRECT_DEBUG_1], ResourceState::UNORDERED_ACCESS, ResourceState::VERTEX_BUFFER | ResourceState::INDIRECT_ARGUMENT | ResourceState::COPY_SRC)); + // Flush buffer updates: barrier_stack_flush(cmd); @@ -6114,18 +6174,21 @@ void DrawShadowmaps( if (object.IsRenderable() && object.IsCastingShadow()) { // Determine which cascades the object is contained in: - uint16_t camera_mask = 0; + uint8_t camera_mask = 0; + uint8_t shadow_lod = 0xFF; for (uint32_t cascade = 0; cascade < cascade_count; ++cascade) { if ((cascade < (cascade_count - object.cascadeMask)) && shcams[cascade].frustum.CheckBoxFast(aabb)) { camera_mask |= 1 << cascade; + uint8_t candidate_lod = (uint8_t)vis.scene->ComputeObjectLODForView(object, aabb, vis.scene->meshes[object.mesh_index], shcams[cascade].view_projection); + shadow_lod = std::min(shadow_lod, candidate_lod); } } if (camera_mask == 0) continue; - renderQueue.add(object.mesh_index, uint32_t(i), 0, object.sort_bits, camera_mask); + renderQueue.add(object.mesh_index, uint32_t(i), 0, object.sort_bits, camera_mask, shadow_lod); const uint32_t filterMask = object.GetFilterMask(); if (filterMask & FILTER_TRANSPARENT || filterMask & FILTER_WATER) @@ -6231,7 +6294,11 @@ void DrawShadowmaps( const ObjectComponent& object = vis.scene->objects[i]; if (object.IsRenderable() && object.IsCastingShadow()) { - renderQueue.add(object.mesh_index, uint32_t(i), 0, object.sort_bits); + uint8_t shadow_lod = 0xFF; + uint8_t candidate_lod = (uint8_t)vis.scene->ComputeObjectLODForView(object, aabb, vis.scene->meshes[object.mesh_index], shcam.view_projection); + shadow_lod = std::min(shadow_lod, candidate_lod); + + renderQueue.add(object.mesh_index, uint32_t(i), 0, object.sort_bits, 0xFF, shadow_lod); const uint32_t filterMask = object.GetFilterMask(); if (filterMask & FILTER_TRANSPARENT || filterMask & FILTER_WATER) @@ -6380,18 +6447,21 @@ void DrawShadowmaps( if (object.IsRenderable() && object.IsCastingShadow()) { // Check for each frustum, if object is visible from it: - uint16_t camera_mask = 0; + uint8_t camera_mask = 0; + uint8_t shadow_lod = 0xFF; for (uint32_t camera_index = 0; camera_index < camera_count; ++camera_index) { if (frusta[camera_index].CheckBoxFast(aabb)) { camera_mask |= 1 << camera_index; + uint8_t candidate_lod = (uint8_t)vis.scene->ComputeObjectLODForView(object, aabb, vis.scene->meshes[object.mesh_index], cameras[camera_index].view_projection); + shadow_lod = std::min(shadow_lod, candidate_lod); } } if (camera_mask == 0) continue; - renderQueue.add(object.mesh_index, uint32_t(i), 0, object.sort_bits, camera_mask); + renderQueue.add(object.mesh_index, uint32_t(i), 0, object.sort_bits, camera_mask, shadow_lod); const uint32_t filterMask = object.GetFilterMask(); if (filterMask & FILTER_TRANSPARENT || filterMask & FILTER_WATER) @@ -6487,7 +6557,7 @@ void DrawShadowmaps( const ObjectComponent& object = vis.scene->objects[i]; if (object.IsRenderable()) { - uint16_t camera_mask = 0; + uint8_t camera_mask = 0; if (shcam.frustum.CheckBoxFast(aabb)) { camera_mask |= 1 << 0; @@ -7187,6 +7257,33 @@ void DrawDebugWorld( device->EventEnd(cmd); } + // GPU-generated indirect debug lines: + { + device->EventBegin("Indirect Debug Lines - 3D", cmd); + + device->BindPipelineState(&PSO_debug[DEBUGRENDERING_LINES], cmd); + + MiscCB sb; + XMStoreFloat4x4(&sb.g_xTransform, camera.GetViewProjection()); + sb.g_xColor = XMFLOAT4(1, 1, 1, 1); + device->BindDynamicConstantBuffer(sb, CB_GETBINDSLOT(MiscCB), cmd); + + const GPUBuffer* vbs[] = { + &buffers[BUFFERTYPE_INDIRECT_DEBUG_1], + }; + const uint32_t strides[] = { + sizeof(XMFLOAT4) + sizeof(XMFLOAT4), + }; + const uint64_t offsets[] = { + sizeof(IndirectDrawArgsInstanced), + }; + device->BindVertexBuffers(vbs, 0, arraysize(vbs), strides, offsets, cmd); + + device->DrawInstancedIndirect(&buffers[BUFFERTYPE_INDIRECT_DEBUG_1], 0, cmd); + + device->EventEnd(cmd); + } + if (!renderableLines2D.empty()) { device->EventBegin("Debug Lines - 2D", cmd); @@ -8565,7 +8662,7 @@ void RefreshEnvProbes(const Visibility& vis, CommandList cmd) const ObjectComponent& object = vis.scene->objects[i]; if (object.IsRenderable() && !object.IsNotVisibleInReflections()) { - uint16_t camera_mask = 0; + uint8_t camera_mask = 0; for (uint32_t camera_index = 0; camera_index < arraysize(cameras); ++camera_index) { if (cameras[camera_index].frustum.CheckBoxFast(aabb)) diff --git a/WickedEngine/wiScene.cpp b/WickedEngine/wiScene.cpp index 23786ebbd..115bc138f 100644 --- a/WickedEngine/wiScene.cpp +++ b/WickedEngine/wiScene.cpp @@ -4405,14 +4405,7 @@ namespace wi::scene // LOD select: if (mesh.subsets_per_lod > 0) { - // LOD selection based on screen projection: - XMFLOAT4 rect = aabb.ProjectToScreen(camera.GetViewProjection()); - float width = rect.z - rect.x; - float height = rect.w - rect.y; - float maxdim = std::max(width, height); - float lod_max = float(mesh.GetLODCount() - 1); - float lod = clamp(std::log2(1.0f / maxdim) + object.lod_bias, 0.0f, lod_max); - object.lod = uint32_t(lod); + object.lod = ComputeObjectLODForView(object, aabb, mesh, camera.GetViewProjection()); } union SortBits @@ -7172,6 +7165,17 @@ namespace wi::scene return ocean.GetDisplacedPosition(worldPosition); } + uint32_t Scene::ComputeObjectLODForView(const ObjectComponent& object, const AABB& aabb, const MeshComponent& mesh, const XMMATRIX& ViewProjection) const + { + XMFLOAT4 rect = aabb.ProjectToScreen(ViewProjection); + float width = rect.z - rect.x; + float height = rect.w - rect.y; + float maxdim = std::max(width, height); + float lod_max = float(mesh.GetLODCount() - 1); + float lod = clamp(std::log2(1.0f / maxdim) + object.lod_bias, 0.0f, lod_max); + return uint32_t(lod); + } + bool Scene::IsWetmapProcessingRequired() const { return wetmap_fadeout_time > 0; diff --git a/WickedEngine/wiScene.h b/WickedEngine/wiScene.h index dea3086fa..3a6932f8b 100644 --- a/WickedEngine/wiScene.h +++ b/WickedEngine/wiScene.h @@ -47,7 +47,7 @@ namespace wi::scene wi::ecs::ComponentManager& animations = componentLibrary.Register("wi::scene::Scene::animations", 2); // version = 2 wi::ecs::ComponentManager& animation_datas = componentLibrary.Register("wi::scene::Scene::animation_datas"); wi::ecs::ComponentManager& emitters = componentLibrary.Register("wi::scene::Scene::emitters", 2); // version = 2 - wi::ecs::ComponentManager& hairs = componentLibrary.Register("wi::scene::Scene::hairs", 1); // version = 1 + wi::ecs::ComponentManager& hairs = componentLibrary.Register("wi::scene::Scene::hairs", 2); // version = 2 wi::ecs::ComponentManager& weathers = componentLibrary.Register("wi::scene::Scene::weathers", 6); // version = 6 wi::ecs::ComponentManager& sounds = componentLibrary.Register("wi::scene::Scene::sounds", 1); // version = 1 wi::ecs::ComponentManager& videos = componentLibrary.Register("wi::scene::Scene::videos"); @@ -574,6 +574,9 @@ namespace wi::scene // Note that the input position to this function will be taken on the XZ plane and modified by the displacement map's XZ value, and the Y (vertical) position will be taken from the ocean water height and displacement map only. XMFLOAT3 GetOceanPosAt(const XMFLOAT3& worldPosition) const; + // Computes the LOD for an object AABB for a given view projection matrix + uint32_t ComputeObjectLODForView(const ObjectComponent& object, const wi::primitive::AABB& aabb, const MeshComponent& mesh, const XMMATRIX& ViewProjection) const; + private: void UpdateHumanoidFacings(); diff --git a/WickedEngine/wiTerrain.cpp b/WickedEngine/wiTerrain.cpp index b5fd57c6c..587402349 100644 --- a/WickedEngine/wiTerrain.cpp +++ b/WickedEngine/wiTerrain.cpp @@ -377,7 +377,6 @@ namespace wi::terrain request.y = 0; } } - locker.unlock(); } Terrain::Terrain() diff --git a/WickedEngine/wiVersion.cpp b/WickedEngine/wiVersion.cpp index 89d64d71b..66f5822f5 100644 --- a/WickedEngine/wiVersion.cpp +++ b/WickedEngine/wiVersion.cpp @@ -9,7 +9,7 @@ namespace wi::version // minor features, major updates, breaking compatibility changes const int minor = 71; // minor bug fixes, alterations, refactors, updates - const int revision = 679; + const int revision = 680; const std::string version_string = std::to_string(major) + "." + std::to_string(minor) + "." + std::to_string(revision);