Skip to content

Commit d9bf2d9

Browse files
authored
Paint: saved meshes follow the paint, new values without the CSG (#65)
* Saved meshes are kept only while their paint is the same The hash that decides whether a model keeps the meshes saved with its scene (and so what a build takes) has each generator's paint and what the shader of each of its materials takes of paint. Paint changed outside the editor, or a shader that takes paint differently, builds the model when the scene opens rather than keeping meshes with the old paint. * Paint: new values are drawn without building a brush When an entity's paint changes only in its values on the same grids, the values are written in place, the hashes of the surfaces they are on are put right in the brushes' cached buffers (the paint's part of a surface's hash is xor-ed in last for that), and the next update copies the meshes again without building a brush. New grids, a tree that kept its saved meshes, or brushes not built yet still have the brushes built. * The saved-mesh hash doesn't hold on to a model's paint The lists the paint part of the hash is gathered in are cleared after it, so a destroyed model's meshes aren't kept loaded through them. * Paint: what a material takes is compared per material Only a material resolved before whose paint attributes changed has every brush built again; a material resolved for the first time (every one, when the editor starts) is in the brushes being built with it, so models keep their saved meshes when a scene opens. The paint manager's work set is a set, emptied after each update so it holds no generator between updates.
1 parent d5e041c commit d9bf2d9

10 files changed

Lines changed: 376 additions & 31 deletions

File tree

‎Components/Managers/ChiselModelManager.cs‎

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -828,6 +828,7 @@ public static Hash128 GetInputHash(ChiselModelComponent model)
828828
return default;
829829
AppendEnvironment(ref hash);
830830
AppendHotspotInputs(ref hash, model);
831+
AppendPaintInputs(ref hash, model);
831832
return hash;
832833
}
833834

@@ -845,6 +846,7 @@ public static Hash128 GetComponentInputHash(ChiselModelComponent model)
845846
return default;
846847
AppendEnvironment(ref hash);
847848
AppendHotspotInputs(ref hash, model);
849+
AppendPaintInputs(ref hash, model);
848850
return hash;
849851
}
850852

@@ -931,6 +933,66 @@ static void AppendHotspotInputs(ref Hash128 hash, ChiselModelComponent model)
931933
s_HotspotMaterials.Clear();
932934
}
933935

936+
static readonly List<ChiselGeneratorComponent> s_PaintGenerators = new();
937+
static readonly List<Material> s_PaintMaterials = new();
938+
static readonly List<ChiselPaintMaterialSlot> s_PaintSlots = new();
939+
940+
// What the meshes take of paint: each generator's paint, and what the shader of each material its surfaces are
941+
// drawn with takes (the attributes a mesh has even where nothing is painted)
942+
static void AppendPaintInputs(ref Hash128 hash, ChiselModelComponent model)
943+
{
944+
s_PaintGenerators.Clear();
945+
s_PaintMaterials.Clear();
946+
model.GetComponentsInChildren(true, s_PaintGenerators);
947+
foreach (var generator in s_PaintGenerators)
948+
{
949+
var paints = generator.SurfacePaint;
950+
var paintCount = paints.Count;
951+
hash.Append(ref paintCount);
952+
foreach (var paint in paints)
953+
{
954+
if (paint == null)
955+
continue;
956+
var header = new Unity.Mathematics.int4(paint.surfaceIndex, (int)paint.attribute, (int)paint.components, paint.values?.Length ?? 0);
957+
hash.Append(ref header);
958+
var defaultValue = paint.defaultValue; hash.Append(ref defaultValue);
959+
var origin = paint.origin; hash.Append(ref origin);
960+
var cellU = paint.cellU; hash.Append(ref cellU);
961+
var cellV = paint.cellV; hash.Append(ref cellV);
962+
var resolution = paint.resolution; hash.Append(ref resolution);
963+
if (paint.values != null && paint.values.Length > 0)
964+
hash.Append(paint.values);
965+
}
966+
967+
var surfaces = generator.SurfaceDefinition?.surfaces;
968+
if (surfaces == null)
969+
continue;
970+
foreach (var surface in surfaces)
971+
{
972+
var material = surface?.RenderMaterial;
973+
if (!material || s_PaintMaterials.Contains(material))
974+
continue;
975+
s_PaintMaterials.Add(material);
976+
s_PaintSlots.Clear();
977+
ChiselPaintMaterials.Resolver?.Invoke(material, s_PaintSlots);
978+
if (s_PaintSlots.Count == 0)
979+
continue;
980+
AppendMaterialIdentity(ref hash, material);
981+
foreach (var slot in s_PaintSlots)
982+
{
983+
var key = new Unity.Mathematics.int2((int)slot.attribute, (int)slot.components);
984+
hash.Append(ref key);
985+
var defaultValue = slot.defaultValue;
986+
hash.Append(ref defaultValue);
987+
}
988+
}
989+
}
990+
// nothing of the model is held on to
991+
s_PaintGenerators.Clear();
992+
s_PaintMaterials.Clear();
993+
s_PaintSlots.Clear();
994+
}
995+
934996
static void AppendMaterialIdentity(ref Hash128 hash, Material material)
935997
{
936998
var identity = material ? GetSurfaceParameterIdentity(EntityId.ToULong(material.GetEntityId())) : default;

‎Components/Managers/ChiselPaintManager.cs‎

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ struct Given
2424
static bool s_All = true;
2525
static readonly HashSet<ChiselGeneratorComponent> s_Changed = new();
2626
static readonly Dictionary<ChiselGeneratorComponent, Given> s_Given = new();
27-
static readonly List<ChiselGeneratorComponent> s_Work = new();
27+
static readonly HashSet<ChiselGeneratorComponent> s_Work = new();
2828

2929
/// <summary>Every generator's paint is looked at again (what shaders take may have changed)</summary>
3030
public static void SetDirty() => s_All = true;
@@ -64,15 +64,12 @@ public static void Update()
6464
}
6565
s_All = false;
6666
}
67-
foreach (var generator in s_Changed)
68-
{
69-
if (!s_Work.Contains(generator))
70-
s_Work.Add(generator);
71-
}
67+
s_Work.UnionWith(s_Changed);
7268
s_Changed.Clear();
7369

7470
foreach (var generator in s_Work)
7571
Give(generator);
72+
s_Work.Clear();
7673
}
7774

7875
// The generator's paint given to its model's tree, and taken from the tree it was in before
@@ -86,7 +83,7 @@ static void Give(ChiselGeneratorComponent generator)
8683
if (given)
8784
{
8885
// built again without it, if it is still there
89-
if (before.tree.Valid && before.tree.SetPaint(before.entityID, default, default) && generator)
86+
if (before.tree.Valid && before.tree.SetPaint(before.entityID, default, default) != ChiselPaintChange.None && generator)
9087
SetBrushesDirty(generator.TopTreeNode);
9188
s_Given.Remove(generator);
9289
}
@@ -136,8 +133,9 @@ static void Give(ChiselGeneratorComponent generator)
136133
v += paint.values.Length;
137134
}
138135
s_Given[generator] = new Given { entityID = entityID, tree = tree, space = space };
139-
// the brushes of a generator whose paint changed are built again, with it
140-
if (tree.SetPaint(entityID, layers, values))
136+
// the brushes of a generator whose grids changed are built again, with them; new values on the same grids only
137+
// have the meshes copied again
138+
if (tree.SetPaint(entityID, layers, values) == ChiselPaintChange.Grids)
141139
SetBrushesDirty(generator.TopTreeNode);
142140
}
143141

‎Components/Tests/ChiselPaintTests.cs‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,67 @@ public void AGridWithoutItsValues_IsMadeAgainWhenPainted()
458458
Assert.That(box.GetSurfacePaint(top, ChiselPaintAttribute.Color), Is.SameAs(paint));
459459
}
460460

461+
// A scene keeps its saved meshes only while their paint is the same: the paint and what the shaders take of it are
462+
// in the hash that decides it
463+
[Test]
464+
public void TheSavedMeshesInputHash_HasThePaintAndWhatTheShaderTakes()
465+
{
466+
var model = Model();
467+
var box = Box(model, Vector3.zero);
468+
Update();
469+
var unpainted = ChiselModelManager.GetComponentInputHash(model);
470+
471+
var paint = PaintTop(box, ChiselPaintAttribute.Color, ChiselPaintComponents.XYZW);
472+
Update();
473+
var painted = ChiselModelManager.GetComponentInputHash(model);
474+
Assert.That(painted, Is.Not.EqualTo(unpainted), "painted");
475+
476+
paint[0, 0] = new Vector4(0.5f, 0.5f, 0.5f, 1);
477+
Assert.That(ChiselModelManager.GetComponentInputHash(model), Is.Not.EqualTo(painted), "a value changed");
478+
var repainted = ChiselModelManager.GetComponentInputHash(model);
479+
480+
ChiselPaintProfiles.Unregister(profile);
481+
Assert.That(ChiselModelManager.GetComponentInputHash(model), Is.Not.EqualTo(repainted), "the shader takes no paint");
482+
ChiselPaintProfiles.Register(profile);
483+
Assert.That(ChiselModelManager.GetComponentInputHash(model), Is.EqualTo(repainted), "and takes it again");
484+
}
485+
486+
// The buffer the box's brush was last built into
487+
static Unity.Entities.BlobAssetReference<ChiselBrushRenderBuffer> BuiltBuffer(ChiselModelComponent model, ChiselBoxComponent box)
488+
{
489+
var entityID = EntityId.ToULong(box.GetEntityId());
490+
var cache = ChiselTreeLookup.Value[model.Node].brushRenderBufferCache;
491+
for (int b = 0; b < cache.Length; b++)
492+
{
493+
if (cache[b].IsCreated && cache[b].Value.entityID == entityID)
494+
return cache[b];
495+
}
496+
Assert.Fail("the box's brush has no buffer");
497+
return default;
498+
}
499+
500+
// New values on the same grid are copied into the mesh without building the brush again; a new grid builds it
501+
[Test]
502+
public void NewValues_AreDrawnWithoutBuildingTheBrush()
503+
{
504+
var model = Model();
505+
var box = Box(model, Vector3.zero);
506+
var paint = PaintTop(box, ChiselPaintAttribute.Color, ChiselPaintComponents.XYZW);
507+
Update();
508+
var built = BuiltBuffer(model, box);
509+
510+
paint[0, 0] = new Vector4(0, 1, 1, 1);
511+
box.PaintChanged();
512+
Update();
513+
AssertValuesAt(RenderMesh(model), VertexAttribute.Color, new Vector3(0, 1, 0), new Vector4(0, 1, 1, 1), "the new value");
514+
Assert.That(BuiltBuffer(model, box), Is.EqualTo(built), "the brush was built again for new values");
515+
516+
box.SetSurfacePaint(ChiselSurfacePaint.Create(paint.surfaceIndex, paint.attribute, paint.components, Vector4.one,
517+
paint.origin, paint.cellU * 0.5f, paint.cellV * 0.5f, new Vector2Int(3, 3)));
518+
Update();
519+
Assert.That(BuiltBuffer(model, box), Is.Not.EqualTo(built), "a new grid didn't build the brush");
520+
}
521+
461522
// The grid moves with its generator
462523
[Test]
463524
public void Paint_MovesWithItsGenerator()

‎Components/Tests/ChiselSavedOutputTests.cs‎

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,84 @@ public void AModelThatChangedWhileClosed_IsBuiltWhenItOpens()
223223
Assert.That(IsMarked(models[1]), Is.True);
224224
}
225225

226+
// Box 0's surfaces drawn with a material whose shader takes color paint, and its top painted red
227+
ChiselSurfacePaint PaintBox0(out Material material, out ChiselPaintProfile profile)
228+
{
229+
material = new Material(Shader.Find("Universal Render Pipeline/Lit"));
230+
profile = new ChiselPaintProfile { shader = material.shader, modes = { ChiselPaintMode.Color("Color") } };
231+
ChiselPaintProfiles.Register(profile);
232+
foreach (var surface in boxes[0].SurfaceDefinition.surfaces)
233+
surface.SetMaterial(material);
234+
boxes[0].OnValidate();
235+
Update();
236+
var top = 0;
237+
var brush = (CSGTreeBrush)boxes[0].TopTreeNode;
238+
ref var mesh = ref BrushMeshManager.GetBrushMeshBlob(brush.BrushMesh).Value;
239+
for (int s = 0; s < mesh.polygons.Length; s++)
240+
{
241+
if (mesh.localPlanes[s].y > 0.999f)
242+
top = mesh.polygons[s].descriptionIndex;
243+
}
244+
var paint = ChiselPaintBrush.PaintFor(boxes[0], top, profile.modes[0]);
245+
ChiselPaintBrush.Fill(paint, profile.modes[0], new Vector4(1, 0, 0, 1));
246+
boxes[0].PaintChanged();
247+
Update();
248+
return paint;
249+
}
250+
251+
static bool HasRed(ChiselModelComponent model)
252+
{
253+
foreach (var color in RenderableWithGeometry(model).sharedMesh.colors)
254+
{
255+
if (color.r > 0.99f && color.g < 0.01f)
256+
return true;
257+
}
258+
return false;
259+
}
260+
261+
// A painted model keeps its saved meshes, and with them its paint
262+
[Test]
263+
public void APaintedModel_KeepsItsSavedMeshes_WithTheirPaint()
264+
{
265+
PaintBox0(out var material, out var profile);
266+
try
267+
{
268+
Assert.That(HasRed(models[0]), Is.True, "painted");
269+
Save();
270+
MarkSavedMeshes();
271+
Reopen();
272+
Assert.That(CompactHierarchyManager.IsTreeUpdateSkipped(models[0].Node), Is.True);
273+
Assert.That(IsMarked(models[0]), Is.True, "the painted model was built again");
274+
Assert.That(HasRed(models[0]), Is.True, "the saved mesh has the paint");
275+
}
276+
finally
277+
{
278+
ChiselPaintProfiles.Unregister(profile);
279+
UnityEngine.Object.DestroyImmediate(material);
280+
}
281+
}
282+
283+
// Paint changed while the scene was closed is built when it opens
284+
[Test]
285+
public void PaintThatChangedWhileClosed_IsBuiltWhenItOpens()
286+
{
287+
var paint = PaintBox0(out var material, out var profile);
288+
try
289+
{
290+
Save();
291+
MarkSavedMeshes();
292+
Reopen(whileClosed: () => ChiselPaintBrush.Fill(paint, profile.modes[0], new Vector4(0, 0, 1, 1)));
293+
Assert.That(IsMarked(models[0]), Is.False, "the repainted model kept its old meshes");
294+
Assert.That(IsMarked(models[1]), Is.True);
295+
Assert.That(HasRed(models[0]), Is.False, "the old paint");
296+
}
297+
finally
298+
{
299+
ChiselPaintProfiles.Unregister(profile);
300+
UnityEngine.Object.DestroyImmediate(material);
301+
}
302+
}
303+
226304
[Test]
227305
public void AModelSavedWithoutAHash_IsBuiltWhenItOpens()
228306
{

‎Core/1.Input/Paint/ChiselPaint.cs‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -148,12 +148,13 @@ public static bool SetPaint(this CSGTree tree, NativeArray<ChiselPaintLayer> lay
148148
}
149149

150150
/// <summary>
151-
/// Replaces the paint of one entity: its layers, and the values their grids take from. True when it changed; the
152-
/// entity's brushes then have to be rebuilt to show it (<see cref="CSGTreeNode.SetSurfacesDirty"/>).
151+
/// Replaces the paint of one entity: its layers, and the values their grids take from. When only values changed on
152+
/// the same grids the meshes are copied again at the next update without building a brush; when the grids changed
153+
/// the entity's brushes have to be built again to show it (<see cref="CSGTreeNode.SetSurfacesDirty"/>).
153154
/// </summary>
154-
public static bool SetPaint(this CSGTree tree, ulong entityID, NativeArray<ChiselPaintLayer> layers, NativeArray<float> values)
155+
public static ChiselPaintChange SetPaint(this CSGTree tree, ulong entityID, NativeArray<ChiselPaintLayer> layers, NativeArray<float> values)
155156
{
156-
return ChiselPaintStore.SetPaint(tree, entityID, layers, values);
157+
return ChiselPaintStore.SetPaintOrValues(tree, entityID, layers, values);
157158
}
158159
}
159160
}

‎Core/1.Input/Paint/ChiselPaintMaterials.cs‎

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ public static class ChiselPaintMaterials
2828
static readonly Dictionary<ulong, Material> s_Materials = new();
2929
static readonly List<ChiselPaintMaterialSlot> s_Slots = new();
3030
static readonly List<ChiselPaintMaterialSlot> s_Resolved = new();
31-
static readonly List<ChiselPaintMaterialSlot> s_Scratch = new();
31+
static readonly Dictionary<ulong, uint> s_Before = new();
32+
static readonly Dictionary<ulong, uint> s_After = new();
3233
static readonly List<ulong> s_Gone = new();
3334
static bool s_Dirty;
3435
static int s_Version;
@@ -45,22 +46,28 @@ public static void Register(Material material)
4546
/// <summary>What shaders take may have changed; looked into at the next <see cref="Update"/></summary>
4647
public static void Changed() => s_Changed = true;
4748

48-
/// <summary>Before the trees are built: every brush is built again when a registered material's attributes changed</summary>
49+
/// <summary>Before the trees are built: every brush is built again when what a material already resolved takes
50+
/// changed (a material resolved for the first time is in the brushes being built with it)</summary>
4951
public static void Update()
5052
{
5153
if (!s_Changed)
5254
return;
5355
s_Changed = false;
5456
s_Dirty = true;
55-
var before = s_Slots.Count;
56-
s_Scratch.Clear();
57-
s_Scratch.AddRange(s_Slots);
57+
s_Before.Clear();
58+
HashByMaterial(s_Slots, s_Before);
5859
Resolve();
59-
var same = before == s_Slots.Count;
60-
for (int i = 0; same && i < before; i++)
60+
s_After.Clear();
61+
HashByMaterial(s_Slots, s_After);
62+
var same = true;
63+
foreach (var entry in s_Before)
6164
{
62-
same = s_Scratch[i].material == s_Slots[i].material && s_Scratch[i].attribute == s_Slots[i].attribute &&
63-
s_Scratch[i].components == s_Slots[i].components && s_Scratch[i].defaultValue.Equals(s_Slots[i].defaultValue);
65+
s_After.TryGetValue(entry.Key, out var after);
66+
if (after != entry.Value)
67+
{
68+
same = false;
69+
break;
70+
}
6471
}
6572
if (same)
6673
return;
@@ -73,6 +80,15 @@ public static void Update()
7380
}
7481
}
7582

83+
static void HashByMaterial(List<ChiselPaintMaterialSlot> slots, Dictionary<ulong, uint> hashes)
84+
{
85+
foreach (var slot in slots)
86+
{
87+
hashes.TryGetValue(slot.material, out var hash);
88+
hashes[slot.material] = math.hash(new uint4(hash, (uint)slot.attribute, (uint)slot.components, math.hash(math.asuint(slot.defaultValue))));
89+
}
90+
}
91+
7692
static void SetBrushesDirty(CSGTreeNode node)
7793
{
7894
if (!node.Valid)

‎Core/2.Processing/Cache/ChiselLookup.cs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ public class Data
4949
public NativeList<ChiselPaintMaterialSlot> paintMaterialSlots;
5050
public NativeParallelHashMap<ulong, ChiselPaintRange> paintMaterials;
5151
public int paintMaterialsVersion = -1;
52+
// paint values changed in place: the meshes are copied again without building a brush
53+
public bool paintValuesChanged;
5254

5355
public NativeParallelHashMap<CompactNodeID, BlobAssetReference<ChiselBrushRenderBuffer>> brushRenderBufferLookup;
5456

‎Core/2.Processing/Jobs/GenerateSurfaceTrianglesJob.cs‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -312,16 +312,17 @@ unsafe void FinishSurface(int surf, int brushNodeOrder, ulong entityID, ref Blob
312312
buf.hotspot = hotspotInfo;
313313
buf.hotspotSource = hotspotSource;
314314

315-
// paint is found by the brush's entity and the surface's index, when the surface's mesh is copied
315+
// the attributes its material's shader takes paint in are in its mesh, painted or not
316+
if (paint.TryGetMaterial(parms.parameter1, out var materialRange))
317+
buf.surfaceHashValue = math.hash(new uint2(buf.surfaceHashValue, materialRange.hash));
318+
// paint is found by the brush's entity and the surface's index, when the surface's mesh is copied; its hash
319+
// goes in last, by xor, so new values can replace it in place (ChiselPaintStore.SetValues)
316320
var paintSurface = baseSurfaces[surf].descriptionIndex;
317321
if (paint.TryGetRange(entityID, paintSurface, out var paintRange))
318322
{
319323
buf.paintSurface = paintSurface;
320-
buf.surfaceHashValue = math.hash(new uint2(buf.surfaceHashValue, paintRange.hash));
324+
buf.surfaceHashValue ^= paintRange.hash;
321325
}
322-
// the attributes its material's shader takes paint in are in its mesh, painted or not
323-
if (paint.TryGetMaterial(parms.parameter1, out var materialRange))
324-
buf.surfaceHashValue = math.hash(new uint2(buf.surfaceHashValue, materialRange.hash));
325326

326327
if (HasBackFace(flags, outputFlags))
327328
FinishBackFace(surf, entityID, flags, parms, outputFlags, surfaceIndexList, ref uniqueVertexMapper, builder, surfaceBuffers, slots);

0 commit comments

Comments
 (0)