- Material Ui Unity
- Materialpropertyblock Example
- Materialpropertyblocks
- Unity Animate Material Property
- Perrendererdata
Tested with: 2020.1.0b16.4138 + URP 9.0.0-preview.38, Windows DX11
You can do this with a MaterialPropertyBlock, just like you would in the old pipeline to reduce the overhead of instantiating modified copies of materials. The only difference is that the shader property is called 'BaseColor' not 'Color'. Here are the examples of the csharp api class UnityEngine.Renderer.SetPropertyBlock(UnityEngine.MaterialPropertyBlock) taken from open source projects. By voting up you can indicate which examples are most useful and appropriate. Say you want to create a ground plane that can be scaled to any size and maintain the same texture size on the surface without having to change the material tiling every time. Hi, I wanna change the texture of my spine animations, with this forum's advice I made it work by using MaterialPropertyBlock. But some animations not working properly, some parts of its attachments are not replaced with the new texture. Graphics: Added array property getters (e.g. GetFloatArray) for Material, MaterialPropertyBlock and Shader class. Graphics: Added bool SystemInfo.usesReversedZBuffer to be able to know if the platform is using a 'reversed' depth buffer. Graphics: Added BuiltinRenderTextureType.ResolvedDepth enum so command buffers can use it.
Objects with same material & same property values
Objects with different material & differentproperty values
Objects with same material & differentproperty values set by MaterialPropertyBlock
According to Unity Documentation about GPU instancing, user can use MaterialProperyBlock to have different material properties for each instances.
Apparently the test showed that this is not true anymore in Universal Render Pipeline.
Apparently the test showed that this is not true anymore in Universal Render Pipeline.
Reason:
URP shader properties are all wrapped by SRP batcher macros “CBUFFER_START(UnityPerMaterial)”.
GPU instancing need to use this macro “UNITY_INSTANCING_BUFFER_START(MyProps)”
GPU instancing need to use this macro “UNITY_INSTANCING_BUFFER_START(MyProps)”
Solution:
What if we really want to have GPU-instanced object with different properties set by MPB?
Edit the URP shader or have a custom shader like this:
Edit the URP shader or have a custom shader like this:
I was curious to see if I could implement belts like in Satisfactory or Factorio with DOTS in a performant manner. In the end, my implementation is highly parallel and works at 60fps for a million items, but let’s start with a deep dive into how both games do it.
Versions used:
- Unity 2020.2.1f1
- Entities 0.17.0-preview.41
- Hybrid Renderer 0.11.0-preview.42
- Burst 1.5.0
- URP 10.2.2
- DOTS Editor 0.12.0-preview.6
Both games might seem similar, but there are a few key differences between their approaches:
- In Satisfactory, a belt has exactly one entry point and one exit point : it always connect one machine’s exit to another’s entry, including mergers and splitters. A belt has one lane of items
- In Factorio, belts have two lanes of items. A belt can drop its content on another belt without needing any extra machine
Given that there are two item lanes, it means only two belts need to be considered at any point for a merge.
If you look closely at the Factorio gif, you’ll notice items get teleported to the destination belt when leaving the source belt, probably because it avoids a bunch of problems - having to reserve a spot on the target belt and freeze items already there while the item would be inserted. Given that Satisfactory always do its merging hidden from view in a merger, they completely sidestepped the issue.
The Satisfactory approach strongly reduces dependencies in terms of processing: each belt between two machines can be processed in parallel. Machines can be processed in parallel. Worst case, there is a frame delay introduced by the machine that can be justified in-game by the fact that the machine itself introduces a delay. That frame delay can probably be avoided by trying to catch up when outputting items.
Material Ui Unity
We’ll still need to process a belt’s segments in reverse order to ensure that, when updating a segment, the next segment is already up to date.
Factorio has more data dependencies. It seems, according to that blog post, that they split the world into chunks, split each chunk into a 3x3 grid, then update all cells #1 in parallel, then all cells #2, etc. making the process 9 sequential steps, each massively parallel, and one final single-threaded step to reconcile deltas recorded for each chunk.
I didn’t want to spend that much time on the topic, so I went with the Satisfactory design. However, there is still a lot to learn from Factorio.
The Factorio blog is fantastic, I highly recommend reading it. In this post the devs describe how belts are implemented and it makes a lot of sense:
- belts are split into segments that might spawn multiple cells
- a belt segment has a reversed list of items, starting at the end of the belt
- each item only stores the distance to the next item and its type for the rendering
That means that updating a belt comes down to decreasing the distance of the last item on the belt that can move. I’ll just show the blog post illustration:
Pushing that further, it also means that items don’t need an actual object/entity for the simulation.
Materialpropertyblock Example
Given that we’ll implement something closer to Satisfactory, the only point where an item can be inserted is the start of the belt. We’ll split belts too long (for rendering), a splitter/merger will also split a belt into separate segments, and we’ll store the distance from the insertion point to the next item to quickly know if there is room on the next segment when an item reaches the end of the previous one.
Let’s start with the item type:
A note on default values
The
None
value as the first one is a very intentional point of design. I tend to implement any value type such as default(T)
is an invalid value. When I work with indices in lists, I tend to add a sentinel value at index 0, meaning any 0
index is either invalid or uninitialized. In some cases, differentiating the two can be useful, and I’ll use Int.MaxValue
as the invalid value.A belt segment has a start position, an end, the distance to use when inserting an item and a couple of entity references used to update segments in order:
Note that storing both the start and end point could be optimized - for the sake of premature optimization, we could decide to store only the length of the belt,and in a separate field, its rotation. In a 2d game, that rotation would be 2 bits. If we decided to cap the length of a belt to 64, we could store it and the two bits of rotation in a byte. Of course all of that is pointless until we actually profile the code and determine that it’s worth it.
DOTS provides a way to attach multiple payloads of the same type on an entity: Dynamic Buffer Components. That’s how we’ll attach items on segments:
The distance is divided by a constant in a singleton component,
Settings.BeltDistanceSubDiv
, to get the actual distance in world units. If that constant is set to 16, each update will move the item 1/16th of a unit. An item moving 1 unit forward will see its Distance
field reduced by 16.Note that a distance of 0 means “The item will be transferred to the next segment.” For a belt without a next belt, the item stopped at the end of the belt would actually have a distance of BeltDistanceSubDiv
. Measuring the distance from the segment’s drop point will make a few things simpler down the line.In that case, there are 6 different segments, belonging to three belts, each with its own color:
We’ll update each belt in parallel, starting from its last segment, going backward:
Thread | First Segment Updated | Second One |
---|---|---|
1 | 2 | 1 |
2 | 4 | 3 |
3 | 6 | 5 |
Meaning, we need to identify those last segments. We’ll tag any segment whose next element is not another segment. To determine if an entity has a component from a job without having to fetch that data, we can use an
EntityQueryMask
:We just need the segment before the one we’re updating to crawl back the chain:
Now the belt update itself. For each last segment, look for the first item still moving and move it. If that move might transfer the item to the next segment, check that there’s room to accommodate it. Windows view markdown.
And that’s it. Of course the complete code is a bit trickier as it also handles belt-to-splitter item transfers, but that’s the gist of it.
Escape hatches
About
if(iter++ >= 1000000) throw
:Whenever I write a loop with no clear exit, I learned to put an iteration counter and throw if it exceeds an arbitrary threshold. In non-burst C#, you might be able to interrupt a loop by putting a breakpoint and editing variables, but that’s impossible with burst if you haven’t enabled native debugging first.The
[NativeDisableContainerSafetyRestriction]
attributes are here because we think we know what we’re doing, as a segment is supposed to belong to one belt at a time, and because we would not allow loops were we to build an actual game or something. Loops would deserve a special case anyway : think of what would happen with a looping belt completely packed.The belts and splitters themselves are rendered using the Hybrid Renderer V2 package and URP. The items, however, are quite a specific case:
- Item rendering screams “Instancing”. In my case, only 2 meshes, each rendered thousands of times
- They don’t need per-instance culling. If a belt is visible, draw all of its items.
So I decided to give a try at drawing them using
Graphics.DrawMeshInstancedProcedural
, which allows to provide a GraphicsBuffer
for the instanced parameters that is filled with a NativeArray
.First, we need a
bool Rendered
in each segment. We’ll compute the frustum planes of the camera, a raw AABB box for each segment and cull those not visible. It starts simply:Materialpropertyblocks
At the same time, we’ll count how many items of each type are rendered, so we’ll modify the code we just wrote:
Now we need to create one
NativeArray<float3>
per item type and compute each position. This one is trickier: to store an array of float3 arrays, we’ll need a float3**
, which will be filled with the burst-compatible, thread-safe equivalent of position[index++] = pos
. Given that we have multiple arrays and indices, it will rather look like index = IndexPerType[itemType]++; RenderedItemPositions[itemType][index] = pos
:Now the job itself:
And we’re almost done. It’s just a matter of drawing each item type now. For that, I had to hack an URP shader with instancing support ; ShaderGraph doesn’t support that yet. See this part of the shader
And that’s it. Culling demo:
Setup | Total Items | Average Frame Time (Update+Rendering) |
---|---|---|
100 000 * 1 belt with 13 items, into a splitter, into 2 belts | 1 300 000 | 11ms |
100 000 belts chained, 100 items | 10 000 000 | 10ms |
In no specific order:
- Rendering could be massively improved
- I didn’t take the time to dig into the Hybrid Renderer to optimize it for item rendering, probably by skipping the HR batch update mechanism and using the InstancedRenderMeshBatchGroup API directly
- Item rendering could spare some memory per item by sending an
float2
as coordinates, or even computing their position on the GPU directly. I’m not that familiar with compute shaders etc. so I’ll leave that as an exercise for the reader. - Belt rendering would probably benefit from StaticOptimizeEntity, as the transform system takes a lot of CPU time
- Belt update:
- In real-world conditions, it would need to be batched and/or time sliced. Updating half of the distant belts but at twice the rate could work. Lots of edge cases.
- Too many
jobhandle.Complete()
in the actual code. Look at that:
- Data representation:
- In Satisfactory, belts are probably stored as bezier curves
- Implementing Factorio’s two-lane belts with random merge points would take a lot more effort, especially given how well the game runs
- I’m not satisfied with the
BeltSubdivision
mechanism. Still thinking I could store a distance using grid cell units and a separatesubcell distance
Unity Animate Material Property
I can’t stress enough how far this demo is from an actual game implementation, but I’m still satisfied with the result. Burst is still an impressive piece of tech, and I can’t wait to see where DOTS and the hybrid renderer are going. That was a fun project. The project is available here on github. A last video: