#Making a gore system in HDRP

1 messages · Page 1 of 1 (latest)

next echo
#

@desert root Hey, I would like to thank you for all the help you recently gave me 😄 . Currently I have something close to the Valve system working but unfortunatly it looks like it will not fit our use case very well. https://advances.realtimerendering.com/s2020/RenderingDoomEternal.pdf I am now looking at the system in Doom slide 54 but thats even less info to go on. I hope you would be willing to help me get a grasp on it as well. The only thing I currently understand is packing the clip and blood weights(or at least I think I understand it) I have no idea how to read a instanced buffer in shadergraph, or how the hell they compute the clip mask outside of the VS/PS. Like sure we can store a number from 0 to 15 in lets say the first 4 bits of a byte indicating the weight of the mask but then what ?

#

I think I can read a structured buffer with a custom function node, but I'm not sure how to dispatch a CS that writes to the same buffer the shader will read.

desert root
#

I must admit that this simple slide is a bit cryptic to me :/
What is a "clip" in that context ?

As for the buffer, I'd says that it is updated in the compute shader before the rendering occurs, and then simply passed to the shader, in Unity using Material.SetBuffer ? https://docs.unity3d.com/ScriptReference/Material.SetBuffer.html

next echo
#

I dont think there is a way they would directly clip vertices

desert root
next echo
#

so you think that the clip masks are in a TextureArray for example

#

and the buffer controls sampling the array

desert root
#

That's my guess yes

#

If you have the game, you could take a renderdoc capture and analyse it 😅

next echo
#

I do have the game haha

#

I know they use a completly bindless pipeline so it makes sense for the clip masks to be in a texture array ( I think)

next echo
desert root
#

So, if I'm not wrong, if you create the buffer once, and "store" the cpu object on an component or a script that isn't destroyed from one frame to the other, the buffer should always exist.
So you only have to assign it to the material once and it should always be available.

To update with the compute shader, you could using camera/frame rendering event to dispatch the compute at the start of rendering.

next echo
#

ah yes from the RenderPipelineManager

#

hmm I'm still very confused as to what the compute shader actually does and what data we feed it. So for example we detect a hit on the head collider, then what? I dont get this byte per vertex thing if their wounds are authored with their own clip and blood masks cant I just flip the corresponding index to a 1 and let the shader sample from that...

#

I'm still installing Doom btw 🙂

#

If the clip mask is already a texture why the need for the whole buffer.

lament yew
#

A guess:

The whole buffer is basically a bunch of booleans that say: this vertex is on or off.

The compute shader running per character holds that array and when you send it a hit position and radius - the compute shader chews through all of the positions and checks if they're within and should be flagged as off.

Because vertex data is interpolated you can blend that so that when you clip it you get smooth transitions.

When you get to the edges you've basically got a single noise texture that roughens up the clipping to make it look natural for wounds.

lament yew
#

But it seems to be limited in platform support

#

You could also just store it in the TexCoords or vertex colors

#

instead of a RW buffer

#

using the compute shader i think

lament yew
#

with a texture

#

The way I got this running (and its not perfect by any means just a starting point) was starting with this repo:
https://github.com/mgstauffer/UnityComputeBufferExample/tree/master

Then adding
#pragma target 5.0 to the shader

changing
uniform StructuredBuffer<float> buffer : register(t1); to uniform RWStructuredBuffer<float> _IsAlive : register(u1);

removing the update loop from Main.cs and adding Graphics.SetRandomWriteTarget(1, computeBuffer, false); to allow the shader to write to the buffer.

Making the data array as large as the amount of vertices in my mesh by grabbing that variable from the meshfilter & initializing all of the floats in that array to 1.0f (bc each vertex is alive on start).

setting isAlive in the vert function

  _IsAlive[v.id] = false;
}

and sending it to the fragment function through an interpolator I added to v2f:
float isAlive : TEXCOORD1;
like this:
o.isAlive = _IsAlive[v.id];

From there the logic in the fragment function is basically just multiplying the alpha by o.isAlive

GitHub

A simple example of using a compute buffer in a vertex-fragment shader (i.e. not in a compute shader) - GitHub - mgstauffer/UnityComputeBufferExample: A simple example of using a compute buffer in ...

#

ComputeBuffers dont need to be textures, they can be most kinds of primitive data. The cool thing about them is they can also be structs so you can be pretty organized.

#

The craziest thing about ComputeBuffers is the idea of RWStructuredBuffer which is capable of reading and writing to an array within a shader.

#

This has potential to run stupid fast because it never has to communicate with the CPU

#

One note:
In my brief research I couldn't figure out how to make it 1bit per vertex so I ended up using floats. Unity wants the stride to be a multiple of 4bytes when you make the compute buffer and 1 is too small.

#

here r my modified files

#

they're very very rough

#

I was hestitant to share bc i didnt see a license but they're mit lol

next echo
#

Thank you for contributing, your idea is neat but I dont think it is very close to what is described in the presentation. I think we can make the buffer be of uint and the 4 stride is not a problem

lament yew
#

Oh sorry! I was just running off of the doom eternal slide! I got ahead of myself. 😅

next echo
#

Sorry the toddler needed me haha.

#

So I gave it some thinking. Your idea might not be far off

#

It can definitely work as the procedural blood on bullet hits. I wonder if we bake the preskinned vert positions if we can do the distance check in a separate compute shader.

#

How ever the presentation specifically mentions a artist made clip mask that goes along with each wound mesh

#

And they somehow pack that info in a buffer as well

#

But i fail to understand why is it per vertex. Unless every enemy in doom has a completely wounded model that gets procedurally exposed

#

Then it kinda makes sense since we can pack a clip weight and blood weight in the byte per vertex, but then what do we need the authored mask for

#

Im going to renderdoc Doom tomorrow and see if I can get a glimpse

#

In the meantime I have contacted the people from the presentation haha

lament yew
#

Awesome please post what they share with you! I'm invested now haha.

#

I think the part I didn't think through is how the mask relates to the parts that are hidden inside the surface of the model (& the artist masks I think are part of that)

#

but yeah I think you could totally use the process I outlined above to do blood and to cut through the shell model

#

you could start w/ them as separate variables then pack them together later if you wanted

lament yew
next echo
#

hmm trying to connect renderdoc or Nsight to a steam game is proving to be rather difficult

next echo
#

So I manged to get captures working, but I'm getting kinda lost . This is the first instance of the enemy being rendered and it already has a hole in it

#

this is the output of the vertex shader at this point

#

So the presentation mentioned that the work is being done in a compute shader, the first mention of compute is Compute Pass #1 way up at the beggining of the frame.

#

now this torso has 17339 verts according to the vs input

#

if it is 1byte/vertex I expect to find a buffer somewhere in the compute pass #1 that has 17339 bytes in it

#

is there a way to sort buffers by size in the Resource Inspector ?

#

wait the buffers seem to be uint so that is 4 bytes so I'm really looking for a buffer with roughly the size of 4335 or something.

#

but nothing is close to that as well. All the ssbo buffers seem really huge and the uniform ones are tiny like 64 bytes

#

I'm very lost on how to proceed.

next echo
#

@lament yew I can confirm that the wound is indeed a seperate mesh, so there is no fully wounded model being exposed

next echo
#

I am now convinced that the specific compute shader is dispatched only on the frame the wound is applied and this will be almost impossible to capture

#

So the question remains how does the computer shader calculate the clip/blood weight for each vertex. I mean its possible to sample a texture in a cs but how will that be cheaper than doing it in just the vs ?

lament yew
#

I think doing it in the cs you can control when it's dispatched (like u said above) maybe that's the advantage?

#

The intersection math doesn't seem that heavy tho. The reason I used a vertex shader w/ rwbuffer is because I didn't want to think about skinning (which I rly should have tested).

#

But my assumption (could b wrong) was that unity does skinning before the vertex shader executes.

#

So by the time you get there you don't need to think about it.

#

I'm sure there's ways to time that w/ c# tho

next echo
#

I suppose the cs effectively bakes the sample into the buffer, then all the vs does is interpolate the data to the ps.

#

That makes sorta sense, but Im thinking what the weights actually are, as far as I can tell the wounds dont have damage stages they are on or off

#

Idk at this point I need and adult haha

lament yew
#

I think you could still make that and not use damage stages!

You have a weight for clip that maybe subtracts alpha so it pushes fragments into below the threshold for discard. That's modulated by the clip texture.

You have a weight for the blood which you could use to reveal the blood texture either with a lerp or another threshold.

#

Somehow they mix them together but the hardest remaining part imo is how they authored the clip texture and the blood texture for each monster - both of which I think rly sell the effect.

#

It being weights I think has the benefit of more smoothly introducing those alpha thresholds.

next echo
#

I just feel that if I have to pass in the uvs to the compute shader in order to sample the masks as well it is kinda redundant. Then again the fact that we sample once per wound and then just have a buffer with the data for the ps vs shaders to read might actually be a win. I just dont want to throw time at this implementation until Im certain it’s close to what they are doing

lament yew
#

Oh! Don't sample the masks in the compute shader that seems like a waste. Just do that in the fragment shader.

#

You lose a lot of the perks of a texture when you sample it per vertex rather than in a fragment function.

#

Might as well just be vertex attributes you painted in in blender or something.

next echo
#

Then the whole compute shader is useless I can just as easily, if not more so, pack the weights directly into the buffer from a job

lament yew
#

Usually the advantage of the compute shader if you set things up right is that you don't need to copy much data back and forth between cpu and gpu. The gpu can be really fast if it has all the data it needs.

I think you're right though you probably could do that.

#

The other piece to consider IMO is the skinning. If you did it on the cpu you'd have to copy the skinned mesh from the gpu to the cpu, run operations, then send it back to the gpu.

#

Where if you had it all in shaders you're just sending commands to the gpu for data it already has.

#

That's my understanding at least.

lament yew
#

But Idk sounds like it's already going to have a very small footprint if you're only doing it every once and a while so it might be worth at least trying in jobs.

next echo
#

I will make a breakdown of my solution but I need to add some more code and cleanup

#

but in the end I ended up sampling the masks in the compute shader, still not sure about the actual weights, right now it's either 0 or 1

next echo
#

So lets start off with the compute shader

#
#pragma enable_d3d11_debug_symbols

RWByteAddressBuffer _woundBuffer;
StructuredBuffer<float2> _uvBuffer;

Texture2D<float4> _clipMaskTexture;
SamplerState pointClampSampler;

Texture2DArray<float3> _clipMaskTextures;

int _slice;

int _bufferLenght;

[numthreads(64,1,1)]
void ComputeMasks(uint3 id : SV_DispatchThreadID)
{
    if(id.x < _bufferLenght)
    {
        float clip = _clipMaskTextures.SampleLevel(pointClampSampler, float3(_uvBuffer[id.x].r, _uvBuffer[id.x].g, _slice), 0).r;
        float blood = _clipMaskTextures.SampleLevel(pointClampSampler, float3(_uvBuffer[id.x].r, _uvBuffer[id.x].g, _slice), 0).g;
    
        if(blood >0)
        {
            uint clipScaled = clip * 0xffff;
            uint bloodScaled = blood * 0xffff;
            uint packedValue = (clipScaled << 16) | (bloodScaled & 0xffff);
            _woundBuffer.Store(id.x * 4, packedValue);
        }
    }
}
#

It's very simple all it does is sample a texture array based on a given slice and then packs the two mask values into the uint .The slice index corresponds to a specific wound so like abdomen, left shoulder, head etc. We also need to pass in the mesh uv's to the compute shader so we can do our sampling.

#

Then to unpack them I have this custom function : ```ByteAddressBuffer _woundBuffer;

void ReadBuffer_float(float i, out float clip, out float blood)
{
uint packedValue = _woundBuffer.Load(i * 4);

clip = (packedValue >> 16) / 65535.0;
blood = (packedValue & 0xffff) / 65535.0;

#ifdef SHADERGRAPH_PREVIEW
clip = 1;
blood =0;
#endif
}```

#

After that the two values simply get interpolated to the fragment shader to be used as we please