From b11378b12dae00b6716d79e4519bfd500c3b4931 Mon Sep 17 00:00:00 2001 From: Sander van Rossen Date: Sun, 25 Oct 2026 12:14:07 +0100 Subject: [PATCH 01/27] Vertex painting: paint profiles, surface paint and the paint stream Paint profiles (ChiselPaintProfile) say what paint means to a shader: modes, each a Color or a named Value, written to a vertex attribute (Color or TexCoord2-7) in some of its components (xyzw, xyz, or one). A generator keeps its surfaces' paint (ChiselSurfacePaint): per surface and attribute a grid of values in the surface's plane, in its own space; the cells are also the surface's subdivision. ChiselPaintManager gives each model's tree its paint in the model's space (CSGTree.SetPaint) and rebuilds the brushes whose paint changed. A render mesh with painted surfaces takes the attributes painted on it in a second vertex stream; its unpainted surfaces take the default. A mesh without paint keeps its layout. The paint of a surface is part of its hash, so a mesh that changes only in paint is not kept. Test shaders and materials with their profiles: Chisel/Test/Paint Color (vertex color) and Chisel/Test/Paint Value (TexCoord2.x). --- .../Base/ChiselGeneratorComponent.cs | 45 ++++ Components/Components/Paint.meta | 8 + .../Components/Paint/ChiselPaintProfile.cs | 72 +++++++ .../Paint/ChiselPaintProfile.cs.meta | 2 + .../Components/Paint/ChiselSurfacePaint.cs | 56 +++++ .../Paint/ChiselSurfacePaint.cs.meta | 2 + Components/Managers/ChiselModelManager.cs | 2 + Components/Managers/ChiselPaintManager.cs | 168 +++++++++++++++ .../Managers/ChiselPaintManager.cs.meta | 2 + Components/Tests/ChiselPaintTests.cs | 195 ++++++++++++++++++ Components/Tests/ChiselPaintTests.cs.meta | 2 + Core/1.Input/Paint.meta | 8 + Core/1.Input/Paint/ChiselPaint.cs | 134 ++++++++++++ Core/1.Input/Paint/ChiselPaint.cs.meta | 2 + Core/2.Processing/Cache/ChiselLookup.cs | 9 + .../Jobs/GenerateSurfaceTrianglesJob.cs | 11 + .../Jobs/JobData/ChiselBrushRenderBuffer.cs | 8 + .../Managers/CSGManager.UpdateTreeMeshes.cs | 8 +- Core/2.Processing/Paint.meta | 8 + Core/2.Processing/Paint/ChiselPaintStore.cs | 176 ++++++++++++++++ .../Paint/ChiselPaintStore.cs.meta | 2 + .../OutputMeshes/ChiselOutputRenderable.cs | 101 ++++++++- .../OutputMeshes/IChiselOutputMeshCopier.cs | 2 + .../OutputMeshes/OutputHotspotSections.cs | 2 + Core/3.Output/OutputMeshes/OutputModelWeld.cs | 2 + Package Resources/Materials/URP/Paint.meta | 8 + .../URP/Paint/ChiselPaintColor.shader | 68 ++++++ .../URP/Paint/ChiselPaintColor.shader.meta | 9 + .../URP/Paint/ChiselPaintCommon.hlsl | 96 +++++++++ .../URP/Paint/ChiselPaintCommon.hlsl.meta | 7 + .../URP/Paint/ChiselPaintValue.shader | 70 +++++++ .../URP/Paint/ChiselPaintValue.shader.meta | 9 + .../URP/Paint/Paint Color Profile.asset | 22 ++ .../URP/Paint/Paint Color Profile.asset.meta | 8 + .../Materials/URP/Paint/Paint Color Test.mat | 36 ++++ .../URP/Paint/Paint Color Test.mat.meta | 8 + .../URP/Paint/Paint Value Profile.asset | 22 ++ .../URP/Paint/Paint Value Profile.asset.meta | 8 + .../Materials/URP/Paint/Paint Value Test.mat | 36 ++++ .../URP/Paint/Paint Value Test.mat.meta | 8 + 40 files changed, 1440 insertions(+), 2 deletions(-) create mode 100644 Components/Components/Paint.meta create mode 100644 Components/Components/Paint/ChiselPaintProfile.cs create mode 100644 Components/Components/Paint/ChiselPaintProfile.cs.meta create mode 100644 Components/Components/Paint/ChiselSurfacePaint.cs create mode 100644 Components/Components/Paint/ChiselSurfacePaint.cs.meta create mode 100644 Components/Managers/ChiselPaintManager.cs create mode 100644 Components/Managers/ChiselPaintManager.cs.meta create mode 100644 Components/Tests/ChiselPaintTests.cs create mode 100644 Components/Tests/ChiselPaintTests.cs.meta create mode 100644 Core/1.Input/Paint.meta create mode 100644 Core/1.Input/Paint/ChiselPaint.cs create mode 100644 Core/1.Input/Paint/ChiselPaint.cs.meta create mode 100644 Core/2.Processing/Paint.meta create mode 100644 Core/2.Processing/Paint/ChiselPaintStore.cs create mode 100644 Core/2.Processing/Paint/ChiselPaintStore.cs.meta create mode 100644 Package Resources/Materials/URP/Paint.meta create mode 100644 Package Resources/Materials/URP/Paint/ChiselPaintColor.shader create mode 100644 Package Resources/Materials/URP/Paint/ChiselPaintColor.shader.meta create mode 100644 Package Resources/Materials/URP/Paint/ChiselPaintCommon.hlsl create mode 100644 Package Resources/Materials/URP/Paint/ChiselPaintCommon.hlsl.meta create mode 100644 Package Resources/Materials/URP/Paint/ChiselPaintValue.shader create mode 100644 Package Resources/Materials/URP/Paint/ChiselPaintValue.shader.meta create mode 100644 Package Resources/Materials/URP/Paint/Paint Color Profile.asset create mode 100644 Package Resources/Materials/URP/Paint/Paint Color Profile.asset.meta create mode 100644 Package Resources/Materials/URP/Paint/Paint Color Test.mat create mode 100644 Package Resources/Materials/URP/Paint/Paint Color Test.mat.meta create mode 100644 Package Resources/Materials/URP/Paint/Paint Value Profile.asset create mode 100644 Package Resources/Materials/URP/Paint/Paint Value Profile.asset.meta create mode 100644 Package Resources/Materials/URP/Paint/Paint Value Test.mat create mode 100644 Package Resources/Materials/URP/Paint/Paint Value Test.mat.meta diff --git a/Components/Components/Base/ChiselGeneratorComponent.cs b/Components/Components/Base/ChiselGeneratorComponent.cs index b1c72370..ab328c03 100644 --- a/Components/Components/Base/ChiselGeneratorComponent.cs +++ b/Components/Components/Base/ChiselGeneratorComponent.cs @@ -185,6 +185,40 @@ public abstract class ChiselGeneratorComponent : ChiselNodeComponent, IChiselHas // Scenes saved before contents existed load as Solid. [SerializeField, HideInInspector] protected int contents; // NOTE: do not rename, name is directly used in editors [SerializeField] protected Vector3 pivotOffset = Vector3.zero; + // The paint of the generator's surfaces, a grid per surface and vertex attribute (ChiselSurfacePaint) + [SerializeField, HideInInspector] System.Collections.Generic.List surfacePaint = new(); + + public System.Collections.Generic.IReadOnlyList SurfacePaint => surfacePaint; + + /// The paint of a surface in an attribute, or null + public ChiselSurfacePaint GetSurfacePaint(int surfaceIndex, ChiselPaintAttribute attribute) + { + foreach (var paint in surfacePaint) + { + if (paint != null && paint.surfaceIndex == surfaceIndex && paint.attribute == attribute) + return paint; + } + return null; + } + + /// Gives a surface this paint in its attribute, in place of what it had + public void SetSurfacePaint(ChiselSurfacePaint paint) + { + if (paint == null) + return; + surfacePaint.RemoveAll(other => other == null || (other.surfaceIndex == paint.surfaceIndex && other.attribute == paint.attribute)); + surfacePaint.Add(paint); + ChiselPaintManager.SetDirty(); + } + + public void RemoveSurfacePaint(int surfaceIndex, ChiselPaintAttribute attribute) + { + if (surfacePaint.RemoveAll(other => other == null || (other.surfaceIndex == surfaceIndex && other.attribute == attribute)) > 0) + ChiselPaintManager.SetDirty(); + } + + /// Called after the values of a surface's paint were changed in place + public void PaintChanged() => ChiselPaintManager.SetDirty(); public override CSGTreeNode TopTreeNode { @@ -416,9 +450,18 @@ protected override void OnValidateState() if (TopTreeNode.Dirty) ChiselNodeHierarchyManager.NotifyContentsModified(this); + if (surfacePaint.Count > 0) + ChiselPaintManager.SetDirty(); base.OnValidateState(); } + protected override void OnEnable() + { + base.OnEnable(); + if (surfacePaint.Count > 0) + ChiselPaintManager.SetDirty(); + } + public override void UpdateTransformation() { // TODO: recalculate transformation based on hierarchy up to (but not including) model @@ -444,6 +487,8 @@ protected override void OnDisable() { base.OnDisable(); DestroyChildTreeNodes(); + if (surfacePaint.Count > 0) + ChiselPaintManager.SetDirty(); } [HideInInspector] int prevMaterialHash; diff --git a/Components/Components/Paint.meta b/Components/Components/Paint.meta new file mode 100644 index 00000000..05a30b69 --- /dev/null +++ b/Components/Components/Paint.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1f13d96dc60716f4d908846d749ad2d0 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Components/Components/Paint/ChiselPaintProfile.cs b/Components/Components/Paint/ChiselPaintProfile.cs new file mode 100644 index 00000000..28241fa4 --- /dev/null +++ b/Components/Components/Paint/ChiselPaintProfile.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using Chisel.Core; +using UnityEngine; + +namespace Chisel.Components +{ + /// What a paint mode writes: a color the brush paints with, or a value per component + public enum ChiselPaintKind : byte + { + /// A color, written to the mode's components (rgba, or rgb) + Color = 0, + /// A number (a height, wetness, a layer's weight): painted up or down with strength and falloff + Value = 1 + } + + /// One thing a shader takes from paint: what it is called, and where it goes in the vertices + [Serializable] + public struct ChiselPaintMode + { + public string name; + public ChiselPaintKind kind; + public ChiselPaintAttribute attribute; + public ChiselPaintComponents components; + /// What an unpainted surface has, in all four components + public Vector4 defaultValue; + /// A value mode's lowest and highest value + public Vector2 range; + + public static ChiselPaintMode Color(string name, ChiselPaintAttribute attribute = ChiselPaintAttribute.Color, + ChiselPaintComponents components = ChiselPaintComponents.XYZW) + { + return new ChiselPaintMode { name = name, kind = ChiselPaintKind.Color, attribute = attribute, components = components, + defaultValue = Vector4.one, range = new Vector2(0, 1) }; + } + + public static ChiselPaintMode Value(string name, ChiselPaintAttribute attribute, ChiselPaintComponents components, + float defaultValue = 0, float min = 0, float max = 1) + { + return new ChiselPaintMode { name = name, kind = ChiselPaintKind.Value, attribute = attribute, components = components, + defaultValue = new Vector4(defaultValue, defaultValue, defaultValue, defaultValue), + range = new Vector2(min, max) }; + } + } + + /// + /// What paint means to a shader: the modes a surface drawn with it can be painted in. The shader shows the paint; + /// Chisel only writes it to the vertex attributes the modes name. Two modes don't write one component of one attribute. + /// + [CreateAssetMenu(menuName = "Chisel/Paint Profile", fileName = "Paint Profile")] + public sealed class ChiselPaintProfile : ScriptableObject + { + public Shader shader; + public List modes = new(); + + /// The first two modes that write one component of one attribute; false when there are none + public bool FindOverlap(out int first, out int second) + { + for (first = 0; first < modes.Count; first++) + { + for (second = first + 1; second < modes.Count; second++) + { + if (modes[first].attribute == modes[second].attribute && + (modes[first].components & modes[second].components) != 0) + return true; + } + } + first = second = -1; + return false; + } + } +} diff --git a/Components/Components/Paint/ChiselPaintProfile.cs.meta b/Components/Components/Paint/ChiselPaintProfile.cs.meta new file mode 100644 index 00000000..8cc65b03 --- /dev/null +++ b/Components/Components/Paint/ChiselPaintProfile.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 83ee62a399ce44d41abc4152089d341e \ No newline at end of file diff --git a/Components/Components/Paint/ChiselSurfacePaint.cs b/Components/Components/Paint/ChiselSurfacePaint.cs new file mode 100644 index 00000000..59e72959 --- /dev/null +++ b/Components/Components/Paint/ChiselSurfacePaint.cs @@ -0,0 +1,56 @@ +using System; +using Chisel.Core; +using UnityEngine; + +namespace Chisel.Components +{ + /// + /// The paint of one surface of a generator in one vertex attribute: a grid of values in the surface's plane, in the + /// generator's own space. Grid point (i, j) is at origin + i * cellU + j * cellV; holds them + /// row by row along cellU. The cells are also how finely the surface is cut to show the paint. + /// + [Serializable] + public sealed class ChiselSurfacePaint + { + /// The surface, as the generator's surface array numbers it + public int surfaceIndex; + public ChiselPaintAttribute attribute; + public ChiselPaintComponents components; + public Vector4 defaultValue; + public Vector3 origin; + public Vector3 cellU; + public Vector3 cellV; + public Vector2Int resolution; + public Vector4[] values = Array.Empty(); + + /// A grid of resolution points from origin along cellU and cellV, every point at the default + public static ChiselSurfacePaint Create(int surfaceIndex, ChiselPaintAttribute attribute, ChiselPaintComponents components, + Vector4 defaultValue, Vector3 origin, Vector3 cellU, Vector3 cellV, Vector2Int resolution) + { + resolution = Vector2Int.Max(resolution, Vector2Int.one); + var values = new Vector4[resolution.x * resolution.y]; + for (int i = 0; i < values.Length; i++) + values[i] = defaultValue; + return new ChiselSurfacePaint + { + surfaceIndex = surfaceIndex, + attribute = attribute, + components = components, + defaultValue = defaultValue, + origin = origin, + cellU = cellU, + cellV = cellV, + resolution = resolution, + values = values + }; + } + + public bool IsValid => resolution.x > 0 && resolution.y > 0 && values != null && values.Length == resolution.x * resolution.y; + + public Vector4 this[int i, int j] + { + get => values[j * resolution.x + i]; + set => values[j * resolution.x + i] = value; + } + } +} diff --git a/Components/Components/Paint/ChiselSurfacePaint.cs.meta b/Components/Components/Paint/ChiselSurfacePaint.cs.meta new file mode 100644 index 00000000..092ae539 --- /dev/null +++ b/Components/Components/Paint/ChiselSurfacePaint.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 50085d8d0ae24854194da1accfa2de4d \ No newline at end of file diff --git a/Components/Managers/ChiselModelManager.cs b/Components/Managers/ChiselModelManager.cs index 77ea8b26..e8fe79d6 100644 --- a/Components/Managers/ChiselModelManager.cs +++ b/Components/Managers/ChiselModelManager.cs @@ -296,6 +296,8 @@ public void UpdateModels() ChiselDecalManager.Update(); // and the surfaces connected or disconnected by hand ChiselHotspotLinks.Update(); + // and the paint of the generators' surfaces + ChiselPaintManager.Update(); // Update the tree meshes Profiler.BeginSample("Flush"); diff --git a/Components/Managers/ChiselPaintManager.cs b/Components/Managers/ChiselPaintManager.cs new file mode 100644 index 00000000..1a781067 --- /dev/null +++ b/Components/Managers/ChiselPaintManager.cs @@ -0,0 +1,168 @@ +using System.Collections.Generic; +using Chisel.Core; +using Unity.Collections; +using Unity.Mathematics; +using UnityEngine; + +namespace Chisel.Components +{ + /// + /// Gives each model's tree the paint of its generators' surfaces (), + /// in the model's space, and rebuilds the brushes whose paint changed. Called before the trees are built. + /// + public static class ChiselPaintManager + { + static bool s_Dirty = true; + static readonly List s_Painted = new(); + // each painted generator's space in its model's, as its paint was last given in + static readonly Dictionary s_Spaces = new(); + static readonly HashSet s_PaintedModels = new(); + static readonly Dictionary> s_ByModel = new(); + // every generator paint was given for, by entity: one that lost all of its paint is rebuilt without it too + static readonly Dictionary s_Known = new(); + + public static void SetDirty() => s_Dirty = true; + + static Matrix4x4 SpaceOf(ChiselGeneratorComponent generator, ChiselModelComponent model) + { + return model.transform.worldToLocalMatrix * generator.transform.localToWorldMatrix; + } + + public static void Update() + { + if (!s_Dirty) + { + foreach (var generator in s_Painted) + { + var model = generator ? generator.hierarchyItem?.Model : null; + if (!generator || !model || !s_Spaces.TryGetValue(generator, out var space) || space != SpaceOf(generator, model)) + { + s_Dirty = true; + break; + } + } + if (!s_Dirty) + return; + } + s_Dirty = false; + + s_Painted.Clear(); + s_Spaces.Clear(); + foreach (var list in s_ByModel.Values) + list.Clear(); + foreach (var generator in Object.FindObjectsByType(FindObjectsInactive.Exclude)) + { + var model = generator.hierarchyItem?.Model; + if (!generator.isActiveAndEnabled || generator.SurfacePaint.Count == 0 || !model) + continue; + s_Painted.Add(generator); + s_Spaces[generator] = SpaceOf(generator, model); + if (!s_ByModel.TryGetValue(model, out var generators)) + s_ByModel[model] = generators = new List(); + generators.Add(generator); + } + + // every model with paint, and every model that had paint (which loses it) + var models = new HashSet(s_PaintedModels); + foreach (var entry in s_ByModel) + { + if (entry.Value.Count > 0) + models.Add(entry.Key); + } + s_PaintedModels.Clear(); + using var changed = new NativeList(16, Allocator.Temp); + foreach (var model in models) + { + if (!model || !model.Node.Valid) + continue; + s_ByModel.TryGetValue(model, out var generators); + if (generators != null && generators.Count > 0) + s_PaintedModels.Add(model); + SetPaint(model, generators, changed); + } + foreach (var model in new List(s_ByModel.Keys)) + { + if (!model) + s_ByModel.Remove(model); + } + } + + static void SetPaint(ChiselModelComponent model, List generators, NativeList changed) + { + var layerCount = 0; + var valueCount = 0; + if (generators != null) + { + foreach (var generator in generators) + { + foreach (var paint in generator.SurfacePaint) + { + if (paint == null || !paint.IsValid) + continue; + layerCount++; + valueCount += paint.values.Length; + } + } + } + + NativeArray layers; + using var _layers = layers = new NativeArray(layerCount, Allocator.Temp); + NativeArray values; + using var _values = values = new NativeArray(valueCount, Allocator.Temp); + int l = 0, v = 0; + if (generators != null) + { + foreach (var generator in generators) + { + var entityID = EntityId.ToULong(generator.GetEntityId()); + s_Known[entityID] = generator; + var space = s_Spaces[generator]; + foreach (var paint in generator.SurfacePaint) + { + if (paint == null || !paint.IsValid) + continue; + layers[l++] = new ChiselPaintLayer + { + entityID = entityID, + surfaceIndex = paint.surfaceIndex, + attribute = paint.attribute, + components = paint.components, + defaultValue = paint.defaultValue, + origin = space.MultiplyPoint3x4(paint.origin), + cellU = space.MultiplyVector(paint.cellU), + cellV = space.MultiplyVector(paint.cellV), + resolution = new int2(paint.resolution.x, paint.resolution.y), + valueStart = v + }; + foreach (var value in paint.values) + values[v++] = value; + } + } + } + + if (!model.Node.SetPaint(layers, values, changed)) + return; + // the brushes of a generator whose paint changed are built again, with it + foreach (var entityID in changed) + { + if (s_Known.TryGetValue(entityID, out var generator) && generator) + SetBrushesDirty(generator.TopTreeNode); + else + s_Known.Remove(entityID); + } + } + + static void SetBrushesDirty(CSGTreeNode node) + { + if (!node.Valid) + return; + if (node.Type == CSGNodeType.Brush) + { + node.SetDirty(); + return; + } + for (int i = 0; i < node.Count; i++) + SetBrushesDirty(node[i]); + } + } +} diff --git a/Components/Managers/ChiselPaintManager.cs.meta b/Components/Managers/ChiselPaintManager.cs.meta new file mode 100644 index 00000000..e8cf85d1 --- /dev/null +++ b/Components/Managers/ChiselPaintManager.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 77ec5d2660bf0364e9b5e3371ea35484 \ No newline at end of file diff --git a/Components/Tests/ChiselPaintTests.cs b/Components/Tests/ChiselPaintTests.cs new file mode 100644 index 00000000..a9645b1d --- /dev/null +++ b/Components/Tests/ChiselPaintTests.cs @@ -0,0 +1,195 @@ +using System.Collections.Generic; +using Chisel.Core; +using NUnit.Framework; +using Unity.Mathematics; +using UnityEngine; +using UnityEngine.Rendering; + +namespace Chisel.Components.Tests +{ + // Surface paint (ChiselSurfacePaint): a grid of values on a surface, written to the vertex attribute it names in the + // render meshes of its material + [TestFixture] + public class ChiselPaintTests + { + readonly List created = new(); + + [SetUp] + public void SetUp() + { + if (Object.FindObjectsByType(FindObjectsInactive.Include).Length > 0) + Assert.Ignore("A Chisel model is loaded; run these in a scene without one"); + } + + [TearDown] + public void TearDown() + { + foreach (var item in created) + { + if (item) + Object.DestroyImmediate(item); + } + created.Clear(); + Update(); + } + + static void Update() + { + for (int i = 0; i < 3; i++) + { + ChiselNodeHierarchyManager.Update(); + ChiselModelManager.Instance.UpdateModels(); + } + } + + ChiselModelComponent Model() + { + var root = new GameObject("paint test"); + created.Add(root); + return root.AddComponent(); + } + + // A unit box with its lowest corner at position + static ChiselBoxComponent Box(ChiselModelComponent model, Vector3 position) + { + var go = new GameObject("Box"); + go.transform.SetParent(model.transform, false); + go.transform.localPosition = position; + var box = go.AddComponent(); + box.Max = new float3(1e4f); + box.Min = float3.zero; + box.Max = new float3(1); + return box; + } + + static int SurfaceFacing(ChiselBoxComponent box, float3 normal) + { + Update(); + var brush = (CSGTreeBrush)box.TopTreeNode; + ref var mesh = ref BrushMeshManager.GetBrushMeshBlob(brush.BrushMesh).Value; + for (int s = 0; s < mesh.polygons.Length; s++) + { + if (math.dot(math.normalize(mesh.localPlanes[s].xyz), normal) > 0.999f) + return mesh.polygons[s].descriptionIndex; + } + Assert.Fail($"the box has no surface facing {normal}"); + return -1; + } + + // The box's top painted on its corners: (0,0) red, (1,0) green, (0,1) blue, (1,1) black + static ChiselSurfacePaint PaintTop(ChiselBoxComponent box, ChiselPaintAttribute attribute, ChiselPaintComponents components) + { + var paint = ChiselSurfacePaint.Create(SurfaceFacing(box, new float3(0, 1, 0)), attribute, components, Vector4.one, + new Vector3(0, 1, 0), Vector3.right, Vector3.forward, new Vector2Int(2, 2)); + paint[0, 0] = new Vector4(1, 0, 0, 1); + paint[1, 0] = new Vector4(0, 1, 0, 1); + paint[0, 1] = new Vector4(0, 0, 1, 1); + paint[1, 1] = new Vector4(0, 0, 0, 0); + box.SetSurfacePaint(paint); + return paint; + } + + static Mesh RenderMesh(ChiselModelComponent model) + { + foreach (var renderable in model.generated.renderables) + { + if (renderable != null && renderable.HasGeometry && (renderable.query & SurfaceDestinationFlags.Renderable) != 0) + return renderable.sharedMesh; + } + Assert.Fail("the model has no render mesh"); + return null; + } + + // The attribute's value at every vertex of an upward surface at this position (a corner is the side faces' too) + static List ValuesAt(Mesh mesh, VertexAttribute attribute, Vector3 position) + { + var values = new List(); + if (attribute == VertexAttribute.Color) + { + foreach (var color in mesh.colors) + values.Add(color); + } else + mesh.GetUVs(attribute - VertexAttribute.TexCoord0, values); + var vertices = mesh.vertices; + var normals = mesh.normals; + var found = new List(); + for (int v = 0; v < vertices.Length; v++) + { + if ((vertices[v] - position).sqrMagnitude < 1e-8f && normals[v].y > 0.99f) + found.Add(values[v]); + } + Assert.That(found, Is.Not.Empty, $"no vertex at {position}"); + return found; + } + + static void AssertValuesAt(Mesh mesh, VertexAttribute attribute, Vector3 position, Vector4 expected, string message) + { + foreach (var value in ValuesAt(mesh, attribute, position)) + Assert.That(Vector4.Distance(value, expected), Is.LessThan(1e-5f), $"{message}: {value} at {position}"); + } + + // The top's corners take the grid's corners; the box's other surfaces, under the same material, the default + [Test] + public void PaintedColors_AreTheVertexColorsOfTheirSurface() + { + var model = Model(); + var box = Box(model, new Vector3(2, 0, 3)); + Update(); + Assert.That(RenderMesh(model).HasVertexAttribute(VertexAttribute.Color), Is.False, "nothing painted: no colors"); + + PaintTop(box, ChiselPaintAttribute.Color, ChiselPaintComponents.XYZW); + Update(); + var mesh = RenderMesh(model); + Assert.That(mesh.HasVertexAttribute(VertexAttribute.Color), Is.True); + AssertValuesAt(mesh, VertexAttribute.Color, new Vector3(2, 1, 3), new Vector4(1, 0, 0, 1), "the top's (0,0)"); + AssertValuesAt(mesh, VertexAttribute.Color, new Vector3(3, 1, 3), new Vector4(0, 1, 0, 1), "the top's (1,0)"); + AssertValuesAt(mesh, VertexAttribute.Color, new Vector3(2, 1, 4), new Vector4(0, 0, 1, 1), "the top's (0,1)"); + var colors = mesh.colors; + var normals = mesh.normals; + for (int v = 0; v < colors.Length; v++) + { + if (normals[v].y < 0.99f) + Assert.That((Vector4)colors[v], Is.EqualTo(Vector4.one), "the other surfaces aren't painted: the default"); + } + + box.RemoveSurfacePaint(SurfaceFacing(box, new float3(0, 1, 0)), ChiselPaintAttribute.Color); + Update(); + Assert.That(RenderMesh(model).HasVertexAttribute(VertexAttribute.Color), Is.False, "the paint removed"); + } + + // Paint in one component of a texture coordinate leaves the others at the default + [Test] + public void PaintInOneComponent_LeavesTheOthersAtTheDefault() + { + var model = Model(); + var box = Box(model, Vector3.zero); + var paint = PaintTop(box, ChiselPaintAttribute.TexCoord3, ChiselPaintComponents.Y); + Update(); + var mesh = RenderMesh(model); + Assert.That(mesh.HasVertexAttribute(VertexAttribute.TexCoord3), Is.True); + Assert.That(mesh.HasVertexAttribute(VertexAttribute.Color), Is.False); + AssertValuesAt(mesh, VertexAttribute.TexCoord3, new Vector3(1, 1, 0), new Vector4(1, 1, 1, 1), "the green corner's y, the default elsewhere"); + AssertValuesAt(mesh, VertexAttribute.TexCoord3, new Vector3(0, 1, 0), new Vector4(1, 0, 1, 1), "the red corner's y"); + + // a value changed in place shows at the next update + paint[0, 0] = new Vector4(0.25f, 0.25f, 0.25f, 0.25f); + box.PaintChanged(); + Update(); + AssertValuesAt(RenderMesh(model), VertexAttribute.TexCoord3, new Vector3(0, 1, 0), new Vector4(1, 0.25f, 1, 1), "changed"); + } + + // The grid moves with its generator + [Test] + public void Paint_MovesWithItsGenerator() + { + var model = Model(); + var box = Box(model, Vector3.zero); + PaintTop(box, ChiselPaintAttribute.Color, ChiselPaintComponents.XYZW); + Update(); + box.transform.localPosition = new Vector3(5, 0, 0); + ChiselNodeHierarchyManager.UpdateTreeNodeTransformation(box); + Update(); + AssertValuesAt(RenderMesh(model), VertexAttribute.Color, new Vector3(5, 1, 0), new Vector4(1, 0, 0, 1), "moved with it"); + } + } +} diff --git a/Components/Tests/ChiselPaintTests.cs.meta b/Components/Tests/ChiselPaintTests.cs.meta new file mode 100644 index 00000000..9a993118 --- /dev/null +++ b/Components/Tests/ChiselPaintTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c8bfb8245d6011a48a48dd9abe60296f \ No newline at end of file diff --git a/Core/1.Input/Paint.meta b/Core/1.Input/Paint.meta new file mode 100644 index 00000000..55d7abba --- /dev/null +++ b/Core/1.Input/Paint.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e2c297f479280d74ea5b2c520191da84 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Core/1.Input/Paint/ChiselPaint.cs b/Core/1.Input/Paint/ChiselPaint.cs new file mode 100644 index 00000000..351afad3 --- /dev/null +++ b/Core/1.Input/Paint/ChiselPaint.cs @@ -0,0 +1,134 @@ +using System; +using Unity.Collections; +using Unity.Mathematics; + +namespace Chisel.Core +{ + /// The vertex attribute paint is written to. What it means is up to the shader of the surface's material. + /// TexCoord0 and TexCoord1 are taken (texture mapping, lightmaps). + public enum ChiselPaintAttribute : byte + { + Color = 0, + TexCoord2 = 2, + TexCoord3 = 3, + TexCoord4 = 4, + TexCoord5 = 5, + TexCoord6 = 6, + TexCoord7 = 7 + } + + /// The components of a vertex attribute paint writes; the others keep their default + [Flags] + public enum ChiselPaintComponents : byte + { + None = 0, + X = 1, + Y = 2, + Z = 4, + W = 8, + XYZ = X | Y | Z, + XYZW = X | Y | Z | W + } + + /// + /// The paint of one surface in one vertex attribute: values on a grid in the surface's plane, in the tree's (model's) + /// space. Grid point (i, j) is at origin + i * cellU + j * cellV; a vertex takes the value at its place on the + /// grid, interpolated between the four points round it, in , and + /// in the others. The grid's cells are also how finely the surface is cut to show it. + /// + public struct ChiselPaintLayer : IEquatable + { + /// The node the surface's brush was created with, as for + public ulong entityID; + /// The surface, as the brush's surface array numbers it + public int surfaceIndex; + public ChiselPaintAttribute attribute; + public ChiselPaintComponents components; + public float4 defaultValue; + public float3 origin; + public float3 cellU; + public float3 cellV; + /// How many grid points there are along cellU and cellV + public int2 resolution; + /// Where the grid's values start in the values given with the layers, row by row along cellU + public int valueStart; + + public readonly int ValueCount => math.max(resolution.x, 0) * math.max(resolution.y, 0); + + public readonly bool Equals(ChiselPaintLayer other) + { + return entityID == other.entityID && surfaceIndex == other.surfaceIndex && + attribute == other.attribute && components == other.components && + math.all(defaultValue == other.defaultValue) && math.all(origin == other.origin) && + math.all(cellU == other.cellU) && math.all(cellV == other.cellV) && + math.all(resolution == other.resolution) && valueStart == other.valueStart; + } + + public override readonly bool Equals(object obj) => obj is ChiselPaintLayer other && Equals(other); + public override readonly int GetHashCode() => (int)math.hash(new uint4((uint)entityID, (uint)surfaceIndex, (uint)attribute, math.hash(origin))); + + /// The layer's value at a point, in all four components (its grid projected along the plane's normal) + public readonly unsafe float4 Sample(NativeArray values, float3 point) + { + return Sample((float4*)Unity.Collections.LowLevel.Unsafe.NativeArrayUnsafeUtility.GetUnsafeReadOnlyPtr(values), point); + } + + internal readonly unsafe float4 Sample(float4* values, float3 point) + { + if (!GridPosition(point, out var cell, out var t)) + return defaultValue; + var row0 = valueStart + cell.y * resolution.x + cell.x; + var row1 = row0 + (resolution.y > 1 ? resolution.x : 0); + var next = resolution.x > 1 ? 1 : 0; + var bottom = math.lerp(values[row0], values[row0 + next], t.x); + var top = math.lerp(values[row1], values[row1 + next], t.x); + return math.lerp(bottom, top, t.y); + } + + // Where a point is on the grid: the cell (its lower grid point) and how far across it, clamped to the grid + readonly bool GridPosition(float3 point, out int2 cell, out float2 t) + { + cell = int2.zero; + t = float2.zero; + if (math.any(resolution < 1)) + return false; + // the point's coordinates along cellU and cellV, which needn't be at right angles + var offset = point - origin; + var uu = math.dot(cellU, cellU); + var uv = math.dot(cellU, cellV); + var vv = math.dot(cellV, cellV); + var determinant = uu * vv - uv * uv; + if (!(math.abs(determinant) > 1e-12f)) + return false; + var du = math.dot(offset, cellU); + var dv = math.dot(offset, cellV); + var grid = new float2((vv * du - uv * dv) / determinant, (uu * dv - uv * du) / determinant); + grid = math.clamp(grid, float2.zero, (float2)(resolution - 1)); + cell = math.min((int2)math.floor(grid), math.max(resolution - 2, 0)); + t = grid - cell; + return true; + } + + /// A value written in the layer's components over a default + public static float4 Apply(float4 value, float4 over, ChiselPaintComponents components) + { + return math.select(over, value, new bool4((components & ChiselPaintComponents.X) != 0, + (components & ChiselPaintComponents.Y) != 0, + (components & ChiselPaintComponents.Z) != 0, + (components & ChiselPaintComponents.W) != 0)); + } + } + + public static class CSGTreePaintExtensions + { + /// + /// Replaces all paint of the tree at once: the layers, and the values their grids take from (each layer's + /// ). Returns the entities whose paint changed; their brushes have to be + /// rebuilt to show it (the caller marks them dirty). + /// + public static bool SetPaint(this CSGTree tree, NativeArray layers, NativeArray values, NativeList changedEntities) + { + return ChiselPaintStore.SetPaint(tree, layers, values, changedEntities); + } + } +} diff --git a/Core/1.Input/Paint/ChiselPaint.cs.meta b/Core/1.Input/Paint/ChiselPaint.cs.meta new file mode 100644 index 00000000..446cb471 --- /dev/null +++ b/Core/1.Input/Paint/ChiselPaint.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: d8b12b16d5b1c9146bf8cfec31e43289 \ No newline at end of file diff --git a/Core/2.Processing/Cache/ChiselLookup.cs b/Core/2.Processing/Cache/ChiselLookup.cs index 9ebdfc42..95ee3121 100644 --- a/Core/2.Processing/Cache/ChiselLookup.cs +++ b/Core/2.Processing/Cache/ChiselLookup.cs @@ -39,6 +39,9 @@ public class Data public NativeList changedDecalBounds; // The surfaces connected or disconnected by hand (ChiselHotspotLinkStore) public NativeList hotspotLinks; + // The painted surfaces' layers, sorted by surface, and their grids' values (ChiselPaintStore) + public NativeList paintLayers; + public NativeList paintValues; public NativeParallelHashMap> brushRenderBufferLookup; @@ -80,6 +83,8 @@ internal void Initialize() decalBounds = new NativeList(16, Allocator.Persistent); decalVolumes = new NativeList(16, Allocator.Persistent); hotspotLinks = new NativeList(4, Allocator.Persistent); + paintLayers = new NativeList(4, Allocator.Persistent); + paintValues = new NativeList(16, Allocator.Persistent); hotspotLayoutHashes = new NativeList(4, Allocator.Persistent); collapseMoves = new NativeList(4, Allocator.Persistent); hotspotCutCache = new NativeParallelHashMap(16, Allocator.Persistent); @@ -233,6 +238,10 @@ internal void Dispose() if (changedDecalBounds.IsCreated) changedDecalBounds.Dispose(); if (hotspotLinks.IsCreated) hotspotLinks.Dispose(); hotspotLinks = default; + if (paintLayers.IsCreated) paintLayers.Dispose(); + paintLayers = default; + if (paintValues.IsCreated) paintValues.Dispose(); + paintValues = default; if (hotspotLayoutHashes.IsCreated) hotspotLayoutHashes.Dispose(); hotspotLayoutHashes = default; if (collapseMoves.IsCreated) collapseMoves.Dispose(); diff --git a/Core/2.Processing/Jobs/GenerateSurfaceTrianglesJob.cs b/Core/2.Processing/Jobs/GenerateSurfaceTrianglesJob.cs index b3a32cdd..c23c9015 100644 --- a/Core/2.Processing/Jobs/GenerateSurfaceTrianglesJob.cs +++ b/Core/2.Processing/Jobs/GenerateSurfaceTrianglesJob.cs @@ -33,6 +33,8 @@ struct GenerateSurfaceTrianglesJob : IJobParallelForDefer [NoAlias, ReadOnly] public NativeArray hotspotLayouts; // the materials layouts with variants are drawn with (HotspotLayoutData.variantStart) [NoAlias, ReadOnly] public NativeArray hotspotVariants; + // The tree's paint, by surface + [NoAlias, ReadOnly] public ChiselPaintLookup paint; // Write [NativeDisableParallelForRestriction] @@ -307,6 +309,15 @@ unsafe void FinishSurface(int surf, int brushNodeOrder, ulong entityID, ref Blob buf.hotspot = hotspotInfo; buf.hotspotSource = hotspotSource; + // paint is found by the brush's entity and the surface's index, when the surface's mesh is copied + var paintSurface = baseSurfaces[surf].descriptionIndex; + if (paint.TryGetRange(entityID, paintSurface, out var paintRange)) + { + buf.paintEntityID = entityID; + buf.paintSurface = paintSurface; + buf.surfaceHashValue = math.hash(new uint2(buf.surfaceHashValue, paintRange.hash)); + } + if (HasBackFace(flags, outputFlags)) FinishBackFace(surf, entityID, flags, parms, outputFlags, surfaceIndexList, ref uniqueVertexMapper, builder, surfaceBuffers, slots); } diff --git a/Core/2.Processing/Jobs/JobData/ChiselBrushRenderBuffer.cs b/Core/2.Processing/Jobs/JobData/ChiselBrushRenderBuffer.cs index d8b5df8a..1cf0cff7 100644 --- a/Core/2.Processing/Jobs/JobData/ChiselBrushRenderBuffer.cs +++ b/Core/2.Processing/Jobs/JobData/ChiselBrushRenderBuffer.cs @@ -19,6 +19,10 @@ internal struct ChiselSurfaceRenderBuffer public int decalStack; // a transparent decal's piece that fades (DecalVolume.Fades): its mesh is its decal's own public bool decalFades; + // a painted surface: its brush's entity and its surface index, which its paint is found by (ChiselPaintLookup); + // 0 for a surface without paint + public ulong paintEntityID; + public int paintSurface; public SurfaceDestinationFlags destinationFlags; public SurfaceDestinationParameters destinationParameters; // How many lightmap texels the surface gets (SurfaceOutputFlags.NoLightmap, SingleLightmapTexel) @@ -198,6 +202,8 @@ public void Construct(BlobBuilder builder, this.decalEntityID = 0; this.decalStack = 0; this.decalFades = false; + this.paintEntityID = 0; + this.paintSurface = 0; this.destinationFlags = destinationFlags; this.destinationParameters = destinationParameters; this.outputFlags = outputFlags; @@ -224,6 +230,8 @@ public void Construct(BlobBuilder builder, this.decalEntityID = 0; this.decalStack = 0; this.decalFades = false; + this.paintEntityID = 0; + this.paintSurface = 0; this.destinationFlags = destinationFlags; this.destinationParameters = destinationParameters; diff --git a/Core/2.Processing/Managers/CSGManager.UpdateTreeMeshes.cs b/Core/2.Processing/Managers/CSGManager.UpdateTreeMeshes.cs index 699a5b6f..9979327a 100644 --- a/Core/2.Processing/Managers/CSGManager.UpdateTreeMeshes.cs +++ b/Core/2.Processing/Managers/CSGManager.UpdateTreeMeshes.cs @@ -290,6 +290,7 @@ internal struct TemporariesStruct public NativeArray changedHotspotLayouts; // and the surfaces connected or disconnected by hand (ChiselHotspotLinkStore) public NativeArray hotspotLinks; + public ChiselPaintLookup paint; } internal TemporariesStruct Temporaries; #endregion @@ -563,6 +564,7 @@ public void Initialize() Temporaries.hotspotTileSides = new NativeArray(hotspotTileSides.AsArray(), defaultAllocator); Temporaries.hotspotVariants = new NativeArray(hotspotVariants.AsArray(), defaultAllocator); Temporaries.hotspotLinks = new NativeArray(chiselLookupValues.hotspotLinks.AsArray(), defaultAllocator); + Temporaries.paint = ChiselPaintLookup.Create(chiselLookupValues.paintLayers, chiselLookupValues.paintValues, defaultAllocator); } Temporaries.subMeshDescriptions = new NativeList(defaultAllocator); @@ -1861,6 +1863,7 @@ public void RunMeshUpdateJobs() hotspotTiles = Temporaries.hotspotTiles, hotspotLayouts = Temporaries.hotspotLayouts, hotspotVariants = Temporaries.hotspotVariants, + paint = Temporaries.paint, // Write brushRenderBufferCache = chiselLookupValues.brushRenderBufferCache @@ -2396,7 +2399,8 @@ public void RunMeshCopyJobs() { subMeshSurfaces = Temporaries.subMeshSurfaces, // PrepareSubSectionsJob -> meshQueries / brushRenderData (FindBrushRenderBuffersJob) subMeshDescriptions = Temporaries.subMeshDescriptions, // SortSurfacesParallelJob -> meshQueries / subMeshSurfaces (PrepareSubSectionsJob) - lightmapUVSettings = lightmapUVSettings + lightmapUVSettings = lightmapUVSettings, + paint = Temporaries.paint }; // the render meshes made ready, then every surface of them copied at once @@ -2697,6 +2701,8 @@ public JobHandle PreMeshUpdateDispose() lastJobHandle.AddDependency(Temporaries.hotspotTiles .SafeDispose(JobHandles[JobHandleType.decalVolumesJobHandle].readWriteBarrier)); lastJobHandle.AddDependency(Temporaries.hotspotLayouts .SafeDispose(JobHandles[JobHandleType.decalVolumesJobHandle].readWriteBarrier)); lastJobHandle.AddDependency(Temporaries.hotspotLinks .SafeDispose(JobHandles[JobHandleType.decalVolumesJobHandle].readWriteBarrier)); + lastJobHandle.AddDependency(Temporaries.paint.Dispose(JobHandle.CombineDependencies(JobHandles[JobHandleType.brushRenderBufferCacheJobHandle].readWriteBarrier, + JobHandles[JobHandleType.vertexBufferContents_meshesJobHandle].readWriteBarrier))); lastJobHandle.AddDependency(Temporaries.hotspotTileSides .SafeDispose(JobHandles[JobHandleType.decalVolumesJobHandle].readWriteBarrier)); lastJobHandle.AddDependency(Temporaries.hotspotVariants .SafeDispose(JobHandles[JobHandleType.decalVolumesJobHandle].readWriteBarrier)); lastJobHandle.AddDependency(Temporaries.changedHotspotLayouts .SafeDispose(JobHandles[JobHandleType.decalVolumesJobHandle].readWriteBarrier)); diff --git a/Core/2.Processing/Paint.meta b/Core/2.Processing/Paint.meta new file mode 100644 index 00000000..29e4f274 --- /dev/null +++ b/Core/2.Processing/Paint.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0ac4546c6d72d08448ff86c988976d39 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Core/2.Processing/Paint/ChiselPaintStore.cs b/Core/2.Processing/Paint/ChiselPaintStore.cs new file mode 100644 index 00000000..2630ad69 --- /dev/null +++ b/Core/2.Processing/Paint/ChiselPaintStore.cs @@ -0,0 +1,176 @@ +using System; +using System.Collections.Generic; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Jobs; +using Unity.Mathematics; + +namespace Chisel.Core +{ + // The surface a paint layer belongs to + internal struct ChiselPaintKey : IEquatable + { + public ulong entityID; + public int surfaceIndex; + + public readonly bool Equals(ChiselPaintKey other) => entityID == other.entityID && surfaceIndex == other.surfaceIndex; + public override readonly int GetHashCode() => (int)math.hash(new uint3((uint)entityID, (uint)(entityID >> 32), (uint)surfaceIndex)); + } + + // A surface's layers in the update's layer array, and a hash of them and their values + internal struct ChiselPaintRange + { + public int start; + public int count; + public uint hash; + } + + // The tree's paint as an update reads it: each painted surface's layers, found by its key + internal struct ChiselPaintLookup + { + [ReadOnly] public NativeParallelHashMap ranges; + [ReadOnly] public NativeArray layers; + [ReadOnly] public NativeArray values; + + public readonly bool IsEmpty => !ranges.IsCreated || ranges.IsEmpty; + + public readonly bool TryGetRange(ulong entityID, int surfaceIndex, out ChiselPaintRange range) + { + range = default; + return entityID != 0 && !IsEmpty && ranges.TryGetValue(new ChiselPaintKey { entityID = entityID, surfaceIndex = surfaceIndex }, out range); + } + + public static ChiselPaintLookup Create(NativeList layers, NativeList values, Allocator allocator) + { + var count = layers.IsCreated ? layers.Length : 0; + var lookup = new ChiselPaintLookup + { + ranges = new NativeParallelHashMap(math.max(1, count), allocator), + layers = new NativeArray(math.max(1, count), allocator), + values = new NativeArray(math.max(1, values.IsCreated ? values.Length : 0), allocator) + }; + if (count == 0) + return lookup; + NativeArray.Copy(layers.AsArray(), lookup.layers, count); + NativeArray.Copy(values.AsArray(), lookup.values, values.Length); + // the layers are sorted by surface (ChiselPaintStore.SetPaint) + for (int start = 0; start < count;) + { + var key = new ChiselPaintKey { entityID = layers[start].entityID, surfaceIndex = layers[start].surfaceIndex }; + var end = start + 1; + while (end < count && layers[end].entityID == key.entityID && layers[end].surfaceIndex == key.surfaceIndex) + end++; + var hash = 0u; + for (int l = start; l < end; l++) + hash = math.hash(new uint2(hash, ChiselPaintStore.HashOf(layers[l], values))); + lookup.ranges.TryAdd(key, new ChiselPaintRange { start = start, count = end - start, hash = hash }); + start = end; + } + return lookup; + } + + public JobHandle Dispose(JobHandle dependency) + { + var handle = dependency; + if (ranges.IsCreated) handle = JobHandle.CombineDependencies(handle, ranges.Dispose(dependency)); + if (layers.IsCreated) handle = JobHandle.CombineDependencies(handle, layers.Dispose(dependency)); + if (values.IsCreated) handle = JobHandle.CombineDependencies(handle, values.Dispose(dependency)); + this = default; + return handle; + } + } + + internal static class ChiselPaintStore + { + struct BySurface : IComparer + { + public int Compare(ChiselPaintLayer x, ChiselPaintLayer y) + { + if (x.entityID != y.entityID) return x.entityID.CompareTo(y.entityID); + if (x.surfaceIndex != y.surfaceIndex) return x.surfaceIndex.CompareTo(y.surfaceIndex); + return ((int)x.attribute).CompareTo((int)y.attribute); + } + } + + // A layer and its values + public static unsafe uint HashOf(in ChiselPaintLayer layer, NativeList values) + { + var copy = layer; + copy.valueStart = 0; + var hash = math.hash(©, UnsafeUtility.SizeOf()); + var count = math.min(layer.ValueCount, values.Length - layer.valueStart); + if (count > 0) + hash = math.hash(new uint2(hash, math.hash((float4*)values.GetUnsafeReadOnlyPtr() + layer.valueStart, count * UnsafeUtility.SizeOf()))); + return hash; + } + + // Each entity's paint, hashed + static void HashByEntity(NativeList layers, NativeList values, NativeParallelHashMap hashes) + { + for (int l = 0; l < layers.Length; l++) + { + var entityID = layers[l].entityID; + hashes.TryGetValue(entityID, out var hash); + hashes[entityID] = math.hash(new uint2(hash, HashOf(layers[l], values))); + } + } + + public static bool SetPaint(CSGTree tree, NativeArray layers, NativeArray values, NativeList changedEntities) + { + if (changedEntities.IsCreated) + changedEntities.Clear(); + if (!tree.Valid) + return false; + var data = ChiselTreeLookup.Value[tree]; + + // the new paint, its layers in their surfaces' order and each with its own values + var count = layers.IsCreated ? layers.Length : 0; + NativeList sorted; + using var _sorted = sorted = new NativeList(math.max(1, count), Allocator.Temp); + NativeList packed; + using var _packed = packed = new NativeList(math.max(1, values.IsCreated ? values.Length : 0), Allocator.Temp); + for (int l = 0; l < count; l++) + { + var layer = layers[l]; + if (layer.ValueCount <= 0 || layer.valueStart < 0 || layer.valueStart + layer.ValueCount > values.Length) + continue; + sorted.Add(layer); + } + sorted.Sort(new BySurface()); + for (int l = 0; l < sorted.Length; l++) + { + var layer = sorted[l]; + var start = packed.Length; + for (int v = 0; v < layer.ValueCount; v++) + packed.Add(values[layer.valueStart + v]); + layer.valueStart = start; + sorted[l] = layer; + } + + // which entities' paint changed: theirs is new, gone, or other + using var before = new NativeParallelHashMap(math.max(1, data.paintLayers.Length), Allocator.Temp); + using var after = new NativeParallelHashMap(math.max(1, sorted.Length), Allocator.Temp); + HashByEntity(data.paintLayers, data.paintValues, before); + HashByEntity(sorted, packed, after); + foreach (var entry in after) + { + if (!before.TryGetValue(entry.Key, out var hash) || hash != entry.Value) + changedEntities.Add(entry.Key); + } + foreach (var entry in before) + { + if (!after.ContainsKey(entry.Key)) + changedEntities.Add(entry.Key); + } + if (changedEntities.Length == 0) + return false; + + data.lastJobHandle.Complete(); + data.paintLayers.Clear(); + data.paintLayers.AddRange(sorted.AsArray()); + data.paintValues.Clear(); + data.paintValues.AddRange(packed.AsArray()); + return true; + } + } +} diff --git a/Core/2.Processing/Paint/ChiselPaintStore.cs.meta b/Core/2.Processing/Paint/ChiselPaintStore.cs.meta new file mode 100644 index 00000000..3201fb30 --- /dev/null +++ b/Core/2.Processing/Paint/ChiselPaintStore.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 545936201c55cb54cb80de9c9199fa4e \ No newline at end of file diff --git a/Core/3.Output/OutputMeshes/ChiselOutputRenderable.cs b/Core/3.Output/OutputMeshes/ChiselOutputRenderable.cs index ef60fd75..698a9d82 100644 --- a/Core/3.Output/OutputMeshes/ChiselOutputRenderable.cs +++ b/Core/3.Output/OutputMeshes/ChiselOutputRenderable.cs @@ -129,12 +129,44 @@ public static unsafe void Prepare([NoAlias, ReadOnly] NativeArray(); + for (int slot = 0; slot < ChiselPaintOutput.kSlotCount; slot++) + paintDefaults.Add(float4.zero); + for (int d = startIndex; d < endIndex; d++) + { + var description = subMeshSource.subMeshDescriptions[d]; + var surfaceArray = subMeshSource.subMeshSurfaces[description.meshQueryIndex]; + for (int s = description.surfacesOffset, last = s + description.surfacesCount; s < last; s++) + { + ref var surface = ref surfaceArray[s].brushRenderBuffer.Value.surfaces[surfaceArray[s].surfaceIndex]; + if (!subMeshSource.paint.TryGetRange(surface.paintEntityID, surface.paintSurface, out var range)) + continue; + for (int l = range.start; l < range.start + range.count; l++) + { + var slot = ChiselPaintOutput.SlotOf(subMeshSource.paint.layers[l].attribute); + if ((paintSlots & (1 << slot)) == 0) + paintDefaults[slot] = subMeshSource.paint.layers[l].defaultValue; + paintSlots |= 1 << slot; + } + } + } + var paintStride = math.countbits(paintSlots); + if (paintSlots != 0) + { + var painted = ChiselPaintOutput.Descriptors(descriptors, paintSlots); + meshData.SetVertexBufferParams(totalVertexCount, painted); + painted.Dispose(); + } else + meshData.SetVertexBufferParams(totalVertexCount, descriptors); meshData.SetIndexBufferParams(totalIndexCount, IndexFormat.UInt32); meshData.subMeshCount = numberOfSubMeshes; var indices = meshData.GetIndexData(); var vertices = meshData.GetVertexData(stream: 0); + var paintPointer = paintSlots != 0 ? (float4*)NativeArrayUnsafeUtility.GetUnsafePtr(meshData.GetVertexData(stream: 1)) : null; var indexBufferLength = indices.Length; var vertexBufferLength = vertices.Length; // the ranges write through these, so no copy asks the mesh for its buffers again @@ -219,8 +251,16 @@ public static unsafe void Prepare([NoAlias, ReadOnly] NativeArray 0 ? (ChiselPaintLayer*)subMeshSource.paint.layers.GetUnsafeReadOnlyPtr() + paintRange.start : null, + paintLayerCount = paintRange.count, + paintValues = paintRange.count > 0 ? (float4*)subMeshSource.paint.values.GetUnsafeReadOnlyPtr() : null, meshIndex = meshIndex, indices = indexPointer, vertices = vertexPointer, @@ -285,6 +325,8 @@ public static unsafe void CopyRange(RenderCopyRange range) for (int v = 0; v < sourceVertices.Length; v++) { var vertex = sourceVertices[v]; + if (range.paint != null) + ChiselPaintOutput.Write(range, vertex.position, range.paint + (range.baseVertex + v) * range.paintStride); vertex.uv1 = range.placement.Place(vertex.uv1, range.chartRect) * range.lightmapScale; vertices[range.baseVertex + v] = vertex; } @@ -303,6 +345,63 @@ internal unsafe struct RenderCopyRange public LightmapChartPlacement placement; public float4 chartRect; public float lightmapScale; + // The mesh's paint stream (null without one): the slots it has, a float4 each, their defaults, and the surface's layers + [NativeDisableUnsafePtrRestriction] public float4* paint; + public int paintSlots; + public int paintStride; + public FixedList128Bytes paintDefaults; + [NativeDisableUnsafePtrRestriction] public ChiselPaintLayer* paintLayers; + public int paintLayerCount; + [NativeDisableUnsafePtrRestriction] public float4* paintValues; + } + + // How paint goes into a render mesh: a float4 in a second vertex stream for every vertex attribute painted on its + // surfaces (a slot), in the order of Unity's attributes + internal static unsafe class ChiselPaintOutput + { + public const int kSlotCount = 7; + + public static int SlotOf(ChiselPaintAttribute attribute) => attribute == ChiselPaintAttribute.Color ? 0 : (int)attribute - 1; + + static VertexAttribute AttributeOf(int slot) => slot == 0 ? VertexAttribute.Color : VertexAttribute.TexCoord0 + (slot + 1); + + // The mesh's attributes with the painted ones: Color after Tangent, the texture coordinates at the end + public static NativeArray Descriptors(NativeArray descriptors, int slots) + { + var result = new NativeList(descriptors.Length + kSlotCount, Allocator.Temp); + for (int i = 0; i < descriptors.Length; i++) + { + result.Add(descriptors[i]); + if (descriptors[i].attribute == VertexAttribute.Tangent && (slots & 1) != 0) + result.Add(new VertexAttributeDescriptor(VertexAttribute.Color, VertexAttributeFormat.Float32, 4, stream: 1)); + } + for (int slot = 1; slot < kSlotCount; slot++) + { + if ((slots & (1 << slot)) != 0) + result.Add(new VertexAttributeDescriptor(AttributeOf(slot), VertexAttributeFormat.Float32, 4, stream: 1)); + } + var array = new NativeArray(result.AsArray(), Allocator.Temp); + result.Dispose(); + return array; + } + + // A vertex's paint: each slot's default, with every layer of the surface in it written over that + public static void Write(in RenderCopyRange range, float3 position, float4* destination) + { + for (int slot = 0, written = 0; slot < kSlotCount; slot++) + { + if ((range.paintSlots & (1 << slot)) == 0) + continue; + var value = range.paintDefaults[slot]; + for (int l = 0; l < range.paintLayerCount; l++) + { + var layer = range.paintLayers[l]; + if (SlotOf(layer.attribute) == slot) + value = ChiselPaintLayer.Apply(layer.Sample(range.paintValues, position), value, layer.components); + } + destination[written++] = value; + } + } } diff --git a/Core/3.Output/OutputMeshes/IChiselOutputMeshCopier.cs b/Core/3.Output/OutputMeshes/IChiselOutputMeshCopier.cs index 3f68370f..3ef7f892 100644 --- a/Core/3.Output/OutputMeshes/IChiselOutputMeshCopier.cs +++ b/Core/3.Output/OutputMeshes/IChiselOutputMeshCopier.cs @@ -12,6 +12,8 @@ struct SubMeshSource [NoAlias, ReadOnly] public NativeArray> subMeshSurfaces; // How the meshes' lightmap coordinates are laid out: the model's (LightmapUVLayout) public LightmapUVSettings lightmapUVSettings; + // The tree's paint: a render mesh with painted surfaces takes it in a second vertex stream (ChiselOutputRenderable) + public ChiselPaintLookup paint; } internal interface IChiselOutputMeshCopier diff --git a/Core/3.Output/OutputMeshes/OutputHotspotSections.cs b/Core/3.Output/OutputMeshes/OutputHotspotSections.cs index ef65fd24..577ed867 100644 --- a/Core/3.Output/OutputMeshes/OutputHotspotSections.cs +++ b/Core/3.Output/OutputMeshes/OutputHotspotSections.cs @@ -502,6 +502,8 @@ static BlobAssetReference Cut(BlobAssetReference Copy(BlobAssetReferenc to.decalEntityID = from.decalEntityID; to.decalStack = from.decalStack; to.decalFades = from.decalFades; + to.paintEntityID = from.paintEntityID; + to.paintSurface = from.paintSurface; to.destinationFlags = from.destinationFlags; to.destinationParameters = from.destinationParameters; to.outputFlags = from.outputFlags; diff --git a/Package Resources/Materials/URP/Paint.meta b/Package Resources/Materials/URP/Paint.meta new file mode 100644 index 00000000..78c7c84a --- /dev/null +++ b/Package Resources/Materials/URP/Paint.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2e49474b03343f04a95222d1f53321ac +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Package Resources/Materials/URP/Paint/ChiselPaintColor.shader b/Package Resources/Materials/URP/Paint/ChiselPaintColor.shader new file mode 100644 index 00000000..d639d3c4 --- /dev/null +++ b/Package Resources/Materials/URP/Paint/ChiselPaintColor.shader @@ -0,0 +1,68 @@ +// Shows a Color paint mode: the base color times the vertex color (Chisel's "Paint Color" test profile) +Shader "Chisel/Test/Paint Color" +{ + Properties + { + _BaseMap ("Base Map", 2D) = "white" {} + _BaseColor ("Base Color", Color) = (1, 1, 1, 1) + [HideInInspector] _ValueColor ("Value Color", Color) = (1, 0, 0, 1) + } + SubShader + { + Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" "Queue" = "Geometry" } + + Pass + { + Name "ForwardOnly" + Tags { "LightMode" = "UniversalForwardOnly" } + HLSLPROGRAM + #pragma vertex PaintVertex + #pragma fragment Fragment + #pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN + #pragma multi_compile_fragment _ _SHADOWS_SOFT + #include "ChiselPaintCommon.hlsl" + + half4 Fragment(PaintVaryings input) : SV_Target + { + half4 albedo = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv) * _BaseColor * input.color; + return half4(PaintLighting(albedo.rgb, input.positionWS, input.normalWS), 1); + } + ENDHLSL + } + + Pass + { + Name "DepthOnly" + Tags { "LightMode" = "DepthOnly" } + ColorMask R + HLSLPROGRAM + #pragma vertex DepthVertex + #pragma fragment DepthFragment + #include "ChiselPaintCommon.hlsl" + ENDHLSL + } + + Pass + { + Name "DepthNormalsOnly" + Tags { "LightMode" = "DepthNormalsOnly" } + HLSLPROGRAM + #pragma vertex DepthVertex + #pragma fragment DepthNormalsFragment + #include "ChiselPaintCommon.hlsl" + ENDHLSL + } + + Pass + { + Name "ShadowCaster" + Tags { "LightMode" = "ShadowCaster" } + ColorMask 0 + HLSLPROGRAM + #pragma vertex ShadowVertex + #pragma fragment DepthFragment + #include "ChiselPaintCommon.hlsl" + ENDHLSL + } + } +} diff --git a/Package Resources/Materials/URP/Paint/ChiselPaintColor.shader.meta b/Package Resources/Materials/URP/Paint/ChiselPaintColor.shader.meta new file mode 100644 index 00000000..83a757c7 --- /dev/null +++ b/Package Resources/Materials/URP/Paint/ChiselPaintColor.shader.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 7dfc31081b24da14a97e061b8ca75c94 +ShaderImporter: + externalObjects: {} + defaultTextures: [] + nonModifiableTextures: [] + userData: + assetBundleName: + assetBundleVariant: diff --git a/Package Resources/Materials/URP/Paint/ChiselPaintCommon.hlsl b/Package Resources/Materials/URP/Paint/ChiselPaintCommon.hlsl new file mode 100644 index 00000000..1109e48e --- /dev/null +++ b/Package Resources/Materials/URP/Paint/ChiselPaintCommon.hlsl @@ -0,0 +1,96 @@ +// What the Chisel paint test shaders share: simple lighting (the main light and the ambient), and the passes URP's +// deferred and forward renderers need from an opaque shader +#ifndef CHISEL_PAINT_COMMON_INCLUDED +#define CHISEL_PAINT_COMMON_INCLUDED + +#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" +#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl" + +CBUFFER_START(UnityPerMaterial) + float4 _BaseMap_ST; + half4 _BaseColor; + half4 _ValueColor; +CBUFFER_END + +TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap); + +struct PaintAttributes +{ + float4 positionOS : POSITION; + float3 normalOS : NORMAL; + float2 uv : TEXCOORD0; + float4 color : COLOR; + float4 paint : TEXCOORD2; +}; + +struct PaintVaryings +{ + float4 positionCS : SV_POSITION; + float3 positionWS : TEXCOORD0; + float3 normalWS : TEXCOORD1; + float2 uv : TEXCOORD2; + float4 color : TEXCOORD3; + float4 paint : TEXCOORD4; +}; + +PaintVaryings PaintVertex(PaintAttributes input) +{ + PaintVaryings output; + output.positionWS = TransformObjectToWorld(input.positionOS.xyz); + output.positionCS = TransformWorldToHClip(output.positionWS); + output.normalWS = TransformObjectToWorldNormal(input.normalOS); + output.uv = TRANSFORM_TEX(input.uv, _BaseMap); + output.color = input.color; + output.paint = input.paint; + return output; +} + +half3 PaintLighting(half3 albedo, float3 positionWS, float3 normalWS) +{ + float3 normal = normalize(normalWS); + Light light = GetMainLight(TransformWorldToShadowCoord(positionWS)); + half3 direct = light.color * saturate(dot(normal, light.direction)) * light.shadowAttenuation; + return albedo * (direct + SampleSH(normal)); +} + +// Depth, depth-normals and shadows: the position and normal only +struct DepthVaryings +{ + float4 positionCS : SV_POSITION; + float3 normalWS : TEXCOORD0; +}; + +DepthVaryings DepthVertex(PaintAttributes input) +{ + DepthVaryings output; + output.positionCS = TransformObjectToHClip(input.positionOS.xyz); + output.normalWS = TransformObjectToWorldNormal(input.normalOS); + return output; +} + +half4 DepthFragment(DepthVaryings input) : SV_Target { return 0; } + +half4 DepthNormalsFragment(DepthVaryings input) : SV_Target +{ + return half4(NormalizeNormalPerPixel(input.normalWS), 0.0); +} + +float3 _LightDirection; + +DepthVaryings ShadowVertex(PaintAttributes input) +{ + DepthVaryings output; + float3 positionWS = TransformObjectToWorld(input.positionOS.xyz); + float3 normalWS = TransformObjectToWorldNormal(input.normalOS); + float4 positionCS = TransformWorldToHClip(ApplyShadowBias(positionWS, normalWS, _LightDirection)); +#if UNITY_REVERSED_Z + positionCS.z = min(positionCS.z, UNITY_NEAR_CLIP_VALUE); +#else + positionCS.z = max(positionCS.z, UNITY_NEAR_CLIP_VALUE); +#endif + output.positionCS = positionCS; + output.normalWS = normalWS; + return output; +} + +#endif diff --git a/Package Resources/Materials/URP/Paint/ChiselPaintCommon.hlsl.meta b/Package Resources/Materials/URP/Paint/ChiselPaintCommon.hlsl.meta new file mode 100644 index 00000000..712f322f --- /dev/null +++ b/Package Resources/Materials/URP/Paint/ChiselPaintCommon.hlsl.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 25fc8f6cb268ad94491a52ca25745f31 +ShaderIncludeImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Package Resources/Materials/URP/Paint/ChiselPaintValue.shader b/Package Resources/Materials/URP/Paint/ChiselPaintValue.shader new file mode 100644 index 00000000..ce5d182c --- /dev/null +++ b/Package Resources/Materials/URP/Paint/ChiselPaintValue.shader @@ -0,0 +1,70 @@ +// Shows a Value paint mode: TEXCOORD2.x from 0 to 1, from the base color to the value color (Chisel's "Paint Value" +// test profile) +Shader "Chisel/Test/Paint Value" +{ + Properties + { + _BaseMap ("Base Map", 2D) = "white" {} + _BaseColor ("Base Color", Color) = (1, 1, 1, 1) + _ValueColor ("Value Color", Color) = (1, 0.3, 0, 1) + } + SubShader + { + Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" "Queue" = "Geometry" } + + Pass + { + Name "ForwardOnly" + Tags { "LightMode" = "UniversalForwardOnly" } + HLSLPROGRAM + #pragma vertex PaintVertex + #pragma fragment Fragment + #pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN + #pragma multi_compile_fragment _ _SHADOWS_SOFT + #include "ChiselPaintCommon.hlsl" + + half4 Fragment(PaintVaryings input) : SV_Target + { + half4 albedo = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv) * _BaseColor; + albedo.rgb = lerp(albedo.rgb, _ValueColor.rgb, saturate(input.paint.x)); + return half4(PaintLighting(albedo.rgb, input.positionWS, input.normalWS), 1); + } + ENDHLSL + } + + Pass + { + Name "DepthOnly" + Tags { "LightMode" = "DepthOnly" } + ColorMask R + HLSLPROGRAM + #pragma vertex DepthVertex + #pragma fragment DepthFragment + #include "ChiselPaintCommon.hlsl" + ENDHLSL + } + + Pass + { + Name "DepthNormalsOnly" + Tags { "LightMode" = "DepthNormalsOnly" } + HLSLPROGRAM + #pragma vertex DepthVertex + #pragma fragment DepthNormalsFragment + #include "ChiselPaintCommon.hlsl" + ENDHLSL + } + + Pass + { + Name "ShadowCaster" + Tags { "LightMode" = "ShadowCaster" } + ColorMask 0 + HLSLPROGRAM + #pragma vertex ShadowVertex + #pragma fragment DepthFragment + #include "ChiselPaintCommon.hlsl" + ENDHLSL + } + } +} diff --git a/Package Resources/Materials/URP/Paint/ChiselPaintValue.shader.meta b/Package Resources/Materials/URP/Paint/ChiselPaintValue.shader.meta new file mode 100644 index 00000000..6b8652e4 --- /dev/null +++ b/Package Resources/Materials/URP/Paint/ChiselPaintValue.shader.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: a553dbbcc51572e46a6e4778d37abca7 +ShaderImporter: + externalObjects: {} + defaultTextures: [] + nonModifiableTextures: [] + userData: + assetBundleName: + assetBundleVariant: diff --git a/Package Resources/Materials/URP/Paint/Paint Color Profile.asset b/Package Resources/Materials/URP/Paint/Paint Color Profile.asset new file mode 100644 index 00000000..7bdb657d --- /dev/null +++ b/Package Resources/Materials/URP/Paint/Paint Color Profile.asset @@ -0,0 +1,22 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 83ee62a399ce44d41abc4152089d341e, type: 3} + m_Name: Paint Color Profile + m_EditorClassIdentifier: com.chisel.unity.components::Chisel.Components.ChiselPaintProfile + shader: {fileID: 4800000, guid: 7dfc31081b24da14a97e061b8ca75c94, type: 3} + modes: + - name: Color + kind: 0 + attribute: 0 + components: 15 + defaultValue: {x: 1, y: 1, z: 1, w: 1} + range: {x: 0, y: 1} diff --git a/Package Resources/Materials/URP/Paint/Paint Color Profile.asset.meta b/Package Resources/Materials/URP/Paint/Paint Color Profile.asset.meta new file mode 100644 index 00000000..d49c258b --- /dev/null +++ b/Package Resources/Materials/URP/Paint/Paint Color Profile.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2681fddd406edda468d9eb4be57aca0f +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Package Resources/Materials/URP/Paint/Paint Color Test.mat b/Package Resources/Materials/URP/Paint/Paint Color Test.mat new file mode 100644 index 00000000..9911cf25 --- /dev/null +++ b/Package Resources/Materials/URP/Paint/Paint Color Test.mat @@ -0,0 +1,36 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Paint Color Test + m_Shader: {fileID: 4800000, guid: 7dfc31081b24da14a97e061b8ca75c94, type: 3} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: [] + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_LockedProperties: + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BaseMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Ints: [] + m_Floats: [] + m_Colors: + - _BaseColor: {r: 1, g: 1, b: 1, a: 1} + - _ValueColor: {r: 1, g: 0, b: 0, a: 1} + m_BuildTextureStacks: [] + m_AllowLocking: 1 diff --git a/Package Resources/Materials/URP/Paint/Paint Color Test.mat.meta b/Package Resources/Materials/URP/Paint/Paint Color Test.mat.meta new file mode 100644 index 00000000..5fda1aa6 --- /dev/null +++ b/Package Resources/Materials/URP/Paint/Paint Color Test.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9ad9332ac60226d43a844ce2a96b2f80 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Package Resources/Materials/URP/Paint/Paint Value Profile.asset b/Package Resources/Materials/URP/Paint/Paint Value Profile.asset new file mode 100644 index 00000000..bf78aac8 --- /dev/null +++ b/Package Resources/Materials/URP/Paint/Paint Value Profile.asset @@ -0,0 +1,22 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 83ee62a399ce44d41abc4152089d341e, type: 3} + m_Name: Paint Value Profile + m_EditorClassIdentifier: com.chisel.unity.components::Chisel.Components.ChiselPaintProfile + shader: {fileID: 4800000, guid: a553dbbcc51572e46a6e4778d37abca7, type: 3} + modes: + - name: Value + kind: 1 + attribute: 2 + components: 1 + defaultValue: {x: 0, y: 0, z: 0, w: 0} + range: {x: 0, y: 1} diff --git a/Package Resources/Materials/URP/Paint/Paint Value Profile.asset.meta b/Package Resources/Materials/URP/Paint/Paint Value Profile.asset.meta new file mode 100644 index 00000000..fd6415e9 --- /dev/null +++ b/Package Resources/Materials/URP/Paint/Paint Value Profile.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7faa6bf764da2834286d8d253fd71536 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Package Resources/Materials/URP/Paint/Paint Value Test.mat b/Package Resources/Materials/URP/Paint/Paint Value Test.mat new file mode 100644 index 00000000..189d87df --- /dev/null +++ b/Package Resources/Materials/URP/Paint/Paint Value Test.mat @@ -0,0 +1,36 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Paint Value Test + m_Shader: {fileID: 4800000, guid: a553dbbcc51572e46a6e4778d37abca7, type: 3} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: [] + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_LockedProperties: + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BaseMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Ints: [] + m_Floats: [] + m_Colors: + - _BaseColor: {r: 1, g: 1, b: 1, a: 1} + - _ValueColor: {r: 1, g: 0.29999998, b: 0, a: 1} + m_BuildTextureStacks: [] + m_AllowLocking: 1 diff --git a/Package Resources/Materials/URP/Paint/Paint Value Test.mat.meta b/Package Resources/Materials/URP/Paint/Paint Value Test.mat.meta new file mode 100644 index 00000000..9bfd76c1 --- /dev/null +++ b/Package Resources/Materials/URP/Paint/Paint Value Test.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: bccd75b0f7957884caa3b4fd03bc1c09 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: From a3b3108de202b9e10cd0c5e84f431c009348ea64 Mon Sep 17 00:00:00 2001 From: Sander van Rossen Date: Sun, 25 Oct 2026 12:37:29 +0100 Subject: [PATCH 02/27] Vertex painting: painted surfaces subdivided at their grid Each grid point of a surface's paint inside the surface becomes a vertex (PaintSubdivision), so the paint has vertices to land on: it is inserted as in an incremental Delaunay triangulation, splitting the triangle it is in or the two on the edge it is on, and the edges round it are flipped until the triangles round it are Delaunay. Points on or near the outline, or near a vertex, are left out, so the outline isn't cut and the surfaces it touches get no T-junctions. Only surfaces in the grid's plane or parallel to it are subdivided, at most 4096 points each. --- Components/Tests/ChiselPaintTests.cs | 36 +++ .../Jobs/GenerateSurfaceTrianglesJob.cs | 3 + Core/2.Processing/Paint/PaintSubdivision.cs | 228 ++++++++++++++++++ .../Paint/PaintSubdivision.cs.meta | 2 + Core/Tests/PaintSubdivisionTests.cs | 75 ++++++ Core/Tests/PaintSubdivisionTests.cs.meta | 2 + 6 files changed, 346 insertions(+) create mode 100644 Core/2.Processing/Paint/PaintSubdivision.cs create mode 100644 Core/2.Processing/Paint/PaintSubdivision.cs.meta create mode 100644 Core/Tests/PaintSubdivisionTests.cs create mode 100644 Core/Tests/PaintSubdivisionTests.cs.meta diff --git a/Components/Tests/ChiselPaintTests.cs b/Components/Tests/ChiselPaintTests.cs index a9645b1d..0560e6af 100644 --- a/Components/Tests/ChiselPaintTests.cs +++ b/Components/Tests/ChiselPaintTests.cs @@ -178,6 +178,42 @@ public void PaintInOneComponent_LeavesTheOthersAtTheDefault() AssertValuesAt(RenderMesh(model), VertexAttribute.TexCoord3, new Vector3(0, 1, 0), new Vector4(1, 0.25f, 1, 1), "changed"); } + // A grid of quarter-metre cells on the top: its 9 points inside the top are vertices, each of them splitting the + // top's triangles further; the 16 on its outline and corners aren't, so the sides keep their two triangles each + [Test] + public void APaintedSurface_IsSubdividedAtItsGridPointsInsideIt() + { + var model = Model(); + var box = Box(model, Vector3.zero); + var top = SurfaceFacing(box, new float3(0, 1, 0)); + var paint = ChiselSurfacePaint.Create(top, ChiselPaintAttribute.Color, ChiselPaintComponents.XYZW, Vector4.one, + new Vector3(0, 1, 0), Vector3.right * 0.25f, Vector3.forward * 0.25f, new Vector2Int(5, 5)); + paint[2, 2] = new Vector4(0, 0, 0, 1); + box.SetSurfacePaint(paint); + Update(); + + var mesh = RenderMesh(model); + var vertices = mesh.vertices; + var normals = mesh.normals; + var triangles = mesh.triangles; + int topVertices = 0, topTriangles = 0, sideTriangles = 0; + for (int v = 0; v < vertices.Length; v++) + { + if (normals[v].y > 0.99f) + topVertices++; + } + for (int t = 0; t < triangles.Length; t += 3) + { + var normal = Vector3.Cross(vertices[triangles[t + 1]] - vertices[triangles[t]], vertices[triangles[t + 2]] - vertices[triangles[t]]).normalized; + if (normal.y > 0.99f) topTriangles++; + else if (Mathf.Abs(normal.y) < 0.01f) sideTriangles++; + } + Assert.That(topVertices, Is.EqualTo(13), "the 4 corners and the 9 points inside"); + Assert.That(topTriangles, Is.EqualTo(20)); + Assert.That(sideTriangles, Is.EqualTo(8), "the sides aren't cut"); + AssertValuesAt(mesh, VertexAttribute.Color, new Vector3(0.5f, 1, 0.5f), new Vector4(0, 0, 0, 1), "the middle point's own value"); + } + // The grid moves with its generator [Test] public void Paint_MovesWithItsGenerator() diff --git a/Core/2.Processing/Jobs/GenerateSurfaceTrianglesJob.cs b/Core/2.Processing/Jobs/GenerateSurfaceTrianglesJob.cs index c23c9015..ef7833cb 100644 --- a/Core/2.Processing/Jobs/GenerateSurfaceTrianglesJob.cs +++ b/Core/2.Processing/Jobs/GenerateSurfaceTrianglesJob.cs @@ -61,6 +61,9 @@ unsafe void FinishSurface(int surf, int brushNodeOrder, ulong entityID, ref Blob NativeArray decalCandidateStart, SlotLayout slots) { var baseSurfaceCount = slots.baseSurfaceCount; + // a painted surface gets its paint's grid points as vertices, before anything is worked out from them + if (paint.TryGetRange(entityID, baseSurfaces[surf].descriptionIndex, out var subdivision)) + PaintSubdivision.Subdivide(surfaceIndexList, ref uniqueVertexMapper, finalFaceNormal, paint, subdivision); if (normalSmoothingAngle > 0.0001f) { var renderVertices = uniqueVertexMapper.surfaceRenderVertices; diff --git a/Core/2.Processing/Paint/PaintSubdivision.cs b/Core/2.Processing/Paint/PaintSubdivision.cs new file mode 100644 index 00000000..9462a678 --- /dev/null +++ b/Core/2.Processing/Paint/PaintSubdivision.cs @@ -0,0 +1,228 @@ +using Unity.Collections; +using Unity.Mathematics; + +namespace Chisel.Core +{ + // A painted surface is subdivided at its paint's grid points, so the paint has vertices to land on: each grid point + // inside the surface becomes a vertex, inserted as in an incremental Delaunay triangulation (it splits the triangle it + // is in, or the two on the edge it is on, and the edges round it are flipped until the triangles round it are + // Delaunay), so no sliver is left for the paint to streak across. The surface's outline is never cut, so the surfaces + // it touches don't get T-junctions. + internal static class PaintSubdivision + { + // Most points one surface gets; a grid of tiny cells on a big surface stops here + public const int kMaxPoints = 4096; + // How close to an edge or a corner a grid point counts as on it, in parts of the grid's cell + const float kSnap = 0.05f; + + public static void Subdivide(NativeList indices, ref UniqueVertexMapper mapper, float3 surfaceNormal, + in ChiselPaintLookup paint, in ChiselPaintRange range) + { + if (range.count <= 0 || indices.Length < 3) + return; + var snap = float.PositiveInfinity; + using var points = new NativeList(64, Allocator.Temp); + using var seen = new NativeParallelHashSet(64, Allocator.Temp); + for (int l = range.start; l < range.start + range.count; l++) + { + var layer = paint.layers[l]; + var normal = math.normalizesafe(math.cross(layer.cellU, layer.cellV)); + // only a surface in the grid's plane, or parallel to it + if (math.abs(math.dot(normal, surfaceNormal)) < 0.999f) + continue; + snap = math.min(snap, kSnap * math.min(math.length(layer.cellU), math.length(layer.cellV))); + for (int j = 0; j < layer.resolution.y && points.Length < kMaxPoints; j++) + { + for (int i = 0; i < layer.resolution.x && points.Length < kMaxPoints; i++) + { + var point = layer.origin + i * layer.cellU + j * layer.cellV; + if (seen.Add((int3)math.round(point * 1e4f))) + points.Add(point); + } + } + } + if (points.Length == 0) + return; + + var triangulation = new Triangulation(indices, ref mapper, surfaceNormal, snap); + for (int p = 0; p < points.Length; p++) + triangulation.Insert(ref mapper, points[p]); + triangulation.Dispose(); + } + + struct Triangulation + { + NativeList indices; + NativeList flat; // every vertex in the surface's plane + NativeList pending; // edges (a, b) of triangles (a, b, v) round a new vertex v, to be made Delaunay + double3 axisU, axisV; + double snap; + + public Triangulation(NativeList indices, ref UniqueVertexMapper mapper, float3 normal, double snap) + { + this.indices = indices; + this.snap = snap; + axisU = math.normalizesafe(math.cross((double3)normal, math.abs(normal.x) < 0.9f ? new double3(1, 0, 0) : new double3(0, 1, 0))); + axisV = math.normalizesafe(math.cross((double3)normal, axisU)); + flat = new NativeList(mapper.surfaceColliderVertices.Length, Allocator.Temp); + pending = new NativeList(16, Allocator.Temp); + for (int i = 0; i < mapper.surfaceColliderVertices.Length; i++) + flat.Add(Flat(mapper.surfaceColliderVertices[i])); + } + + public void Dispose() + { + flat.Dispose(); + pending.Dispose(); + } + + readonly double2 Flat(float3 position) => new double2(math.dot((double3)position, axisU), math.dot((double3)position, axisV)); + + public void Insert(ref UniqueVertexMapper mapper, float3 position) + { + var point = Flat(position); + for (int t = 0; t + 2 < indices.Length; t += 3) + { + int i0 = indices[t], i1 = indices[t + 1], i2 = indices[t + 2]; + var a = flat[i0]; var b = flat[i1]; var c = flat[i2]; + var twiceArea = Cross(a, b, c); + if (!(math.abs(twiceArea) > 1e-18)) + continue; + // the point's weights, and how far it is from each edge (the one opposite each corner) + var w = new double3(Cross(point, b, c), Cross(a, point, c), Cross(a, b, point)) / twiceArea; + var distance = w * math.abs(twiceArea) / new double3(math.distance(b, c), math.distance(c, a), math.distance(a, b)); + if (math.cmin(distance) < -snap) + continue; + if (math.distance(point, a) < snap || math.distance(point, b) < snap || math.distance(point, c) < snap) + return; // there is a vertex already + + if (math.cmin(distance) >= snap) + { + // inside: three triangles round it + var v = AddVertex(ref mapper, i0, i1, i2, w); + indices[t + 2] = v; + indices.Add(i1); indices.Add(i2); indices.Add(v); + indices.Add(i2); indices.Add(i0); indices.Add(v); + pending.Add(new int2(i0, i1)); + pending.Add(new int2(i1, i2)); + pending.Add(new int2(i2, i0)); + Legalize(v); + return; + } + + // on an edge (the nearest): it splits both of its triangles, or none on the surface's outline + var nearest = distance.x <= distance.y && distance.x <= distance.z ? 0 : distance.y <= distance.z ? 1 : 2; + int opposite = nearest == 0 ? i0 : nearest == 1 ? i1 : i2; + int e0 = nearest == 0 ? i1 : nearest == 1 ? i2 : i0; + int e1 = nearest == 0 ? i2 : nearest == 1 ? i0 : i1; + var other = FindEdge(e1, e0, out var otherOpposite); + if (other < 0) + return; + // exactly on the edge + w = math.max(w, 0); + w[nearest] = 0; + w /= math.csum(w); + var vertex = AddVertex(ref mapper, i0, i1, i2, w); + indices[t] = opposite; indices[t + 1] = e0; indices[t + 2] = vertex; + indices.Add(opposite); indices.Add(vertex); indices.Add(e1); + indices[other] = otherOpposite; indices[other + 1] = e1; indices[other + 2] = vertex; + indices.Add(otherOpposite); indices.Add(vertex); indices.Add(e0); + pending.Add(new int2(opposite, e0)); + pending.Add(new int2(e1, opposite)); + pending.Add(new int2(otherOpposite, e1)); + pending.Add(new int2(e0, otherOpposite)); + Legalize(vertex); + return; + } + } + + // Each edge (a, b) of a triangle (a, b, v) is flipped when the far corner d of the triangle across it is in + // the circle through a, b and v; the two edges that then face v are looked at in turn + void Legalize(int v) + { + while (pending.Length > 0) + { + var edge = pending[pending.Length - 1]; + pending.RemoveAt(pending.Length - 1); + int a = edge.x, b = edge.y; + var t = FindEdge(a, b, out var third); + if (t < 0 || third != v) + continue; + var u = FindEdge(b, a, out var d); + if (u < 0) + continue; // the outline + if (!InCircle(flat[a], flat[b], flat[v], flat[d]) || !Convex(flat[a], flat[d], flat[b], flat[v])) + continue; + // (a, b, v) and (b, a, d) become (v, a, d) and (d, b, v) + indices[t] = v; indices[t + 1] = a; indices[t + 2] = d; + indices[u] = d; indices[u + 1] = b; indices[u + 2] = v; + pending.Add(new int2(a, d)); + pending.Add(new int2(d, b)); + } + } + + // The triangle with the edge from a to b (in its winding), and its third corner; -1 when there is none + readonly int FindEdge(int a, int b, out int opposite) + { + for (int t = 0; t + 2 < indices.Length; t += 3) + { + for (int k = 0; k < 3; k++) + { + if (indices[t + k] == a && indices[t + (k + 1) % 3] == b) + { + opposite = indices[t + (k + 2) % 3]; + return t; + } + } + } + opposite = -1; + return -1; + } + + // A vertex where the weights put it in the triangle, every stream interpolated + int AddVertex(ref UniqueVertexMapper mapper, int i0, int i1, int i2, double3 w) + { + var f = (float3)w; + var r0 = mapper.surfaceRenderVertices[i0]; + var r1 = mapper.surfaceRenderVertices[i1]; + var r2 = mapper.surfaceRenderVertices[i2]; + var render = new RenderVertex + { + position = r0.position * f.x + r1.position * f.y + r2.position * f.z, + normal = math.normalizesafe(r0.normal * f.x + r1.normal * f.y + r2.normal * f.z), + tangent = r0.tangent * f.x + r1.tangent * f.y + r2.tangent * f.z, + uv0 = r0.uv0 * f.x + r1.uv0 * f.y + r2.uv0 * f.z, + uv1 = r0.uv1 * f.x + r1.uv1 * f.y + r2.uv1 * f.z + }; + var select = mapper.surfaceSelectVertices[i0]; + select.position = render.position; + var index = mapper.surfaceRenderVertices.Length; + mapper.surfaceRenderVertices.Add(render); + mapper.surfaceColliderVertices.Add(render.position); + mapper.surfaceSelectVertices.Add(select); + flat.Add(flat[i0] * w.x + flat[i1] * w.y + flat[i2] * w.z); + return index; + } + } + + static double Cross(double2 o, double2 p, double2 q) => (p.x - o.x) * (q.y - o.y) - (p.y - o.y) * (q.x - o.x); + + // Whether d is inside the circle through a, b and c, whichever way round they go (cocircular points are not) + static bool InCircle(double2 a, double2 b, double2 c, double2 d) + { + var ad = a - d; var bd = b - d; var cd = c - d; + var det = math.lengthsq(ad) * (bd.x * cd.y - cd.x * bd.y) - + math.lengthsq(bd) * (ad.x * cd.y - cd.x * ad.y) + + math.lengthsq(cd) * (ad.x * bd.y - bd.x * ad.y); + var scale = math.lengthsq(ad) * math.lengthsq(bd) + 1e-30; + return Cross(a, b, c) > 0 ? det > 1e-9 * scale : det < -1e-9 * scale; + } + + // Whether the quadrilateral a, d, b, c (in order) is convex, so its diagonal c-d lies inside it + static bool Convex(double2 a, double2 d, double2 b, double2 c) + { + var s0 = Cross(a, d, b); var s1 = Cross(d, b, c); var s2 = Cross(b, c, a); var s3 = Cross(c, a, d); + return (s0 > 0 && s1 > 0 && s2 > 0 && s3 > 0) || (s0 < 0 && s1 < 0 && s2 < 0 && s3 < 0); + } + } +} diff --git a/Core/2.Processing/Paint/PaintSubdivision.cs.meta b/Core/2.Processing/Paint/PaintSubdivision.cs.meta new file mode 100644 index 00000000..916d3c2b --- /dev/null +++ b/Core/2.Processing/Paint/PaintSubdivision.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 68ea7d5ea6ef55849b37801acc96d539 \ No newline at end of file diff --git a/Core/Tests/PaintSubdivisionTests.cs b/Core/Tests/PaintSubdivisionTests.cs new file mode 100644 index 00000000..7e2de7a3 --- /dev/null +++ b/Core/Tests/PaintSubdivisionTests.cs @@ -0,0 +1,75 @@ +using NUnit.Framework; +using Unity.Collections; +using Unity.Mathematics; + +namespace Chisel.Core.Tests +{ + // A painted surface subdivided at its grid points (PaintSubdivision): every point inside it a vertex, its outline + // never cut, and no sliver left by the flips + public sealed class PaintSubdivisionTests + { + // A square of side 2 in the xz plane at offset, two triangles, and a grid of 0.1 cells over it exactly + static void Subdivide(float3 offset, bool otherDiagonal, out int vertexCount, out int triangleCount, out float minAngle, out string worst) + { + var mapper = new UniqueVertexMapper + { + surfaceColliderVertices = new NativeList(Allocator.Temp), + surfaceSelectVertices = new NativeList(Allocator.Temp), + surfaceRenderVertices = new NativeList(Allocator.Temp) + }; + using var indices = new NativeList(Allocator.Temp); + var corners = new[] { new float3(0, 0, 0), new float3(0, 0, 2), new float3(2, 0, 2), new float3(2, 0, 0) }; + foreach (var corner in corners) + { + mapper.surfaceColliderVertices.Add(offset + corner); + mapper.surfaceSelectVertices.Add(new SelectVertex { position = offset + corner }); + mapper.surfaceRenderVertices.Add(new RenderVertex { position = offset + corner, normal = new float3(0, 1, 0) }); + } + if (otherDiagonal) { indices.Add(0); indices.Add(1); indices.Add(3); indices.Add(1); indices.Add(2); indices.Add(3); } + else { indices.Add(0); indices.Add(1); indices.Add(2); indices.Add(0); indices.Add(2); indices.Add(3); } + + NativeArray layers; + using var _layers = layers = new NativeArray(1, Allocator.Temp); + using var values = new NativeArray(21 * 21, Allocator.Temp); + layers[0] = new ChiselPaintLayer { origin = offset, cellU = new float3(0.1f, 0, 0), cellV = new float3(0, 0, 0.1f), resolution = new int2(21, 21) }; + var lookup = new ChiselPaintLookup { layers = layers, values = values }; + PaintSubdivision.Subdivide(indices, ref mapper, new float3(0, 1, 0), lookup, new ChiselPaintRange { start = 0, count = 1 }); + + vertexCount = mapper.surfaceColliderVertices.Length; + triangleCount = indices.Length / 3; + minAngle = 180; + worst = null; + for (int t = 0; t < indices.Length; t += 3) + { + // the outline isn't cut: its corners fan to the points nearest it, thin by nature + if (indices[t] < 4 || indices[t + 1] < 4 || indices[t + 2] < 4) + continue; + var a = mapper.surfaceColliderVertices[indices[t]]; + var b = mapper.surfaceColliderVertices[indices[t + 1]]; + var c = mapper.surfaceColliderVertices[indices[t + 2]]; + var angle = math.min(Angle(a, b, c), math.min(Angle(b, c, a), Angle(c, a, b))); + if (angle < minAngle) + { + minAngle = angle; + worst = $"{a - offset} {b - offset} {c - offset}"; + } + } + mapper.surfaceColliderVertices.Dispose(); + mapper.surfaceSelectVertices.Dispose(); + mapper.surfaceRenderVertices.Dispose(); + } + + static float Angle(float3 o, float3 p, float3 q) => math.degrees(math.acos(math.clamp(math.dot(math.normalize(p - o), math.normalize(q - o)), -1, 1))); + + // 19 x 19 points inside; the 80 on the outline aren't vertices. Away from the outline's corners the triangles are the + // grid's half cells, whichever way the square was cut and however far from the origin it is + [Test] + public void EveryGridPointInside_IsAVertex_AndNoTriangleIsASliver([Values(false, true)] bool otherDiagonal, [Values(0, 5000)] float offset) + { + Subdivide(new float3(offset, offset * 0.5f, -offset), otherDiagonal, out var vertices, out var triangles, out var minAngle, out var worst); + Assert.That(vertices, Is.EqualTo(4 + 19 * 19)); + Assert.That(triangles, Is.EqualTo(2 + 2 * 19 * 19)); + Assert.That(minAngle, Is.GreaterThan(30), "no sliver; the thinnest: " + worst); + } + } +} diff --git a/Core/Tests/PaintSubdivisionTests.cs.meta b/Core/Tests/PaintSubdivisionTests.cs.meta new file mode 100644 index 00000000..41b6757f --- /dev/null +++ b/Core/Tests/PaintSubdivisionTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c47d53bfb2073d1478eb870c47b3f8b6 \ No newline at end of file From fa066debac133ecb2bb42ef2406e5f5f1c06a402 Mon Sep 17 00:00:00 2001 From: Sander van Rossen Date: Sun, 25 Oct 2026 12:50:42 +0100 Subject: [PATCH 03/27] Vertex painting: the paint tool, fill, sample, channels, weight groups The Chisel Paint tool (Tools > Chisel > Paint) paints the surface under the mouse in a mode of the paint profile of its material's shader: a color, or a named value, with a round brush (radius, strength, hardness) that paints, adds, subtracts, smooths or erases. A surface painted for the first time gets a grid over it of the tool's cell size (ChiselPaintBrush). Only the hit surface is painted; a stroke is one undo. Fill Surface sets the whole surface, control-click samples the paint under the mouse, and the channel buttons copy, swap or invert components. Modes in one group (ChiselPaintMode.group) are weights that sum to one: painting one takes from the others in their proportion. --- .../Components/Paint/ChiselPaintBrush.cs | 285 ++++++++++++++++ .../Components/Paint/ChiselPaintBrush.cs.meta | 2 + .../Components/Paint/ChiselPaintProfile.cs | 17 + .../Components/Paint/ChiselSurfacePaint.cs | 26 ++ Components/Tests/ChiselPaintTests.cs | 116 +++++++ Editor/EditorTools/ChiselPaintTool.cs | 311 ++++++++++++++++++ Editor/EditorTools/ChiselPaintTool.cs.meta | 2 + 7 files changed, 759 insertions(+) create mode 100644 Components/Components/Paint/ChiselPaintBrush.cs create mode 100644 Components/Components/Paint/ChiselPaintBrush.cs.meta create mode 100644 Editor/EditorTools/ChiselPaintTool.cs create mode 100644 Editor/EditorTools/ChiselPaintTool.cs.meta diff --git a/Components/Components/Paint/ChiselPaintBrush.cs b/Components/Components/Paint/ChiselPaintBrush.cs new file mode 100644 index 00000000..15372839 --- /dev/null +++ b/Components/Components/Paint/ChiselPaintBrush.cs @@ -0,0 +1,285 @@ +using System.Collections.Generic; +using Chisel.Core; +using Unity.Mathematics; +using UnityEngine; + +namespace Chisel.Components +{ + /// What a dab of the paint brush does to the paint under it + public enum ChiselPaintOperation : byte + { + /// Towards the brush's color or value + Paint = 0, + /// Up by the brush's value (a value mode) + Add = 1, + /// Down by the brush's value (a value mode) + Subtract = 2, + /// Towards the average of the neighbouring grid points + Smooth = 3, + /// Back towards the mode's default + Erase = 4 + } + + /// + /// The paint brush's work on a generator's surface paint: the grid a surface gets when it is first painted, and what + /// a dab does to the grid points under it. A dab weighs each point by the brush's strength, fully within its hardness + /// (a part of its radius) and falling off smoothly to its edge. + /// + public static class ChiselPaintBrush + { + /// Most grid points a surface gets: the cells grow until it fits (PaintSubdivision takes as many) + public const int kMaxGridPoints = 4096; + + static readonly List s_Corners = new(); + + /// + /// The corners of a generator's surface, in the generator's own space, and its normal: of every brush polygon the + /// generator gives that surface. False when it has none. + /// + public static bool SurfaceOutline(ChiselGeneratorComponent generator, int surfaceIndex, List corners, out Vector3 normal) + { + corners.Clear(); + normal = Vector3.zero; + var model = generator ? generator.hierarchyItem?.Model : null; + if (!model) + return false; + var modelToGenerator = (model.transform.worldToLocalMatrix * generator.transform.localToWorldMatrix).inverse; + AddOutline(generator.TopTreeNode, surfaceIndex, modelToGenerator, corners, ref normal); + if (corners.Count < 3 || normal == Vector3.zero) + return false; + normal.Normalize(); + return true; + } + + static void AddOutline(CSGTreeNode node, int surfaceIndex, Matrix4x4 treeToGenerator, List corners, ref Vector3 normal) + { + if (!node.Valid) + return; + if (node.Type != CSGNodeType.Brush) + { + for (int i = 0; i < node.Count; i++) + AddOutline(node[i], surfaceIndex, treeToGenerator, corners, ref normal); + return; + } + var brush = (CSGTreeBrush)node; + var blob = BrushMeshManager.GetBrushMeshBlob(brush.BrushMesh); + if (!blob.IsCreated) + return; + ref var mesh = ref blob.Value; + var toGenerator = treeToGenerator * (Matrix4x4)brush.NodeToTreeSpaceMatrix; + for (int p = 0; p < mesh.polygons.Length; p++) + { + if (mesh.polygons[p].descriptionIndex != surfaceIndex) + continue; + if (normal == Vector3.zero && p < mesh.localPlanes.Length) + normal = toGenerator.inverse.transpose.MultiplyVector((Vector3)mesh.localPlanes[p].xyz); + var polygon = mesh.polygons[p]; + for (int e = polygon.firstEdge; e < polygon.firstEdge + polygon.edgeCount; e++) + corners.Add(toGenerator.MultiplyPoint3x4(mesh.localVertices[mesh.halfEdges[e].vertexIndex])); + } + } + + /// + /// A grid over a surface of a generator, in the surface's plane, its cells about cellSize (larger when it would + /// have more than ), every point at the default. Null when the surface has no outline. + /// + public static ChiselSurfacePaint CreateGrid(ChiselGeneratorComponent generator, int surfaceIndex, ChiselPaintAttribute attribute, + ChiselPaintComponents components, Vector4 defaultValue, float cellSize) + { + if (!SurfaceOutline(generator, surfaceIndex, s_Corners, out var normal)) + return null; + var axisU = Vector3.Cross(normal, Mathf.Abs(normal.y) < 0.99f ? Vector3.up : Vector3.forward).normalized; + var axisV = Vector3.Cross(normal, axisU).normalized; + var min = new Vector2(float.PositiveInfinity, float.PositiveInfinity); + var max = new Vector2(float.NegativeInfinity, float.NegativeInfinity); + foreach (var corner in s_Corners) + { + var flat = new Vector2(Vector3.Dot(corner, axisU), Vector3.Dot(corner, axisV)); + min = Vector2.Min(min, flat); + max = Vector2.Max(max, flat); + } + var size = max - min; + cellSize = Mathf.Max(cellSize, 1e-3f); + // grow the cells until the grid fits + var resolution = Vector2Int.one; + for (int tries = 0; tries < 64; tries++) + { + resolution = new Vector2Int(Mathf.CeilToInt(size.x / cellSize) + 1, Mathf.CeilToInt(size.y / cellSize) + 1); + if (resolution.x * resolution.y <= kMaxGridPoints) + break; + cellSize *= 1.25f; + } + var planePoint = s_Corners[0]; + var origin = planePoint + (min.x - Vector3.Dot(planePoint, axisU)) * axisU + (min.y - Vector3.Dot(planePoint, axisV)) * axisV; + return ChiselSurfacePaint.Create(surfaceIndex, attribute, components, defaultValue, origin, axisU * cellSize, axisV * cellSize, resolution); + } + + /// + /// The paint a mode writes on a surface: the surface's paint in the mode's attribute, with the mode's components + /// (at its default where they weren't painted yet), made with a grid of cellSize when it has none + /// + public static ChiselSurfacePaint PaintFor(ChiselGeneratorComponent generator, int surfaceIndex, in ChiselPaintMode mode, float cellSize) + { + var paint = generator.GetSurfacePaint(surfaceIndex, mode.attribute); + if (paint == null) + { + paint = CreateGrid(generator, surfaceIndex, mode.attribute, mode.components, mode.defaultValue, cellSize); + if (paint != null) + generator.SetSurfacePaint(paint); + return paint; + } + var added = mode.components & ~paint.components; + if (added != ChiselPaintComponents.None) + { + // another mode of the attribute painted it already: its components stay, the mode's start at its default + paint.defaultValue = ChiselPaintLayer.Apply(mode.defaultValue, paint.defaultValue, added); + for (int i = 0; i < paint.values.Length; i++) + paint.values[i] = ChiselPaintLayer.Apply(mode.defaultValue, paint.values[i], added); + paint.components |= added; + generator.PaintChanged(); + } + return paint; + } + + /// How much a dab counts at a distance from its middle: strength within hardness of the radius, then + /// falling off smoothly to nothing at the radius + public static float Weight(float distance, float radius, float strength, float hardness) + { + if (!(radius > 0) || distance >= radius) + return 0; + var f = distance / radius; + hardness = Mathf.Clamp01(hardness); + if (f <= hardness) + return strength; + var t = (f - hardness) / Mathf.Max(1 - hardness, 1e-5f); + return strength * (1 - t * t * (3 - 2 * t)); + } + + /// + /// One dab on the paint, round a point in the generator's space (measured in the surface's plane), in the mode's + /// components. target is the brush's color or value. False when no grid point was in reach. + /// + public static bool Dab(ChiselSurfacePaint paint, in ChiselPaintMode mode, ChiselPaintOperation operation, Vector4 target, + Vector3 center, float radius, float strength, float hardness, + ChiselPaintComponents group = ChiselPaintComponents.None) + { + if (paint == null || !paint.IsValid) + return false; + var normal = Vector3.Cross(paint.cellU, paint.cellV).normalized; + var before = operation == ChiselPaintOperation.Smooth ? (Vector4[])paint.values.Clone() : null; + var changed = false; + for (int j = 0; j < paint.resolution.y; j++) + { + for (int i = 0; i < paint.resolution.x; i++) + { + var offset = paint.origin + i * paint.cellU + j * paint.cellV - center; + offset -= normal * Vector3.Dot(offset, normal); + var weight = Weight(offset.magnitude, radius, strength, hardness); + if (weight <= 0) + continue; + var value = paint[i, j]; + Vector4 result; + switch (operation) + { + case ChiselPaintOperation.Add: result = value + target * weight; break; + case ChiselPaintOperation.Subtract: result = value - target * weight; break; + case ChiselPaintOperation.Erase: result = Vector4.Lerp(value, mode.defaultValue, weight); break; + case ChiselPaintOperation.Smooth: result = Vector4.Lerp(value, Average(before, paint.resolution, i, j), weight); break; + default: result = Vector4.Lerp(value, target, weight); break; + } + if (mode.kind == ChiselPaintKind.Value) + result = Vector4.Max(Vector4.Min(result, Vector4.one * mode.range.y), Vector4.one * mode.range.x); + result = ChiselPaintLayer.Apply(result, value, mode.components); + if (group != ChiselPaintComponents.None) + result = Normalize(result, mode.components, group); + paint[i, j] = result; + changed = true; + } + } + return changed; + } + + /// The whole surface set to the brush's color or value, in the mode's components + public static void Fill(ChiselSurfacePaint paint, in ChiselPaintMode mode, Vector4 target, ChiselPaintComponents group = ChiselPaintComponents.None) + { + if (paint == null || !paint.IsValid) + return; + for (int i = 0; i < paint.values.Length; i++) + { + Vector4 result = ChiselPaintLayer.Apply(target, paint.values[i], mode.components); + if (group != ChiselPaintComponents.None) + result = Normalize(result, mode.components, group); + paint.values[i] = result; + } + } + + /// One component's paint copied to another (0 x, 1 y, 2 z, 3 w) + public static void CopyComponent(ChiselSurfacePaint paint, int from, int to) + { + for (int i = 0; paint != null && i < paint.values.Length; i++) + paint.values[i][to] = paint.values[i][from]; + } + + /// Two components' paint swapped + public static void SwapComponents(ChiselSurfacePaint paint, int a, int b) + { + for (int i = 0; paint != null && i < paint.values.Length; i++) + { + var value = paint.values[i]; + (value[a], value[b]) = (value[b], value[a]); + paint.values[i] = value; + } + } + + /// A component's paint turned over within a range: its lowest becomes its highest + public static void InvertComponent(ChiselSurfacePaint paint, int component, Vector2 range) + { + for (int i = 0; paint != null && i < paint.values.Length; i++) + paint.values[i][component] = range.x + range.y - paint.values[i][component]; + } + + // The weights of a group made to sum to one again after the painted components changed: the others share what is + // left, in the proportion they had (equally when they were all 0) + static Vector4 Normalize(Vector4 value, ChiselPaintComponents painted, ChiselPaintComponents others) + { + float paintedSum = 0, othersSum = 0; + int otherCount = 0; + for (int c = 0; c < 4; c++) + { + var bit = (ChiselPaintComponents)(1 << c); + if ((painted & bit) != 0) paintedSum += value[c]; + else if ((others & bit) != 0) { othersSum += value[c]; otherCount++; } + } + if (otherCount == 0) + return value; + var left = Mathf.Max(0, 1 - paintedSum); + for (int c = 0; c < 4; c++) + { + var bit = (ChiselPaintComponents)(1 << c); + if ((painted & bit) != 0 || (others & bit) == 0) + continue; + value[c] = othersSum > 1e-6f ? value[c] * left / othersSum : left / otherCount; + } + return value; + } + + // The average of a grid point and its neighbours + static Vector4 Average(Vector4[] values, Vector2Int resolution, int i, int j) + { + var sum = Vector4.zero; + var count = 0; + for (int dj = -1; dj <= 1; dj++) + { + for (int di = -1; di <= 1; di++) + { + int x = i + di, y = j + dj; + if (x < 0 || y < 0 || x >= resolution.x || y >= resolution.y) + continue; + sum += values[y * resolution.x + x]; + count++; + } + } + return sum / count; + } + } +} diff --git a/Components/Components/Paint/ChiselPaintBrush.cs.meta b/Components/Components/Paint/ChiselPaintBrush.cs.meta new file mode 100644 index 00000000..90edea46 --- /dev/null +++ b/Components/Components/Paint/ChiselPaintBrush.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a14984064c563544b8de74d1a2bd441f \ No newline at end of file diff --git a/Components/Components/Paint/ChiselPaintProfile.cs b/Components/Components/Paint/ChiselPaintProfile.cs index 28241fa4..1dc67a08 100644 --- a/Components/Components/Paint/ChiselPaintProfile.cs +++ b/Components/Components/Paint/ChiselPaintProfile.cs @@ -26,6 +26,9 @@ public struct ChiselPaintMode public Vector4 defaultValue; /// A value mode's lowest and highest value public Vector2 range; + /// Modes of one group (above 0), in components of one attribute, are weights that sum to one: painting + /// one of them takes from the others (blending material layers) + public int group; public static ChiselPaintMode Color(string name, ChiselPaintAttribute attribute = ChiselPaintAttribute.Color, ChiselPaintComponents components = ChiselPaintComponents.XYZW) @@ -53,6 +56,20 @@ public sealed class ChiselPaintProfile : ScriptableObject public Shader shader; public List modes = new(); + /// The components of the other modes in a mode's group (in its attribute); None when it has no group + public ChiselPaintComponents GroupOf(int mode) + { + var result = ChiselPaintComponents.None; + if (mode < 0 || mode >= modes.Count || modes[mode].group <= 0) + return result; + for (int i = 0; i < modes.Count; i++) + { + if (i != mode && modes[i].group == modes[mode].group && modes[i].attribute == modes[mode].attribute) + result |= modes[i].components; + } + return result & ~modes[mode].components; + } + /// The first two modes that write one component of one attribute; false when there are none public bool FindOverlap(out int first, out int second) { diff --git a/Components/Components/Paint/ChiselSurfacePaint.cs b/Components/Components/Paint/ChiselSurfacePaint.cs index 59e72959..9f29d520 100644 --- a/Components/Components/Paint/ChiselSurfacePaint.cs +++ b/Components/Components/Paint/ChiselSurfacePaint.cs @@ -47,6 +47,32 @@ public static ChiselSurfacePaint Create(int surfaceIndex, ChiselPaintAttribute a public bool IsValid => resolution.x > 0 && resolution.y > 0 && values != null && values.Length == resolution.x * resolution.y; + /// The paint at a point (in the generator's space), between the four grid points round it; the grid is + /// projected along its plane's normal, and a point beyond it takes the nearest edge's paint + public Vector4 Sample(Vector3 point) + { + if (!IsValid) + return defaultValue; + var offset = point - origin; + var uu = Vector3.Dot(cellU, cellU); + var uv = Vector3.Dot(cellU, cellV); + var vv = Vector3.Dot(cellV, cellV); + var determinant = uu * vv - uv * uv; + if (!(Mathf.Abs(determinant) > 1e-12f)) + return defaultValue; + var du = Vector3.Dot(offset, cellU); + var dv = Vector3.Dot(offset, cellV); + var x = Mathf.Clamp((vv * du - uv * dv) / determinant, 0, resolution.x - 1); + var y = Mathf.Clamp((uu * dv - uv * du) / determinant, 0, resolution.y - 1); + var i = Mathf.Min((int)x, Mathf.Max(resolution.x - 2, 0)); + var j = Mathf.Min((int)y, Mathf.Max(resolution.y - 2, 0)); + var i1 = Mathf.Min(i + 1, resolution.x - 1); + var j1 = Mathf.Min(j + 1, resolution.y - 1); + var bottom = Vector4.Lerp(this[i, j], this[i1, j], x - i); + var top = Vector4.Lerp(this[i, j1], this[i1, j1], x - i); + return Vector4.Lerp(bottom, top, y - j); + } + public Vector4 this[int i, int j] { get => values[j * resolution.x + i]; diff --git a/Components/Tests/ChiselPaintTests.cs b/Components/Tests/ChiselPaintTests.cs index 0560e6af..ed23ee9e 100644 --- a/Components/Tests/ChiselPaintTests.cs +++ b/Components/Tests/ChiselPaintTests.cs @@ -214,6 +214,122 @@ public void APaintedSurface_IsSubdividedAtItsGridPointsInsideIt() AssertValuesAt(mesh, VertexAttribute.Color, new Vector3(0.5f, 1, 0.5f), new Vector4(0, 0, 0, 1), "the middle point's own value"); } + // Painted for the first time, a surface gets a grid over it of the brush's cell size; a dab in its middle paints + // there, and the mesh shows it + [Test] + public void TheBrush_GivesASurfaceAGrid_AndPaintsWhereItDabs() + { + var model = Model(); + var box = Box(model, Vector3.zero); + var top = SurfaceFacing(box, new float3(0, 1, 0)); + var mode = ChiselPaintMode.Color("Color"); + var paint = ChiselPaintBrush.PaintFor(box, top, mode, 0.25f); + Assert.That(paint, Is.Not.Null); + Assert.That(paint.resolution, Is.EqualTo(new Vector2Int(5, 5)), "a metre in quarters: 5 points along each side"); + Assert.That(paint.cellU.magnitude, Is.EqualTo(0.25f).Within(1e-5f)); + Assert.That(Mathf.Abs(Vector3.Dot(Vector3.Cross(paint.cellU, paint.cellV).normalized, Vector3.up)), Is.EqualTo(1).Within(1e-5f), "in the top's plane"); + Assert.That(box.GetSurfacePaint(top, ChiselPaintAttribute.Color), Is.SameAs(paint)); + + Assert.That(ChiselPaintBrush.Dab(paint, mode, ChiselPaintOperation.Paint, new Vector4(1, 0, 0, 1), new Vector3(0.5f, 1, 0.5f), 0.3f, 1, 1), Is.True); + box.PaintChanged(); + Update(); + var mesh = RenderMesh(model); + AssertValuesAt(mesh, VertexAttribute.Color, new Vector3(0.5f, 1, 0.5f), new Vector4(1, 0, 0, 1), "the middle"); + AssertValuesAt(mesh, VertexAttribute.Color, new Vector3(0.25f, 1, 0.25f), Vector4.one, "out of reach (0.35 away)"); + } + + // Two modes in different components of one attribute share its paint: painting the second keeps the first + [Test] + public void ModesOfOneAttribute_ShareItsPaint() + { + var model = Model(); + var box = Box(model, Vector3.zero); + var top = SurfaceFacing(box, new float3(0, 1, 0)); + var height = ChiselPaintMode.Value("Height", ChiselPaintAttribute.TexCoord2, ChiselPaintComponents.X, 0, 0, 1); + var wet = ChiselPaintMode.Value("Wetness", ChiselPaintAttribute.TexCoord2, ChiselPaintComponents.Y, 0, 0, 1); + var paint = ChiselPaintBrush.PaintFor(box, top, height, 0.25f); + ChiselPaintBrush.Dab(paint, height, ChiselPaintOperation.Add, Vector4.one * 0.5f, new Vector3(0.5f, 1, 0.5f), 2, 1, 1); + Assert.That(ChiselPaintBrush.PaintFor(box, top, wet, 0.25f), Is.SameAs(paint)); + ChiselPaintBrush.Dab(paint, wet, ChiselPaintOperation.Paint, Vector4.one * 0.75f, new Vector3(0.5f, 1, 0.5f), 2, 1, 1); + Assert.That(paint.components, Is.EqualTo(ChiselPaintComponents.X | ChiselPaintComponents.Y)); + Assert.That(paint[2, 2].x, Is.EqualTo(0.5f).Within(1e-5f), "the height stays"); + Assert.That(paint[2, 2].y, Is.EqualTo(0.75f).Within(1e-5f)); + + ChiselPaintBrush.Dab(paint, height, ChiselPaintOperation.Add, Vector4.one, new Vector3(0.5f, 1, 0.5f), 2, 1, 1); + Assert.That(paint[2, 2].x, Is.EqualTo(1).Within(1e-5f), "clamped to the mode's range"); + ChiselPaintBrush.Dab(paint, wet, ChiselPaintOperation.Erase, Vector4.zero, new Vector3(0.5f, 1, 0.5f), 2, 1, 1); + Assert.That(paint[2, 2], Is.EqualTo(new Vector4(1, 0, 0, 0)), "only the wetness erased"); + } + + // Smoothing evens out a spike + [Test] + public void Smoothing_EvensOutASpike() + { + var model = Model(); + var box = Box(model, Vector3.zero); + var top = SurfaceFacing(box, new float3(0, 1, 0)); + var mode = ChiselPaintMode.Value("Value", ChiselPaintAttribute.TexCoord2, ChiselPaintComponents.X); + var paint = ChiselPaintBrush.PaintFor(box, top, mode, 0.25f); + paint[2, 2] = new Vector4(1, 0, 0, 0); + ChiselPaintBrush.Dab(paint, mode, ChiselPaintOperation.Smooth, Vector4.zero, new Vector3(0.5f, 1, 0.5f), 2, 1, 1); + Assert.That(paint[2, 2].x, Is.EqualTo(1f / 9).Within(1e-5f), "the average of it and its 8 neighbours"); + Assert.That(paint[1, 1].x, Is.EqualTo(1f / 9).Within(1e-5f)); + } + + // Three layer weights in one group: painting one takes from the other two, in their proportion, so they always + // sum to one + [Test] + public void TheWeightsOfAGroup_AlwaysSumToOne() + { + var model = Model(); + var box = Box(model, Vector3.zero); + var top = SurfaceFacing(box, new float3(0, 1, 0)); + var profile = ScriptableObject.CreateInstance(); + created.Add(profile); + profile.modes.Add(ChiselPaintMode.Value("Rock", ChiselPaintAttribute.TexCoord4, ChiselPaintComponents.X, 1)); + profile.modes.Add(ChiselPaintMode.Value("Moss", ChiselPaintAttribute.TexCoord4, ChiselPaintComponents.Y, 0)); + profile.modes.Add(ChiselPaintMode.Value("Sand", ChiselPaintAttribute.TexCoord4, ChiselPaintComponents.Z, 0)); + for (int i = 0; i < 3; i++) + { + var weighted = profile.modes[i]; + weighted.group = 1; + profile.modes[i] = weighted; + } + Assert.That(profile.GroupOf(1), Is.EqualTo(ChiselPaintComponents.X | ChiselPaintComponents.Z)); + + var paint = ChiselPaintBrush.PaintFor(box, top, profile.modes[0], 0.25f); + ChiselPaintBrush.PaintFor(box, top, profile.modes[1], 0.25f); + ChiselPaintBrush.PaintFor(box, top, profile.modes[2], 0.25f); + // the weights are x, y and z; w is no mode's + Assert.That((Vector3)paint[2, 2], Is.EqualTo(new Vector3(1, 0, 0)), "all rock"); + + ChiselPaintBrush.Fill(paint, profile.modes[1], Vector4.one * 0.5f, profile.GroupOf(1)); + Assert.That(Vector3.Distance(paint[2, 2], new Vector3(0.5f, 0.5f, 0)), Is.LessThan(1e-5f), "half moss: the rock gives it"); + ChiselPaintBrush.Dab(paint, profile.modes[2], ChiselPaintOperation.Paint, Vector4.one * 0.5f, new Vector3(0.5f, 1, 0.5f), 0.1f, 1, 1, profile.GroupOf(2)); + Assert.That(Vector3.Distance(paint[2, 2], new Vector3(0.25f, 0.25f, 0.5f)), Is.LessThan(1e-5f), "half sand: rock and moss give it alike"); + } + + // Sampling takes the paint between grid points; the channel buttons copy, swap and invert components + [Test] + public void Sampling_AndTheChannelUtilities() + { + var model = Model(); + var box = Box(model, Vector3.zero); + var top = SurfaceFacing(box, new float3(0, 1, 0)); + var mode = ChiselPaintMode.Color("Color"); + var paint = ChiselPaintBrush.PaintFor(box, top, mode, 1); + paint.values = new[] { new Vector4(0, 0, 0, 1), new Vector4(1, 0, 0, 1), new Vector4(0, 1, 0, 1), new Vector4(1, 1, 0, 1) }; + var middle = paint.Sample(paint.origin + (paint.cellU + paint.cellV) * 0.5f + Vector3.up * 3); + Assert.That(Vector4.Distance(middle, new Vector4(0.5f, 0.5f, 0, 1)), Is.LessThan(1e-5f), "between the four, the grid projected along its normal"); + + ChiselPaintBrush.CopyComponent(paint, 0, 2); + Assert.That(paint.values[1], Is.EqualTo(new Vector4(1, 0, 1, 1))); + ChiselPaintBrush.SwapComponents(paint, 0, 1); + Assert.That(paint.values[1], Is.EqualTo(new Vector4(0, 1, 1, 1))); + ChiselPaintBrush.InvertComponent(paint, 3, new Vector2(0, 1)); + Assert.That(paint.values[1].w, Is.EqualTo(0)); + } + // The grid moves with its generator [Test] public void Paint_MovesWithItsGenerator() diff --git a/Editor/EditorTools/ChiselPaintTool.cs b/Editor/EditorTools/ChiselPaintTool.cs new file mode 100644 index 00000000..fa9a0571 --- /dev/null +++ b/Editor/EditorTools/ChiselPaintTool.cs @@ -0,0 +1,311 @@ +using System.Collections.Generic; +using Chisel.Components; +using Chisel.Core; +using UnityEditor; +using UnityEditor.EditorTools; +using UnityEngine; + +namespace Chisel.Editors +{ + /// The paint profiles of the project, by the shader they are for + static class ChiselPaintProfiles + { + static Dictionary s_ByShader; + + [InitializeOnLoadMethod] + static void Initialize() => EditorApplication.projectChanged += () => s_ByShader = null; + + public static ChiselPaintProfile Find(Shader shader) + { + if (shader == null) + return null; + if (s_ByShader == null) + { + s_ByShader = new Dictionary(); + foreach (var guid in AssetDatabase.FindAssets("t:" + nameof(ChiselPaintProfile))) + { + var profile = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(guid)); + if (profile != null && profile.shader != null) + s_ByShader.TryAdd(profile.shader, profile); + } + } + return s_ByShader.TryGetValue(shader, out var found) ? found : null; + } + } + + /// + /// Paints the surface under the mouse in a mode of the paint profile of its material's shader: a color, or a named + /// value, with a round brush. Only the surface that is hit is painted; a stroke is one undo. + /// + [EditorTool("Chisel Paint")] + sealed class ChiselPaintTool : EditorTool + { + static readonly GUIContent kModeContent = new("Mode", "What to paint: the modes of the paint profile of the surface's shader."); + static readonly GUIContent kOperationContent = new("Operation", "Paint: towards the color or value. Add / Subtract: a value up or down. Smooth: towards the neighbouring paint. Erase: back to the default."); + static readonly GUIContent kColorContent = new("Color"); + static readonly GUIContent kRadiusContent = new("Radius", "The brush's radius, in metres."); + static readonly GUIContent kStrengthContent = new("Strength", "How much a dab moves the paint towards what it paints."); + static readonly GUIContent kHardnessContent = new("Hardness", "How much of the radius is painted at full strength; beyond it the brush falls off smoothly."); + static readonly GUIContent kCellSizeContent = new("Cell Size", "How finely a surface painted for the first time is divided: its paint is a grid of this size, and the surface is cut at its points."); + static readonly GUIContent kFillContent = new("Fill Surface", "Sets the whole of the last surface under the mouse to the color or value."); + static readonly GUIContent kChannelsContent = new("Channels", "Works on the paint of the last surface under the mouse, in the mode's attribute."); + static readonly GUIContent kCopyContent = new("Copy", "Copies the first component's paint to the second."); + static readonly GUIContent kSwapContent = new("Swap", "Swaps the two components' paint."); + static readonly GUIContent kInvertContent = new("Invert", "Turns the first component's paint over within the mode's range."); + static readonly string[] kComponentNames = { "X (R)", "Y (G)", "Z (B)", "W (A)" }; + + static int s_Mode; + static ChiselPaintOperation s_Operation = ChiselPaintOperation.Paint; + static Color s_Color = Color.red; + static float s_Value = 1; + static float s_Radius = 0.5f; + static float s_Strength = 0.5f; + static float s_Hardness = 0.5f; + static float s_CellSize = 0.25f; + + static bool s_ShowChannels; + static int s_ChannelFrom, s_ChannelTo = 1; + // the surface last under the mouse: what Fill and the channel buttons work on + static Hit s_Last; + + static readonly List s_Intersections = new(); + static Rect s_Window = new(10, 30, 260, 200); + static int s_UndoGroup = -1; + + public override GUIContent toolbarIcon => new("Paint", "Chisel Paint: paint vertex data onto surfaces, in the modes of their shaders' paint profiles"); + + [MenuItem("Tools/Chisel/Paint")] + static void Activate() => ToolManager.SetActiveTool(); + + struct Hit + { + public bool valid; + public ChiselGeneratorComponent generator; + public int surfaceIndex; + public Vector3 point; + public Vector3 normal; + public ChiselPaintProfile profile; + public Shader shader; + } + + static Hit Pick(Vector2 mousePosition) + { + ChiselSelectionManager.GetOverlappingObjects(mousePosition, s_Intersections); + foreach (var intersection in s_Intersections) + { + if (intersection.treeNode is not ChiselGeneratorComponent generator || intersection.model == null || + !intersection.brushIntersection.brush.Valid) + continue; + var surfaceIndex = ChiselTrimDecals.SurfaceOf(intersection.brushIntersection.brush, intersection.brushIntersection.surfaceIndex); + var surfaces = generator.SurfaceDefinition?.surfaces; + if (surfaceIndex < 0 || surfaces == null || surfaceIndex >= surfaces.Length) + continue; + var shader = surfaces[surfaceIndex].RenderMaterial ? surfaces[surfaceIndex].RenderMaterial.shader : null; + return new Hit + { + valid = true, + generator = generator, + surfaceIndex = surfaceIndex, + point = intersection.worldPlaneIntersection, + normal = intersection.worldPlane.normal, + shader = shader, + profile = ChiselPaintProfiles.Find(shader) + }; + } + return default; + } + + public override void OnToolGUI(EditorWindow window) + { + if (window is not SceneView) + return; + var evt = Event.current; + var id = GUIUtility.GetControlID(FocusType.Passive); + var hit = Pick(evt.mousePosition); + if (hit.valid && hit.profile != null) + s_Last = hit; + + DrawPanel(hit); + if (hit.valid) + { + Handles.color = hit.profile != null ? new Color(1, 1, 1, 0.8f) : new Color(1, 0.3f, 0.3f, 0.8f); + Handles.DrawWireDisc(hit.point, hit.normal, s_Radius); + Handles.DrawWireDisc(hit.point, hit.normal, s_Radius * Mathf.Clamp01(s_Hardness)); + } + + switch (evt.GetTypeForControl(id)) + { + case EventType.Layout: + HandleUtility.AddDefaultControl(id); + break; + case EventType.MouseMove: + HandleUtility.Repaint(); + break; + case EventType.MouseDown: + if (evt.button != 0 || evt.alt || !hit.valid || hit.profile == null || s_Window.Contains(evt.mousePosition)) + break; + // control-click takes the paint under the mouse as the brush's color or value + if (evt.control) + { + Sample(hit); + evt.Use(); + break; + } + GUIUtility.hotControl = id; + Undo.IncrementCurrentGroup(); + Undo.SetCurrentGroupName("Chisel Paint"); + s_UndoGroup = Undo.GetCurrentGroup(); + Dab(hit); + evt.Use(); + break; + case EventType.MouseDrag: + if (GUIUtility.hotControl != id) + break; + if (hit.valid && hit.profile != null) + Dab(hit); + evt.Use(); + break; + case EventType.MouseUp: + if (GUIUtility.hotControl != id) + break; + GUIUtility.hotControl = 0; + if (s_UndoGroup >= 0) + Undo.CollapseUndoOperations(s_UndoGroup); + s_UndoGroup = -1; + evt.Use(); + break; + } + } + + static void Sample(Hit hit) + { + if (s_Mode < 0 || s_Mode >= hit.profile.modes.Count) + return; + var mode = hit.profile.modes[s_Mode]; + var paint = hit.generator.GetSurfacePaint(hit.surfaceIndex, mode.attribute); + var value = paint != null ? paint.Sample(hit.generator.transform.InverseTransformPoint(hit.point)) : mode.defaultValue; + if (mode.kind == ChiselPaintKind.Color) + s_Color = value; + else + { + // the mode's first component + for (int c = 0; c < 4; c++) + { + if (((int)mode.components & (1 << c)) != 0) + { + s_Value = value[c]; + break; + } + } + } + } + + // The paint of the last surface under the mouse in the mode's attribute, made when it has none + static bool LastPaint(out ChiselSurfacePaint paint, out ChiselPaintMode mode) + { + paint = null; + mode = default; + if (!s_Last.valid || !s_Last.generator || s_Last.profile == null || s_Mode < 0 || s_Mode >= s_Last.profile.modes.Count) + return false; + mode = s_Last.profile.modes[s_Mode]; + Undo.RecordObject(s_Last.generator, "Chisel Paint"); + paint = ChiselPaintBrush.PaintFor(s_Last.generator, s_Last.surfaceIndex, mode, s_CellSize); + return paint != null; + } + + static void Changed() + { + s_Last.generator.PaintChanged(); + EditorUtility.SetDirty(s_Last.generator); + } + + static void Dab(Hit hit) + { + if (s_Mode < 0 || s_Mode >= hit.profile.modes.Count) + return; + var mode = hit.profile.modes[s_Mode]; + Undo.RecordObject(hit.generator, "Chisel Paint"); + var paint = ChiselPaintBrush.PaintFor(hit.generator, hit.surfaceIndex, mode, s_CellSize); + if (paint == null) + return; + var target = mode.kind == ChiselPaintKind.Color ? (Vector4)s_Color : Vector4.one * s_Value; + var scale = hit.generator.transform.lossyScale; + var radius = s_Radius / Mathf.Max(Mathf.Abs(scale.x), Mathf.Max(Mathf.Abs(scale.y), Mathf.Abs(scale.z)), 1e-5f); + var center = hit.generator.transform.InverseTransformPoint(hit.point); + if (ChiselPaintBrush.Dab(paint, mode, s_Operation, target, center, radius, s_Strength, s_Hardness, hit.profile.GroupOf(s_Mode))) + { + hit.generator.PaintChanged(); + EditorUtility.SetDirty(hit.generator); + } + } + + static void DrawPanel(Hit hit) + { + Handles.BeginGUI(); + s_Window = GUILayout.Window(typeof(ChiselPaintTool).GetHashCode(), s_Window, id => + { + var profile = hit.profile; + if (!hit.valid) + EditorGUILayout.HelpBox("Hover over a surface to paint it.", MessageType.None); + else if (profile == null || profile.modes.Count == 0) + EditorGUILayout.HelpBox($"The shader {(hit.shader ? hit.shader.name : "(none)")} has no paint profile (Assets > Create > Chisel > Paint Profile).", MessageType.Info); + else + { + var names = new GUIContent[profile.modes.Count]; + for (int i = 0; i < names.Length; i++) + names[i] = new GUIContent(string.IsNullOrEmpty(profile.modes[i].name) ? $"Mode {i}" : profile.modes[i].name); + s_Mode = Mathf.Clamp(EditorGUILayout.Popup(kModeContent, s_Mode, names), 0, names.Length - 1); + var mode = profile.modes[s_Mode]; + if (mode.kind == ChiselPaintKind.Color) + s_Color = EditorGUILayout.ColorField(kColorContent, s_Color, true, true, false); + else + s_Value = EditorGUILayout.Slider(new GUIContent(names[s_Mode].text), s_Value, mode.range.x, mode.range.y); + } + s_Operation = (ChiselPaintOperation)EditorGUILayout.EnumPopup(kOperationContent, s_Operation); + s_Radius = Mathf.Max(0.01f, EditorGUILayout.FloatField(kRadiusContent, s_Radius)); + s_Strength = EditorGUILayout.Slider(kStrengthContent, s_Strength, 0, 1); + s_Hardness = EditorGUILayout.Slider(kHardnessContent, s_Hardness, 0, 1); + s_CellSize = Mathf.Max(0.01f, EditorGUILayout.FloatField(kCellSizeContent, s_CellSize)); + using (new EditorGUI.DisabledScope(!s_Last.valid)) + { + if (GUILayout.Button(kFillContent) && LastPaint(out var paint, out var mode)) + { + var target = mode.kind == ChiselPaintKind.Color ? (Vector4)s_Color : Vector4.one * s_Value; + ChiselPaintBrush.Fill(paint, mode, target, s_Last.profile.GroupOf(s_Mode)); + Changed(); + } + s_ShowChannels = EditorGUILayout.Foldout(s_ShowChannels, kChannelsContent, true); + if (s_ShowChannels) + { + using (new EditorGUILayout.HorizontalScope()) + { + s_ChannelFrom = EditorGUILayout.Popup(s_ChannelFrom, kComponentNames); + s_ChannelTo = EditorGUILayout.Popup(s_ChannelTo, kComponentNames); + } + using (new EditorGUILayout.HorizontalScope()) + { + if (GUILayout.Button(kCopyContent) && LastPaint(out var copyPaint, out _)) + { + ChiselPaintBrush.CopyComponent(copyPaint, s_ChannelFrom, s_ChannelTo); + Changed(); + } + if (GUILayout.Button(kSwapContent) && LastPaint(out var swapPaint, out _)) + { + ChiselPaintBrush.SwapComponents(swapPaint, s_ChannelFrom, s_ChannelTo); + Changed(); + } + if (GUILayout.Button(kInvertContent) && LastPaint(out var invertPaint, out var invertMode)) + { + ChiselPaintBrush.InvertComponent(invertPaint, s_ChannelFrom, invertMode.range); + Changed(); + } + } + } + } + EditorGUILayout.HelpBox("Control-click takes the paint under the mouse.", MessageType.None); + GUI.DragWindow(); + }, "Chisel Paint"); + Handles.EndGUI(); + } + } +} diff --git a/Editor/EditorTools/ChiselPaintTool.cs.meta b/Editor/EditorTools/ChiselPaintTool.cs.meta new file mode 100644 index 00000000..58a1737d --- /dev/null +++ b/Editor/EditorTools/ChiselPaintTool.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b3c00758d6aaaca4eaa8a1ee19c04765 \ No newline at end of file From 51dfda8096c3c9253419890c656ca50505a88e5e Mon Sep 17 00:00:00 2001 From: Sander van Rossen Date: Mon, 26 Oct 2026 08:21:46 +0100 Subject: [PATCH 04/27] Paint tool: pick only on mouse events, panel without a GUI window Picking renders, and the tool picked on every event, also inside the scene view's layout and repaint ("GUI Window tried to begin rendering while something else had not finished rendering"). It now picks on mouse move, down and drag only, and draws what it last picked. The panel was a GUI window, which needs the scene view's BeginWindows; it is a plain area now, and shows the modes of the surface last painted on, so they stay while the mouse is over the panel. --- Editor/EditorTools/ChiselPaintTool.cs | 44 +++++++++++++++++++-------- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/Editor/EditorTools/ChiselPaintTool.cs b/Editor/EditorTools/ChiselPaintTool.cs index fa9a0571..df173e7f 100644 --- a/Editor/EditorTools/ChiselPaintTool.cs +++ b/Editor/EditorTools/ChiselPaintTool.cs @@ -69,7 +69,10 @@ sealed class ChiselPaintTool : EditorTool static Hit s_Last; static readonly List s_Intersections = new(); - static Rect s_Window = new(10, 30, 260, 200); + // the surface under the mouse, as it was last picked + static Hit s_Hover; + // where the panel was drawn, so a click on it doesn't paint + static Rect s_Panel; static int s_UndoGroup = -1; public override GUIContent toolbarIcon => new("Paint", "Chisel Paint: paint vertex data onto surfaces, in the modes of their shaders' paint profiles"); @@ -121,9 +124,15 @@ public override void OnToolGUI(EditorWindow window) return; var evt = Event.current; var id = GUIUtility.GetControlID(FocusType.Passive); - var hit = Pick(evt.mousePosition); - if (hit.valid && hit.profile != null) - s_Last = hit; + // picking renders: only on the mouse's own events, never inside the scene view's layout or repaint + var type = evt.GetTypeForControl(id); + if (type == EventType.MouseMove || type == EventType.MouseDown || type == EventType.MouseDrag) + { + s_Hover = s_Panel.Contains(evt.mousePosition) ? default : Pick(evt.mousePosition); + if (s_Hover.valid && s_Hover.profile != null) + s_Last = s_Hover; + } + var hit = s_Hover; DrawPanel(hit); if (hit.valid) @@ -142,7 +151,7 @@ public override void OnToolGUI(EditorWindow window) HandleUtility.Repaint(); break; case EventType.MouseDown: - if (evt.button != 0 || evt.alt || !hit.valid || hit.profile == null || s_Window.Contains(evt.mousePosition)) + if (evt.button != 0 || evt.alt || !hit.valid || hit.profile == null || s_Panel.Contains(evt.mousePosition)) break; // control-click takes the paint under the mouse as the brush's color or value if (evt.control) @@ -241,14 +250,21 @@ static void Dab(Hit hit) static void DrawPanel(Hit hit) { + // a plain area, not a GUI window: a tool draws outside the scene view's BeginWindows / EndWindows Handles.BeginGUI(); - s_Window = GUILayout.Window(typeof(ChiselPaintTool).GetHashCode(), s_Window, id => + var area = new Rect(10, 10, 280, Mathf.Max(100, Screen.height)); + GUILayout.BeginArea(area); + var labelWidth = EditorGUIUtility.labelWidth; + EditorGUIUtility.labelWidth = 90; + var panel = EditorGUILayout.BeginVertical(EditorStyles.helpBox); { - var profile = hit.profile; - if (!hit.valid) - EditorGUILayout.HelpBox("Hover over a surface to paint it.", MessageType.None); - else if (profile == null || profile.modes.Count == 0) + GUILayout.Label("Chisel Paint", EditorStyles.boldLabel); + // the modes of the surface last painted on, so they stay while the mouse is over the panel + var profile = s_Last.valid ? s_Last.profile : null; + if (hit.valid && hit.profile == null) EditorGUILayout.HelpBox($"The shader {(hit.shader ? hit.shader.name : "(none)")} has no paint profile (Assets > Create > Chisel > Paint Profile).", MessageType.Info); + if (profile == null || profile.modes.Count == 0) + EditorGUILayout.HelpBox("Hover over a surface whose shader has a paint profile.", MessageType.None); else { var names = new GUIContent[profile.modes.Count]; @@ -303,8 +319,12 @@ static void DrawPanel(Hit hit) } } EditorGUILayout.HelpBox("Control-click takes the paint under the mouse.", MessageType.None); - GUI.DragWindow(); - }, "Chisel Paint"); + } + EditorGUILayout.EndVertical(); + EditorGUIUtility.labelWidth = labelWidth; + GUILayout.EndArea(); + if (Event.current.type == EventType.Repaint) + s_Panel = new Rect(area.x + panel.x, area.y + panel.y, panel.width, panel.height); Handles.EndGUI(); } } From de708350ff5604cef75af22f09cd1021a5557e9c Mon Sep 17 00:00:00 2001 From: Sander van Rossen Date: Mon, 26 Oct 2026 08:24:35 +0100 Subject: [PATCH 05/27] Paint tool: its settings in a scene view overlay The settings were drawn straight into the scene view, see-through and not like the other tool windows. They are a Chisel Paint overlay now, shown while the tool is active. --- Editor/EditorTools/ChiselPaintTool.cs | 38 ++++++++++++++------------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/Editor/EditorTools/ChiselPaintTool.cs b/Editor/EditorTools/ChiselPaintTool.cs index df173e7f..9a1b2614 100644 --- a/Editor/EditorTools/ChiselPaintTool.cs +++ b/Editor/EditorTools/ChiselPaintTool.cs @@ -71,8 +71,6 @@ sealed class ChiselPaintTool : EditorTool static readonly List s_Intersections = new(); // the surface under the mouse, as it was last picked static Hit s_Hover; - // where the panel was drawn, so a click on it doesn't paint - static Rect s_Panel; static int s_UndoGroup = -1; public override GUIContent toolbarIcon => new("Paint", "Chisel Paint: paint vertex data onto surfaces, in the modes of their shaders' paint profiles"); @@ -128,13 +126,11 @@ public override void OnToolGUI(EditorWindow window) var type = evt.GetTypeForControl(id); if (type == EventType.MouseMove || type == EventType.MouseDown || type == EventType.MouseDrag) { - s_Hover = s_Panel.Contains(evt.mousePosition) ? default : Pick(evt.mousePosition); + s_Hover = Pick(evt.mousePosition); if (s_Hover.valid && s_Hover.profile != null) s_Last = s_Hover; } var hit = s_Hover; - - DrawPanel(hit); if (hit.valid) { Handles.color = hit.profile != null ? new Color(1, 1, 1, 0.8f) : new Color(1, 0.3f, 0.3f, 0.8f); @@ -151,7 +147,7 @@ public override void OnToolGUI(EditorWindow window) HandleUtility.Repaint(); break; case EventType.MouseDown: - if (evt.button != 0 || evt.alt || !hit.valid || hit.profile == null || s_Panel.Contains(evt.mousePosition)) + if (evt.button != 0 || evt.alt || !hit.valid || hit.profile == null) break; // control-click takes the paint under the mouse as the brush's color or value if (evt.control) @@ -248,17 +244,13 @@ static void Dab(Hit hit) } } - static void DrawPanel(Hit hit) + // The tool's settings, in its overlay + internal static void PanelGUI() { - // a plain area, not a GUI window: a tool draws outside the scene view's BeginWindows / EndWindows - Handles.BeginGUI(); - var area = new Rect(10, 10, 280, Mathf.Max(100, Screen.height)); - GUILayout.BeginArea(area); + var hit = s_Hover; var labelWidth = EditorGUIUtility.labelWidth; EditorGUIUtility.labelWidth = 90; - var panel = EditorGUILayout.BeginVertical(EditorStyles.helpBox); { - GUILayout.Label("Chisel Paint", EditorStyles.boldLabel); // the modes of the surface last painted on, so they stay while the mouse is over the panel var profile = s_Last.valid ? s_Last.profile : null; if (hit.valid && hit.profile == null) @@ -320,12 +312,22 @@ static void DrawPanel(Hit hit) } EditorGUILayout.HelpBox("Control-click takes the paint under the mouse.", MessageType.None); } - EditorGUILayout.EndVertical(); EditorGUIUtility.labelWidth = labelWidth; - GUILayout.EndArea(); - if (Event.current.type == EventType.Repaint) - s_Panel = new Rect(area.x + panel.x, area.y + panel.y, panel.width, panel.height); - Handles.EndGUI(); + } + } + + /// The Chisel Paint tool's settings, while it is the active tool + [UnityEditor.Overlays.Overlay(typeof(SceneView), kOverlayTitle)] + sealed class ChiselPaintOverlay : UnityEditor.Overlays.IMGUIOverlay, UnityEditor.Overlays.ITransientOverlay + { + const string kOverlayTitle = "Chisel Paint"; + + public bool visible => ToolManager.activeToolType == typeof(ChiselPaintTool); + + public override void OnGUI() + { + EditorGUILayout.GetControlRect(false, 0, GUILayout.MinWidth(260)); + ChiselPaintTool.PanelGUI(); } } } From abd59174c5c3ab51d74e49dbcd02b25e2d972261 Mon Sep 17 00:00:00 2001 From: Sander van Rossen Date: Mon, 26 Oct 2026 08:32:25 +0100 Subject: [PATCH 06/27] Paint profiles: an inspector for their modes A mode shows what its kind uses: a color mode its default color, a value mode one default for its components, its range and its group; the components are toggles, named RGBA for the Color attribute. A profile says when it has no shader, and warns when two modes write one component of one attribute. Also: FindObjectsByType without the obsolete sort mode in the decal editor. --- .../Decals/ChiselDecalEditor.cs | 2 +- .../PropertyDrawers/ChiselPaintModeDrawer.cs | 129 ++++++++++++++++++ .../ChiselPaintModeDrawer.cs.meta | 2 + 3 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 Editor/PropertyDrawers/ChiselPaintModeDrawer.cs create mode 100644 Editor/PropertyDrawers/ChiselPaintModeDrawer.cs.meta diff --git a/Editor/ComponentEditors/Decals/ChiselDecalEditor.cs b/Editor/ComponentEditors/Decals/ChiselDecalEditor.cs index 3a915543..01b11afc 100644 --- a/Editor/ComponentEditors/Decals/ChiselDecalEditor.cs +++ b/Editor/ComponentEditors/Decals/ChiselDecalEditor.cs @@ -313,7 +313,7 @@ static void PlaceWhereMaterialsMeet(ChiselDecalComponent template) return; var existing = new HashSet<(ChiselNodeComponent, int, ChiselNodeComponent, int)>(); - foreach (var decal in Object.FindObjectsByType(FindObjectsSortMode.None)) + foreach (var decal in Object.FindObjectsByType(FindObjectsInactive.Exclude)) { if (decal.Shape == ChiselDecalShape.Trim && decal.Targets.Count >= 2) existing.Add((decal.Targets[0].brush, decal.Targets[0].surfaceIndex, decal.Targets[1].brush, decal.Targets[1].surfaceIndex)); diff --git a/Editor/PropertyDrawers/ChiselPaintModeDrawer.cs b/Editor/PropertyDrawers/ChiselPaintModeDrawer.cs new file mode 100644 index 00000000..0a0aac9f --- /dev/null +++ b/Editor/PropertyDrawers/ChiselPaintModeDrawer.cs @@ -0,0 +1,129 @@ +using Chisel.Components; +using Chisel.Core; +using UnityEditor; +using UnityEngine; + +namespace Chisel.Editors +{ + // A paint mode shows what its kind uses: a color mode its default color, a value mode its default, range and group, + // and the components as toggles (named RGBA for the Color attribute) + [CustomPropertyDrawer(typeof(ChiselPaintMode))] + sealed class ChiselPaintModeDrawer : PropertyDrawer + { + static readonly GUIContent kNameContent = new("Name", "What the mode is called in the paint tool."); + static readonly GUIContent kKindContent = new("Kind", "Color: the brush paints a color. Value: the brush paints a number (a height, a layer's weight), up or down."); + static readonly GUIContent kAttributeContent = new("Attribute", "The vertex attribute the paint is written to; the shader reads it from there. TexCoord0 and TexCoord1 are taken."); + static readonly GUIContent kComponentsContent = new("Components", "The components of the attribute the mode writes; the others keep their default."); + static readonly GUIContent kDefaultColor = new("Default", "What an unpainted surface has."); + static readonly GUIContent kDefaultValue = new("Default", "What an unpainted surface has, in each of the mode's components."); + static readonly GUIContent kRangeContent = new("Range", "The lowest and highest value: the brush keeps paint between them, and Invert turns it over within them."); + static readonly GUIContent kGroupContent = new("Group", "Value modes with the same group (above 0), in one attribute, are weights that always sum to one: painting one takes from the others. 0 is no group."); + static readonly string[] kXYZW = { "X", "Y", "Z", "W" }; + static readonly string[] kRGBA = { "R", "G", "B", "A" }; + + static float Line => EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing; + + public override float GetPropertyHeight(SerializedProperty property, GUIContent label) + { + if (!property.isExpanded) + return EditorGUIUtility.singleLineHeight; + var kind = property.FindPropertyRelative(nameof(ChiselPaintMode.kind)); + var value = kind.hasMultipleDifferentValues || kind.enumValueIndex == (int)ChiselPaintKind.Value; + // foldout, name, kind, attribute, components, default, and range and group for a value + return Line * (value ? 8 : 6); + } + + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) + { + var name = property.FindPropertyRelative(nameof(ChiselPaintMode.name)); + var kind = property.FindPropertyRelative(nameof(ChiselPaintMode.kind)); + var attribute = property.FindPropertyRelative(nameof(ChiselPaintMode.attribute)); + var components = property.FindPropertyRelative(nameof(ChiselPaintMode.components)); + var defaults = property.FindPropertyRelative(nameof(ChiselPaintMode.defaultValue)); + var range = property.FindPropertyRelative(nameof(ChiselPaintMode.range)); + var group = property.FindPropertyRelative(nameof(ChiselPaintMode.group)); + + var row = new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight); + var title = string.IsNullOrEmpty(name.stringValue) ? label : new GUIContent(name.stringValue); + property.isExpanded = EditorGUI.Foldout(row, property.isExpanded, title, true); + if (!property.isExpanded) + return; + + using (new EditorGUI.IndentLevelScope()) + { + row.y += Line; EditorGUI.PropertyField(row, name, kNameContent); + row.y += Line; EditorGUI.PropertyField(row, kind, kKindContent); + row.y += Line; EditorGUI.PropertyField(row, attribute, kAttributeContent); + row.y += Line; ComponentToggles(row, components, attribute.enumValueIndex == 0 ? kRGBA : kXYZW); + + var isValue = kind.hasMultipleDifferentValues || kind.enumValueIndex == (int)ChiselPaintKind.Value; + row.y += Line; + if (!isValue) + { + EditorGUI.BeginChangeCheck(); + var color = EditorGUI.ColorField(row, kDefaultColor, (Vector4)defaults.vector4Value, true, true, false); + if (EditorGUI.EndChangeCheck()) + defaults.vector4Value = color; + return; + } + + // one default for all of a value mode's components + EditorGUI.BeginChangeCheck(); + var first = FirstComponent(components.intValue); + var value = EditorGUI.FloatField(row, kDefaultValue, defaults.vector4Value[first]); + if (EditorGUI.EndChangeCheck()) + defaults.vector4Value = new Vector4(value, value, value, value); + row.y += Line; EditorGUI.PropertyField(row, range, kRangeContent); + row.y += Line; group.intValue = Mathf.Max(0, EditorGUI.IntField(row, kGroupContent, group.intValue)); + } + } + + static int FirstComponent(int components) + { + for (int c = 0; c < 4; c++) + { + if ((components & (1 << c)) != 0) + return c; + } + return 0; + } + + // The components as four toggle buttons + static void ComponentToggles(Rect row, SerializedProperty components, string[] names) + { + using (new EditorGUI.PropertyScope(row, kComponentsContent, components)) + { + var field = EditorGUI.PrefixLabel(row, kComponentsContent); + var indent = EditorGUI.indentLevel; + EditorGUI.indentLevel = 0; + var width = field.width / 4; + var value = components.intValue; + EditorGUI.BeginChangeCheck(); + for (int c = 0; c < 4; c++) + { + var style = c == 0 ? EditorStyles.miniButtonLeft : c == 3 ? EditorStyles.miniButtonRight : EditorStyles.miniButtonMid; + var on = GUI.Toggle(new Rect(field.x + width * c, field.y, width, field.height), (value & (1 << c)) != 0, names[c], style); + value = on ? value | (1 << c) : value & ~(1 << c); + } + if (EditorGUI.EndChangeCheck()) + components.intValue = value; + EditorGUI.indentLevel = indent; + } + } + } + + // A profile warns about two modes writing one component of one attribute + [CustomEditor(typeof(ChiselPaintProfile))] + sealed class ChiselPaintProfileEditor : Editor + { + public override void OnInspectorGUI() + { + DrawDefaultInspector(); + var profile = (ChiselPaintProfile)target; + if (profile.shader == null) + EditorGUILayout.HelpBox("Give the profile the shader it is for: surfaces drawn with that shader are painted in its modes.", MessageType.Info); + if (profile.FindOverlap(out var first, out var second)) + EditorGUILayout.HelpBox($"Modes {first} and {second} both write a component of {profile.modes[first].attribute}: one of them overwrites the other.", MessageType.Warning); + } + } +} diff --git a/Editor/PropertyDrawers/ChiselPaintModeDrawer.cs.meta b/Editor/PropertyDrawers/ChiselPaintModeDrawer.cs.meta new file mode 100644 index 00000000..3e5de774 --- /dev/null +++ b/Editor/PropertyDrawers/ChiselPaintModeDrawer.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 45cefae5bc727ed47b878a87fc36bf11 \ No newline at end of file From 852eed338a6e699d1a9317a0d06710c49070160c Mon Sep 17 00:00:00 2001 From: Sander van Rossen Date: Mon, 26 Oct 2026 08:42:03 +0100 Subject: [PATCH 07/27] Paint: the outline cut at the grid too, and a not-paintable cursor A painted surface's outline is cut where the grid meets it (its points on it, and where its lines cross it), so the surface is the grid's cells right up to its edges instead of fans from its corners. A point is put on an outline edge within a twentieth of a cell, on an edge inside only when it is on it. Over a surface whose shader has no paint profile the tool shows no brush but a not-allowed cursor, with a solid tooltip saying why; the overlay keeps one width. --- Components/Tests/ChiselPaintTests.cs | 8 +- Core/2.Processing/Paint/PaintSubdivision.cs | 105 +++++++++++++++++--- Core/Tests/PaintSubdivisionTests.cs | 26 +++-- Editor/EditorTools/ChiselPaintTool.cs | 36 +++++-- 4 files changed, 139 insertions(+), 36 deletions(-) diff --git a/Components/Tests/ChiselPaintTests.cs b/Components/Tests/ChiselPaintTests.cs index ed23ee9e..6d906624 100644 --- a/Components/Tests/ChiselPaintTests.cs +++ b/Components/Tests/ChiselPaintTests.cs @@ -178,8 +178,8 @@ public void PaintInOneComponent_LeavesTheOthersAtTheDefault() AssertValuesAt(RenderMesh(model), VertexAttribute.TexCoord3, new Vector3(0, 1, 0), new Vector4(1, 0.25f, 1, 1), "changed"); } - // A grid of quarter-metre cells on the top: its 9 points inside the top are vertices, each of them splitting the - // top's triangles further; the 16 on its outline and corners aren't, so the sides keep their two triangles each + // A grid of quarter-metre cells on the top: all of its 25 points are vertices, the top is the grid's 32 half cells, + // and the sides, which aren't painted, keep their two triangles each [Test] public void APaintedSurface_IsSubdividedAtItsGridPointsInsideIt() { @@ -208,8 +208,8 @@ public void APaintedSurface_IsSubdividedAtItsGridPointsInsideIt() if (normal.y > 0.99f) topTriangles++; else if (Mathf.Abs(normal.y) < 0.01f) sideTriangles++; } - Assert.That(topVertices, Is.EqualTo(13), "the 4 corners and the 9 points inside"); - Assert.That(topTriangles, Is.EqualTo(20)); + Assert.That(topVertices, Is.EqualTo(25), "every grid point"); + Assert.That(topTriangles, Is.EqualTo(32)); Assert.That(sideTriangles, Is.EqualTo(8), "the sides aren't cut"); AssertValuesAt(mesh, VertexAttribute.Color, new Vector3(0.5f, 1, 0.5f), new Vector4(0, 0, 0, 1), "the middle point's own value"); } diff --git a/Core/2.Processing/Paint/PaintSubdivision.cs b/Core/2.Processing/Paint/PaintSubdivision.cs index 9462a678..576f3673 100644 --- a/Core/2.Processing/Paint/PaintSubdivision.cs +++ b/Core/2.Processing/Paint/PaintSubdivision.cs @@ -3,11 +3,11 @@ namespace Chisel.Core { - // A painted surface is subdivided at its paint's grid points, so the paint has vertices to land on: each grid point - // inside the surface becomes a vertex, inserted as in an incremental Delaunay triangulation (it splits the triangle it - // is in, or the two on the edge it is on, and the edges round it are flipped until the triangles round it are - // Delaunay), so no sliver is left for the paint to streak across. The surface's outline is never cut, so the surfaces - // it touches don't get T-junctions. + // A painted surface is subdivided at its paint's grid, so the paint has vertices to land on: each grid point in the + // surface becomes a vertex, and so does each place a grid line crosses its outline, inserted as in an incremental + // Delaunay triangulation (it splits the triangle it is in, or the triangles of the edge it is on, and the edges round + // it are flipped until the triangles round it are Delaunay), so no sliver is left for the paint to streak across. + // The outline is cut too: the surfaces on the other side of it don't get those vertices (T-junctions). internal static class PaintSubdivision { // Most points one surface gets; a grid of tiny cells on a big surface stops here @@ -44,12 +44,76 @@ public static void Subdivide(NativeList indices, ref UniqueVertexMapper map if (points.Length == 0) return; + // where the grid's lines cross the outline, before the points: an outline that isn't along the grid's lines + // is cut where they meet it + using var crossings = new NativeList(16, Allocator.Temp); + for (int l = range.start; l < range.start + range.count; l++) + { + var layer = paint.layers[l]; + var normal = math.normalizesafe(math.cross(layer.cellU, layer.cellV)); + if (math.abs(math.dot(normal, surfaceNormal)) < 0.999f) + continue; + AddOutlineCrossings(indices, mapper.surfaceColliderVertices, layer, crossings); + } + var triangulation = new Triangulation(indices, ref mapper, surfaceNormal, snap); + for (int p = 0; p < crossings.Length; p++) + triangulation.Insert(ref mapper, crossings[p]); for (int p = 0; p < points.Length; p++) triangulation.Insert(ref mapper, points[p]); triangulation.Dispose(); } + // Where the grid's lines (whole numbers of cells along cellU or cellV) cross the edges of the outline (the edges + // only one triangle has), away from the edges' ends + static void AddOutlineCrossings(NativeList indices, NativeList positions, in ChiselPaintLayer layer, NativeList crossings) + { + using var edges = new NativeParallelHashSet(indices.Length, Allocator.Temp); + for (int t = 0; t + 2 < indices.Length; t += 3) + { + for (int k = 0; k < 3; k++) + edges.Add(new int2(indices[t + k], indices[t + (k + 1) % 3])); + } + const double kAwayFromEnds = 0.05; // in cells + for (int t = 0; t + 2 < indices.Length; t += 3) + { + for (int k = 0; k < 3 && crossings.Length < kMaxPoints; k++) + { + var a = indices[t + k]; + var b = indices[t + (k + 1) % 3]; + if (edges.Contains(new int2(b, a))) + continue; // inside: two triangles have it + var from = GridCoordinates(layer, positions[a]); + var to = GridCoordinates(layer, positions[b]); + for (int axis = 0; axis < 2; axis++) + { + var lo = math.min(from[axis], to[axis]); + var hi = math.max(from[axis], to[axis]); + if (!(hi - lo > 2 * kAwayFromEnds)) + continue; + for (var line = math.ceil(lo + kAwayFromEnds); line < hi - kAwayFromEnds && crossings.Length < kMaxPoints; line++) + { + var along = (line - from[axis]) / (to[axis] - from[axis]); + crossings.Add((float3)math.lerp((double3)positions[a], (double3)positions[b], along)); + } + } + } + } + } + + // A point's place on the grid, in cells along cellU and cellV (which needn't be at right angles) + static double2 GridCoordinates(in ChiselPaintLayer layer, float3 point) + { + var offset = (double3)(point - layer.origin); + double3 u = layer.cellU, v = layer.cellV; + var uu = math.dot(u, u); var uv = math.dot(u, v); var vv = math.dot(v, v); + var determinant = uu * vv - uv * uv; + if (!(math.abs(determinant) > 1e-18)) + return double2.zero; + var du = math.dot(offset, u); var dv = math.dot(offset, v); + return new double2((vv * du - uv * dv) / determinant, (uu * dv - uv * du) / determinant); + } + struct Triangulation { NativeList indices; @@ -96,7 +160,18 @@ public void Insert(ref UniqueVertexMapper mapper, float3 position) if (math.distance(point, a) < snap || math.distance(point, b) < snap || math.distance(point, c) < snap) return; // there is a vertex already - if (math.cmin(distance) >= snap) + // the nearest edge: a point near the outline is put on it (or float noise would leave a sliver along + // it, which can't be flipped), one near an edge inside only when it is on it (the flips mend the rest) + var nearest = distance.x <= distance.y && distance.x <= distance.z ? 0 : distance.y <= distance.z ? 1 : 2; + int opposite = nearest == 0 ? i0 : nearest == 1 ? i1 : i2; + int e0 = nearest == 0 ? i1 : nearest == 1 ? i2 : i0; + int e1 = nearest == 0 ? i2 : nearest == 1 ? i0 : i1; + var other = FindEdge(e1, e0, out var otherOpposite); + var onEdge = other < 0 ? snap : snap * 1e-3; + if (distance[nearest] < -onEdge) + continue; // across an edge inside: in the triangle on its other side + + if (distance[nearest] >= onEdge) { // inside: three triangles round it var v = AddVertex(ref mapper, i0, i1, i2, w); @@ -110,19 +185,21 @@ public void Insert(ref UniqueVertexMapper mapper, float3 position) return; } - // on an edge (the nearest): it splits both of its triangles, or none on the surface's outline - var nearest = distance.x <= distance.y && distance.x <= distance.z ? 0 : distance.y <= distance.z ? 1 : 2; - int opposite = nearest == 0 ? i0 : nearest == 1 ? i1 : i2; - int e0 = nearest == 0 ? i1 : nearest == 1 ? i2 : i0; - int e1 = nearest == 0 ? i2 : nearest == 1 ? i0 : i1; - var other = FindEdge(e1, e0, out var otherOpposite); - if (other < 0) - return; + // on the edge: it splits both of its triangles, or the one on the surface's outline // exactly on the edge w = math.max(w, 0); w[nearest] = 0; w /= math.csum(w); var vertex = AddVertex(ref mapper, i0, i1, i2, w); + if (other < 0) + { + indices[t] = opposite; indices[t + 1] = e0; indices[t + 2] = vertex; + indices.Add(opposite); indices.Add(vertex); indices.Add(e1); + pending.Add(new int2(opposite, e0)); + pending.Add(new int2(e1, opposite)); + Legalize(vertex); + return; + } indices[t] = opposite; indices[t + 1] = e0; indices[t + 2] = vertex; indices.Add(opposite); indices.Add(vertex); indices.Add(e1); indices[other] = otherOpposite; indices[other + 1] = e1; indices[other + 2] = vertex; diff --git a/Core/Tests/PaintSubdivisionTests.cs b/Core/Tests/PaintSubdivisionTests.cs index 7e2de7a3..e597a1f0 100644 --- a/Core/Tests/PaintSubdivisionTests.cs +++ b/Core/Tests/PaintSubdivisionTests.cs @@ -4,12 +4,12 @@ namespace Chisel.Core.Tests { - // A painted surface subdivided at its grid points (PaintSubdivision): every point inside it a vertex, its outline - // never cut, and no sliver left by the flips + // A painted surface subdivided at its grid (PaintSubdivision): every grid point in it a vertex, its outline cut where + // the grid meets it, and no sliver left by the flips public sealed class PaintSubdivisionTests { // A square of side 2 in the xz plane at offset, two triangles, and a grid of 0.1 cells over it exactly - static void Subdivide(float3 offset, bool otherDiagonal, out int vertexCount, out int triangleCount, out float minAngle, out string worst) + static void Subdivide(float3 offset, bool otherDiagonal, out int vertexCount, out int triangleCount, out float minAngle, out string worst, out string offGrid) { var mapper = new UniqueVertexMapper { @@ -36,14 +36,18 @@ static void Subdivide(float3 offset, bool otherDiagonal, out int vertexCount, ou PaintSubdivision.Subdivide(indices, ref mapper, new float3(0, 1, 0), lookup, new ChiselPaintRange { start = 0, count = 1 }); vertexCount = mapper.surfaceColliderVertices.Length; + offGrid = ""; + for (int v = 0; v < vertexCount; v++) + { + var cell = (mapper.surfaceColliderVertices[v] - offset) / 0.1f; + if (math.any(math.abs(cell - math.round(cell)) > 0.01f)) + offGrid += " " + (mapper.surfaceColliderVertices[v] - offset); + } triangleCount = indices.Length / 3; minAngle = 180; worst = null; for (int t = 0; t < indices.Length; t += 3) { - // the outline isn't cut: its corners fan to the points nearest it, thin by nature - if (indices[t] < 4 || indices[t + 1] < 4 || indices[t + 2] < 4) - continue; var a = mapper.surfaceColliderVertices[indices[t]]; var b = mapper.surfaceColliderVertices[indices[t + 1]]; var c = mapper.surfaceColliderVertices[indices[t + 2]]; @@ -61,14 +65,14 @@ static void Subdivide(float3 offset, bool otherDiagonal, out int vertexCount, ou static float Angle(float3 o, float3 p, float3 q) => math.degrees(math.acos(math.clamp(math.dot(math.normalize(p - o), math.normalize(q - o)), -1, 1))); - // 19 x 19 points inside; the 80 on the outline aren't vertices. Away from the outline's corners the triangles are the - // grid's half cells, whichever way the square was cut and however far from the origin it is + // All 21 x 21 points, the outline's too: the triangles are the grid's half cells, whichever way the square was cut and + // however far from the origin it is [Test] public void EveryGridPointInside_IsAVertex_AndNoTriangleIsASliver([Values(false, true)] bool otherDiagonal, [Values(0, 5000)] float offset) { - Subdivide(new float3(offset, offset * 0.5f, -offset), otherDiagonal, out var vertices, out var triangles, out var minAngle, out var worst); - Assert.That(vertices, Is.EqualTo(4 + 19 * 19)); - Assert.That(triangles, Is.EqualTo(2 + 2 * 19 * 19)); + Subdivide(new float3(offset, offset * 0.5f, -offset), otherDiagonal, out var vertices, out var triangles, out var minAngle, out var worst, out var offGrid); + Assert.That(vertices, Is.EqualTo(21 * 21), "not on a grid point:" + offGrid); + Assert.That(triangles, Is.EqualTo(2 * 20 * 20)); Assert.That(minAngle, Is.GreaterThan(30), "no sliver; the thinnest: " + worst); } } diff --git a/Editor/EditorTools/ChiselPaintTool.cs b/Editor/EditorTools/ChiselPaintTool.cs index 9a1b2614..6e191ff7 100644 --- a/Editor/EditorTools/ChiselPaintTool.cs +++ b/Editor/EditorTools/ChiselPaintTool.cs @@ -131,12 +131,13 @@ public override void OnToolGUI(EditorWindow window) s_Last = s_Hover; } var hit = s_Hover; - if (hit.valid) + if (hit.valid && hit.profile != null) { - Handles.color = hit.profile != null ? new Color(1, 1, 1, 0.8f) : new Color(1, 0.3f, 0.3f, 0.8f); + Handles.color = new Color(1, 1, 1, 0.8f); Handles.DrawWireDisc(hit.point, hit.normal, s_Radius); Handles.DrawWireDisc(hit.point, hit.normal, s_Radius * Mathf.Clamp01(s_Hardness)); - } + } else if (hit.valid) + NotPaintable(window, hit, evt.mousePosition); switch (evt.GetTypeForControl(id)) { @@ -244,6 +245,28 @@ static void Dab(Hit hit) } } + static GUIStyle s_TooltipStyle; + + // Over a surface that can't be painted: no brush, a cursor that says so, and why next to it + static void NotPaintable(EditorWindow window, Hit hit, Vector2 mousePosition) + { + EditorGUIUtility.AddCursorRect(new Rect(0, 0, window.position.width, window.position.height), MouseCursor.NotAllowed); + if (Event.current.type != EventType.Repaint) + return; + // solid, as a tooltip is: a panel's color with a border round it + s_TooltipStyle ??= new GUIStyle(EditorStyles.label) { padding = new RectOffset(6, 6, 3, 3), wordWrap = false }; + var content = new GUIContent(hit.shader + ? $"The shader {hit.shader.name} is not set up to be paintable: it has no paint profile." + : "The surface has no shader to paint for."); + var size = s_TooltipStyle.CalcSize(content); + var rect = new Rect(mousePosition + new Vector2(16, 20), size); + Handles.BeginGUI(); + EditorGUI.DrawRect(new Rect(rect.x - 1, rect.y - 1, rect.width + 2, rect.height + 2), EditorGUIUtility.isProSkin ? new Color(0.1f, 0.1f, 0.1f, 1) : new Color(0.5f, 0.5f, 0.5f, 1)); + EditorGUI.DrawRect(rect, EditorGUIUtility.isProSkin ? new Color(0.24f, 0.24f, 0.24f, 1) : new Color(0.94f, 0.94f, 0.94f, 1)); + s_TooltipStyle.Draw(rect, content, false, false, false, false); + Handles.EndGUI(); + } + // The tool's settings, in its overlay internal static void PanelGUI() { @@ -253,8 +276,6 @@ internal static void PanelGUI() { // the modes of the surface last painted on, so they stay while the mouse is over the panel var profile = s_Last.valid ? s_Last.profile : null; - if (hit.valid && hit.profile == null) - EditorGUILayout.HelpBox($"The shader {(hit.shader ? hit.shader.name : "(none)")} has no paint profile (Assets > Create > Chisel > Paint Profile).", MessageType.Info); if (profile == null || profile.modes.Count == 0) EditorGUILayout.HelpBox("Hover over a surface whose shader has a paint profile.", MessageType.None); else @@ -326,8 +347,9 @@ sealed class ChiselPaintOverlay : UnityEditor.Overlays.IMGUIOverlay, UnityEditor public override void OnGUI() { - EditorGUILayout.GetControlRect(false, 0, GUILayout.MinWidth(260)); - ChiselPaintTool.PanelGUI(); + // one width, whatever it shows + using (new EditorGUILayout.VerticalScope(GUILayout.Width(260))) + ChiselPaintTool.PanelGUI(); } } } From 58ecbdd0b9f9e249f88636a4a583eb8329adf635 Mon Sep 17 00:00:00 2001 From: Sander van Rossen Date: Mon, 26 Oct 2026 08:47:54 +0100 Subject: [PATCH 08/27] Paint: drawn only under a shader that takes it The profile registry (ChiselPaintProfiles) moved to the components, with profiles registered in code next to the project's profile assets. Paint of a surface whose material's shader has no profile with a mode in its attribute is kept on the generator, but doesn't subdivide the surface or reach its mesh; it comes back with a material that takes it. --- .../Components/Paint/ChiselPaintProfiles.cs | 81 +++++++++++++++++++ .../Paint/ChiselPaintProfiles.cs.meta | 2 + Components/Managers/ChiselPaintManager.cs | 16 +++- Components/Tests/ChiselPaintTests.cs | 36 +++++++++ Editor/EditorTools/ChiselPaintTool.cs | 26 ------ 5 files changed, 133 insertions(+), 28 deletions(-) create mode 100644 Components/Components/Paint/ChiselPaintProfiles.cs create mode 100644 Components/Components/Paint/ChiselPaintProfiles.cs.meta diff --git a/Components/Components/Paint/ChiselPaintProfiles.cs b/Components/Components/Paint/ChiselPaintProfiles.cs new file mode 100644 index 00000000..19e21796 --- /dev/null +++ b/Components/Components/Paint/ChiselPaintProfiles.cs @@ -0,0 +1,81 @@ +using System.Collections.Generic; +using Chisel.Core; +using UnityEngine; + +namespace Chisel.Components +{ + /// The project's paint profiles, by the shader they are for + public static class ChiselPaintProfiles + { + static Dictionary s_ByShader; + // profiles made in code (not assets), which come first + static readonly List s_Registered = new(); + + public static void Register(ChiselPaintProfile profile) + { + if (profile != null && !s_Registered.Contains(profile)) + s_Registered.Add(profile); + ChiselPaintManager.SetDirty(); + } + + public static void Unregister(ChiselPaintProfile profile) + { + s_Registered.Remove(profile); + ChiselPaintManager.SetDirty(); + } + + /// Called when profiles may have been added, removed or changed + public static void SetDirty() => s_ByShader = null; + + public static ChiselPaintProfile Find(Shader shader) + { + if (shader == null) + return null; + foreach (var registered in s_Registered) + { + if (registered != null && registered.shader == shader) + return registered; + } + if (s_ByShader == null) + { + s_ByShader = new Dictionary(); +#if UNITY_EDITOR + foreach (var guid in UnityEditor.AssetDatabase.FindAssets("t:" + nameof(ChiselPaintProfile))) + { + var profile = UnityEditor.AssetDatabase.LoadAssetAtPath(UnityEditor.AssetDatabase.GUIDToAssetPath(guid)); + if (profile != null && profile.shader != null) + s_ByShader.TryAdd(profile.shader, profile); + } +#endif + } + return s_ByShader.TryGetValue(shader, out var found) ? found : null; + } + + /// Whether surfaces drawn with this material take paint in this attribute: its shader's profile has a mode + /// in it. Paint of a surface whose material doesn't is kept, but not drawn and not subdividing it. + public static bool TakesPaint(Material material, ChiselPaintAttribute attribute) + { + var profile = material ? Find(material.shader) : null; + if (profile == null) + return false; + foreach (var mode in profile.modes) + { + if (mode.attribute == attribute) + return true; + } + return false; + } + +#if UNITY_EDITOR + [UnityEditor.InitializeOnLoadMethod] + static void Initialize() + { + UnityEditor.EditorApplication.projectChanged += () => + { + SetDirty(); + ChiselPaintManager.SetDirty(); + }; + } +#endif + } +} diff --git a/Components/Components/Paint/ChiselPaintProfiles.cs.meta b/Components/Components/Paint/ChiselPaintProfiles.cs.meta new file mode 100644 index 00000000..94b3a79f --- /dev/null +++ b/Components/Components/Paint/ChiselPaintProfiles.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9af8a5779d3706a4f8ec01a1daf34639 \ No newline at end of file diff --git a/Components/Managers/ChiselPaintManager.cs b/Components/Managers/ChiselPaintManager.cs index 1a781067..878e3088 100644 --- a/Components/Managers/ChiselPaintManager.cs +++ b/Components/Managers/ChiselPaintManager.cs @@ -97,7 +97,7 @@ static void SetPaint(ChiselModelComponent model, List { foreach (var paint in generator.SurfacePaint) { - if (paint == null || !paint.IsValid) + if (!Drawn(generator, paint)) continue; layerCount++; valueCount += paint.values.Length; @@ -119,7 +119,7 @@ static void SetPaint(ChiselModelComponent model, List var space = s_Spaces[generator]; foreach (var paint in generator.SurfacePaint) { - if (paint == null || !paint.IsValid) + if (!Drawn(generator, paint)) continue; layers[l++] = new ChiselPaintLayer { @@ -152,6 +152,18 @@ static void SetPaint(ChiselModelComponent model, List } } + // Paint is drawn while its surface's material's shader takes paint in its attribute; otherwise it is kept on the + // generator, but doesn't subdivide the surface or reach its mesh + static bool Drawn(ChiselGeneratorComponent generator, ChiselSurfacePaint paint) + { + if (paint == null || !paint.IsValid) + return false; + var surfaces = generator.SurfaceDefinition?.surfaces; + if (surfaces == null || paint.surfaceIndex < 0 || paint.surfaceIndex >= surfaces.Length) + return false; + return ChiselPaintProfiles.TakesPaint(surfaces[paint.surfaceIndex].RenderMaterial, paint.attribute); + } + static void SetBrushesDirty(CSGTreeNode node) { if (!node.Valid) diff --git a/Components/Tests/ChiselPaintTests.cs b/Components/Tests/ChiselPaintTests.cs index 6d906624..80004202 100644 --- a/Components/Tests/ChiselPaintTests.cs +++ b/Components/Tests/ChiselPaintTests.cs @@ -13,17 +13,30 @@ namespace Chisel.Components.Tests public class ChiselPaintTests { readonly List created = new(); + ChiselPaintProfile profile; + static Material s_Material; [SetUp] public void SetUp() { if (Object.FindObjectsByType(FindObjectsInactive.Include).Length > 0) Assert.Ignore("A Chisel model is loaded; run these in a scene without one"); + // the boxes' material takes paint in the attributes these paint in (its own, so a project's materials don't matter) + s_Material = new Material(Shader.Find("Universal Render Pipeline/Lit")) { name = "PaintTestMaterial" }; + created.Add(s_Material); + profile = ScriptableObject.CreateInstance(); + profile.shader = s_Material.shader; + profile.modes.Add(ChiselPaintMode.Color("Color")); + profile.modes.Add(ChiselPaintMode.Value("Value", ChiselPaintAttribute.TexCoord2, ChiselPaintComponents.XYZW)); + profile.modes.Add(ChiselPaintMode.Value("Mask", ChiselPaintAttribute.TexCoord3, ChiselPaintComponents.XYZW)); + created.Add(profile); + ChiselPaintProfiles.Register(profile); } [TearDown] public void TearDown() { + ChiselPaintProfiles.Unregister(profile); foreach (var item in created) { if (item) @@ -59,6 +72,8 @@ static ChiselBoxComponent Box(ChiselModelComponent model, Vector3 position) box.Max = new float3(1e4f); box.Min = float3.zero; box.Max = new float3(1); + foreach (var surface in box.SurfaceDefinition.surfaces) + surface.SetMaterial(s_Material); return box; } @@ -330,6 +345,27 @@ public void Sampling_AndTheChannelUtilities() Assert.That(paint.values[1].w, Is.EqualTo(0)); } + // Paint under a shader that doesn't take it (its profile gone, or another material) is kept, but neither drawn nor + // subdividing the surface; it comes back with a shader that does + [Test] + public void PaintUnderAShaderWithoutAProfile_IsKeptButNotDrawn() + { + var model = Model(); + var box = Box(model, Vector3.zero); + PaintTop(box, ChiselPaintAttribute.Color, ChiselPaintComponents.XYZW); + Update(); + Assert.That(RenderMesh(model).HasVertexAttribute(VertexAttribute.Color), Is.True); + + ChiselPaintProfiles.Unregister(profile); + Update(); + Assert.That(RenderMesh(model).HasVertexAttribute(VertexAttribute.Color), Is.False, "no profile: not drawn"); + Assert.That(box.SurfacePaint.Count, Is.EqualTo(1), "but kept"); + + ChiselPaintProfiles.Register(profile); + Update(); + Assert.That(RenderMesh(model).HasVertexAttribute(VertexAttribute.Color), Is.True, "drawn again"); + } + // The grid moves with its generator [Test] public void Paint_MovesWithItsGenerator() diff --git a/Editor/EditorTools/ChiselPaintTool.cs b/Editor/EditorTools/ChiselPaintTool.cs index 6e191ff7..f86eb5b6 100644 --- a/Editor/EditorTools/ChiselPaintTool.cs +++ b/Editor/EditorTools/ChiselPaintTool.cs @@ -7,32 +7,6 @@ namespace Chisel.Editors { - /// The paint profiles of the project, by the shader they are for - static class ChiselPaintProfiles - { - static Dictionary s_ByShader; - - [InitializeOnLoadMethod] - static void Initialize() => EditorApplication.projectChanged += () => s_ByShader = null; - - public static ChiselPaintProfile Find(Shader shader) - { - if (shader == null) - return null; - if (s_ByShader == null) - { - s_ByShader = new Dictionary(); - foreach (var guid in AssetDatabase.FindAssets("t:" + nameof(ChiselPaintProfile))) - { - var profile = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(guid)); - if (profile != null && profile.shader != null) - s_ByShader.TryAdd(profile.shader, profile); - } - } - return s_ByShader.TryGetValue(shader, out var found) ? found : null; - } - } - /// /// Paints the surface under the mouse in a mode of the paint profile of its material's shader: a color, or a named /// value, with a round brush. Only the surface that is hit is painted; a stroke is one undo. From e67ae74ff472b2cb040ea1fd741a872bbdedb94f Mon Sep 17 00:00:00 2001 From: Sander van Rossen Date: Mon, 26 Oct 2026 09:02:25 +0100 Subject: [PATCH 09/27] Paint: spacing per surface, modes as toggles The spacing of the points a surface is painted at is the surface's Paint Spacing (ChiselSurface, beside its material: only painting reads it, so it stays out of the details every brush polygon carries), shown for materials whose shader has a paint profile. Changing it resamples the surface's paint onto a grid of the new spacing. The paint tool's overlay shows every mode of the project's profiles, by name, as wrapping toggle buttons; a surface takes the chosen mode when its shader's profile has one of that name. Over a surface that doesn't, the tooltip says why, on three lines. --- .../Components/Paint/ChiselPaintBrush.cs | 62 ++++++- .../Components/Paint/ChiselPaintProfiles.cs | 26 ++- .../Components/Paint/ChiselSurfacePaint.cs | 3 + Components/Managers/ChiselPaintManager.cs | 1 + Components/Tests/ChiselPaintTests.cs | 49 +++++- Core/1.Input/Surfaces/ChiselSurface.cs | 7 + Editor/EditorTools/ChiselPaintTool.cs | 161 ++++++++++++++---- Editor/EditorTools/ChiselUVToolCommon.cs | 10 ++ .../SurfaceDetailsPropertyDrawer.cs | 54 +++++- 9 files changed, 321 insertions(+), 52 deletions(-) diff --git a/Components/Components/Paint/ChiselPaintBrush.cs b/Components/Components/Paint/ChiselPaintBrush.cs index 15372839..f5cc6839 100644 --- a/Components/Components/Paint/ChiselPaintBrush.cs +++ b/Components/Components/Paint/ChiselPaintBrush.cs @@ -80,12 +80,13 @@ static void AddOutline(CSGTreeNode node, int surfaceIndex, Matrix4x4 treeToGener } /// - /// A grid over a surface of a generator, in the surface's plane, its cells about cellSize (larger when it would + /// A grid over a surface of a generator, in the surface's plane, its cells about spacing (larger when it would /// have more than ), every point at the default. Null when the surface has no outline. /// public static ChiselSurfacePaint CreateGrid(ChiselGeneratorComponent generator, int surfaceIndex, ChiselPaintAttribute attribute, - ChiselPaintComponents components, Vector4 defaultValue, float cellSize) + ChiselPaintComponents components, Vector4 defaultValue, float spacing) { + var cellSize = spacing; if (!SurfaceOutline(generator, surfaceIndex, s_Corners, out var normal)) return null; var axisU = Vector3.Cross(normal, Mathf.Abs(normal.y) < 0.99f ? Vector3.up : Vector3.forward).normalized; @@ -111,19 +112,68 @@ public static ChiselSurfacePaint CreateGrid(ChiselGeneratorComponent generator, } var planePoint = s_Corners[0]; var origin = planePoint + (min.x - Vector3.Dot(planePoint, axisU)) * axisU + (min.y - Vector3.Dot(planePoint, axisV)) * axisV; - return ChiselSurfacePaint.Create(surfaceIndex, attribute, components, defaultValue, origin, axisU * cellSize, axisV * cellSize, resolution); + var paint = ChiselSurfacePaint.Create(surfaceIndex, attribute, components, defaultValue, origin, axisU * cellSize, axisV * cellSize, resolution); + paint.spacing = spacing; + return paint; + } + + /// The paint spacing of a generator's surface () + public static float SpacingOf(ChiselGeneratorComponent generator, int surfaceIndex) + { + var surfaces = generator ? generator.SurfaceDefinition?.surfaces : null; + return surfaces != null && surfaceIndex >= 0 && surfaceIndex < surfaces.Length && surfaces[surfaceIndex] != null + ? surfaces[surfaceIndex].PaintSpacing : ChiselSurface.kDefaultPaintSpacing; + } + + /// + /// Puts the paint of every surface of a generator whose paint spacing changed on a new grid of that spacing, + /// resampled from the old one. True when any changed. + /// + public static bool Respace(ChiselGeneratorComponent generator) + { + var changed = false; + var all = generator.SurfacePaint; + for (int p = 0; p < all.Count; p++) + { + var paint = all[p]; + if (paint == null) + continue; + var spacing = SpacingOf(generator, paint.surfaceIndex); + if (paint.spacing == spacing) + continue; + if (!(paint.spacing > 0)) + { + paint.spacing = spacing; + changed = true; + continue; + } + var grid = CreateGrid(generator, paint.surfaceIndex, paint.attribute, paint.components, paint.defaultValue, spacing); + if (grid == null) + continue; + for (int j = 0; j < grid.resolution.y; j++) + { + for (int i = 0; i < grid.resolution.x; i++) + grid[i, j] = paint.Sample(grid.origin + i * grid.cellU + j * grid.cellV); + } + // the old grid leaves the list, the new one goes to its end: the next paint is at p now + generator.SetSurfacePaint(grid); + p--; + changed = true; + } + return changed; } /// /// The paint a mode writes on a surface: the surface's paint in the mode's attribute, with the mode's components - /// (at its default where they weren't painted yet), made with a grid of cellSize when it has none + /// (at its default where they weren't painted yet), made with a grid of the surface's paint spacing when it has none /// - public static ChiselSurfacePaint PaintFor(ChiselGeneratorComponent generator, int surfaceIndex, in ChiselPaintMode mode, float cellSize) + public static ChiselSurfacePaint PaintFor(ChiselGeneratorComponent generator, int surfaceIndex, in ChiselPaintMode mode) { + Respace(generator); var paint = generator.GetSurfacePaint(surfaceIndex, mode.attribute); if (paint == null) { - paint = CreateGrid(generator, surfaceIndex, mode.attribute, mode.components, mode.defaultValue, cellSize); + paint = CreateGrid(generator, surfaceIndex, mode.attribute, mode.components, mode.defaultValue, SpacingOf(generator, surfaceIndex)); if (paint != null) generator.SetSurfacePaint(paint); return paint; diff --git a/Components/Components/Paint/ChiselPaintProfiles.cs b/Components/Components/Paint/ChiselPaintProfiles.cs index 19e21796..2e2b6e92 100644 --- a/Components/Components/Paint/ChiselPaintProfiles.cs +++ b/Components/Components/Paint/ChiselPaintProfiles.cs @@ -36,6 +36,31 @@ public static ChiselPaintProfile Find(Shader shader) if (registered != null && registered.shader == shader) return registered; } + Load(); + return s_ByShader.TryGetValue(shader, out var found) ? found : null; + } + + /// Every profile: those made in code, then the project's (but for a shader one made in code is for) + public static IEnumerable All + { + get + { + foreach (var registered in s_Registered) + { + if (registered != null) + yield return registered; + } + Load(); + foreach (var profile in s_ByShader.Values) + { + if (profile != null && Find(profile.shader) == profile) + yield return profile; + } + } + } + + static void Load() + { if (s_ByShader == null) { s_ByShader = new Dictionary(); @@ -48,7 +73,6 @@ public static ChiselPaintProfile Find(Shader shader) } #endif } - return s_ByShader.TryGetValue(shader, out var found) ? found : null; } /// Whether surfaces drawn with this material take paint in this attribute: its shader's profile has a mode diff --git a/Components/Components/Paint/ChiselSurfacePaint.cs b/Components/Components/Paint/ChiselSurfacePaint.cs index 9f29d520..3a5fbde1 100644 --- a/Components/Components/Paint/ChiselSurfacePaint.cs +++ b/Components/Components/Paint/ChiselSurfacePaint.cs @@ -21,6 +21,9 @@ public sealed class ChiselSurfacePaint public Vector3 cellU; public Vector3 cellV; public Vector2Int resolution; + /// The surface's paint spacing the grid was made for (its cells are larger when that would have been + /// too many points); 0 when not known + public float spacing; public Vector4[] values = Array.Empty(); /// A grid of resolution points from origin along cellU and cellV, every point at the default diff --git a/Components/Managers/ChiselPaintManager.cs b/Components/Managers/ChiselPaintManager.cs index 878e3088..e97873cd 100644 --- a/Components/Managers/ChiselPaintManager.cs +++ b/Components/Managers/ChiselPaintManager.cs @@ -55,6 +55,7 @@ public static void Update() var model = generator.hierarchyItem?.Model; if (!generator.isActiveAndEnabled || generator.SurfacePaint.Count == 0 || !model) continue; + ChiselPaintBrush.Respace(generator); s_Painted.Add(generator); s_Spaces[generator] = SpaceOf(generator, model); if (!s_ByModel.TryGetValue(model, out var generators)) diff --git a/Components/Tests/ChiselPaintTests.cs b/Components/Tests/ChiselPaintTests.cs index 80004202..c4335afb 100644 --- a/Components/Tests/ChiselPaintTests.cs +++ b/Components/Tests/ChiselPaintTests.cs @@ -238,7 +238,7 @@ public void TheBrush_GivesASurfaceAGrid_AndPaintsWhereItDabs() var box = Box(model, Vector3.zero); var top = SurfaceFacing(box, new float3(0, 1, 0)); var mode = ChiselPaintMode.Color("Color"); - var paint = ChiselPaintBrush.PaintFor(box, top, mode, 0.25f); + var paint = ChiselPaintBrush.PaintFor(box, top, mode); Assert.That(paint, Is.Not.Null); Assert.That(paint.resolution, Is.EqualTo(new Vector2Int(5, 5)), "a metre in quarters: 5 points along each side"); Assert.That(paint.cellU.magnitude, Is.EqualTo(0.25f).Within(1e-5f)); @@ -262,9 +262,9 @@ public void ModesOfOneAttribute_ShareItsPaint() var top = SurfaceFacing(box, new float3(0, 1, 0)); var height = ChiselPaintMode.Value("Height", ChiselPaintAttribute.TexCoord2, ChiselPaintComponents.X, 0, 0, 1); var wet = ChiselPaintMode.Value("Wetness", ChiselPaintAttribute.TexCoord2, ChiselPaintComponents.Y, 0, 0, 1); - var paint = ChiselPaintBrush.PaintFor(box, top, height, 0.25f); + var paint = ChiselPaintBrush.PaintFor(box, top, height); ChiselPaintBrush.Dab(paint, height, ChiselPaintOperation.Add, Vector4.one * 0.5f, new Vector3(0.5f, 1, 0.5f), 2, 1, 1); - Assert.That(ChiselPaintBrush.PaintFor(box, top, wet, 0.25f), Is.SameAs(paint)); + Assert.That(ChiselPaintBrush.PaintFor(box, top, wet), Is.SameAs(paint)); ChiselPaintBrush.Dab(paint, wet, ChiselPaintOperation.Paint, Vector4.one * 0.75f, new Vector3(0.5f, 1, 0.5f), 2, 1, 1); Assert.That(paint.components, Is.EqualTo(ChiselPaintComponents.X | ChiselPaintComponents.Y)); Assert.That(paint[2, 2].x, Is.EqualTo(0.5f).Within(1e-5f), "the height stays"); @@ -284,7 +284,7 @@ public void Smoothing_EvensOutASpike() var box = Box(model, Vector3.zero); var top = SurfaceFacing(box, new float3(0, 1, 0)); var mode = ChiselPaintMode.Value("Value", ChiselPaintAttribute.TexCoord2, ChiselPaintComponents.X); - var paint = ChiselPaintBrush.PaintFor(box, top, mode, 0.25f); + var paint = ChiselPaintBrush.PaintFor(box, top, mode); paint[2, 2] = new Vector4(1, 0, 0, 0); ChiselPaintBrush.Dab(paint, mode, ChiselPaintOperation.Smooth, Vector4.zero, new Vector3(0.5f, 1, 0.5f), 2, 1, 1); Assert.That(paint[2, 2].x, Is.EqualTo(1f / 9).Within(1e-5f), "the average of it and its 8 neighbours"); @@ -312,9 +312,9 @@ public void TheWeightsOfAGroup_AlwaysSumToOne() } Assert.That(profile.GroupOf(1), Is.EqualTo(ChiselPaintComponents.X | ChiselPaintComponents.Z)); - var paint = ChiselPaintBrush.PaintFor(box, top, profile.modes[0], 0.25f); - ChiselPaintBrush.PaintFor(box, top, profile.modes[1], 0.25f); - ChiselPaintBrush.PaintFor(box, top, profile.modes[2], 0.25f); + var paint = ChiselPaintBrush.PaintFor(box, top, profile.modes[0]); + ChiselPaintBrush.PaintFor(box, top, profile.modes[1]); + ChiselPaintBrush.PaintFor(box, top, profile.modes[2]); // the weights are x, y and z; w is no mode's Assert.That((Vector3)paint[2, 2], Is.EqualTo(new Vector3(1, 0, 0)), "all rock"); @@ -332,7 +332,8 @@ public void Sampling_AndTheChannelUtilities() var box = Box(model, Vector3.zero); var top = SurfaceFacing(box, new float3(0, 1, 0)); var mode = ChiselPaintMode.Color("Color"); - var paint = ChiselPaintBrush.PaintFor(box, top, mode, 1); + box.SurfaceDefinition.surfaces[top].paintSpacing = 1; + var paint = ChiselPaintBrush.PaintFor(box, top, mode); paint.values = new[] { new Vector4(0, 0, 0, 1), new Vector4(1, 0, 0, 1), new Vector4(0, 1, 0, 1), new Vector4(1, 1, 0, 1) }; var middle = paint.Sample(paint.origin + (paint.cellU + paint.cellV) * 0.5f + Vector3.up * 3); Assert.That(Vector4.Distance(middle, new Vector4(0.5f, 0.5f, 0, 1)), Is.LessThan(1e-5f), "between the four, the grid projected along its normal"); @@ -366,6 +367,38 @@ public void PaintUnderAShaderWithoutAProfile_IsKeptButNotDrawn() Assert.That(RenderMesh(model).HasVertexAttribute(VertexAttribute.Color), Is.True, "drawn again"); } + // A surface's paint is on a grid of its paint spacing; changing the spacing resamples the paint onto a new grid + [Test] + public void ChangingThePaintSpacing_ResamplesThePaint() + { + var model = Model(); + var box = Box(model, Vector3.zero); + var top = SurfaceFacing(box, new float3(0, 1, 0)); + var paint = ChiselPaintBrush.PaintFor(box, top, ChiselPaintMode.Color("Color")); + Assert.That(paint.resolution, Is.EqualTo(new Vector2Int(5, 5)), "the default spacing, 25 cm"); + Assert.That(ChiselPaintBrush.Respace(box), Is.False, "nothing changed"); + + var old = PaintTop(box, ChiselPaintAttribute.Color, ChiselPaintComponents.XYZW); + ChiselPaintBrush.Respace(box); + Assert.That(box.GetSurfacePaint(top, ChiselPaintAttribute.Color), Is.SameAs(old), "a grid of unknown spacing takes the surface's"); + + box.SurfaceDefinition.surfaces[top].paintSpacing = 0.5f; + Assert.That(ChiselPaintBrush.Respace(box), Is.True); + var respaced = box.GetSurfacePaint(top, ChiselPaintAttribute.Color); + Assert.That(respaced, Is.Not.SameAs(old)); + Assert.That(respaced.resolution, Is.EqualTo(new Vector2Int(3, 3))); + Assert.That(respaced.spacing, Is.EqualTo(0.5f)); + for (int j = 0; j < 3; j++) + { + for (int i = 0; i < 3; i++) + { + var point = respaced.origin + i * respaced.cellU + j * respaced.cellV; + Assert.That(Vector4.Distance(respaced[i, j], old.Sample(point)), Is.LessThan(1e-5f), $"point {i}, {j}"); + } + } + Assert.That(box.SurfacePaint.Count, Is.EqualTo(1)); + } + // The grid moves with its generator [Test] public void Paint_MovesWithItsGenerator() diff --git a/Core/1.Input/Surfaces/ChiselSurface.cs b/Core/1.Input/Surfaces/ChiselSurface.cs index f5157db3..a04e770e 100644 --- a/Core/1.Input/Surfaces/ChiselSurface.cs +++ b/Core/1.Input/Surfaces/ChiselSurface.cs @@ -70,6 +70,13 @@ public sealed class ChiselSurface [SerializeField] internal ChiselMaterial chiselMaterial = new(); public SurfaceDetails surfaceDetails = SurfaceDetails.Default; + // How far apart the points the surface is painted at (and cut at) are, in metres; 0 for the default. Only painting + // reads it, so it stays out of the details that go into every brush polygon. + public float paintSpacing; + + public const string kPaintSpacingName = nameof(paintSpacing); + public const float kDefaultPaintSpacing = 0.25f; + public float PaintSpacing => paintSpacing > 0 ? paintSpacing : kDefaultPaintSpacing; public bool HasMaterial { get { return chiselMaterial.material != null; } } diff --git a/Editor/EditorTools/ChiselPaintTool.cs b/Editor/EditorTools/ChiselPaintTool.cs index f86eb5b6..7a1a2581 100644 --- a/Editor/EditorTools/ChiselPaintTool.cs +++ b/Editor/EditorTools/ChiselPaintTool.cs @@ -14,13 +14,12 @@ namespace Chisel.Editors [EditorTool("Chisel Paint")] sealed class ChiselPaintTool : EditorTool { - static readonly GUIContent kModeContent = new("Mode", "What to paint: the modes of the paint profile of the surface's shader."); + static readonly GUIContent kModeContent = new("Mode", "What to paint: the modes of the project's paint profiles, by name. A surface takes a mode when the paint profile of its shader has one of that name."); static readonly GUIContent kOperationContent = new("Operation", "Paint: towards the color or value. Add / Subtract: a value up or down. Smooth: towards the neighbouring paint. Erase: back to the default."); static readonly GUIContent kColorContent = new("Color"); static readonly GUIContent kRadiusContent = new("Radius", "The brush's radius, in metres."); static readonly GUIContent kStrengthContent = new("Strength", "How much a dab moves the paint towards what it paints."); static readonly GUIContent kHardnessContent = new("Hardness", "How much of the radius is painted at full strength; beyond it the brush falls off smoothly."); - static readonly GUIContent kCellSizeContent = new("Cell Size", "How finely a surface painted for the first time is divided: its paint is a grid of this size, and the surface is cut at its points."); static readonly GUIContent kFillContent = new("Fill Surface", "Sets the whole of the last surface under the mouse to the color or value."); static readonly GUIContent kChannelsContent = new("Channels", "Works on the paint of the last surface under the mouse, in the mode's attribute."); static readonly GUIContent kCopyContent = new("Copy", "Copies the first component's paint to the second."); @@ -28,14 +27,14 @@ sealed class ChiselPaintTool : EditorTool static readonly GUIContent kInvertContent = new("Invert", "Turns the first component's paint over within the mode's range."); static readonly string[] kComponentNames = { "X (R)", "Y (G)", "Z (B)", "W (A)" }; - static int s_Mode; + // the mode painted in, by name: a surface takes it when its shader's profile has a mode of that name + static string s_ModeName; static ChiselPaintOperation s_Operation = ChiselPaintOperation.Paint; static Color s_Color = Color.red; static float s_Value = 1; static float s_Radius = 0.5f; static float s_Strength = 0.5f; static float s_Hardness = 0.5f; - static float s_CellSize = 0.25f; static bool s_ShowChannels; static int s_ChannelFrom, s_ChannelTo = 1; @@ -90,6 +89,27 @@ static Hit Pick(Vector2 mousePosition) return default; } + static string NameOf(ChiselPaintProfile profile, int index) + { + var name = profile.modes[index].name; + return string.IsNullOrEmpty(name) ? $"Mode {index}" : name; + } + + // The profile's mode of the chosen name, -1 when it has none + static int ModeIndex(ChiselPaintProfile profile) + { + if (profile == null) + return -1; + for (int i = 0; i < profile.modes.Count; i++) + { + if (NameOf(profile, i) == s_ModeName) + return i; + } + return -1; + } + + static bool Paintable(Hit hit) => hit.valid && ModeIndex(hit.profile) >= 0; + public override void OnToolGUI(EditorWindow window) { if (window is not SceneView) @@ -102,10 +122,14 @@ public override void OnToolGUI(EditorWindow window) { s_Hover = Pick(evt.mousePosition); if (s_Hover.valid && s_Hover.profile != null) + { s_Last = s_Hover; + if (string.IsNullOrEmpty(s_ModeName) && s_Hover.profile.modes.Count > 0) + s_ModeName = NameOf(s_Hover.profile, 0); + } } var hit = s_Hover; - if (hit.valid && hit.profile != null) + if (Paintable(hit)) { Handles.color = new Color(1, 1, 1, 0.8f); Handles.DrawWireDisc(hit.point, hit.normal, s_Radius); @@ -122,7 +146,7 @@ public override void OnToolGUI(EditorWindow window) HandleUtility.Repaint(); break; case EventType.MouseDown: - if (evt.button != 0 || evt.alt || !hit.valid || hit.profile == null) + if (evt.button != 0 || evt.alt || !Paintable(hit)) break; // control-click takes the paint under the mouse as the brush's color or value if (evt.control) @@ -141,7 +165,7 @@ public override void OnToolGUI(EditorWindow window) case EventType.MouseDrag: if (GUIUtility.hotControl != id) break; - if (hit.valid && hit.profile != null) + if (Paintable(hit)) Dab(hit); evt.Use(); break; @@ -159,9 +183,10 @@ public override void OnToolGUI(EditorWindow window) static void Sample(Hit hit) { - if (s_Mode < 0 || s_Mode >= hit.profile.modes.Count) + var index = ModeIndex(hit.profile); + if (index < 0) return; - var mode = hit.profile.modes[s_Mode]; + var mode = hit.profile.modes[index]; var paint = hit.generator.GetSurfacePaint(hit.surfaceIndex, mode.attribute); var value = paint != null ? paint.Sample(hit.generator.transform.InverseTransformPoint(hit.point)) : mode.defaultValue; if (mode.kind == ChiselPaintKind.Color) @@ -185,11 +210,12 @@ static bool LastPaint(out ChiselSurfacePaint paint, out ChiselPaintMode mode) { paint = null; mode = default; - if (!s_Last.valid || !s_Last.generator || s_Last.profile == null || s_Mode < 0 || s_Mode >= s_Last.profile.modes.Count) + var index = ModeIndex(s_Last.profile); + if (!s_Last.valid || !s_Last.generator || index < 0) return false; - mode = s_Last.profile.modes[s_Mode]; + mode = s_Last.profile.modes[index]; Undo.RecordObject(s_Last.generator, "Chisel Paint"); - paint = ChiselPaintBrush.PaintFor(s_Last.generator, s_Last.surfaceIndex, mode, s_CellSize); + paint = ChiselPaintBrush.PaintFor(s_Last.generator, s_Last.surfaceIndex, mode); return paint != null; } @@ -201,18 +227,19 @@ static void Changed() static void Dab(Hit hit) { - if (s_Mode < 0 || s_Mode >= hit.profile.modes.Count) + var index = ModeIndex(hit.profile); + if (index < 0) return; - var mode = hit.profile.modes[s_Mode]; + var mode = hit.profile.modes[index]; Undo.RecordObject(hit.generator, "Chisel Paint"); - var paint = ChiselPaintBrush.PaintFor(hit.generator, hit.surfaceIndex, mode, s_CellSize); + var paint = ChiselPaintBrush.PaintFor(hit.generator, hit.surfaceIndex, mode); if (paint == null) return; var target = mode.kind == ChiselPaintKind.Color ? (Vector4)s_Color : Vector4.one * s_Value; var scale = hit.generator.transform.lossyScale; var radius = s_Radius / Mathf.Max(Mathf.Abs(scale.x), Mathf.Max(Mathf.Abs(scale.y), Mathf.Abs(scale.z)), 1e-5f); var center = hit.generator.transform.InverseTransformPoint(hit.point); - if (ChiselPaintBrush.Dab(paint, mode, s_Operation, target, center, radius, s_Strength, s_Hardness, hit.profile.GroupOf(s_Mode))) + if (ChiselPaintBrush.Dab(paint, mode, s_Operation, target, center, radius, s_Strength, s_Hardness, hit.profile.GroupOf(index))) { hit.generator.PaintChanged(); EditorUtility.SetDirty(hit.generator); @@ -229,9 +256,9 @@ static void NotPaintable(EditorWindow window, Hit hit, Vector2 mousePosition) return; // solid, as a tooltip is: a panel's color with a border round it s_TooltipStyle ??= new GUIStyle(EditorStyles.label) { padding = new RectOffset(6, 6, 3, 3), wordWrap = false }; - var content = new GUIContent(hit.shader - ? $"The shader {hit.shader.name} is not set up to be paintable: it has no paint profile." - : "The surface has no shader to paint for."); + var content = new GUIContent(!hit.shader ? "The surface\nhas no shader\nto paint for" + : hit.profile == null ? $"The shader {hit.shader.name}\nis not set up to be paintable\nit has no paint profile" + : $"The shader {hit.shader.name}\nhas no paint mode \"{s_ModeName}\"\nin its paint profile"); var size = s_TooltipStyle.CalcSize(content); var rect = new Rect(mousePosition + new Vector2(16, 20), size); Handles.BeginGUI(); @@ -241,40 +268,102 @@ static void NotPaintable(EditorWindow window, Hit hit, Vector2 mousePosition) Handles.EndGUI(); } + // The overlay's width, and what its rows have of it + internal const float kPanelWidth = 260; + const float kRowWidth = kPanelWidth - 6; + + static readonly List s_ModeNames = new(); + + // The mode of the chosen name: the last painted surface's, else the first profile's that has one + static bool ChosenMode(out ChiselPaintMode mode) + { + var index = ModeIndex(s_Last.profile); + if (index >= 0) + { + mode = s_Last.profile.modes[index]; + return true; + } + foreach (var profile in ChiselPaintProfiles.All) + { + index = ModeIndex(profile); + if (index >= 0) + { + mode = profile.modes[index]; + return true; + } + } + mode = default; + return false; + } + + // Every mode name of the project's profiles, as toggle buttons wrapping within the panel + static void ModeButtons() + { + s_ModeNames.Clear(); + foreach (var profile in ChiselPaintProfiles.All) + { + for (int i = 0; i < profile.modes.Count; i++) + { + var name = NameOf(profile, i); + if (!s_ModeNames.Contains(name)) + s_ModeNames.Add(name); + } + } + if (s_ModeNames.Count == 0) + { + EditorGUILayout.HelpBox("The project has no paint profiles.", MessageType.None); + return; + } + if (!s_ModeNames.Contains(s_ModeName)) + s_ModeName = s_ModeNames[0]; + + EditorGUILayout.LabelField(kModeContent); + var style = EditorStyles.miniButton; + for (int n = 0; n < s_ModeNames.Count;) + { + using (new EditorGUILayout.HorizontalScope()) + { + var used = 0f; + do + { + var content = new GUIContent(s_ModeNames[n]); + var width = Mathf.Min(kRowWidth, style.CalcSize(content).x + 8); + if (used > 0 && used + width > kRowWidth) + break; + if (GUILayout.Toggle(s_ModeNames[n] == s_ModeName, content, style, GUILayout.Width(width))) + s_ModeName = s_ModeNames[n]; + used += width + style.margin.horizontal; + n++; + } while (n < s_ModeNames.Count); + GUILayout.FlexibleSpace(); + } + } + } + // The tool's settings, in its overlay internal static void PanelGUI() { - var hit = s_Hover; var labelWidth = EditorGUIUtility.labelWidth; EditorGUIUtility.labelWidth = 90; { - // the modes of the surface last painted on, so they stay while the mouse is over the panel - var profile = s_Last.valid ? s_Last.profile : null; - if (profile == null || profile.modes.Count == 0) - EditorGUILayout.HelpBox("Hover over a surface whose shader has a paint profile.", MessageType.None); - else + ModeButtons(); + if (ChosenMode(out var chosen)) { - var names = new GUIContent[profile.modes.Count]; - for (int i = 0; i < names.Length; i++) - names[i] = new GUIContent(string.IsNullOrEmpty(profile.modes[i].name) ? $"Mode {i}" : profile.modes[i].name); - s_Mode = Mathf.Clamp(EditorGUILayout.Popup(kModeContent, s_Mode, names), 0, names.Length - 1); - var mode = profile.modes[s_Mode]; - if (mode.kind == ChiselPaintKind.Color) + if (chosen.kind == ChiselPaintKind.Color) s_Color = EditorGUILayout.ColorField(kColorContent, s_Color, true, true, false); else - s_Value = EditorGUILayout.Slider(new GUIContent(names[s_Mode].text), s_Value, mode.range.x, mode.range.y); + s_Value = EditorGUILayout.Slider(new GUIContent(s_ModeName), s_Value, chosen.range.x, chosen.range.y); } s_Operation = (ChiselPaintOperation)EditorGUILayout.EnumPopup(kOperationContent, s_Operation); s_Radius = Mathf.Max(0.01f, EditorGUILayout.FloatField(kRadiusContent, s_Radius)); s_Strength = EditorGUILayout.Slider(kStrengthContent, s_Strength, 0, 1); s_Hardness = EditorGUILayout.Slider(kHardnessContent, s_Hardness, 0, 1); - s_CellSize = Mathf.Max(0.01f, EditorGUILayout.FloatField(kCellSizeContent, s_CellSize)); - using (new EditorGUI.DisabledScope(!s_Last.valid)) + using (new EditorGUI.DisabledScope(!Paintable(s_Last))) { if (GUILayout.Button(kFillContent) && LastPaint(out var paint, out var mode)) { var target = mode.kind == ChiselPaintKind.Color ? (Vector4)s_Color : Vector4.one * s_Value; - ChiselPaintBrush.Fill(paint, mode, target, s_Last.profile.GroupOf(s_Mode)); + ChiselPaintBrush.Fill(paint, mode, target, s_Last.profile.GroupOf(ModeIndex(s_Last.profile))); Changed(); } s_ShowChannels = EditorGUILayout.Foldout(s_ShowChannels, kChannelsContent, true); @@ -322,7 +411,7 @@ sealed class ChiselPaintOverlay : UnityEditor.Overlays.IMGUIOverlay, UnityEditor public override void OnGUI() { // one width, whatever it shows - using (new EditorGUILayout.VerticalScope(GUILayout.Width(260))) + using (new EditorGUILayout.VerticalScope(GUILayout.Width(ChiselPaintTool.kPanelWidth))) ChiselPaintTool.PanelGUI(); } } diff --git a/Editor/EditorTools/ChiselUVToolCommon.cs b/Editor/EditorTools/ChiselUVToolCommon.cs index 0f96290d..d7bc1e86 100644 --- a/Editor/EditorTools/ChiselUVToolCommon.cs +++ b/Editor/EditorTools/ChiselUVToolCommon.cs @@ -92,6 +92,7 @@ void Reset() surfaceFlagsProp = null; UV0Prop = null; hotspotProp = null; + paintSpacingProp = null; initialized = false; } @@ -129,6 +130,7 @@ void UpdateSurfaceSelection() hotspotProp = surfaceDetailsProp.FindPropertyRelative(nameof(SurfaceDetails.hotspot)); } chiselMaterialProp = surfaceProp.FindPropertyRelative(ChiselSurface.kChiselMaterialName); + paintSpacingProp = surfaceProp.FindPropertyRelative(ChiselSurface.kPaintSpacingName); initialized = true; } @@ -147,6 +149,7 @@ void UpdateAllSurfaces() SerializedProperty surfaceFlagsProp; SerializedProperty UV0Prop; SerializedProperty hotspotProp; + SerializedProperty paintSpacingProp; [SerializeField] UnityEngine.Object[] undoableObjects = Array.Empty(); [SerializeField] ChiselSelectedSurface[] surfaces = Array.Empty(); @@ -536,6 +539,12 @@ public void OnSceneSettingsGUI(SceneView sceneView) EditorGUI.PropertyField(position, UV0Prop, SurfaceDetailsPropertyDrawer.kUV0Contents, true); } + if (paintSpacingProp != null && SurfaceDetailsPropertyDrawer.TakesPaint(surfaces[0].Surface.RenderMaterial)) + { + position = EditorGUILayout.GetControlRect(true, EditorGUIUtility.singleLineHeight); + SurfaceDetailsPropertyDrawer.PaintSpacingField(position, paintSpacingProp, SurfaceDetailsPropertyDrawer.kPaintSpacingContents); + } + EditorGUIUtility.labelWidth = prevLabelWidth; } if (EditorGUI.EndChangeCheck() && undoableObjects.Length > 0) @@ -555,6 +564,7 @@ public void OnSceneSettingsGUI(SceneView sceneView) serializedObject.Update(); } ChiselHotspotGroupEdit.Spread(edited, undoableObjects.Length > 1 ? "Modified materials" : "Modified material"); + SurfaceDetailsPropertyDrawer.Respace(edited, undoableObjects.Length > 1 ? "Modified materials" : "Modified material"); } } diff --git a/Editor/PropertyDrawers/SurfaceDetailsPropertyDrawer.cs b/Editor/PropertyDrawers/SurfaceDetailsPropertyDrawer.cs index 231045d1..26dd5eb5 100644 --- a/Editor/PropertyDrawers/SurfaceDetailsPropertyDrawer.cs +++ b/Editor/PropertyDrawers/SurfaceDetailsPropertyDrawer.cs @@ -13,6 +13,39 @@ public sealed class SurfaceDetailsPropertyDrawer : PropertyDrawer public readonly static GUIContent kUV0Contents = new("UV"); public readonly static GUIContent kSurfaceFlagsContents = new("Surface Flags"); public readonly static GUIContent kSmoothingGroupContents = new("Smoothing Groups"); + public readonly static GUIContent kPaintSpacingContents = new("Paint Spacing", "How far apart the points this surface is painted at are, in metres: the surface is cut at them. Changing it resamples the paint it has."); + + // Whether the material's shader takes paint (has a paint profile) + public static bool TakesPaint(Material material) + { + return material && ChiselPaintProfiles.Find(material.shader) != null; + } + + // A paint spacing field; 0 (not set) shows the default + public static void PaintSpacingField(Rect position, SerializedProperty spacingProp, GUIContent label) + { + EditorGUI.BeginProperty(position, label, spacingProp); + EditorGUI.showMixedValue = spacingProp.hasMultipleDifferentValues; + EditorGUI.BeginChangeCheck(); + var spacing = EditorGUI.FloatField(position, label, spacingProp.floatValue > 0 ? spacingProp.floatValue : ChiselSurface.kDefaultPaintSpacing); + if (EditorGUI.EndChangeCheck()) + spacingProp.floatValue = Mathf.Max(0.01f, spacing); + EditorGUI.showMixedValue = false; + EditorGUI.EndProperty(); + } + + // Resamples the paint of the generators these details are on, after their paint spacing changed + public static void Respace(System.Collections.Generic.List surfaces, string undoName) + { + foreach (var surface in surfaces) + { + if (surface.node is not ChiselGeneratorComponent generator || generator.SurfacePaint.Count == 0) + continue; + Undo.RecordObject(generator, undoName); + if (ChiselPaintBrush.Respace(generator)) + EditorUtility.SetDirty(generator); + } + } // The material beside these details (the first selected surface's) static Material MaterialOf(SerializedProperty property) @@ -34,6 +67,15 @@ static ChiselHotspotLayout LayoutOf(SerializedProperty property) return materialProp != null ? HotspotTransformGUI.LayoutOf(materialProp.objectReferenceValue as Material) : null; } + // The paint spacing beside these details (on their surface) + static SerializedProperty PaintSpacingOf(SerializedProperty property) + { + var path = property.propertyPath; + var dot = path.LastIndexOf('.'); + var parent = dot < 0 ? string.Empty : path.Substring(0, dot + 1); + return property.serializedObject.FindProperty(parent + ChiselSurface.kPaintSpacingName); + } + static readonly System.Text.RegularExpressions.Regex kDescriptionIndex = new(@"data\[(\d+)\]\.[^.\[]+$"); static readonly System.Collections.Generic.List s_Surfaces = new(); @@ -77,7 +119,8 @@ public override float GetPropertyHeight(SerializedProperty property, GUIContent kSpacing + SurfaceFlagsPropertyDrawer.DefaultHeight + kSpacing + - SmoothingGroupPropertyDrawer.GetDefaultHeight(smoothingGroupProp.propertyPath, true); + SmoothingGroupPropertyDrawer.GetDefaultHeight(smoothingGroupProp.propertyPath, true) + + (TakesPaint(MaterialOf(property)) && PaintSpacingOf(property) != null ? kSpacing + EditorGUIUtility.singleLineHeight : 0); } public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) @@ -126,6 +169,14 @@ public override void OnGUI(Rect position, SerializedProperty property, GUIConten position.height = SmoothingGroupPropertyDrawer.GetDefaultHeight(smoothingGroupProp.propertyPath, true); EditorGUI.PropertyField(position, smoothingGroupProp, !hasLabel ? GUIContent.none : kSmoothingGroupContents, false); + + var paintSpacingProp = PaintSpacingOf(property); + if (paintSpacingProp != null && TakesPaint(MaterialOf(property))) + { + position.y += position.height + kSpacing; + position.height = EditorGUIUtility.singleLineHeight; + PaintSpacingField(position, paintSpacingProp, !hasLabel ? GUIContent.none : kPaintSpacingContents); + } } if (EditorGUI.EndChangeCheck()) { @@ -134,6 +185,7 @@ public override void OnGUI(Rect position, SerializedProperty property, GUIConten if (ChiselTextureLockEdit.KeepLook(surfaces, wasWorld, "Modified surface")) property.serializedObject.Update(); SpreadToConnected(property); + Respace(surfaces, "Modified surface"); } } catch (ExitGUIException) { } From 6f01c4f8a1ca7e2204253a2ca78176bc5a1a6870 Mon Sep 17 00:00:00 2001 From: Sander van Rossen Date: Mon, 26 Oct 2026 09:12:12 +0100 Subject: [PATCH 10/27] Paint: one settings asset, a tool among the Chisel tools The paint profiles are plain data in one project asset (ChiselPaintSettings, Assets/Chisel/ChiselPaintSettings.asset), edited in Project Settings > Chisel > Paint: the shaders, each with its remove button, and the chosen one's modes. A material's Chisel section says whether its shader is paintable, and its Configure Paintability Of Shader button opens that page on the shader's profile, adding one when it has none. The package's per-shader profile assets are gone; a new settings asset starts with the test shaders. The paint tool is one of the Chisel tools, with a brush icon. Its overlay shows the operations as wrapping toggles with icons (Add and Subtract only for a value), swatches and the recently painted colors under the color, and a thumbnail of the blob a dab paints next to strength and hardness. --- .../Components/Paint/ChiselPaintProfile.cs | 4 +- .../Components/Paint/ChiselPaintProfiles.cs | 15 +- .../Components/Paint/ChiselPaintSettings.cs | 76 ++++++++ .../Paint/ChiselPaintSettings.cs.meta | 11 ++ Components/Tests/ChiselPaintTests.cs | 7 +- Editor Resources/Icons/d_paint_add On.png | Bin 0 -> 302 bytes .../Icons/d_paint_add On.png.meta | 108 +++++++++++ Editor Resources/Icons/d_paint_add On@2x.png | Bin 0 -> 471 bytes .../Icons/d_paint_add On@2x.png.meta | 108 +++++++++++ Editor Resources/Icons/d_paint_add.png | Bin 0 -> 289 bytes Editor Resources/Icons/d_paint_add.png.meta | 108 +++++++++++ Editor Resources/Icons/d_paint_add@2x.png | Bin 0 -> 471 bytes .../Icons/d_paint_add@2x.png.meta | 108 +++++++++++ Editor Resources/Icons/d_paint_erase On.png | Bin 0 -> 356 bytes .../Icons/d_paint_erase On.png.meta | 108 +++++++++++ .../Icons/d_paint_erase On@2x.png | Bin 0 -> 573 bytes .../Icons/d_paint_erase On@2x.png.meta | 108 +++++++++++ Editor Resources/Icons/d_paint_erase.png | Bin 0 -> 371 bytes Editor Resources/Icons/d_paint_erase.png.meta | 108 +++++++++++ Editor Resources/Icons/d_paint_erase@2x.png | Bin 0 -> 584 bytes .../Icons/d_paint_erase@2x.png.meta | 108 +++++++++++ Editor Resources/Icons/d_paint_paint On.png | Bin 0 -> 303 bytes .../Icons/d_paint_paint On.png.meta | 108 +++++++++++ .../Icons/d_paint_paint On@2x.png | Bin 0 -> 585 bytes .../Icons/d_paint_paint On@2x.png.meta | 108 +++++++++++ Editor Resources/Icons/d_paint_paint.png | Bin 0 -> 299 bytes Editor Resources/Icons/d_paint_paint.png.meta | 108 +++++++++++ Editor Resources/Icons/d_paint_paint@2x.png | Bin 0 -> 546 bytes .../Icons/d_paint_paint@2x.png.meta | 108 +++++++++++ Editor Resources/Icons/d_paint_smooth On.png | Bin 0 -> 312 bytes .../Icons/d_paint_smooth On.png.meta | 108 +++++++++++ .../Icons/d_paint_smooth On@2x.png | Bin 0 -> 566 bytes .../Icons/d_paint_smooth On@2x.png.meta | 108 +++++++++++ Editor Resources/Icons/d_paint_smooth.png | Bin 0 -> 295 bytes .../Icons/d_paint_smooth.png.meta | 108 +++++++++++ Editor Resources/Icons/d_paint_smooth@2x.png | Bin 0 -> 577 bytes .../Icons/d_paint_smooth@2x.png.meta | 108 +++++++++++ .../Icons/d_paint_subtract On.png | Bin 0 -> 193 bytes .../Icons/d_paint_subtract On.png.meta | 108 +++++++++++ .../Icons/d_paint_subtract On@2x.png | Bin 0 -> 247 bytes .../Icons/d_paint_subtract On@2x.png.meta | 108 +++++++++++ Editor Resources/Icons/d_paint_subtract.png | Bin 0 -> 185 bytes .../Icons/d_paint_subtract.png.meta | 108 +++++++++++ .../Icons/d_paint_subtract@2x.png | Bin 0 -> 257 bytes .../Icons/d_paint_subtract@2x.png.meta | 108 +++++++++++ Editor Resources/Icons/d_paint_tool On.png | Bin 0 -> 599 bytes .../Icons/d_paint_tool On.png.meta | 108 +++++++++++ Editor Resources/Icons/d_paint_tool.png | Bin 0 -> 601 bytes Editor Resources/Icons/d_paint_tool.png.meta | 108 +++++++++++ Editor Resources/Icons/paint_add On.png | Bin 0 -> 302 bytes Editor Resources/Icons/paint_add On.png.meta | 108 +++++++++++ Editor Resources/Icons/paint_add On@2x.png | Bin 0 -> 471 bytes .../Icons/paint_add On@2x.png.meta | 108 +++++++++++ Editor Resources/Icons/paint_add.png | Bin 0 -> 319 bytes Editor Resources/Icons/paint_add.png.meta | 108 +++++++++++ Editor Resources/Icons/paint_add@2x.png | Bin 0 -> 509 bytes Editor Resources/Icons/paint_add@2x.png.meta | 108 +++++++++++ Editor Resources/Icons/paint_erase On.png | Bin 0 -> 356 bytes .../Icons/paint_erase On.png.meta | 108 +++++++++++ Editor Resources/Icons/paint_erase On@2x.png | Bin 0 -> 573 bytes .../Icons/paint_erase On@2x.png.meta | 108 +++++++++++ Editor Resources/Icons/paint_erase.png | Bin 0 -> 385 bytes Editor Resources/Icons/paint_erase.png.meta | 108 +++++++++++ Editor Resources/Icons/paint_erase@2x.png | Bin 0 -> 630 bytes .../Icons/paint_erase@2x.png.meta | 108 +++++++++++ Editor Resources/Icons/paint_paint On.png | Bin 0 -> 303 bytes .../Icons/paint_paint On.png.meta | 108 +++++++++++ Editor Resources/Icons/paint_paint On@2x.png | Bin 0 -> 585 bytes .../Icons/paint_paint On@2x.png.meta | 108 +++++++++++ Editor Resources/Icons/paint_paint.png | Bin 0 -> 324 bytes Editor Resources/Icons/paint_paint.png.meta | 108 +++++++++++ Editor Resources/Icons/paint_paint@2x.png | Bin 0 -> 602 bytes .../Icons/paint_paint@2x.png.meta | 108 +++++++++++ Editor Resources/Icons/paint_smooth On.png | Bin 0 -> 312 bytes .../Icons/paint_smooth On.png.meta | 108 +++++++++++ Editor Resources/Icons/paint_smooth On@2x.png | Bin 0 -> 566 bytes .../Icons/paint_smooth On@2x.png.meta | 108 +++++++++++ Editor Resources/Icons/paint_smooth.png | Bin 0 -> 334 bytes Editor Resources/Icons/paint_smooth.png.meta | 108 +++++++++++ Editor Resources/Icons/paint_smooth@2x.png | Bin 0 -> 629 bytes .../Icons/paint_smooth@2x.png.meta | 108 +++++++++++ Editor Resources/Icons/paint_subtract On.png | Bin 0 -> 193 bytes .../Icons/paint_subtract On.png.meta | 108 +++++++++++ .../Icons/paint_subtract On@2x.png | Bin 0 -> 247 bytes .../Icons/paint_subtract On@2x.png.meta | 108 +++++++++++ Editor Resources/Icons/paint_subtract.png | Bin 0 -> 191 bytes .../Icons/paint_subtract.png.meta | 108 +++++++++++ Editor Resources/Icons/paint_subtract@2x.png | Bin 0 -> 286 bytes .../Icons/paint_subtract@2x.png.meta | 108 +++++++++++ Editor Resources/Icons/paint_tool On.png | Bin 0 -> 602 bytes Editor Resources/Icons/paint_tool On.png.meta | 108 +++++++++++ Editor Resources/Icons/paint_tool.png | Bin 0 -> 602 bytes Editor Resources/Icons/paint_tool.png.meta | 108 +++++++++++ Editor/EditorTools/ChiselPaintTool.cs | 175 ++++++++++++++++-- .../PropertyDrawers/ChiselPaintModeDrawer.cs | 15 -- Editor/SceneView/Overlays/ChiselGlobal.cs | 3 +- .../Settings/ChiselPaintSettingsProvider.cs | 170 +++++++++++++++++ .../ChiselPaintSettingsProvider.cs.meta | 11 ++ .../ChiselSurfaceMetadataEditor.cs | 18 ++ .../URP/Paint/Paint Color Profile.asset | 22 --- .../URP/Paint/Paint Color Profile.asset.meta | 8 - .../URP/Paint/Paint Value Profile.asset | 22 --- .../URP/Paint/Paint Value Profile.asset.meta | 8 - 103 files changed, 5214 insertions(+), 103 deletions(-) create mode 100644 Components/Components/Paint/ChiselPaintSettings.cs create mode 100644 Components/Components/Paint/ChiselPaintSettings.cs.meta create mode 100644 Editor Resources/Icons/d_paint_add On.png create mode 100644 Editor Resources/Icons/d_paint_add On.png.meta create mode 100644 Editor Resources/Icons/d_paint_add On@2x.png create mode 100644 Editor Resources/Icons/d_paint_add On@2x.png.meta create mode 100644 Editor Resources/Icons/d_paint_add.png create mode 100644 Editor Resources/Icons/d_paint_add.png.meta create mode 100644 Editor Resources/Icons/d_paint_add@2x.png create mode 100644 Editor Resources/Icons/d_paint_add@2x.png.meta create mode 100644 Editor Resources/Icons/d_paint_erase On.png create mode 100644 Editor Resources/Icons/d_paint_erase On.png.meta create mode 100644 Editor Resources/Icons/d_paint_erase On@2x.png create mode 100644 Editor Resources/Icons/d_paint_erase On@2x.png.meta create mode 100644 Editor Resources/Icons/d_paint_erase.png create mode 100644 Editor Resources/Icons/d_paint_erase.png.meta create mode 100644 Editor Resources/Icons/d_paint_erase@2x.png create mode 100644 Editor Resources/Icons/d_paint_erase@2x.png.meta create mode 100644 Editor Resources/Icons/d_paint_paint On.png create mode 100644 Editor Resources/Icons/d_paint_paint On.png.meta create mode 100644 Editor Resources/Icons/d_paint_paint On@2x.png create mode 100644 Editor Resources/Icons/d_paint_paint On@2x.png.meta create mode 100644 Editor Resources/Icons/d_paint_paint.png create mode 100644 Editor Resources/Icons/d_paint_paint.png.meta create mode 100644 Editor Resources/Icons/d_paint_paint@2x.png create mode 100644 Editor Resources/Icons/d_paint_paint@2x.png.meta create mode 100644 Editor Resources/Icons/d_paint_smooth On.png create mode 100644 Editor Resources/Icons/d_paint_smooth On.png.meta create mode 100644 Editor Resources/Icons/d_paint_smooth On@2x.png create mode 100644 Editor Resources/Icons/d_paint_smooth On@2x.png.meta create mode 100644 Editor Resources/Icons/d_paint_smooth.png create mode 100644 Editor Resources/Icons/d_paint_smooth.png.meta create mode 100644 Editor Resources/Icons/d_paint_smooth@2x.png create mode 100644 Editor Resources/Icons/d_paint_smooth@2x.png.meta create mode 100644 Editor Resources/Icons/d_paint_subtract On.png create mode 100644 Editor Resources/Icons/d_paint_subtract On.png.meta create mode 100644 Editor Resources/Icons/d_paint_subtract On@2x.png create mode 100644 Editor Resources/Icons/d_paint_subtract On@2x.png.meta create mode 100644 Editor Resources/Icons/d_paint_subtract.png create mode 100644 Editor Resources/Icons/d_paint_subtract.png.meta create mode 100644 Editor Resources/Icons/d_paint_subtract@2x.png create mode 100644 Editor Resources/Icons/d_paint_subtract@2x.png.meta create mode 100644 Editor Resources/Icons/d_paint_tool On.png create mode 100644 Editor Resources/Icons/d_paint_tool On.png.meta create mode 100644 Editor Resources/Icons/d_paint_tool.png create mode 100644 Editor Resources/Icons/d_paint_tool.png.meta create mode 100644 Editor Resources/Icons/paint_add On.png create mode 100644 Editor Resources/Icons/paint_add On.png.meta create mode 100644 Editor Resources/Icons/paint_add On@2x.png create mode 100644 Editor Resources/Icons/paint_add On@2x.png.meta create mode 100644 Editor Resources/Icons/paint_add.png create mode 100644 Editor Resources/Icons/paint_add.png.meta create mode 100644 Editor Resources/Icons/paint_add@2x.png create mode 100644 Editor Resources/Icons/paint_add@2x.png.meta create mode 100644 Editor Resources/Icons/paint_erase On.png create mode 100644 Editor Resources/Icons/paint_erase On.png.meta create mode 100644 Editor Resources/Icons/paint_erase On@2x.png create mode 100644 Editor Resources/Icons/paint_erase On@2x.png.meta create mode 100644 Editor Resources/Icons/paint_erase.png create mode 100644 Editor Resources/Icons/paint_erase.png.meta create mode 100644 Editor Resources/Icons/paint_erase@2x.png create mode 100644 Editor Resources/Icons/paint_erase@2x.png.meta create mode 100644 Editor Resources/Icons/paint_paint On.png create mode 100644 Editor Resources/Icons/paint_paint On.png.meta create mode 100644 Editor Resources/Icons/paint_paint On@2x.png create mode 100644 Editor Resources/Icons/paint_paint On@2x.png.meta create mode 100644 Editor Resources/Icons/paint_paint.png create mode 100644 Editor Resources/Icons/paint_paint.png.meta create mode 100644 Editor Resources/Icons/paint_paint@2x.png create mode 100644 Editor Resources/Icons/paint_paint@2x.png.meta create mode 100644 Editor Resources/Icons/paint_smooth On.png create mode 100644 Editor Resources/Icons/paint_smooth On.png.meta create mode 100644 Editor Resources/Icons/paint_smooth On@2x.png create mode 100644 Editor Resources/Icons/paint_smooth On@2x.png.meta create mode 100644 Editor Resources/Icons/paint_smooth.png create mode 100644 Editor Resources/Icons/paint_smooth.png.meta create mode 100644 Editor Resources/Icons/paint_smooth@2x.png create mode 100644 Editor Resources/Icons/paint_smooth@2x.png.meta create mode 100644 Editor Resources/Icons/paint_subtract On.png create mode 100644 Editor Resources/Icons/paint_subtract On.png.meta create mode 100644 Editor Resources/Icons/paint_subtract On@2x.png create mode 100644 Editor Resources/Icons/paint_subtract On@2x.png.meta create mode 100644 Editor Resources/Icons/paint_subtract.png create mode 100644 Editor Resources/Icons/paint_subtract.png.meta create mode 100644 Editor Resources/Icons/paint_subtract@2x.png create mode 100644 Editor Resources/Icons/paint_subtract@2x.png.meta create mode 100644 Editor Resources/Icons/paint_tool On.png create mode 100644 Editor Resources/Icons/paint_tool On.png.meta create mode 100644 Editor Resources/Icons/paint_tool.png create mode 100644 Editor Resources/Icons/paint_tool.png.meta create mode 100644 Editor/Settings/ChiselPaintSettingsProvider.cs create mode 100644 Editor/Settings/ChiselPaintSettingsProvider.cs.meta delete mode 100644 Package Resources/Materials/URP/Paint/Paint Color Profile.asset delete mode 100644 Package Resources/Materials/URP/Paint/Paint Color Profile.asset.meta delete mode 100644 Package Resources/Materials/URP/Paint/Paint Value Profile.asset delete mode 100644 Package Resources/Materials/URP/Paint/Paint Value Profile.asset.meta diff --git a/Components/Components/Paint/ChiselPaintProfile.cs b/Components/Components/Paint/ChiselPaintProfile.cs index 1dc67a08..d4ee97b1 100644 --- a/Components/Components/Paint/ChiselPaintProfile.cs +++ b/Components/Components/Paint/ChiselPaintProfile.cs @@ -50,8 +50,8 @@ public static ChiselPaintMode Value(string name, ChiselPaintAttribute attribute, /// What paint means to a shader: the modes a surface drawn with it can be painted in. The shader shows the paint; /// Chisel only writes it to the vertex attributes the modes name. Two modes don't write one component of one attribute. /// - [CreateAssetMenu(menuName = "Chisel/Paint Profile", fileName = "Paint Profile")] - public sealed class ChiselPaintProfile : ScriptableObject + [Serializable] + public sealed class ChiselPaintProfile { public Shader shader; public List modes = new(); diff --git a/Components/Components/Paint/ChiselPaintProfiles.cs b/Components/Components/Paint/ChiselPaintProfiles.cs index 2e2b6e92..da4ebfca 100644 --- a/Components/Components/Paint/ChiselPaintProfiles.cs +++ b/Components/Components/Paint/ChiselPaintProfiles.cs @@ -4,7 +4,7 @@ namespace Chisel.Components { - /// The project's paint profiles, by the shader they are for + /// The project's paint profiles (), by the shader they are for public static class ChiselPaintProfiles { static Dictionary s_ByShader; @@ -64,14 +64,15 @@ static void Load() if (s_ByShader == null) { s_ByShader = new Dictionary(); -#if UNITY_EDITOR - foreach (var guid in UnityEditor.AssetDatabase.FindAssets("t:" + nameof(ChiselPaintProfile))) + var settings = ChiselPaintSettings.Project; + if (settings) { - var profile = UnityEditor.AssetDatabase.LoadAssetAtPath(UnityEditor.AssetDatabase.GUIDToAssetPath(guid)); - if (profile != null && profile.shader != null) - s_ByShader.TryAdd(profile.shader, profile); + foreach (var profile in settings.profiles) + { + if (profile != null && profile.shader != null) + s_ByShader.TryAdd(profile.shader, profile); + } } -#endif } } diff --git a/Components/Components/Paint/ChiselPaintSettings.cs b/Components/Components/Paint/ChiselPaintSettings.cs new file mode 100644 index 00000000..4800cd8b --- /dev/null +++ b/Components/Components/Paint/ChiselPaintSettings.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace Chisel.Components +{ + /// + /// The project's paint profiles: one per shader whose surfaces can be painted, in one asset, edited in + /// Project Settings > Chisel > Paint + /// + public sealed class ChiselPaintSettings : ScriptableObject + { + public const string kAssetPath = "Assets/Chisel/ChiselPaintSettings.asset"; + + public List profiles = new(); + + static ChiselPaintSettings s_Project; + + /// The project's paint settings, null when it has none + public static ChiselPaintSettings Project + { + get + { +#if UNITY_EDITOR + if (!s_Project) + { + s_Project = UnityEditor.AssetDatabase.LoadAssetAtPath(kAssetPath); + if (!s_Project) + { + foreach (var guid in UnityEditor.AssetDatabase.FindAssets("t:" + nameof(ChiselPaintSettings))) + { + s_Project = UnityEditor.AssetDatabase.LoadAssetAtPath(UnityEditor.AssetDatabase.GUIDToAssetPath(guid)); + if (s_Project) + break; + } + } + } +#endif + return s_Project; + } + set + { + s_Project = value; + Changed(); + } + } + + /// The first profile for a shader, or null + public ChiselPaintProfile Find(Shader shader) + { + if (!shader) + return null; + foreach (var profile in profiles) + { + if (profile != null && profile.shader == shader) + return profile; + } + return null; + } + + /// Called after the profiles changed + public static void Changed() + { + ChiselPaintProfiles.SetDirty(); + ChiselPaintManager.SetDirty(); + } + + void OnEnable() + { + if (!s_Project) + s_Project = this; + Changed(); + } + + void OnValidate() => Changed(); + } +} diff --git a/Components/Components/Paint/ChiselPaintSettings.cs.meta b/Components/Components/Paint/ChiselPaintSettings.cs.meta new file mode 100644 index 00000000..dac6f671 --- /dev/null +++ b/Components/Components/Paint/ChiselPaintSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b5b91d9b1dbe48f4bddbe9e18c8945c2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Components/Tests/ChiselPaintTests.cs b/Components/Tests/ChiselPaintTests.cs index c4335afb..c92a9048 100644 --- a/Components/Tests/ChiselPaintTests.cs +++ b/Components/Tests/ChiselPaintTests.cs @@ -24,12 +24,10 @@ public void SetUp() // the boxes' material takes paint in the attributes these paint in (its own, so a project's materials don't matter) s_Material = new Material(Shader.Find("Universal Render Pipeline/Lit")) { name = "PaintTestMaterial" }; created.Add(s_Material); - profile = ScriptableObject.CreateInstance(); - profile.shader = s_Material.shader; + profile = new ChiselPaintProfile { shader = s_Material.shader }; profile.modes.Add(ChiselPaintMode.Color("Color")); profile.modes.Add(ChiselPaintMode.Value("Value", ChiselPaintAttribute.TexCoord2, ChiselPaintComponents.XYZW)); profile.modes.Add(ChiselPaintMode.Value("Mask", ChiselPaintAttribute.TexCoord3, ChiselPaintComponents.XYZW)); - created.Add(profile); ChiselPaintProfiles.Register(profile); } @@ -299,8 +297,7 @@ public void TheWeightsOfAGroup_AlwaysSumToOne() var model = Model(); var box = Box(model, Vector3.zero); var top = SurfaceFacing(box, new float3(0, 1, 0)); - var profile = ScriptableObject.CreateInstance(); - created.Add(profile); + var profile = new ChiselPaintProfile(); profile.modes.Add(ChiselPaintMode.Value("Rock", ChiselPaintAttribute.TexCoord4, ChiselPaintComponents.X, 1)); profile.modes.Add(ChiselPaintMode.Value("Moss", ChiselPaintAttribute.TexCoord4, ChiselPaintComponents.Y, 0)); profile.modes.Add(ChiselPaintMode.Value("Sand", ChiselPaintAttribute.TexCoord4, ChiselPaintComponents.Z, 0)); diff --git a/Editor Resources/Icons/d_paint_add On.png b/Editor Resources/Icons/d_paint_add On.png new file mode 100644 index 0000000000000000000000000000000000000000..89e4ca8bc0d0dbf514ff1ade7bc583dfc25610c3 GIT binary patch literal 302 zcmV+}0nz@6P)Nklhe!=O#x!f5S9mNL?4=Wl1< zys;}Y<3AD#@d}8YTlpKE{nyMa*IMV&ZnDU7R}FX2?SUgO0bU%k;s~`XikG&sYu9Mv z>P~rsQ`Wq5_5nES>>;Q`&Qh>{p5N2f_T`3DzkOC*MY zSem;hm6@dx-hr9o(Qkm2F0X-ak!Tw8%a6c-Hg1fMQeDVkrWHoUr!E&s3(`&~ifgQ8 zKfA(oF<5(`{?%1EH)L^t+09dHio>H1LPXGV_07*qoM6N<$f?z_0 AOaK4? literal 0 HcmV?d00001 diff --git a/Editor Resources/Icons/d_paint_add On.png.meta b/Editor Resources/Icons/d_paint_add On.png.meta new file mode 100644 index 00000000..264ec84d --- /dev/null +++ b/Editor Resources/Icons/d_paint_add On.png.meta @@ -0,0 +1,108 @@ +fileFormatVersion: 2 +guid: c3443b72b9844bf1b5e0465ecaadcc2f +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 2 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 1 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor Resources/Icons/d_paint_add On@2x.png b/Editor Resources/Icons/d_paint_add On@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9daa98ca0bf1ceab2d6af3886c4b10a7570b9731 GIT binary patch literal 471 zcmV;|0Vw{7P)MfqDsG+xlNbSasI*w?Bm!0sDuJ;|scY-+CnD=e zPsd|f#7SV&O6;0^U6>uNfH^Qxar2A7rWFd(@TTGR$!Aj&+9ncsP4wFsfg(2n$xUEm zC{}d1RrWFBM)gHQn06V}v@K7Tm_3ky4?lG*MX+*@|w`24K901dXf4ErVS-9T-PPYX-1BX7(EV&_n z;0lN&;Th16l%r3uJx#=UW&H(sT#|&MRWe$Zgs6NIY9W{UHB%Lbo0h({Gl~e)rbguI zd^@7+R~peQwGq*f$>`=`i2dgN;HGBu150MuL_d?8z$`a`-AxfF68#36h(2Lswckku zM8{k8?W?%?U0~D6y8scfI+h|bRw)BrYcc=vX-3d7{o^$4OL9NYzW^&wb*<}FQF#CW N002ovPDHLkV1gj_&(i<^ literal 0 HcmV?d00001 diff --git a/Editor Resources/Icons/d_paint_add On@2x.png.meta b/Editor Resources/Icons/d_paint_add On@2x.png.meta new file mode 100644 index 00000000..0b2ea81c --- /dev/null +++ b/Editor Resources/Icons/d_paint_add On@2x.png.meta @@ -0,0 +1,108 @@ +fileFormatVersion: 2 +guid: b623d48380aa4c51be7fda749d92d58b +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 1 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor Resources/Icons/d_paint_add.png b/Editor Resources/Icons/d_paint_add.png new file mode 100644 index 0000000000000000000000000000000000000000..86582aa635bd76ae1c6bbc51c341304aeef4d1d4 GIT binary patch literal 289 zcmV++0p9+JP)!sih)W8#%N%K|(S#n=xNNv0yTuCksLUPB+j)(De zmx7hazS-l2Oq&BsQ!@ez;PjSTePUdTVLD|fYC^*8fwCzDf6&Y9-0sq(kEJ#Nw)Xh| zTqH)16;<#ik_H>89lI2e#1zaZM`20^;zwhQK^jbrj~}8h9;?RNxXL=!4fUn^TI03` n{wU@gql$2UdNokQbUN_@|3^9q04#|q00000NkvXXu0mjf4Fq(0 literal 0 HcmV?d00001 diff --git a/Editor Resources/Icons/d_paint_add.png.meta b/Editor Resources/Icons/d_paint_add.png.meta new file mode 100644 index 00000000..ce62b28d --- /dev/null +++ b/Editor Resources/Icons/d_paint_add.png.meta @@ -0,0 +1,108 @@ +fileFormatVersion: 2 +guid: 20df1199ee464d91b95351397b9e6105 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 2 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 1 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor Resources/Icons/d_paint_add@2x.png b/Editor Resources/Icons/d_paint_add@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..7051013b01f895f49c47af383c479eb7784d2b93 GIT binary patch literal 471 zcmV;|0Vw{7P)qF)O|0~;d_4}l|*T>zJXt~CNi9Bc!} zf!z1sf&027Yy$!w`@jhJlIHu}XiRHM;PE2l^-FEjM6^wefaivGb0e^nn}EwrU~VW9 zGBYNjlyFLKL~kcV&P^iV#)Q~!H;>PRPPe0unF>Whwgc<|Z@_D$(;~AKyel@PI;INI zDzF0F0)syjmcOT=8XW*9w2M>VN_|=pD&t;I;f7;cD$H(zjV@6y^l8Zq8?9wuWF09s zRV_u_Ot9&QT|e|$L`&e;<^$LjUA)~iS2iRObt(MBPrzeE68iUV;4Vl)SG5ZUai%8F z6#}872jH9q6rvHZoX}~AbxbRT2$?*kcY))ni;*QWY(qPfo4_zPf%SP2C>q+WMJHeF z-|?mqaPqtkDyD9~n`lfc5peRg{{R0hx|r0y6L{!xoDFsc+y8Mn{Q*1vanUldDn$SQ N002ovPDHLkV1fi%$jtx% literal 0 HcmV?d00001 diff --git a/Editor Resources/Icons/d_paint_add@2x.png.meta b/Editor Resources/Icons/d_paint_add@2x.png.meta new file mode 100644 index 00000000..80e5c08e --- /dev/null +++ b/Editor Resources/Icons/d_paint_add@2x.png.meta @@ -0,0 +1,108 @@ +fileFormatVersion: 2 +guid: bc0386e8f7ba4b239af7def8f3521c47 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 1 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor Resources/Icons/d_paint_erase On.png b/Editor Resources/Icons/d_paint_erase On.png new file mode 100644 index 0000000000000000000000000000000000000000..9b805386d1023da43ae1ab89913434bf5450d392 GIT binary patch literal 356 zcmV-q0h|7bP)h$ADqsMt0E=E5CBlXMA#m4LMZcLlj{A}x zC2dGLamFv-MBa~2fOH~hHxcYDBMEktC8HHXQa5>T-+*^X>ypl_NvcsxP?@Bc2^C2` zBrRms86zl65d1n(KH7n#d3$Xyr}e>wMSGUCHKoyhykUV&;2d~xdv61;z%8)k(!~}I z<2B$IxQ_&@zz#6ygD?IE%h^Y2Y7c$j26&1D|F&<~fCM1C?Rt~|0000W^Rm literal 0 HcmV?d00001 diff --git a/Editor Resources/Icons/d_paint_erase On.png.meta b/Editor Resources/Icons/d_paint_erase On.png.meta new file mode 100644 index 00000000..df31fdca --- /dev/null +++ b/Editor Resources/Icons/d_paint_erase On.png.meta @@ -0,0 +1,108 @@ +fileFormatVersion: 2 +guid: 19a4a1e941b44564ae4e5b99d1c6310c +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 2 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 1 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor Resources/Icons/d_paint_erase On@2x.png b/Editor Resources/Icons/d_paint_erase On@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..68031153f9703a866a3027d3b98cea4109470f4f GIT binary patch literal 573 zcmV-D0>b@?P)a_y6iGpuHYEiWB|$)&Djg+lq7;E3B~JWC z8_!v*&3(+qHXdoUK6`iHXJ=<-&xItGY2{MYfoee;3U1Z^1}K4X^jvr#$~lQwCjJWe z46K@X|N4?~SC;Erz&dd0tRrF03h)Q` zc&9kYf96jmZP@3X#Jz1+d+6Mb?M@)#6n|!7KP9c(ce_0hP2%->E`TZtxUntx%>m&O z?*eE`wUGnDE#3yum*-s&7dhfh0Ny-rf~W)YuU9+;s0+kvXP(CpLtEp6q&HsiCV|gs%+bx1=Zc*>T#c=(Y>D)+VqG9NTO4 z02l$kfPcUP8+!!21m4;2Mc=!xwGCh&IGk!~LNc(vKJ!J|L9AItgu&her@%>a*Vo>N z@A&vnh{wYCN5EdJS=+?_`_A;fR>#8BdnrJ!TW7W#F{AL*kDr#D00000 LNkvXXu0mjf3nc|i literal 0 HcmV?d00001 diff --git a/Editor Resources/Icons/d_paint_erase On@2x.png.meta b/Editor Resources/Icons/d_paint_erase On@2x.png.meta new file mode 100644 index 00000000..4abd817b --- /dev/null +++ b/Editor Resources/Icons/d_paint_erase On@2x.png.meta @@ -0,0 +1,108 @@ +fileFormatVersion: 2 +guid: 2ec66989b1e948e8bb2eb98cfd5ef902 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 1 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor Resources/Icons/d_paint_erase.png b/Editor Resources/Icons/d_paint_erase.png new file mode 100644 index 0000000000000000000000000000000000000000..2f62b7a0a692aa730e60a1e08623af837b40d4f6 GIT binary patch literal 371 zcmV-(0gV2MP)TG2F zE(96y4J-p2@&~}8?)yGk8dBK+tH7FkOHP5ra!xFs52-u}j4M_JA)WjsR%;4o6%K$0 z;7ai_5S#(`PMQmxDQO+m|imO=-! zx*RB8N{4-52lxsa)s})tr`qR~WNn(A>zu-iR6atkBo7<|H}XG#BZ+BU*)jrK^{4G4 z%mC}ayDC_9yITTQ8f_bR8n^^ro!||)3vEBiHzC`8B-6(*bqCh&ZA;=v`vJDMQ783< R3daBd002ovPDHLkV1hC0ndkrj literal 0 HcmV?d00001 diff --git a/Editor Resources/Icons/d_paint_erase.png.meta b/Editor Resources/Icons/d_paint_erase.png.meta new file mode 100644 index 00000000..acaa6958 --- /dev/null +++ b/Editor Resources/Icons/d_paint_erase.png.meta @@ -0,0 +1,108 @@ +fileFormatVersion: 2 +guid: 9dc2cef8ce2845d9b0b982143b21fe18 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 2 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 1 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor Resources/Icons/d_paint_erase@2x.png b/Editor Resources/Icons/d_paint_erase@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..100939d42a9f0c6fc76705f12fe8181005d094e7 GIT binary patch literal 584 zcmV-O0=NB%P)S|;a;inAO-|JXK z_!i(~MBGb&Gs3Gx%%?8+25?=w3A{GIV-Z@IFQP;m32*`n7Oq6qZn<9z^AGSj-3H&_ zA+roT06qaHQMKpb;r<)o5cr-xF}(pkmaKQ>DHrzfqlnNw&cXMAvnUyp13oIa9hvu{ zVyY8S1s|6|tgSW?&}v468Ll7;Tz|Ba5ix86FB4$t^EwfsFb7unUtn}3)9QRA{ko9p z^O1-NpaTJK%lw-KUgf|uW1q)P1h*D}BY~@v+3?Y4ZI2JYsj<%$)Dr=&tktXnkQ)bL z>8Y{LgH!K-AJKJl_>yZ*s<=)>+G^)bGp`chWN1sh2QFl;nN6b152fhz4CoERRdqpK zgtOk0D8nmp=qLA#B5A=T;3;q|*S*;POYH8*+5lb`Q6t0YSA~a{*;n9FnMJbjxxBY? z@mR)P;vul7lOp-@82Bv?_)N<-Ex55b@Z~Nw&{`D9T=%lk|GNgEuKjZ_74YAJw&NCR W6tPv5*;!Kn0000Z#iF`-v7aKb=s`60o)yc7uknj;B5Ed z()us}Phy;@Cu$w=Ssxw>P?CM)kJPCb`E=xKoB*G|Ay5Mkz+3Fy#r-$_-G^g0^S=5V zS+CSn^-|p%%HEbxg*G)m2E2^%dZe+P03{{|cI90?3b1YLn(m}(K7beC3AhI?fZM3M z*&F^G=`&43UwiMrY3!)v02yk#J(_tez}k1+Bwr&)>S`NE&Cmb<002ovPDHLkV1hg; BgrWcd literal 0 HcmV?d00001 diff --git a/Editor Resources/Icons/d_paint_paint On.png.meta b/Editor Resources/Icons/d_paint_paint On.png.meta new file mode 100644 index 00000000..6aaf67ed --- /dev/null +++ b/Editor Resources/Icons/d_paint_paint On.png.meta @@ -0,0 +1,108 @@ +fileFormatVersion: 2 +guid: 00eb6e2e9b2e413a8703d1c41c9014af +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 2 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 1 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor Resources/Icons/d_paint_paint On@2x.png b/Editor Resources/Icons/d_paint_paint On@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..a9d9ed911e080575f67a7569d1e751884f809774 GIT binary patch literal 585 zcmV-P0=E5$P)n(8VfF_0BN2~6B(of<|!glpdSFaW$X>=bYd zIOp$C1jGz1v6c3xCB4zib|kG!irEig$&rtNRp1%04txhr0vpakSa#&AlC~ZBFZ;L7 zNX$Sn6#S?CmZUY0&o+RDgKxV(e~y0&U!;Ie@V){O4^n5Skk7UKib0E3K)69%ZBH+Z~v+dyp4b~+0iW02pvm0kTj9>)XUKr z9^0tFe|mi80C)tv2d)E~*(o>p<7z9Z?1KTopsf01-PopMbx zKm+)F1Ab?)yP1e+dy+0x3bmJj(*C9aX`S_l*~kHBl-9`H4L@9S)%nz2uYy#SsAFM)RkCy~A2 z(@CaV;cq+Q_HE8s%uM{zLT)t0Ap&w(T8gyR3%yDeH3F*Fsijz`85`u%7>0oS*8=_k X-JqZ>jTi#j00000NkvXXu0mjf=YLT(P)m^RK-U)oVP*IG4`>Q4~^NsJ`Q*fx46phn6ahOQ#0L=b0C5$= zHh#oF5_Xl5|B-RIyV?8cf#J^XoSEN$&YZb3*EJdqDzpf=Q+cqsN;=R^QdG7%Jx72= z;8oZdR{;0n9s@oAlfVtI3p9OuQ~`Vx*b)4XBJtXMq6*;efob3$@Io^`kzYHm5Iz8$ z1AD-gIY$-4>w>#71h_un*MVQ50$dh&UI8vIJdc2Iu*~qR0&K7e)yK2Ky`|lTHrN#K z5jX~33jP-uOao6Vz)rGRU=tVymeke~{2~oJjR2>U%tN;HYz2uJ>Fsuq25ueu@$rML z0n5OCm%jdx`IQFVkARwHz9RUl*1Rr|!TfVxr=I=JEuf|_zX9H=?#sGBCc^vu20qyl zTSNq$U<<%IMIln@6mbcBRsyDiKY$YMgf_?=ur0WS8@07H^F!bWxXl9Z76H4#Y;=f9 zs?P{s%K}dlV1p0o^3a)3cUl4u{0@8#mMfFRUH)$eH&_!91xa@Xd4c?ZEl1aIIWwDBry<1K8(BWR;w;{g;45y8qv^pmG~G7h^c z%n#vZ!k_;oFPR3>2ZLVO5ix4{O$y5ME&X57WDCs<@~rqb1})M`L`)*$G$Qsp)l3sE z7J&g4apfn@uv99<2&LL#@*}OO&T%GP;s7_T@8woPwJ{bx+sAH_U$BvcHC$)>96MOY z9!_wF2W%Je7H)Bv$#Z$G;-b~kYp2D>DiD~jJ>@xU#gA~0r)+70&3v~g33UqHk(Jkf zPk)>{^W2?A^N&5KOI0ySZV^`n{=(PHr(Dur<=-&#mfk;8Z-Ec3+%i?UrnTiB0bQN)VJ$fl9nYMO4^h3DF_^oH-Km}Hr@eVa~g-h2C!&zI>oo=GKCvo z@IG*2q&3h5dO^rXV7U8w^8#@i8bHL@M5Znl) zzWDR-k%uS?BFxKTz5{D59=HlT1zrLlfG@y1U@43BNjtXa4bmDS{$d`<9?ln1`xs@9R=PGI!)e`wXm~an3#K z9{}Hih~whaIB9%_3H&1jG9e2?c0NqRO<<3+@dkJTtb{@~uw=a;meyE`+u-x^xGL$f zq!me*By}YnNO~n{*UET_0cQ%$D0y`rp^}$9VL{TIq@JXs@H{m%GTOS~C`h4u)(x$^ zZfjg=%zwr1#SF&rLozBx-c6+mqnZlPGz1ey_1_El1+4jSO8#=OT>t<807*qoM6N<$ Ef>Z7L9{>OV literal 0 HcmV?d00001 diff --git a/Editor Resources/Icons/d_paint_smooth On@2x.png.meta b/Editor Resources/Icons/d_paint_smooth On@2x.png.meta new file mode 100644 index 00000000..dec457fb --- /dev/null +++ b/Editor Resources/Icons/d_paint_smooth On@2x.png.meta @@ -0,0 +1,108 @@ +fileFormatVersion: 2 +guid: d76d31cea9c14bbd8ddf5481a5e4101d +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 1 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor Resources/Icons/d_paint_smooth.png b/Editor Resources/Icons/d_paint_smooth.png new file mode 100644 index 0000000000000000000000000000000000000000..4725b3e8fd6787111ad13aa1e4207fc8d7584d31 GIT binary patch literal 295 zcmV+?0oeYDP)z%mM^u#dUOuV59& z7{o7qMD4c@d$HLL^9@dLhh-dKI?UOaq8}fXI#|L6UU7tbTw*t%dpN}d=CO_iOr-h} zZ+OC4m{aZIP`o$mhY=KLNBrS@}f1Xr1)`fKTevIH4SYGcEuC002ovPDHLkV1h;Efd>Ep literal 0 HcmV?d00001 diff --git a/Editor Resources/Icons/d_paint_smooth.png.meta b/Editor Resources/Icons/d_paint_smooth.png.meta new file mode 100644 index 00000000..2bc33c03 --- /dev/null +++ b/Editor Resources/Icons/d_paint_smooth.png.meta @@ -0,0 +1,108 @@ +fileFormatVersion: 2 +guid: 03deb3be725549f4ac62e768f30fa2b2 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 2 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 1 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor Resources/Icons/d_paint_smooth@2x.png b/Editor Resources/Icons/d_paint_smooth@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..bcb36b356f1d05682be545ac40a2824c099336a3 GIT binary patch literal 577 zcmV-H0>1r;P)pOeQ9hglXo7!#?|R_W!TzOfxq(XP9V*%5EgUCHPf_x#?cHkC{#M-L5X`9|R;z}YL3n4Rj8)w@P zF@c*nmd-APCcbM1tZS3?=dm*gnz;Br?%_73(q-Kcd_6qTO@iy?xr7&DEN8aFnO;r8 z^0Ei#QalUELrf=+Ji%vNO(80KH}D`lRgsf(J5J#cc3~V}!nhB$3NFOhjhN2{VYOC} zO^2AtWYwmaAH-99!`3kBB#wr+GRpo%+|6ZJW84di`b(@@TrHhU>*c$%I1}YM-j`qV zDFih%f6<9?DqOVwhmU zW50jnHJ;;DbkowvWC59Qr5&!GE8_{Aj<}Z``i+lxftPr*)c4IQ{}c9$-5Bnk4^L*) z{ObMB&+&4|UK6C1IQN0t3l^bcZ7?Wgi>w`l){xPT1Q>;2$Y@6b3>kj_A2E$5Uvcn@ P00000NkvXXu0mjfHy;H2 literal 0 HcmV?d00001 diff --git a/Editor Resources/Icons/d_paint_smooth@2x.png.meta b/Editor Resources/Icons/d_paint_smooth@2x.png.meta new file mode 100644 index 00000000..15a25544 --- /dev/null +++ b/Editor Resources/Icons/d_paint_smooth@2x.png.meta @@ -0,0 +1,108 @@ +fileFormatVersion: 2 +guid: a5cc05f531f74511a0c876c70057a4da +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 1 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor Resources/Icons/d_paint_subtract On.png b/Editor Resources/Icons/d_paint_subtract On.png new file mode 100644 index 0000000000000000000000000000000000000000..3d39c07566300b735242f28ee2b2e9c0261a6754 GIT binary patch literal 193 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`9iA?ZAr*7ZPITmAR^)M=e=U{& z!p=bP;`W^>;(G(D8{VDzGQA^6kd1rS2WJnSfAc>)R=LQ0|L8m?1(C0sPp4_>J+-yF zl9}YuwQLiU{)50TuTEZzZZIzBw)GU7T~}eQbl9S$dF{Wp$y362Gl@S4mI!rzS>*p9 s@BbYR;nT))zUvkyrhkcjqsg*w;r*!U;A7_tfNo;&boFyt=akR{00*~CF8}}l literal 0 HcmV?d00001 diff --git a/Editor Resources/Icons/d_paint_subtract On.png.meta b/Editor Resources/Icons/d_paint_subtract On.png.meta new file mode 100644 index 00000000..686d2cef --- /dev/null +++ b/Editor Resources/Icons/d_paint_subtract On.png.meta @@ -0,0 +1,108 @@ +fileFormatVersion: 2 +guid: fd378fe425624525b31016e599baa716 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 2 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 1 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor Resources/Icons/d_paint_subtract On@2x.png b/Editor Resources/Icons/d_paint_subtract On@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..6bb4963d07002638a73648ea22e4c51ae8b3d8f1 GIT binary patch literal 247 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=ffJeV#6kAr*7pPIKfsWFX*re@Rz* z!E;8I_lh0rFBDxr$ThJSJePDyNl{>xZd$$Nagy8=r=VZ74mr_>~p}9b(>)WzqNTzIuV4x5m+_IVMWQ v{fd>z*P1;Z1$7x~@(2i8Og_YLu7+W?rsTQrQ_d{_`h>yL)z4*}Q$iB}S!!Nu literal 0 HcmV?d00001 diff --git a/Editor Resources/Icons/d_paint_subtract On@2x.png.meta b/Editor Resources/Icons/d_paint_subtract On@2x.png.meta new file mode 100644 index 00000000..7937664c --- /dev/null +++ b/Editor Resources/Icons/d_paint_subtract On@2x.png.meta @@ -0,0 +1,108 @@ +fileFormatVersion: 2 +guid: c1430c9c5523401cb76a13d7ddd3bbbb +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 1 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor Resources/Icons/d_paint_subtract.png b/Editor Resources/Icons/d_paint_subtract.png new file mode 100644 index 0000000000000000000000000000000000000000..3bc8c8cf51a3d915ab0b8453fc421afdff4a7fa6 GIT binary patch literal 185 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`4W2HJAr*7ZPCCfjpupoQ@Ab%6 zy^vvhp~?0{hR19=3(Wc1l9@Oa9aU{L#AjXl|7)Q}w(g|R&MW&FUTB21Monj$bH$PW z&4E{UmmHYzx`Ay*@&Se(J3F~q*XrgoEY9cZT9?qko3Z!oo#wX9tIIkWTUttbUM)TM j^wjYc1^w3CfR(OI zbsYQpci=P_!nbCy{w0h2`$Z(b5^@8d5R4HwfVaRH^A@}SK)xk{_>H|r00000NkvXX Hu0mjfy zJ!=&~6o#Lf-S~lpAVgvWL9tS>38V-_kR)g!DAi$AA}te*;E=i;99TGr*UCbHGpF zh&*4=7F0eH{0DFlcrEWg=-b+7f*%6j0JFe*lbHC7@EKs!e8(^a3 z2QD>*M~|9VAxJ1D^MZG^6_wT(fy*iI=v9+%0_-914!Ej>D{NN-J_6e&M;e*mXMIHQ zd%#`bJ}|C|<}~7IxUQ}j=dA^d06PLd&~p&;xJN{Y&}U#$jV6j}C7=-Srb1r{6m#K` zyq!~RzorCC3V%NVF@dfCPxM1?LSC3iT+moHN0c@KoPZ(3PkY|m=iN>QOaPy>T}htr zC7=e5>hfT-9L;wG47Z%yCL#k_+2?&Fk7~08g~SB-8gRoztRA)i*VFByg@oGx?f_p> z3Qnp3bZrW-SHKFes$&+wm`>QrT%TTWK;!FQ*1SSKaM+tf;FpFsTVZozXjbmA787i! lz?4#4|4VQa0sI#N{s3K(sbXTzzzhHY002ovPDHLkV1i+@0lEMH literal 0 HcmV?d00001 diff --git a/Editor Resources/Icons/d_paint_tool On.png.meta b/Editor Resources/Icons/d_paint_tool On.png.meta new file mode 100644 index 00000000..489ed423 --- /dev/null +++ b/Editor Resources/Icons/d_paint_tool On.png.meta @@ -0,0 +1,108 @@ +fileFormatVersion: 2 +guid: aff3c09b63ec41b5a6b5f4b5df21411c +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor Resources/Icons/d_paint_tool.png b/Editor Resources/Icons/d_paint_tool.png new file mode 100644 index 0000000000000000000000000000000000000000..5fcf7710de6af7ec40fe6d00ca475e8140824fcc GIT binary patch literal 601 zcmV-f0;c_mP)y z&r4N76vsbz?v;!dLSU#tP(+IYH$saRA(+@ANN^)YD|c=CFQRS#fG*tVvLecjytN2v z9BKFp=Qhz!%^Vu;C>pCL(+U`034~P5~Xks|qkx;Z+0} zmD8jOyi)Nklhe!=O#x!f5S9mNL?4=Wl1< zys;}Y<3AD#@d}8YTlpKE{nyMa*IMV&ZnDU7R}FX2?SUgO0bU%k;s~`XikG&sYu9Mv z>P~rsQ`Wq5_5nES>>;Q`&Qh>{p5N2f_T`3DzkOC*MY zSem;hm6@dx-hr9o(Qkm2F0X-ak!Tw8%a6c-Hg1fMQeDVkrWHoUr!E&s3(`&~ifgQ8 zKfA(oF<5(`{?%1EH)L^t+09dHio>H1LPXGV_07*qoM6N<$f?z_0 AOaK4? literal 0 HcmV?d00001 diff --git a/Editor Resources/Icons/paint_add On.png.meta b/Editor Resources/Icons/paint_add On.png.meta new file mode 100644 index 00000000..24eed2bd --- /dev/null +++ b/Editor Resources/Icons/paint_add On.png.meta @@ -0,0 +1,108 @@ +fileFormatVersion: 2 +guid: 94520502ee1240849707f3f9082aea34 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 2 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 1 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor Resources/Icons/paint_add On@2x.png b/Editor Resources/Icons/paint_add On@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9daa98ca0bf1ceab2d6af3886c4b10a7570b9731 GIT binary patch literal 471 zcmV;|0Vw{7P)MfqDsG+xlNbSasI*w?Bm!0sDuJ;|scY-+CnD=e zPsd|f#7SV&O6;0^U6>uNfH^Qxar2A7rWFd(@TTGR$!Aj&+9ncsP4wFsfg(2n$xUEm zC{}d1RrWFBM)gHQn06V}v@K7Tm_3ky4?lG*MX+*@|w`24K901dXf4ErVS-9T-PPYX-1BX7(EV&_n z;0lN&;Th16l%r3uJx#=UW&H(sT#|&MRWe$Zgs6NIY9W{UHB%Lbo0h({Gl~e)rbguI zd^@7+R~peQwGq*f$>`=`i2dgN;HGBu150MuL_d?8z$`a`-AxfF68#36h(2Lswckku zM8{k8?W?%?U0~D6y8scfI+h|bRw)BrYcc=vX-3d7{o^$4OL9NYzW^&wb*<}FQF#CW N002ovPDHLkV1gj_&(i<^ literal 0 HcmV?d00001 diff --git a/Editor Resources/Icons/paint_add On@2x.png.meta b/Editor Resources/Icons/paint_add On@2x.png.meta new file mode 100644 index 00000000..e1b43fdc --- /dev/null +++ b/Editor Resources/Icons/paint_add On@2x.png.meta @@ -0,0 +1,108 @@ +fileFormatVersion: 2 +guid: 134fe52db2b14593b42d0d0b9748595f +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 1 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor Resources/Icons/paint_add.png b/Editor Resources/Icons/paint_add.png new file mode 100644 index 0000000000000000000000000000000000000000..95899e79a5346f7ac3b4993947e0d2fba81c548a GIT binary patch literal 319 zcmV-F0l@x=P)tQ^Gi-i{b}o?}4M%D+Yzc8H@I>_9I7m2)wBKBhUpt?7Ywzr%o{6IXZSp--CQJ zV3`xKX);+v(Od!>pfA65dIIK3UI1SbG3lD0{t7u*fwh`UfcIQbbarqfG{t-FtgF~4 zjZ)hfgY1C|tP>4hf=+PBxuZoB*!MIV(-s R2Q~lz002ovPDHLkV1iYIg~k8? literal 0 HcmV?d00001 diff --git a/Editor Resources/Icons/paint_add.png.meta b/Editor Resources/Icons/paint_add.png.meta new file mode 100644 index 00000000..7db630aa --- /dev/null +++ b/Editor Resources/Icons/paint_add.png.meta @@ -0,0 +1,108 @@ +fileFormatVersion: 2 +guid: a4deb5a7ab534b2480bff519dd5a7f4a +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 2 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 1 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor Resources/Icons/paint_add@2x.png b/Editor Resources/Icons/paint_add@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..b6f2d6f2fb393b96b8b1d23e990249793f75762e GIT binary patch literal 509 zcmV<-+@XYc_;5E}&>z3Y4dALAZEw6U-e;k2@{(??LS69g-(gWX#R zOkfPT-MifF?GM9dGc%h%*_la_<#|q*Fe~`>fK0@-lVB75+@Pg;FQu>A#0nT0Xb8ND z45iPsy#hU%?LKg&`d#}QcvLnE!7NF@EwjTZFaxF{u6-QXv>F4SLf!|TNeyU|NZ=#U zZ*2r@*95Ar39Jparoz?P$B1X5w+$=zFGX$SBXv1k$n!U%n~L7s%)`HmX(NgQ;0XAV zbki`B^Ly&uV;2}H8;t?54cr40O(K)s&{wZB;F@-E4&3-Ljj-{IFI9yrY9yQ?GNU>l zUoqVnNIUC13rw(!jiudJ5gn7JEQm>8{Kk508|>%q-~0fF;t-~z53pPUai;p;ftNW! zSjjJVP(fIRSx|{$B2tHw2!UtdqIuEhry-(fDnu@zwjw(H+ltge!~%zjrY!I_3&GGa zOrl@bHG!gQ0z2y>U=#fY5{N!vW4YhT3RLpERTa}$xVyln6#}LBJjvGkCNh#cfm!BG z;NZ98Wf8lEZR~1BP%-`GH0hwa%JDk9oF?H9e~ESjD)O+200000NkvXXu0mjf&D`e9 literal 0 HcmV?d00001 diff --git a/Editor Resources/Icons/paint_add@2x.png.meta b/Editor Resources/Icons/paint_add@2x.png.meta new file mode 100644 index 00000000..3156a0ed --- /dev/null +++ b/Editor Resources/Icons/paint_add@2x.png.meta @@ -0,0 +1,108 @@ +fileFormatVersion: 2 +guid: f5a53f297efc4643a1c121f1d9b78c1b +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 1 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor Resources/Icons/paint_erase On.png b/Editor Resources/Icons/paint_erase On.png new file mode 100644 index 0000000000000000000000000000000000000000..9b805386d1023da43ae1ab89913434bf5450d392 GIT binary patch literal 356 zcmV-q0h|7bP)h$ADqsMt0E=E5CBlXMA#m4LMZcLlj{A}x zC2dGLamFv-MBa~2fOH~hHxcYDBMEktC8HHXQa5>T-+*^X>ypl_NvcsxP?@Bc2^C2` zBrRms86zl65d1n(KH7n#d3$Xyr}e>wMSGUCHKoyhykUV&;2d~xdv61;z%8)k(!~}I z<2B$IxQ_&@zz#6ygD?IE%h^Y2Y7c$j26&1D|F&<~fCM1C?Rt~|0000W^Rm literal 0 HcmV?d00001 diff --git a/Editor Resources/Icons/paint_erase On.png.meta b/Editor Resources/Icons/paint_erase On.png.meta new file mode 100644 index 00000000..9dfd36bd --- /dev/null +++ b/Editor Resources/Icons/paint_erase On.png.meta @@ -0,0 +1,108 @@ +fileFormatVersion: 2 +guid: 8d60dee2ab074adca6beb6b156b1a40d +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 2 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 1 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor Resources/Icons/paint_erase On@2x.png b/Editor Resources/Icons/paint_erase On@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..68031153f9703a866a3027d3b98cea4109470f4f GIT binary patch literal 573 zcmV-D0>b@?P)a_y6iGpuHYEiWB|$)&Djg+lq7;E3B~JWC z8_!v*&3(+qHXdoUK6`iHXJ=<-&xItGY2{MYfoee;3U1Z^1}K4X^jvr#$~lQwCjJWe z46K@X|N4?~SC;Erz&dd0tRrF03h)Q` zc&9kYf96jmZP@3X#Jz1+d+6Mb?M@)#6n|!7KP9c(ce_0hP2%->E`TZtxUntx%>m&O z?*eE`wUGnDE#3yum*-s&7dhfh0Ny-rf~W)YuU9+;s0+kvXP(CpLtEp6q&HsiCV|gs%+bx1=Zc*>T#c=(Y>D)+VqG9NTO4 z02l$kfPcUP8+!!21m4;2Mc=!xwGCh&IGk!~LNc(vKJ!J|L9AItgu&her@%>a*Vo>N z@A&vnh{wYCN5EdJS=+?_`_A;fR>#8BdnrJ!TW7W#F{AL*kDr#D00000 LNkvXXu0mjf3nc|i literal 0 HcmV?d00001 diff --git a/Editor Resources/Icons/paint_erase On@2x.png.meta b/Editor Resources/Icons/paint_erase On@2x.png.meta new file mode 100644 index 00000000..b4e966b0 --- /dev/null +++ b/Editor Resources/Icons/paint_erase On@2x.png.meta @@ -0,0 +1,108 @@ +fileFormatVersion: 2 +guid: 348b001c443443a58ff2403c71ff3c95 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 1 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor Resources/Icons/paint_erase.png b/Editor Resources/Icons/paint_erase.png new file mode 100644 index 0000000000000000000000000000000000000000..5e14bdcfa3d04e9b5b1db5f4e4c00613df995042 GIT binary patch literal 385 zcmV-{0e=38P)5Jk__jv^?6g1N!`gP4l=3E~=$8SnGX(9Iyji3tC1KO^3FCTg_j9Rqdau@5qA)B~RS-r@9n*pewurE5HKq zEl=X>j19{)-cX4>8Rn2#;0;&;eiD!V{mRJP)@o0dgMr0K8E4cYp`ccpZEZ zXh+FpRlr9ES^;K&)4*$qxLqNl48BLK%Yd8P`%yKjL}bIu1XT68uMyE{0v{$Ir_ako zm@&6=;r6f?9?HA`JgMpPRK${u*%deo9-@f$ga%rch>kLYs8h1TCFlK27pJYZ)->}n0ZzxP zJP@{5>&e4Sq>Z<=2SoGg4p$|hr))d=`~jQ=o@)*ND*>N@Q@}gj_e=A0j^3~~bQ(Ab z+ycG|7)Z#iF`-v7aKb=s`60o)yc7uknj;B5Ed z()us}Phy;@Cu$w=Ssxw>P?CM)kJPCb`E=xKoB*G|Ay5Mkz+3Fy#r-$_-G^g0^S=5V zS+CSn^-|p%%HEbxg*G)m2E2^%dZe+P03{{|cI90?3b1YLn(m}(K7beC3AhI?fZM3M z*&F^G=`&43UwiMrY3!)v02yk#J(_tez}k1+Bwr&)>S`NE&Cmb<002ovPDHLkV1hg; BgrWcd literal 0 HcmV?d00001 diff --git a/Editor Resources/Icons/paint_paint On.png.meta b/Editor Resources/Icons/paint_paint On.png.meta new file mode 100644 index 00000000..807e7b31 --- /dev/null +++ b/Editor Resources/Icons/paint_paint On.png.meta @@ -0,0 +1,108 @@ +fileFormatVersion: 2 +guid: 0b51fa44c4284e59ad42c4bedf4fbca0 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 2 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 1 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor Resources/Icons/paint_paint On@2x.png b/Editor Resources/Icons/paint_paint On@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..a9d9ed911e080575f67a7569d1e751884f809774 GIT binary patch literal 585 zcmV-P0=E5$P)n(8VfF_0BN2~6B(of<|!glpdSFaW$X>=bYd zIOp$C1jGz1v6c3xCB4zib|kG!irEig$&rtNRp1%04txhr0vpakSa#&AlC~ZBFZ;L7 zNX$Sn6#S?CmZUY0&o+RDgKxV(e~y0&U!;Ie@V){O4^n5Skk7UKib0E3K)69%ZBH+Z~v+dyp4b~+0iW02pvm0kTj9>)XUKr z9^0tFe|mi80C)tv2d)E~*(o>p<7z9Z?1KTopsf01-PopMbx zKm+)F1Ab?)yP1e+dy+0x3bmJj(*C9aX`S_l*~kHBl-9`H4L@9S)%nz2uYy#SsAFM)RkCy~A2 z(@CaV;cq+Q_HE8s%uM{zLT)t0Ap&w(T8gyR3%yDeH3F*Fsijz`85`u%7>0oS*8=_k X-JqZ>jTi#j00000NkvXXu0mjf=Y@}{*ur@M5A)aPD!2_e=?d$`^Dnp_VlVEx zf=fK&Eo%1xWrJ^ZRp&WeU;(!{!V-?FbUzH5a7}l<3HTM80c})+{dMOi3iDWw^BKJ3 zFzUTga6QBUUhsqmT;pzB^wJ;=F6kMI5x-2B>thYFQslG}l&3wm^C*L){LfG9TIvhm W-amf^lA7uO0000y zKWh|07>A#o-9yD9ASh};h#V;tYy=An5u%tPpkNassqAd?EyUI@poNW=Dxx;`0YV@u zNg=T^!9O4*Au)G*8z%e)hQ*vayX!Q*Fx<}0+&;heop;`uq}%Nh$r2z1hH3|vMT8f? zpw!1ifTx0;0_K48>L)QGVBtA%2Iv8Hy}tujfzLJoCIb94@K*3`;3RNOQ7~Bq_=_{A@EQEnYPY#;0163nAg}q z@Rbm_^QelYg4km6O7MY}BG>pE;8qCSc~xbOg*^to05eLs)IZgLZ@>>PM=F{5%Xmld z1>gbj5IC!fmZ-Via7U+}>o*qA0r~n*xUVT>~b8tv~$7DCD)nK1zVIJvHMkg`X3xSvF$K oC0MJ#kWyUyOK=qd{1*cD0jfo#u&AROiU0rr07*qoM6N<$g6HH3P5=M^ literal 0 HcmV?d00001 diff --git a/Editor Resources/Icons/paint_paint@2x.png.meta b/Editor Resources/Icons/paint_paint@2x.png.meta new file mode 100644 index 00000000..b20fab3b --- /dev/null +++ b/Editor Resources/Icons/paint_paint@2x.png.meta @@ -0,0 +1,108 @@ +fileFormatVersion: 2 +guid: 11176c3f4fee4800bbc95fc646106a67 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 1 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor Resources/Icons/paint_smooth On.png b/Editor Resources/Icons/paint_smooth On.png new file mode 100644 index 0000000000000000000000000000000000000000..d1f9b5f07bb08c8370ece1dbf638e7bbac442125 GIT binary patch literal 312 zcmV-80muG{P)4c?ZEl1aIIWwDBry<1K8(BWR;w;{g;45y8qv^pmG~G7h^c z%n#vZ!k_;oFPR3>2ZLVO5ix4{O$y5ME&X57WDCs<@~rqb1})M`L`)*$G$Qsp)l3sE z7J&g4apfn@uv99<2&LL#@*}OO&T%GP;s7_T@8woPwJ{bx+sAH_U$BvcHC$)>96MOY z9!_wF2W%Je7H)Bv$#Z$G;-b~kYp2D>DiD~jJ>@xU#gA~0r)+70&3v~g33UqHk(Jkf zPk)>{^W2?A^N&5KOI0ySZV^`n{=(PHr(Dur<=-&#mfk;8Z-Ec3+%i?UrnTiB0bQN)VJ$fl9nYMO4^h3DF_^oH-Km}Hr@eVa~g-h2C!&zI>oo=GKCvo z@IG*2q&3h5dO^rXV7U8w^8#@i8bHL@M5Znl) zzWDR-k%uS?BFxKTz5{D59=HlT1zrLlfG@y1U@43BNjtXa4bmDS{$d`<9?ln1`xs@9R=PGI!)e`wXm~an3#K z9{}Hih~whaIB9%_3H&1jG9e2?c0NqRO<<3+@dkJTtb{@~uw=a;meyE`+u-x^xGL$f zq!me*By}YnNO~n{*UET_0cQ%$D0y`rp^}$9VL{TIq@JXs@H{m%GTOS~C`h4u)(x$^ zZfjg=%zwr1#SF&rLozBx-c6+mqnZlPGz1ey_1_El1+4jSO8#=OT>t<807*qoM6N<$ Ef>Z7L9{>OV literal 0 HcmV?d00001 diff --git a/Editor Resources/Icons/paint_smooth On@2x.png.meta b/Editor Resources/Icons/paint_smooth On@2x.png.meta new file mode 100644 index 00000000..50579055 --- /dev/null +++ b/Editor Resources/Icons/paint_smooth On@2x.png.meta @@ -0,0 +1,108 @@ +fileFormatVersion: 2 +guid: c48acb41ff5847f29236bf04a6af63e8 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 1 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor Resources/Icons/paint_smooth.png b/Editor Resources/Icons/paint_smooth.png new file mode 100644 index 0000000000000000000000000000000000000000..23220337b5bc5f79b9299e804d1b7201fd1d35dc GIT binary patch literal 334 zcmV-U0kQsxP)xe! zqnf5=@hQTY#swB5VI3RT#(U)TYNUS^=5aU(@GW+6k2PFj2H&+7`*Gn^70b%9T*W!Q zuz&~5;SBS*!5$v56jd!_99u;IpK%!A3A{uFCs@H{R8imB>#&FNS4fjVlm_ZC z#9E=&`d0)}0F0nD^@~mtr~ys=9|HCPpMY1uQXtss1c4k_1C9ZAfFohsBdJ<27o@z89B^D~HbWu{ ztqq*1Z3i9%uw3I$fHzXeT{Hwt0i){o{pOOn_7|b0m}Ule9oVB=W(u;iz-a~JHI2Kg zjPO$_(GUs0P|`Zjnl)kci##60NfY~l%Nlf$8^B|2va#DDa1vMrwkhQf2ZBh(hQKBQn2|NR3)}-f0K0UlQ@{zSRkr8A zSPY;fa69yT$a60`F7u@NMgl2eBrXCMR5JzxF?6v)`!@t(uFh3p1Q-Tx0dIej=MC+d z6tD&DPW`t8FjNBVS9ALaEDAGo7kG-8R4WxKooB;A;J$+NS7x>25iqZ6y4(h>eI z>$wtuy~g;9>XmhaJgylf-YP7?+JVZizt#gj&B;(G(D8{VDzGQA^6kd1rS2WJnSfAc>)R=LQ0|L8m?1(C0sPp4_>J+-yF zl9}YuwQLiU{)50TuTEZzZZIzBw)GU7T~}eQbl9S$dF{Wp$y362Gl@S4mI!rzS>*p9 s@BbYR;nT))zUvkyrhkcjqsg*w;r*!U;A7_tfNo;&boFyt=akR{00*~CF8}}l literal 0 HcmV?d00001 diff --git a/Editor Resources/Icons/paint_subtract On.png.meta b/Editor Resources/Icons/paint_subtract On.png.meta new file mode 100644 index 00000000..3e5f32b2 --- /dev/null +++ b/Editor Resources/Icons/paint_subtract On.png.meta @@ -0,0 +1,108 @@ +fileFormatVersion: 2 +guid: 2bfe2d983bed47968321a163048c38d9 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 2 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 1 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor Resources/Icons/paint_subtract On@2x.png b/Editor Resources/Icons/paint_subtract On@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..6bb4963d07002638a73648ea22e4c51ae8b3d8f1 GIT binary patch literal 247 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=ffJeV#6kAr*7pPIKfsWFX*re@Rz* z!E;8I_lh0rFBDxr$ThJSJePDyNl{>xZd$$Nagy8=r=VZ74mr_>~p}9b(>)WzqNTzIuV4x5m+_IVMWQ v{fd>z*P1;Z1$7x~@(2i8Og_YLu7+W?rsTQrQ_d{_`h>yL)z4*}Q$iB}S!!Nu literal 0 HcmV?d00001 diff --git a/Editor Resources/Icons/paint_subtract On@2x.png.meta b/Editor Resources/Icons/paint_subtract On@2x.png.meta new file mode 100644 index 00000000..025bb7c0 --- /dev/null +++ b/Editor Resources/Icons/paint_subtract On@2x.png.meta @@ -0,0 +1,108 @@ +fileFormatVersion: 2 +guid: 68442b83d534442a9b787c00956b52a9 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 1 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor Resources/Icons/paint_subtract.png b/Editor Resources/Icons/paint_subtract.png new file mode 100644 index 0000000000000000000000000000000000000000..81c0225ecab66f7210cc1be1e30d85bbaddf827b GIT binary patch literal 191 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`ZJsWUAr*7ZPTbAgV8G)N@8reS zVE1?fa}S$!zy`q!!Ay=5V_Yl`N6gts(uh{K`RL0W6y@{_bGR(EitSE6}KjXmo=Air0f=PE+cph?c pd=$~^3%o1(Pi%3WvRC#2#@6c+(=@IM?E<=p!PC{xWt~$(69D9CMxp=! literal 0 HcmV?d00001 diff --git a/Editor Resources/Icons/paint_subtract.png.meta b/Editor Resources/Icons/paint_subtract.png.meta new file mode 100644 index 00000000..c0b733a7 --- /dev/null +++ b/Editor Resources/Icons/paint_subtract.png.meta @@ -0,0 +1,108 @@ +fileFormatVersion: 2 +guid: 2a0a438dd7f54880982d03057866af29 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 2 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 1 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor Resources/Icons/paint_subtract@2x.png b/Editor Resources/Icons/paint_subtract@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..1841a977c4b22330b2ac6a3389a77101971b3e6e GIT binary patch literal 286 zcmV+(0pb3MP)fZ~1vbDwa*Fpp`W+OVff?}B1zjQ(I0i1jJocHx7)~pXLMnYO z)t)X#so$y z&r4N76vsbz?v;!dLSU#tP(+IYH$saRA(+@ANN^)YD|c=CFQRS#fG*tVvLecjySEV` zja)>nH2eWVMy2n1W#l_BJoI_z-Ro>RFuXf==H1WtoH=u5l18IJBuju4I8r;YEF!!B znk{`y1b8agHDC(3rG6440v4VF{lF4%8`uUO0N-o?Oayoz@KNyJKrir6Q7~Bq_#$u@ z_y=5(=kK)yxrqe-3tR%$)z)&B!r4uOXX$h38afd$|y@LXd} z!M}#UokwLX6~q>kcY-&y6uHJf0AnF==T(_G7WM}C0X$K{rT(b~Yymr7j+8QU&UlyL zFM$`pD_}qsEm3p3VO*!4>sJ=g1sn?eTF+L@Y>NmRp*`Tf4jLz_l7LjeNrgTa$mPO} zyd70-f2srw3jbLGTmn4?=JY}1g*^2lF{rlWZBeQSNCdPYo^N^YKcAdrz!0#jy{SMp#_D}hbQOG+EdpiNn4%Cdd6n;+BW?7pt omteI5LrQURk>D}{_%8$;1J)R&pZ}VYvj6}907*qoM6N<$f+9o%>i_@% literal 0 HcmV?d00001 diff --git a/Editor Resources/Icons/paint_tool On.png.meta b/Editor Resources/Icons/paint_tool On.png.meta new file mode 100644 index 00000000..e396746f --- /dev/null +++ b/Editor Resources/Icons/paint_tool On.png.meta @@ -0,0 +1,108 @@ +fileFormatVersion: 2 +guid: 2d0065cdb5554763b3d8f951cbb6ecb1 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor Resources/Icons/paint_tool.png b/Editor Resources/Icons/paint_tool.png new file mode 100644 index 0000000000000000000000000000000000000000..d63b4c864a61a6f5eb41df674e3360ad0f44a8a5 GIT binary patch literal 602 zcmV-g0;Ty zPfJxn7>A!T=ZH!RK`<&oFv3Mb8=*yu2uyMz7{moBTD5E2w}`fV0bR7IWkr;m`vF8K zMHf;lmHvRBlIA_082Jqhm%N?1=QtZ*819)fbI0 zUkiadkIGmoh%F}X1aE06a*clg?uEdeS7qi{*h}CWFr$P^{ZkG24*c|Tq?DQejQ0rs z40sAW2d=21C2DRr+}Ek+`jrLr07nA9(6be@&?dr0Xa~5dgT{%fBp?-VPNB~Qa=9=s zZ>Ln-XO)0K;Xh4)OQ31sl|E>^kf&ZGZm2cAElL#uiGVi5uiM`H&nG7tFaiA1as_$b zNk9P%=yohvV*(vXHc~p2ykV=e&e*qqN5i9BzkhGn4&TPW115be6 zkb)B`09~6bY#CSrK5LsodS;a`xO`i_Wto7qmXwU_D%wv{ZTXCQusMhn`Irw oT!PgK3@OFMS%S+5;9Lm!3+x7@L#X1xwEzGB07*qoM6N<$g4aL-X#fBK literal 0 HcmV?d00001 diff --git a/Editor Resources/Icons/paint_tool.png.meta b/Editor Resources/Icons/paint_tool.png.meta new file mode 100644 index 00000000..75f746c6 --- /dev/null +++ b/Editor Resources/Icons/paint_tool.png.meta @@ -0,0 +1,108 @@ +fileFormatVersion: 2 +guid: fe450bfcb18643728ca3584a60732c57 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/EditorTools/ChiselPaintTool.cs b/Editor/EditorTools/ChiselPaintTool.cs index 7a1a2581..6ca87673 100644 --- a/Editor/EditorTools/ChiselPaintTool.cs +++ b/Editor/EditorTools/ChiselPaintTool.cs @@ -11,7 +11,6 @@ namespace Chisel.Editors /// Paints the surface under the mouse in a mode of the paint profile of its material's shader: a color, or a named /// value, with a round brush. Only the surface that is hit is painted; a stroke is one undo. /// - [EditorTool("Chisel Paint")] sealed class ChiselPaintTool : EditorTool { static readonly GUIContent kModeContent = new("Mode", "What to paint: the modes of the project's paint profiles, by name. A surface takes a mode when the paint profile of its shader has one of that name."); @@ -46,7 +45,9 @@ sealed class ChiselPaintTool : EditorTool static Hit s_Hover; static int s_UndoGroup = -1; - public override GUIContent toolbarIcon => new("Paint", "Chisel Paint: paint vertex data onto surfaces, in the modes of their shaders' paint profiles"); + static GUIContent s_ToolbarIcon; + public override GUIContent toolbarIcon => s_ToolbarIcon ??= new GUIContent(ChiselEditorResources.LoadIconImages("paint_tool")?[0], + "Chisel Paint Tool: paint vertex data onto surfaces, in the modes of their shaders' paint profiles"); [MenuItem("Tools/Chisel/Paint")] static void Activate() => ToolManager.SetActiveTool(); @@ -159,6 +160,8 @@ public override void OnToolGUI(EditorWindow window) Undo.IncrementCurrentGroup(); Undo.SetCurrentGroupName("Chisel Paint"); s_UndoGroup = Undo.GetCurrentGroup(); + if (hit.profile.modes[ModeIndex(hit.profile)].kind == ChiselPaintKind.Color && s_Operation == ChiselPaintOperation.Paint) + Painted(s_Color); Dab(hit); evt.Use(); break; @@ -318,26 +321,156 @@ static void ModeButtons() s_ModeName = s_ModeNames[0]; EditorGUILayout.LabelField(kModeContent); + if (s_ModeContents.Length != s_ModeNames.Count) + s_ModeContents = new GUIContent[s_ModeNames.Count]; + var chosen = -1; + for (int n = 0; n < s_ModeNames.Count; n++) + { + s_ModeContents[n] ??= new GUIContent(); + s_ModeContents[n].text = s_ModeNames[n]; + if (s_ModeNames[n] == s_ModeName) + chosen = n; + } + s_ModeName = s_ModeNames[Toggles(chosen, s_ModeContents, null)]; + } + + static GUIContent[] s_ModeContents = System.Array.Empty(); + + // Toggle buttons wrapping within the panel, one of them on; disabled (when given) are drawn but can't be chosen + static int Toggles(int chosen, GUIContent[] contents, bool[] disabled) + { var style = EditorStyles.miniButton; - for (int n = 0; n < s_ModeNames.Count;) + for (int n = 0; n < contents.Length;) { using (new EditorGUILayout.HorizontalScope()) { var used = 0f; do { - var content = new GUIContent(s_ModeNames[n]); - var width = Mathf.Min(kRowWidth, style.CalcSize(content).x + 8); + var width = Mathf.Min(kRowWidth, style.CalcSize(contents[n]).x + 8); if (used > 0 && used + width > kRowWidth) break; - if (GUILayout.Toggle(s_ModeNames[n] == s_ModeName, content, style, GUILayout.Width(width))) - s_ModeName = s_ModeNames[n]; + using (new EditorGUI.DisabledScope(disabled != null && disabled[n])) + { + if (GUILayout.Toggle(n == chosen, contents[n], style, GUILayout.Width(width), GUILayout.Height(20))) + chosen = n; + } used += width + style.margin.horizontal; n++; - } while (n < s_ModeNames.Count); + } while (n < contents.Length); GUILayout.FlexibleSpace(); } } + return Mathf.Max(chosen, 0); + } + + static readonly (ChiselPaintOperation operation, string icon, string name, string tooltip)[] kOperations = + { + (ChiselPaintOperation.Paint, "paint_paint", "Paint", "Towards the color or value"), + (ChiselPaintOperation.Add, "paint_add", "Add", "Raises the value"), + (ChiselPaintOperation.Subtract, "paint_subtract", "Subtract", "Lowers the value"), + (ChiselPaintOperation.Smooth, "paint_smooth", "Smooth", "Towards the neighbouring paint"), + (ChiselPaintOperation.Erase, "paint_erase", "Erase", "Back to the default"), + }; + static readonly GUIContent[] s_OperationContents = new GUIContent[kOperations.Length]; + static readonly bool[] s_OperationDisabled = new bool[kOperations.Length]; + + // The operations, with their icons; adding and subtracting only for a value + static void OperationButtons(bool isValue) + { + var chosen = 0; + for (int i = 0; i < kOperations.Length; i++) + { + var icons = ChiselEditorResources.GetIconContent(kOperations[i].icon, kOperations[i].tooltip); + var on = kOperations[i].operation == s_Operation; + s_OperationContents[i] ??= new GUIContent(); + s_OperationContents[i].text = kOperations[i].name; + s_OperationContents[i].image = icons[on ? 1 : 0].image; + s_OperationContents[i].tooltip = kOperations[i].tooltip; + s_OperationDisabled[i] = !isValue && (kOperations[i].operation == ChiselPaintOperation.Add || kOperations[i].operation == ChiselPaintOperation.Subtract); + if (on) + chosen = i; + } + if (s_OperationDisabled[chosen]) + s_Operation = ChiselPaintOperation.Paint; + EditorGUILayout.LabelField(kOperationContent); + s_Operation = kOperations[Toggles(chosen, s_OperationContents, s_OperationDisabled)].operation; + } + + const int kFalloffSize = 40; + static Texture2D s_Falloff; + static float s_FalloffStrength = -1, s_FalloffHardness = -1; + + // A blob as a dab paints it: how much the brush counts across its radius + static Texture2D Falloff() + { + if (s_Falloff && s_FalloffStrength == s_Strength && s_FalloffHardness == s_Hardness) + return s_Falloff; + if (!s_Falloff) + s_Falloff = new Texture2D(kFalloffSize, kFalloffSize, TextureFormat.RGBA32, false) { hideFlags = HideFlags.HideAndDontSave, filterMode = FilterMode.Bilinear }; + s_FalloffStrength = s_Strength; + s_FalloffHardness = s_Hardness; + var pixels = new Color32[kFalloffSize * kFalloffSize]; + var radius = kFalloffSize * 0.5f; + for (int y = 0; y < kFalloffSize; y++) + { + for (int x = 0; x < kFalloffSize; x++) + { + var distance = Vector2.Distance(new Vector2(x + 0.5f, y + 0.5f), new Vector2(radius, radius)); + var weight = ChiselPaintBrush.Weight(distance, radius, s_Strength, s_Hardness); + var shade = (byte)Mathf.RoundToInt(Mathf.Lerp(32, 255, weight)); + pixels[y * kFalloffSize + x] = new Color32(shade, shade, shade, 255); + } + } + s_Falloff.SetPixels32(pixels); + s_Falloff.Apply(); + return s_Falloff; + } + + static readonly Color[] kSwatches = + { + Color.white, new(0.75f, 0.75f, 0.75f), new(0.5f, 0.5f, 0.5f), new(0.25f, 0.25f, 0.25f), Color.black, + new(0.9f, 0.1f, 0.1f), new(1, 0.5f, 0), new(1, 0.9f, 0.1f), new(0.2f, 0.8f, 0.2f), new(0.1f, 0.8f, 0.8f), + new(0.15f, 0.35f, 0.95f), new(0.55f, 0.25f, 0.85f), new(0.95f, 0.3f, 0.7f), new(0.5f, 0.3f, 0.15f), + }; + // the colors painted with last, newest first + static readonly List s_Recent = new(); + const int kMaxRecent = 9; + const float kSwatchSize = 16; + + internal static void Painted(Color color) + { + s_Recent.Remove(color); + s_Recent.Insert(0, color); + if (s_Recent.Count > kMaxRecent) + s_Recent.RemoveAt(s_Recent.Count - 1); + } + + // A row of swatches; clicking one takes its color + static void SwatchRow(IReadOnlyList colors) + { + var perRow = Mathf.Max(1, Mathf.FloorToInt(kRowWidth / (kSwatchSize + 2))); + for (int start = 0; start < colors.Count; start += perRow) + { + var rect = GUILayoutUtility.GetRect(kRowWidth, kSwatchSize + 2); + for (int i = start; i < Mathf.Min(start + perRow, colors.Count); i++) + { + var swatch = new Rect(rect.x + (i - start) * (kSwatchSize + 2), rect.y + 1, kSwatchSize, kSwatchSize); + if (Event.current.type == EventType.Repaint) + { + var selected = colors[i] == s_Color; + EditorGUI.DrawRect(swatch, selected ? (EditorGUIUtility.isProSkin ? Color.white : Color.black) : new Color(0, 0, 0, 0.5f)); + EditorGUI.DrawRect(new Rect(swatch.x + 1, swatch.y + 1, swatch.width - 2, swatch.height - 2), colors[i]); + } + EditorGUIUtility.AddCursorRect(swatch, MouseCursor.Link); + if (Event.current.type == EventType.MouseDown && Event.current.button == 0 && swatch.Contains(Event.current.mousePosition)) + { + s_Color = colors[i]; + GUI.changed = true; + Event.current.Use(); + } + } + } } // The tool's settings, in its overlay @@ -347,17 +480,33 @@ internal static void PanelGUI() EditorGUIUtility.labelWidth = 90; { ModeButtons(); - if (ChosenMode(out var chosen)) + var hasMode = ChosenMode(out var chosen); + OperationButtons(hasMode && chosen.kind == ChiselPaintKind.Value); + EditorGUILayout.Space(2); + if (hasMode) { if (chosen.kind == ChiselPaintKind.Color) + { s_Color = EditorGUILayout.ColorField(kColorContent, s_Color, true, true, false); + SwatchRow(kSwatches); + if (s_Recent.Count > 0) + SwatchRow(s_Recent); + } else s_Value = EditorGUILayout.Slider(new GUIContent(s_ModeName), s_Value, chosen.range.x, chosen.range.y); } - s_Operation = (ChiselPaintOperation)EditorGUILayout.EnumPopup(kOperationContent, s_Operation); - s_Radius = Mathf.Max(0.01f, EditorGUILayout.FloatField(kRadiusContent, s_Radius)); - s_Strength = EditorGUILayout.Slider(kStrengthContent, s_Strength, 0, 1); - s_Hardness = EditorGUILayout.Slider(kHardnessContent, s_Hardness, 0, 1); + s_Radius = Mathf.Max(0.01f, EditorGUILayout.FloatField(kRadiusContent, s_Radius)); + using (new EditorGUILayout.HorizontalScope()) + { + using (new EditorGUILayout.VerticalScope()) + { + s_Strength = EditorGUILayout.Slider(kStrengthContent, s_Strength, 0, 1); + s_Hardness = EditorGUILayout.Slider(kHardnessContent, s_Hardness, 0, 1); + } + var falloff = GUILayoutUtility.GetRect(kFalloffSize, kFalloffSize, GUILayout.Width(kFalloffSize), GUILayout.Height(kFalloffSize)); + if (Event.current.type == EventType.Repaint) + GUI.DrawTexture(falloff, Falloff(), ScaleMode.ScaleToFit); + } using (new EditorGUI.DisabledScope(!Paintable(s_Last))) { if (GUILayout.Button(kFillContent) && LastPaint(out var paint, out var mode)) diff --git a/Editor/PropertyDrawers/ChiselPaintModeDrawer.cs b/Editor/PropertyDrawers/ChiselPaintModeDrawer.cs index 0a0aac9f..76bd0c6b 100644 --- a/Editor/PropertyDrawers/ChiselPaintModeDrawer.cs +++ b/Editor/PropertyDrawers/ChiselPaintModeDrawer.cs @@ -111,19 +111,4 @@ static void ComponentToggles(Rect row, SerializedProperty components, string[] n } } } - - // A profile warns about two modes writing one component of one attribute - [CustomEditor(typeof(ChiselPaintProfile))] - sealed class ChiselPaintProfileEditor : Editor - { - public override void OnInspectorGUI() - { - DrawDefaultInspector(); - var profile = (ChiselPaintProfile)target; - if (profile.shader == null) - EditorGUILayout.HelpBox("Give the profile the shader it is for: surfaces drawn with that shader are painted in its modes.", MessageType.Info); - if (profile.FindOverlap(out var first, out var second)) - EditorGUILayout.HelpBox($"Modes {first} and {second} both write a component of {profile.modes[first].attribute}: one of them overwrites the other.", MessageType.Warning); - } - } } diff --git a/Editor/SceneView/Overlays/ChiselGlobal.cs b/Editor/SceneView/Overlays/ChiselGlobal.cs index 2855c7db..b9a3b8d6 100644 --- a/Editor/SceneView/Overlays/ChiselGlobal.cs +++ b/Editor/SceneView/Overlays/ChiselGlobal.cs @@ -25,7 +25,8 @@ public override IEnumerable GetAdditionalToolTypes() typeof(ChiselUVMoveTool), typeof(ChiselUVRotateTool), typeof(ChiselUVScaleTool), - typeof(ChiselUVConnectTool) + typeof(ChiselUVConnectTool), + typeof(ChiselPaintTool) }; } diff --git a/Editor/Settings/ChiselPaintSettingsProvider.cs b/Editor/Settings/ChiselPaintSettingsProvider.cs new file mode 100644 index 00000000..86f12ef9 --- /dev/null +++ b/Editor/Settings/ChiselPaintSettingsProvider.cs @@ -0,0 +1,170 @@ +using System.Collections.Generic; +using Chisel.Components; +using Chisel.Core; +using UnityEditor; +using UnityEngine; + +namespace Chisel.Editors +{ + // The project's paint profiles: a list of the shaders that can be painted, and the modes of the one chosen + static class ChiselPaintSettingsGUI + { + static readonly GUIContent kExplanation = EditorGUIUtility.TrTextContent( + "Surfaces drawn with one of these shaders can be painted with the Chisel Paint tool, in its modes. " + + "The shader shows the paint; Chisel writes it to the vertex attributes the modes name."); + static readonly GUIContent kAddContent = EditorGUIUtility.TrTextContent("Add Shader", "Makes surfaces drawn with this shader paintable"); + static readonly GUIContent kRemoveContent = EditorGUIUtility.TrTextContent("Remove", "Surfaces drawn with this shader can't be painted any more (their paint is kept)"); + static readonly GUIContent kModesContent = EditorGUIUtility.TrTextContent("Modes", "What a surface drawn with the shader can be painted with"); + + internal static int s_Selected; + static SerializedObject s_Serialized; + + public static void OnGUI(ChiselPaintSettings settings) + { + EditorGUILayout.LabelField(kExplanation, EditorStyles.wordWrappedMiniLabel); + EditorGUILayout.Space(); + + if (s_Serialized == null || s_Serialized.targetObject != settings) + s_Serialized = new SerializedObject(settings); + s_Serialized.Update(); + var profilesProp = s_Serialized.FindProperty(nameof(ChiselPaintSettings.profiles)); + + var added = (Shader)EditorGUILayout.ObjectField(kAddContent, null, typeof(Shader), false); + if (added) + { + s_Selected = ChiselPaintSettingsProvider.Add(settings, added); + s_Serialized.Update(); + } + for (int i = 0; i < profilesProp.arraySize; i++) + { + var shader = profilesProp.GetArrayElementAtIndex(i).FindPropertyRelative(nameof(ChiselPaintProfile.shader)).objectReferenceValue as Shader; + using (new EditorGUILayout.HorizontalScope()) + { + if (GUILayout.Toggle(i == s_Selected, shader ? shader.name : "(no shader)", EditorStyles.toolbarButton)) + s_Selected = i; + if (GUILayout.Button(kRemoveContent, EditorStyles.miniButton, GUILayout.Width(60))) + { + profilesProp.DeleteArrayElementAtIndex(i); + s_Selected = Mathf.Min(s_Selected, profilesProp.arraySize - 1); + s_Serialized.ApplyModifiedProperties(); + ChiselPaintSettings.Changed(); + GUIUtility.ExitGUI(); + } + } + } + + EditorGUI.BeginChangeCheck(); + if (s_Selected >= 0 && s_Selected < profilesProp.arraySize) + { + EditorGUILayout.Space(); + var modesProp = profilesProp.GetArrayElementAtIndex(s_Selected).FindPropertyRelative(nameof(ChiselPaintProfile.modes)); + modesProp.isExpanded = true; + EditorGUILayout.PropertyField(modesProp, kModesContent, true); + } + if (EditorGUI.EndChangeCheck()) + { + s_Serialized.ApplyModifiedProperties(); + ChiselPaintSettings.Changed(); + } + + if (s_Selected >= 0 && s_Selected < settings.profiles.Count) + { + var profile = settings.profiles[s_Selected]; + if (profile.shader != null && settings.Find(profile.shader) != profile) + EditorGUILayout.HelpBox("Another profile is for this shader too; only the first one is used.", MessageType.Warning); + if (profile.FindOverlap(out var first, out var second)) + EditorGUILayout.HelpBox($"Modes {first} and {second} both write a component of {profile.modes[first].attribute}: one of them overwrites the other.", MessageType.Warning); + } + } + } + + // Project Settings > Chisel > Paint + class ChiselPaintSettingsProvider : SettingsProvider + { + public const string kPath = "Project/Chisel/Paint"; + + ChiselPaintSettingsProvider(string path, SettingsScope scope) : base(path, scope) { } + + [SettingsProvider] + public static SettingsProvider CreatePaintSettingsProvider() + { + return new ChiselPaintSettingsProvider(kPath, SettingsScope.Project) + { + label = "Paint", + keywords = new HashSet(new[] { "chisel", "paint", "vertex", "color", "shader", "profile" }) + }; + } + + // The project's settings, with the package's test shaders in them + public static ChiselPaintSettings CreateProjectAsset() + { + var folder = System.IO.Path.GetDirectoryName(ChiselPaintSettings.kAssetPath).Replace('\\', '/'); + if (!AssetDatabase.IsValidFolder(folder)) + AssetDatabase.CreateFolder("Assets", System.IO.Path.GetFileName(folder)); + var settings = ScriptableObject.CreateInstance(); + var color = Shader.Find("Chisel/Test/Paint Color"); + if (color) + settings.profiles.Add(new ChiselPaintProfile { shader = color, modes = { ChiselPaintMode.Color("Color") } }); + var value = Shader.Find("Chisel/Test/Paint Value"); + if (value) + settings.profiles.Add(new ChiselPaintProfile { shader = value, modes = { ChiselPaintMode.Value("Value", ChiselPaintAttribute.TexCoord2, ChiselPaintComponents.X) } }); + AssetDatabase.CreateAsset(settings, AssetDatabase.GenerateUniqueAssetPath(ChiselPaintSettings.kAssetPath)); + AssetDatabase.SaveAssets(); + ChiselPaintSettings.Project = settings; + return settings; + } + + // The index of the shader's profile, added when it has none + public static int Add(ChiselPaintSettings settings, Shader shader) + { + var index = settings.profiles.FindIndex(profile => profile != null && profile.shader == shader); + if (index >= 0) + return index; + Undo.RecordObject(settings, "Make Shader Paintable"); + settings.profiles.Add(new ChiselPaintProfile { shader = shader }); + EditorUtility.SetDirty(settings); + ChiselPaintSettings.Changed(); + return settings.profiles.Count - 1; + } + + // Opens the page on the shader's profile, adding one when it has none + public static void Configure(Shader shader) + { + var settings = ChiselPaintSettings.Project; + if (!settings) + settings = CreateProjectAsset(); + if (shader) + ChiselPaintSettingsGUI.s_Selected = Add(settings, shader); + SettingsService.OpenProjectSettings(kPath); + } + + public override void OnGUI(string searchContext) + { + EditorGUIUtility.labelWidth = 120; + EditorGUILayout.Space(); + var settings = ChiselPaintSettings.Project; + if (!settings) + { + EditorGUILayout.HelpBox("This project has no paint settings, so no surface can be painted.", MessageType.Info); + if (GUILayout.Button("Create Paint Settings", GUILayout.Width(200))) + CreateProjectAsset(); + return; + } + ChiselPaintSettingsGUI.OnGUI(settings); + } + } + + [CustomEditor(typeof(ChiselPaintSettings))] + class ChiselPaintSettingsEditor : Editor + { + public override void OnInspectorGUI() + { + var settings = (ChiselPaintSettings)target; + if (settings != ChiselPaintSettings.Project) + EditorGUILayout.HelpBox("These aren't the project's paint settings, so nothing uses them.", MessageType.Info); + if (GUILayout.Button("Open In Project Settings")) + SettingsService.OpenProjectSettings(ChiselPaintSettingsProvider.kPath); + ChiselPaintSettingsGUI.OnGUI(settings); + } + } +} diff --git a/Editor/Settings/ChiselPaintSettingsProvider.cs.meta b/Editor/Settings/ChiselPaintSettingsProvider.cs.meta new file mode 100644 index 00000000..5021eec5 --- /dev/null +++ b/Editor/Settings/ChiselPaintSettingsProvider.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0ac853b3745e48dcbc1c4cec153cf607 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/SurfaceEditors/ChiselSurfaceMetadataEditor.cs b/Editor/SurfaceEditors/ChiselSurfaceMetadataEditor.cs index a4067b38..1c25bcdd 100644 --- a/Editor/SurfaceEditors/ChiselSurfaceMetadataEditor.cs +++ b/Editor/SurfaceEditors/ChiselSurfaceMetadataEditor.cs @@ -2,6 +2,7 @@ using UnityEditor; using System; using Chisel.Core; +using Chisel.Components; using UnityEngine.Profiling; namespace Chisel.Editors @@ -158,6 +159,22 @@ void HotspotGUI() ChiselHotspotLayoutWindow.RefreshSurfacesUsing(MaterialOf(metadata)); } + static readonly GUIContent kConfigurePaintContents = new("Configure Paintability Of Shader", "Opens Project Settings > Chisel > Paint on the paint profile of the material's shader, adding one when it has none"); + + // Whether the material's shader can be painted, and the way to its paint profile + void PaintGUI() + { + var material = MaterialOf(target); + if (!material || !material.shader) + return; + using (new EditorGUILayout.HorizontalScope()) + { + EditorGUILayout.PrefixLabel(ChiselPaintProfiles.Find(material.shader) != null ? "Paintable" : "Not Paintable"); + if (GUILayout.Button(kConfigurePaintContents, EditorStyles.miniButton)) + ChiselPaintSettingsProvider.Configure(material.shader); + } + } + public override void OnInspectorGUI() { Profiler.BeginSample("OnInspectorGUI"); @@ -188,6 +205,7 @@ public override void OnInspectorGUI() foreach (var metadata in targets) ChiselHotspotLayoutWindow.RefreshSurfacesUsing(MaterialOf(metadata)); } + PaintGUI(); } catch (ExitGUIException) { } catch (Exception ex) { Debug.LogException(ex); } diff --git a/Package Resources/Materials/URP/Paint/Paint Color Profile.asset b/Package Resources/Materials/URP/Paint/Paint Color Profile.asset deleted file mode 100644 index 7bdb657d..00000000 --- a/Package Resources/Materials/URP/Paint/Paint Color Profile.asset +++ /dev/null @@ -1,22 +0,0 @@ -%YAML 1.1 -%TAG !u! tag:unity3d.com,2011: ---- !u!114 &11400000 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 83ee62a399ce44d41abc4152089d341e, type: 3} - m_Name: Paint Color Profile - m_EditorClassIdentifier: com.chisel.unity.components::Chisel.Components.ChiselPaintProfile - shader: {fileID: 4800000, guid: 7dfc31081b24da14a97e061b8ca75c94, type: 3} - modes: - - name: Color - kind: 0 - attribute: 0 - components: 15 - defaultValue: {x: 1, y: 1, z: 1, w: 1} - range: {x: 0, y: 1} diff --git a/Package Resources/Materials/URP/Paint/Paint Color Profile.asset.meta b/Package Resources/Materials/URP/Paint/Paint Color Profile.asset.meta deleted file mode 100644 index d49c258b..00000000 --- a/Package Resources/Materials/URP/Paint/Paint Color Profile.asset.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 2681fddd406edda468d9eb4be57aca0f -NativeFormatImporter: - externalObjects: {} - mainObjectFileID: 11400000 - userData: - assetBundleName: - assetBundleVariant: diff --git a/Package Resources/Materials/URP/Paint/Paint Value Profile.asset b/Package Resources/Materials/URP/Paint/Paint Value Profile.asset deleted file mode 100644 index bf78aac8..00000000 --- a/Package Resources/Materials/URP/Paint/Paint Value Profile.asset +++ /dev/null @@ -1,22 +0,0 @@ -%YAML 1.1 -%TAG !u! tag:unity3d.com,2011: ---- !u!114 &11400000 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 83ee62a399ce44d41abc4152089d341e, type: 3} - m_Name: Paint Value Profile - m_EditorClassIdentifier: com.chisel.unity.components::Chisel.Components.ChiselPaintProfile - shader: {fileID: 4800000, guid: a553dbbcc51572e46a6e4778d37abca7, type: 3} - modes: - - name: Value - kind: 1 - attribute: 2 - components: 1 - defaultValue: {x: 0, y: 0, z: 0, w: 0} - range: {x: 0, y: 1} diff --git a/Package Resources/Materials/URP/Paint/Paint Value Profile.asset.meta b/Package Resources/Materials/URP/Paint/Paint Value Profile.asset.meta deleted file mode 100644 index fd6415e9..00000000 --- a/Package Resources/Materials/URP/Paint/Paint Value Profile.asset.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 7faa6bf764da2834286d8d253fd71536 -NativeFormatImporter: - externalObjects: {} - mainObjectFileID: 11400000 - userData: - assetBundleName: - assetBundleVariant: From 8f42409b401d2a42ce47e9bc3163b0e24e7f01ba Mon Sep 17 00:00:00 2001 From: Sander van Rossen Date: Mon, 26 Oct 2026 09:21:33 +0100 Subject: [PATCH 11/27] Paint overlay: swatches, icon operations, modes as a list The color's swatches are kept per user and wrap over as many rows as they need; the chosen one is outlined, each has a border, a drag reorders them and a middle-click removes one. The last cell, the current color with a plus over it, adds a swatch and opens Unity's color picker on it. The operations are icon toggles (named in their tooltips) under Operations; the Mode label is gone. The not-paintable cursor stays where the scene had the mouse, so it isn't shown over the overlays. The settings page lists a profile's modes as a list with a header instead of a foldout. --- Editor/EditorTools/ChiselPaintTool.cs | 207 ++++++++++++++---- .../Settings/ChiselPaintSettingsProvider.cs | 25 ++- 2 files changed, 190 insertions(+), 42 deletions(-) diff --git a/Editor/EditorTools/ChiselPaintTool.cs b/Editor/EditorTools/ChiselPaintTool.cs index 6ca87673..c9032fe3 100644 --- a/Editor/EditorTools/ChiselPaintTool.cs +++ b/Editor/EditorTools/ChiselPaintTool.cs @@ -13,8 +13,8 @@ namespace Chisel.Editors /// sealed class ChiselPaintTool : EditorTool { - static readonly GUIContent kModeContent = new("Mode", "What to paint: the modes of the project's paint profiles, by name. A surface takes a mode when the paint profile of its shader has one of that name."); - static readonly GUIContent kOperationContent = new("Operation", "Paint: towards the color or value. Add / Subtract: a value up or down. Smooth: towards the neighbouring paint. Erase: back to the default."); + const string kModeTooltip = "A surface takes this mode when the paint profile of its shader has a mode of this name"; + static readonly GUIContent kOperationContent = new("Operations", "Paint: towards the color or value. Add / Subtract: a value up or down. Smooth: towards the neighbouring paint. Erase: back to the default."); static readonly GUIContent kColorContent = new("Color"); static readonly GUIContent kRadiusContent = new("Radius", "The brush's radius, in metres."); static readonly GUIContent kStrengthContent = new("Strength", "How much a dab moves the paint towards what it paints."); @@ -43,6 +43,8 @@ sealed class ChiselPaintTool : EditorTool static readonly List s_Intersections = new(); // the surface under the mouse, as it was last picked static Hit s_Hover; + // where the mouse was in the scene when it was picked (an overlay takes the mouse events over it) + static Vector2 s_HoverMouse; static int s_UndoGroup = -1; static GUIContent s_ToolbarIcon; @@ -119,9 +121,15 @@ public override void OnToolGUI(EditorWindow window) var id = GUIUtility.GetControlID(FocusType.Passive); // picking renders: only on the mouse's own events, never inside the scene view's layout or repaint var type = evt.GetTypeForControl(id); + if (type == EventType.MouseLeaveWindow) + { + s_Hover = default; + HandleUtility.Repaint(); + } if (type == EventType.MouseMove || type == EventType.MouseDown || type == EventType.MouseDrag) { s_Hover = Pick(evt.mousePosition); + s_HoverMouse = evt.mousePosition; if (s_Hover.valid && s_Hover.profile != null) { s_Last = s_Hover; @@ -136,7 +144,7 @@ public override void OnToolGUI(EditorWindow window) Handles.DrawWireDisc(hit.point, hit.normal, s_Radius); Handles.DrawWireDisc(hit.point, hit.normal, s_Radius * Mathf.Clamp01(s_Hardness)); } else if (hit.valid) - NotPaintable(window, hit, evt.mousePosition); + NotPaintable(window, hit, s_HoverMouse); switch (evt.GetTypeForControl(id)) { @@ -160,8 +168,6 @@ public override void OnToolGUI(EditorWindow window) Undo.IncrementCurrentGroup(); Undo.SetCurrentGroupName("Chisel Paint"); s_UndoGroup = Undo.GetCurrentGroup(); - if (hit.profile.modes[ModeIndex(hit.profile)].kind == ChiselPaintKind.Color && s_Operation == ChiselPaintOperation.Paint) - Painted(s_Color); Dab(hit); evt.Use(); break; @@ -254,7 +260,8 @@ static void Dab(Hit hit) // Over a surface that can't be painted: no brush, a cursor that says so, and why next to it static void NotPaintable(EditorWindow window, Hit hit, Vector2 mousePosition) { - EditorGUIUtility.AddCursorRect(new Rect(0, 0, window.position.width, window.position.height), MouseCursor.NotAllowed); + // only round where the scene last had the mouse: over an overlay (which takes the mouse events) it isn't + EditorGUIUtility.AddCursorRect(new Rect(mousePosition.x - 4, mousePosition.y - 4, 8, 8), MouseCursor.NotAllowed); if (Event.current.type != EventType.Repaint) return; // solid, as a tooltip is: a panel's color with a border round it @@ -320,14 +327,14 @@ static void ModeButtons() if (!s_ModeNames.Contains(s_ModeName)) s_ModeName = s_ModeNames[0]; - EditorGUILayout.LabelField(kModeContent); if (s_ModeContents.Length != s_ModeNames.Count) s_ModeContents = new GUIContent[s_ModeNames.Count]; var chosen = -1; for (int n = 0; n < s_ModeNames.Count; n++) { s_ModeContents[n] ??= new GUIContent(); - s_ModeContents[n].text = s_ModeNames[n]; + s_ModeContents[n].text = s_ModeNames[n]; + s_ModeContents[n].tooltip = kModeTooltip; if (s_ModeNames[n] == s_ModeName) chosen = n; } @@ -384,9 +391,9 @@ static void OperationButtons(bool isValue) var icons = ChiselEditorResources.GetIconContent(kOperations[i].icon, kOperations[i].tooltip); var on = kOperations[i].operation == s_Operation; s_OperationContents[i] ??= new GUIContent(); - s_OperationContents[i].text = kOperations[i].name; + s_OperationContents[i].text = null; s_OperationContents[i].image = icons[on ? 1 : 0].image; - s_OperationContents[i].tooltip = kOperations[i].tooltip; + s_OperationContents[i].tooltip = $"{kOperations[i].name}: {kOperations[i].tooltip}"; s_OperationDisabled[i] = !isValue && (kOperations[i].operation == ChiselPaintOperation.Add || kOperations[i].operation == ChiselPaintOperation.Subtract); if (on) chosen = i; @@ -427,52 +434,176 @@ static Texture2D Falloff() return s_Falloff; } - static readonly Color[] kSwatches = + static readonly Color[] kDefaultSwatches = { Color.white, new(0.75f, 0.75f, 0.75f), new(0.5f, 0.5f, 0.5f), new(0.25f, 0.25f, 0.25f), Color.black, new(0.9f, 0.1f, 0.1f), new(1, 0.5f, 0), new(1, 0.9f, 0.1f), new(0.2f, 0.8f, 0.2f), new(0.1f, 0.8f, 0.8f), new(0.15f, 0.35f, 0.95f), new(0.55f, 0.25f, 0.85f), new(0.95f, 0.3f, 0.7f), new(0.5f, 0.3f, 0.15f), }; - // the colors painted with last, newest first - static readonly List s_Recent = new(); - const int kMaxRecent = 9; - const float kSwatchSize = 16; + const string kSwatchesPref = "Chisel.Paint.Swatches"; + const float kSwatchSize = 18; + const float kSwatchGap = 3; + static List s_Swatches; + static readonly GUIContent kAddSwatchContent = new(string.Empty, "Adds a swatch, picking its color (middle-click a swatch to remove it)"); + + // the swatches, kept per user + static List Swatches + { + get + { + if (s_Swatches != null) + return s_Swatches; + s_Swatches = new List(); + foreach (var hex in EditorPrefs.GetString(kSwatchesPref, string.Empty).Split(';')) + { + if (UnityEngine.ColorUtility.TryParseHtmlString("#" + hex, out var color)) + s_Swatches.Add(color); + } + if (s_Swatches.Count == 0 && !EditorPrefs.HasKey(kSwatchesPref)) + s_Swatches.AddRange(kDefaultSwatches); + return s_Swatches; + } + } - internal static void Painted(Color color) + static void SaveSwatches() { - s_Recent.Remove(color); - s_Recent.Insert(0, color); - if (s_Recent.Count > kMaxRecent) - s_Recent.RemoveAt(s_Recent.Count - 1); + var hexes = new string[Swatches.Count]; + for (int i = 0; i < hexes.Length; i++) + hexes[i] = UnityEngine.ColorUtility.ToHtmlStringRGBA(Swatches[i]); + EditorPrefs.SetString(kSwatchesPref, string.Join(";", hexes)); } - // A row of swatches; clicking one takes its color - static void SwatchRow(IReadOnlyList colors) + static int ChosenSwatch() { - var perRow = Mathf.Max(1, Mathf.FloorToInt(kRowWidth / (kSwatchSize + 2))); - for (int start = 0; start < colors.Count; start += perRow) + for (int i = 0; i < Swatches.Count; i++) { - var rect = GUILayoutUtility.GetRect(kRowWidth, kSwatchSize + 2); - for (int i = start; i < Mathf.Min(start + perRow, colors.Count); i++) + if (Swatches[i] == s_Color) + return i; + } + return -1; + } + + // Unity's color picker, giving every change to the callback + static void PickColor(Color color, System.Action changed) + { + var picker = typeof(EditorWindow).Assembly.GetType("UnityEditor.ColorPicker"); + foreach (var method in picker?.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static) ?? System.Array.Empty()) + { + var parameters = method.GetParameters(); + if (method.Name != "Show" || parameters.Length != 4 || parameters[0].ParameterType != typeof(System.Action)) + continue; + method.Invoke(null, new object[] { changed, color, true, false }); + return; + } + } + + // The swatches, wrapping over as many rows as they need, then + (a new swatch, picked); middle-click removes one + static int s_DraggedSwatch = -1; + + static void SwatchesGUI() + { + var swatches = Swatches; + var chosen = ChosenSwatch(); + var count = swatches.Count + 1; + var perRow = Mathf.Max(1, Mathf.FloorToInt((kRowWidth + kSwatchGap) / (kSwatchSize + kSwatchGap))); + var rows = (count + perRow - 1) / perRow; + var area = GUILayoutUtility.GetRect(kRowWidth, rows * (kSwatchSize + kSwatchGap)); + var evt = Event.current; + var id = GUIUtility.GetControlID(FocusType.Passive); + var border = EditorGUIUtility.isProSkin ? new Color(0.6f, 0.6f, 0.6f) : new Color(0.3f, 0.3f, 0.3f); + var highlight = new Color(0.25f, 0.6f, 1f); + Rect CellOf(int i) => new(area.x + (i % perRow) * (kSwatchSize + kSwatchGap), area.y + (i / perRow) * (kSwatchSize + kSwatchGap) + 1, kSwatchSize, kSwatchSize); + + // dragging a swatch moves it to the cell under the mouse + if (GUIUtility.hotControl == id) + { + if (evt.type == EventType.MouseDrag && s_DraggedSwatch >= 0 && s_DraggedSwatch < swatches.Count) { - var swatch = new Rect(rect.x + (i - start) * (kSwatchSize + 2), rect.y + 1, kSwatchSize, kSwatchSize); - if (Event.current.type == EventType.Repaint) + for (int i = 0; i < swatches.Count; i++) { - var selected = colors[i] == s_Color; - EditorGUI.DrawRect(swatch, selected ? (EditorGUIUtility.isProSkin ? Color.white : Color.black) : new Color(0, 0, 0, 0.5f)); - EditorGUI.DrawRect(new Rect(swatch.x + 1, swatch.y + 1, swatch.width - 2, swatch.height - 2), colors[i]); + var cell = CellOf(i); + cell.xMax += kSwatchGap; + cell.yMax += kSwatchGap; + if (i == s_DraggedSwatch || !cell.Contains(evt.mousePosition)) + continue; + var moved = swatches[s_DraggedSwatch]; + swatches.RemoveAt(s_DraggedSwatch); + swatches.Insert(i, moved); + s_DraggedSwatch = i; + SaveSwatches(); + break; } - EditorGUIUtility.AddCursorRect(swatch, MouseCursor.Link); - if (Event.current.type == EventType.MouseDown && Event.current.button == 0 && swatch.Contains(Event.current.mousePosition)) + evt.Use(); + } else if (evt.type == EventType.MouseUp) + { + GUIUtility.hotControl = 0; + s_DraggedSwatch = -1; + evt.Use(); + } + } + + for (int i = 0; i < count; i++) + { + var cell = CellOf(i); + var inner = new Rect(cell.x + 1, cell.y + 1, cell.width - 2, cell.height - 2); + if (i == swatches.Count) + { + // as the color picker's own: the current color with a plus over it + if (evt.type == EventType.Repaint) + { + EditorGUI.DrawRect(cell, border); + EditorGUI.DrawRect(inner, s_Color); + var ink = s_Color.grayscale > 0.5f ? Color.black : Color.white; + EditorGUI.DrawRect(new Rect(cell.center.x - 5, cell.center.y - 1, 10, 2), ink); + EditorGUI.DrawRect(new Rect(cell.center.x - 1, cell.center.y - 5, 2, 10), ink); + } + if (GUI.Button(cell, kAddSwatchContent, GUIStyle.none)) + { + var index = swatches.Count; + swatches.Add(s_Color); + SaveSwatches(); + PickColor(s_Color, color => + { + if (index < Swatches.Count) + Swatches[index] = color; + s_Color = color; + SaveSwatches(); + SceneView.RepaintAll(); + }); + } + continue; + } + if (evt.type == EventType.Repaint) + { + if (i == chosen) + { + EditorGUI.DrawRect(new Rect(cell.x - 2, cell.y - 2, cell.width + 4, cell.height + 4), highlight); + EditorGUI.DrawRect(cell, Color.black); + } else + EditorGUI.DrawRect(cell, border); + EditorGUI.DrawRect(inner, swatches[i]); + } + EditorGUIUtility.AddCursorRect(cell, GUIUtility.hotControl == id ? MouseCursor.Pan : MouseCursor.Link); + if (evt.type == EventType.MouseDown && cell.Contains(evt.mousePosition)) + { + if (evt.button == 0) { - s_Color = colors[i]; + // a click takes the color; a drag moves the swatch + s_Color = swatches[i]; + s_DraggedSwatch = i; + GUIUtility.hotControl = id; GUI.changed = true; - Event.current.Use(); + evt.Use(); + } else if (evt.button == 2) + { + swatches.RemoveAt(i); + SaveSwatches(); + evt.Use(); + GUIUtility.ExitGUI(); } } } } - // The tool's settings, in its overlay internal static void PanelGUI() { @@ -488,9 +619,7 @@ internal static void PanelGUI() if (chosen.kind == ChiselPaintKind.Color) { s_Color = EditorGUILayout.ColorField(kColorContent, s_Color, true, true, false); - SwatchRow(kSwatches); - if (s_Recent.Count > 0) - SwatchRow(s_Recent); + SwatchesGUI(); } else s_Value = EditorGUILayout.Slider(new GUIContent(s_ModeName), s_Value, chosen.range.x, chosen.range.y); diff --git a/Editor/Settings/ChiselPaintSettingsProvider.cs b/Editor/Settings/ChiselPaintSettingsProvider.cs index 86f12ef9..0ecd1b2e 100644 --- a/Editor/Settings/ChiselPaintSettingsProvider.cs +++ b/Editor/Settings/ChiselPaintSettingsProvider.cs @@ -17,6 +17,27 @@ static class ChiselPaintSettingsGUI static readonly GUIContent kModesContent = EditorGUIUtility.TrTextContent("Modes", "What a surface drawn with the shader can be painted with"); internal static int s_Selected; + static UnityEditorInternal.ReorderableList s_ModesList; + + // The modes of the chosen profile: a list with a header, not a foldout + static UnityEditorInternal.ReorderableList ModesList(SerializedProperty modesProp) + { + if (s_ModesList != null && s_ModesList.serializedProperty.propertyPath == modesProp.propertyPath && + s_ModesList.serializedProperty.serializedObject == modesProp.serializedObject) + return s_ModesList; + s_ModesList = new UnityEditorInternal.ReorderableList(modesProp.serializedObject, modesProp, true, true, true, true) + { + drawHeaderCallback = rect => EditorGUI.LabelField(rect, kModesContent), + elementHeightCallback = index => EditorGUI.GetPropertyHeight(modesProp.GetArrayElementAtIndex(index), true) + EditorGUIUtility.standardVerticalSpacing, + drawElementCallback = (rect, index, active, focused) => + { + rect.xMin += 10; + rect.height = EditorGUI.GetPropertyHeight(modesProp.GetArrayElementAtIndex(index), true); + EditorGUI.PropertyField(rect, modesProp.GetArrayElementAtIndex(index), true); + } + }; + return s_ModesList; + } static SerializedObject s_Serialized; public static void OnGUI(ChiselPaintSettings settings) @@ -57,9 +78,7 @@ public static void OnGUI(ChiselPaintSettings settings) if (s_Selected >= 0 && s_Selected < profilesProp.arraySize) { EditorGUILayout.Space(); - var modesProp = profilesProp.GetArrayElementAtIndex(s_Selected).FindPropertyRelative(nameof(ChiselPaintProfile.modes)); - modesProp.isExpanded = true; - EditorGUILayout.PropertyField(modesProp, kModesContent, true); + ModesList(profilesProp.GetArrayElementAtIndex(s_Selected).FindPropertyRelative(nameof(ChiselPaintProfile.modes))).DoLayoutList(); } if (EditorGUI.EndChangeCheck()) { From 24263d9963c3230fe51674c4a30b692a3e78ef13 Mon Sep 17 00:00:00 2001 From: Sander van Rossen Date: Mon, 26 Oct 2026 09:23:33 +0100 Subject: [PATCH 12/27] Paint overlay: the add-swatch cell is the current color, inset As in Unity's color picker: no plus, the current color drawn smaller than a swatch inside an empty cell. --- Editor/EditorTools/ChiselPaintTool.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Editor/EditorTools/ChiselPaintTool.cs b/Editor/EditorTools/ChiselPaintTool.cs index c9032fe3..e6a5f5d7 100644 --- a/Editor/EditorTools/ChiselPaintTool.cs +++ b/Editor/EditorTools/ChiselPaintTool.cs @@ -444,7 +444,7 @@ static Texture2D Falloff() const float kSwatchSize = 18; const float kSwatchGap = 3; static List s_Swatches; - static readonly GUIContent kAddSwatchContent = new(string.Empty, "Adds a swatch, picking its color (middle-click a swatch to remove it)"); + static readonly GUIContent kAddSwatchContent = new(string.Empty, "Adds the current color as a swatch, and picks its color (middle-click a swatch to remove it)"); // the swatches, kept per user static List Swatches @@ -497,7 +497,8 @@ static void PickColor(Color color, System.Action changed) } } - // The swatches, wrapping over as many rows as they need, then + (a new swatch, picked); middle-click removes one + // The swatches, wrapping over as many rows as they need, then the current color (adds it as a swatch, picked); + // a drag reorders them, a middle-click removes one static int s_DraggedSwatch = -1; static void SwatchesGUI() @@ -548,14 +549,12 @@ static void SwatchesGUI() var inner = new Rect(cell.x + 1, cell.y + 1, cell.width - 2, cell.height - 2); if (i == swatches.Count) { - // as the color picker's own: the current color with a plus over it + // as the color picker's own: the current color, smaller than a swatch, in an empty cell if (evt.type == EventType.Repaint) { EditorGUI.DrawRect(cell, border); - EditorGUI.DrawRect(inner, s_Color); - var ink = s_Color.grayscale > 0.5f ? Color.black : Color.white; - EditorGUI.DrawRect(new Rect(cell.center.x - 5, cell.center.y - 1, 10, 2), ink); - EditorGUI.DrawRect(new Rect(cell.center.x - 1, cell.center.y - 5, 2, 10), ink); + EditorGUI.DrawRect(inner, EditorGUIUtility.isProSkin ? new Color(0.22f, 0.22f, 0.22f) : new Color(0.76f, 0.76f, 0.76f)); + EditorGUI.DrawRect(new Rect(cell.x + 4, cell.y + 4, cell.width - 8, cell.height - 8), s_Color); } if (GUI.Button(cell, kAddSwatchContent, GUIStyle.none)) { From cadf173df387e946d3713e0b81ee800a8ccd3c7d Mon Sep 17 00:00:00 2001 From: Sander van Rossen Date: Mon, 26 Oct 2026 09:26:43 +0100 Subject: [PATCH 13/27] Paint overlay: smaller swatches without gaps Swatches are 14 pixels and share their border with their neighbours; the chosen one is outlined inside its cell. --- Editor/EditorTools/ChiselPaintTool.cs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/Editor/EditorTools/ChiselPaintTool.cs b/Editor/EditorTools/ChiselPaintTool.cs index e6a5f5d7..85d2c691 100644 --- a/Editor/EditorTools/ChiselPaintTool.cs +++ b/Editor/EditorTools/ChiselPaintTool.cs @@ -441,8 +441,9 @@ static Texture2D Falloff() new(0.15f, 0.35f, 0.95f), new(0.55f, 0.25f, 0.85f), new(0.95f, 0.3f, 0.7f), new(0.5f, 0.3f, 0.15f), }; const string kSwatchesPref = "Chisel.Paint.Swatches"; - const float kSwatchSize = 18; - const float kSwatchGap = 3; + const float kSwatchSize = 14; + // neighbours share their border + const float kSwatchGap = -1; static List s_Swatches; static readonly GUIContent kAddSwatchContent = new(string.Empty, "Adds the current color as a swatch, and picks its color (middle-click a swatch to remove it)"); @@ -508,7 +509,7 @@ static void SwatchesGUI() var count = swatches.Count + 1; var perRow = Mathf.Max(1, Mathf.FloorToInt((kRowWidth + kSwatchGap) / (kSwatchSize + kSwatchGap))); var rows = (count + perRow - 1) / perRow; - var area = GUILayoutUtility.GetRect(kRowWidth, rows * (kSwatchSize + kSwatchGap)); + var area = GUILayoutUtility.GetRect(kRowWidth, rows * (kSwatchSize + kSwatchGap) - kSwatchGap + 2); var evt = Event.current; var id = GUIUtility.GetControlID(FocusType.Passive); var border = EditorGUIUtility.isProSkin ? new Color(0.6f, 0.6f, 0.6f) : new Color(0.3f, 0.3f, 0.3f); @@ -554,7 +555,7 @@ static void SwatchesGUI() { EditorGUI.DrawRect(cell, border); EditorGUI.DrawRect(inner, EditorGUIUtility.isProSkin ? new Color(0.22f, 0.22f, 0.22f) : new Color(0.76f, 0.76f, 0.76f)); - EditorGUI.DrawRect(new Rect(cell.x + 4, cell.y + 4, cell.width - 8, cell.height - 8), s_Color); + EditorGUI.DrawRect(new Rect(cell.x + 3, cell.y + 3, cell.width - 6, cell.height - 6), s_Color); } if (GUI.Button(cell, kAddSwatchContent, GUIStyle.none)) { @@ -576,11 +577,14 @@ static void SwatchesGUI() { if (i == chosen) { - EditorGUI.DrawRect(new Rect(cell.x - 2, cell.y - 2, cell.width + 4, cell.height + 4), highlight); - EditorGUI.DrawRect(cell, Color.black); + // inside the cell: there is no room between the swatches + EditorGUI.DrawRect(cell, highlight); + EditorGUI.DrawRect(new Rect(cell.x + 2, cell.y + 2, cell.width - 4, cell.height - 4), swatches[i]); } else + { EditorGUI.DrawRect(cell, border); - EditorGUI.DrawRect(inner, swatches[i]); + EditorGUI.DrawRect(inner, swatches[i]); + } } EditorGUIUtility.AddCursorRect(cell, GUIUtility.hotControl == id ? MouseCursor.Pan : MouseCursor.Link); if (evt.type == EventType.MouseDown && cell.Contains(evt.mousePosition)) From eb4e402ecbc15665b1b1152b5d828c5f8f1c1528 Mon Sep 17 00:00:00 2001 From: Sander van Rossen Date: Mon, 26 Oct 2026 09:28:04 +0100 Subject: [PATCH 14/27] Paint overlay: swatches start where the labels' text does --- Editor/EditorTools/ChiselPaintTool.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Editor/EditorTools/ChiselPaintTool.cs b/Editor/EditorTools/ChiselPaintTool.cs index 85d2c691..3bf7122f 100644 --- a/Editor/EditorTools/ChiselPaintTool.cs +++ b/Editor/EditorTools/ChiselPaintTool.cs @@ -507,9 +507,12 @@ static void SwatchesGUI() var swatches = Swatches; var chosen = ChosenSwatch(); var count = swatches.Count + 1; - var perRow = Mathf.Max(1, Mathf.FloorToInt((kRowWidth + kSwatchGap) / (kSwatchSize + kSwatchGap))); + // starting where the labels' text does + var indent = EditorStyles.label.margin.left + EditorStyles.label.padding.left; + var perRow = Mathf.Max(1, Mathf.FloorToInt((kRowWidth - indent + kSwatchGap) / (kSwatchSize + kSwatchGap))); var rows = (count + perRow - 1) / perRow; var area = GUILayoutUtility.GetRect(kRowWidth, rows * (kSwatchSize + kSwatchGap) - kSwatchGap + 2); + area.xMin += indent; var evt = Event.current; var id = GUIUtility.GetControlID(FocusType.Passive); var border = EditorGUIUtility.isProSkin ? new Color(0.6f, 0.6f, 0.6f) : new Color(0.3f, 0.3f, 0.3f); From 3674bfbee77c3ebf63868326af63dd9d756bc57c Mon Sep 17 00:00:00 2001 From: Sander van Rossen Date: Mon, 26 Oct 2026 09:29:53 +0100 Subject: [PATCH 15/27] Check that compiled code lays out brush blobs as managed code does Once per domain, before the first brush registration, the sizes and offsets of BrushMeshBlob.Polygon, InternalChiselSurface and SurfaceDetails are measured managed and in a Burst-compiled job; a difference is logged as one error naming the fields. Growing SurfaceDetails once left code compiled for the old layout writing blobs managed code misread (polygons 8 bytes short), which broke every brush. --- Core/1.Input/BrushMesh/BrushMeshBlobLayout.cs | 106 ++++++++++++++++++ .../BrushMesh/BrushMeshBlobLayout.cs.meta | 2 + .../BrushMesh/BrushMeshManager.Internal.cs | 1 + Core/Tests/BrushMeshBlobLayoutTests.cs | 51 +++++++++ Core/Tests/BrushMeshBlobLayoutTests.cs.meta | 2 + 5 files changed, 162 insertions(+) create mode 100644 Core/1.Input/BrushMesh/BrushMeshBlobLayout.cs create mode 100644 Core/1.Input/BrushMesh/BrushMeshBlobLayout.cs.meta create mode 100644 Core/Tests/BrushMeshBlobLayoutTests.cs create mode 100644 Core/Tests/BrushMeshBlobLayoutTests.cs.meta diff --git a/Core/1.Input/BrushMesh/BrushMeshBlobLayout.cs b/Core/1.Input/BrushMesh/BrushMeshBlobLayout.cs new file mode 100644 index 00000000..b8023d3d --- /dev/null +++ b/Core/1.Input/BrushMesh/BrushMeshBlobLayout.cs @@ -0,0 +1,106 @@ +using System.Reflection; +using System.Text; +using Unity.Burst; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Jobs; +using UnityEngine; + +namespace Chisel.Core +{ + // Sizes and offsets of the structs every brush blob is made of. Native code compiled for + // another layout than the managed one writes blobs that managed code misreads. + public struct BrushMeshBlobLayout + { + public int polygonSize; + public int polygonFirstEdge; + public int polygonEdgeCount; + public int polygonDescriptionIndex; + public int polygonSurface; + + public int surfaceSize; + public int surfaceDetails; + public int surfaceParameters; + public int surfaceDestinationFlags; + public int surfaceOutputFlags; + public int surfaceHotspotLayout; + + public int detailsSize; + + public static unsafe BrushMeshBlobLayout Measure() + { + var polygon = default(BrushMeshBlob.Polygon); + var p = (byte*)&polygon; + var s = (byte*)&polygon.surface; + return new BrushMeshBlobLayout + { + polygonSize = UnsafeUtility.SizeOf(), + polygonFirstEdge = (int)((byte*)&polygon.firstEdge - p), + polygonEdgeCount = (int)((byte*)&polygon.edgeCount - p), + polygonDescriptionIndex = (int)((byte*)&polygon.descriptionIndex - p), + polygonSurface = (int)(s - p), + + surfaceSize = UnsafeUtility.SizeOf(), + surfaceDetails = (int)((byte*)&polygon.surface.details - s), + surfaceParameters = (int)((byte*)&polygon.surface.parameters - s), + surfaceDestinationFlags = (int)((byte*)&polygon.surface.destinationFlags - s), + surfaceOutputFlags = (int)((byte*)&polygon.surface.outputFlags - s), + surfaceHotspotLayout = (int)((byte*)&polygon.surface.hotspotLayout - s), + + detailsSize = UnsafeUtility.SizeOf() + }; + } + + [BurstCompile(CompileSynchronously = true)] + struct MeasureJob : IJob + { + [WriteOnly] public NativeReference layout; + public void Execute() { layout.Value = Measure(); } + } + + // Measured by the job system: Burst-compiled unless Burst is off + public static BrushMeshBlobLayout MeasureCompiled() + { + using var layout = new NativeReference(Allocator.TempJob); + new MeasureJob { layout = layout }.Run(); + return layout.Value; + } + + // The fields that differ, as "name managed/compiled"; empty when none do + public static string Differences(in BrushMeshBlobLayout managed, in BrushMeshBlobLayout compiled) + { + var text = new StringBuilder(); + object a = managed, b = compiled; + foreach (var field in typeof(BrushMeshBlobLayout).GetFields(BindingFlags.Instance | BindingFlags.Public)) + { + var x = (int)field.GetValue(a); + var y = (int)field.GetValue(b); + if (x != y) + text.Append(text.Length > 0 ? ", " : "").Append(field.Name).Append(' ').Append(x).Append('/').Append(y); + } + return text.ToString(); + } + + // Logs an error when the layouts differ + internal static bool Check(in BrushMeshBlobLayout managed, in BrushMeshBlobLayout compiled) + { + var differences = Differences(managed, compiled); + if (differences.Length == 0) + return true; + Debug.LogError("Chisel: compiled code has another brush blob layout than managed code (managed/compiled: " + differences + + "); every brush blob it writes is misread"); + return false; + } + + static bool s_Checked; + + // Once per domain, before the first brush registration + public static bool CheckOnce() + { + if (s_Checked) + return true; + s_Checked = true; + return Check(Measure(), MeasureCompiled()); + } + } +} diff --git a/Core/1.Input/BrushMesh/BrushMeshBlobLayout.cs.meta b/Core/1.Input/BrushMesh/BrushMeshBlobLayout.cs.meta new file mode 100644 index 00000000..08bb41c8 --- /dev/null +++ b/Core/1.Input/BrushMesh/BrushMeshBlobLayout.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 14827083611189347a271b52d513ab06 \ No newline at end of file diff --git a/Core/1.Input/BrushMesh/BrushMeshManager.Internal.cs b/Core/1.Input/BrushMesh/BrushMeshManager.Internal.cs index 7c9f0c58..a8e6cea3 100644 --- a/Core/1.Input/BrushMesh/BrushMeshManager.Internal.cs +++ b/Core/1.Input/BrushMesh/BrushMeshManager.Internal.cs @@ -867,6 +867,7 @@ internal static JobHandle ScheduleBrushRegistration(bool runInParallel, { Debug.Assert(generatedNodeDefinitions.IsCreated); } + BrushMeshBlobLayout.CheckOnce(); var brushMeshBlobCache = ChiselMeshLookup.Value.brushMeshBlobCache; var registerBrushesJob = new RegisterBrushMeshesJob { diff --git a/Core/Tests/BrushMeshBlobLayoutTests.cs b/Core/Tests/BrushMeshBlobLayoutTests.cs new file mode 100644 index 00000000..b7a979b3 --- /dev/null +++ b/Core/Tests/BrushMeshBlobLayoutTests.cs @@ -0,0 +1,51 @@ +using System.Text.RegularExpressions; +using NUnit.Framework; +using Unity.Burst; +using Unity.Collections.LowLevel.Unsafe; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Chisel.Core.Tests +{ + [TestFixture] + public sealed class BrushMeshBlobLayoutTests + { + // Burst-compiled jobs lay out brush blobs exactly as managed code reads them + [Test] + public void CompiledLayout_IsTheManagedLayout() + { + Assume.That(BurstCompiler.IsEnabled, Is.True, "Burst is off: the compiled layout is the managed one"); + var managed = BrushMeshBlobLayout.Measure(); + var compiled = BrushMeshBlobLayout.MeasureCompiled(); + Assert.That(BrushMeshBlobLayout.Differences(managed, compiled), Is.Empty); + } + + [Test] + public void Measure_ReadsTheManagedStructs() + { + var layout = BrushMeshBlobLayout.Measure(); + Assert.That(layout.polygonSize, Is.EqualTo(UnsafeUtility.SizeOf())); + Assert.That(layout.surfaceSize, Is.EqualTo(UnsafeUtility.SizeOf())); + Assert.That(layout.detailsSize, Is.EqualTo(UnsafeUtility.SizeOf())); + Assert.That(layout.polygonFirstEdge, Is.EqualTo(0)); + Assert.That(layout.polygonSurface + layout.surfaceSize, Is.LessThanOrEqualTo(layout.polygonSize)); + Assert.That(layout.surfaceDetails + layout.detailsSize, Is.LessThanOrEqualTo(layout.surfaceParameters)); + } + + // The 10-26 case: old code wrote 120 byte polygons of 104 byte surfaces, read at 128 and 112 + [Test] + public void Check_LogsEveryFieldThatDiffers() + { + var managed = BrushMeshBlobLayout.Measure(); + var stale = managed; + stale.polygonSize -= 8; + stale.surfaceParameters -= 8; + Assert.That(BrushMeshBlobLayout.Differences(managed, stale), + Is.EqualTo($"polygonSize {managed.polygonSize}/{stale.polygonSize}, surfaceParameters {managed.surfaceParameters}/{stale.surfaceParameters}")); + + LogAssert.Expect(LogType.Error, new Regex("another brush blob layout.*polygonSize")); + Assert.That(BrushMeshBlobLayout.Check(managed, stale), Is.False); + Assert.That(BrushMeshBlobLayout.Check(managed, managed), Is.True); + } + } +} diff --git a/Core/Tests/BrushMeshBlobLayoutTests.cs.meta b/Core/Tests/BrushMeshBlobLayoutTests.cs.meta new file mode 100644 index 00000000..0f6fe193 --- /dev/null +++ b/Core/Tests/BrushMeshBlobLayoutTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 032a40a01d5552d459f736166922ec9c \ No newline at end of file From 3e7695e10d38e2396bcb471789c5270ea4127894 Mon Sep 17 00:00:00 2001 From: Sander van Rossen Date: Mon, 26 Oct 2026 09:35:27 +0100 Subject: [PATCH 16/27] Paint: fill as an operation; swatches without duplicates Fill is one of the operations (a bucket icon): clicking a surface sets all of it to the color or value, and a stroke fills each surface it goes over once. The brush settings are disabled while it is chosen; the Fill Surface button is gone. Swatches hold a color once, compared as they are kept (8 bits a component): adding the current color when a swatch has it already chooses that swatch, and a color picked into a new swatch that another swatch has takes that one. The add cell takes only a left click. A right click on a swatch opens a menu to delete it, for those without a middle mouse button. The swatch borders are more subtle. --- .../Components/Paint/ChiselPaintBrush.cs | 9 +- Components/Tests/ChiselPaintTests.cs | 14 ++ Editor Resources/Icons/d_paint tool On.png | Bin 0 -> 599 bytes .../Icons/d_paint tool On.png.meta | 119 ++++++++++++++++ Editor Resources/Icons/d_paint tool.png | Bin 0 -> 601 bytes Editor Resources/Icons/d_paint tool.png.meta | 119 ++++++++++++++++ Editor Resources/Icons/d_paint_fill On.png | Bin 0 -> 419 bytes .../Icons/d_paint_fill On.png.meta | 108 +++++++++++++++ Editor Resources/Icons/d_paint_fill On@2x.png | Bin 0 -> 868 bytes .../Icons/d_paint_fill On@2x.png.meta | 108 +++++++++++++++ Editor Resources/Icons/d_paint_fill.png | Bin 0 -> 411 bytes Editor Resources/Icons/d_paint_fill.png.meta | 108 +++++++++++++++ Editor Resources/Icons/d_paint_fill@2x.png | Bin 0 -> 897 bytes .../Icons/d_paint_fill@2x.png.meta | 108 +++++++++++++++ Editor Resources/Icons/paint tool On.png | Bin 0 -> 602 bytes Editor Resources/Icons/paint tool On.png.meta | 119 ++++++++++++++++ Editor Resources/Icons/paint tool.png | Bin 0 -> 602 bytes Editor Resources/Icons/paint tool.png.meta | 119 ++++++++++++++++ Editor Resources/Icons/paint_fill On.png | Bin 0 -> 419 bytes Editor Resources/Icons/paint_fill On.png.meta | 108 +++++++++++++++ Editor Resources/Icons/paint_fill On@2x.png | Bin 0 -> 868 bytes .../Icons/paint_fill On@2x.png.meta | 108 +++++++++++++++ Editor Resources/Icons/paint_fill.png | Bin 0 -> 427 bytes Editor Resources/Icons/paint_fill.png.meta | 108 +++++++++++++++ Editor Resources/Icons/paint_fill@2x.png | Bin 0 -> 996 bytes Editor Resources/Icons/paint_fill@2x.png.meta | 108 +++++++++++++++ Editor/EditorTools/ChiselPaintTool.cs | 127 +++++++++++++----- 27 files changed, 1452 insertions(+), 38 deletions(-) create mode 100644 Editor Resources/Icons/d_paint tool On.png create mode 100644 Editor Resources/Icons/d_paint tool On.png.meta create mode 100644 Editor Resources/Icons/d_paint tool.png create mode 100644 Editor Resources/Icons/d_paint tool.png.meta create mode 100644 Editor Resources/Icons/d_paint_fill On.png create mode 100644 Editor Resources/Icons/d_paint_fill On.png.meta create mode 100644 Editor Resources/Icons/d_paint_fill On@2x.png create mode 100644 Editor Resources/Icons/d_paint_fill On@2x.png.meta create mode 100644 Editor Resources/Icons/d_paint_fill.png create mode 100644 Editor Resources/Icons/d_paint_fill.png.meta create mode 100644 Editor Resources/Icons/d_paint_fill@2x.png create mode 100644 Editor Resources/Icons/d_paint_fill@2x.png.meta create mode 100644 Editor Resources/Icons/paint tool On.png create mode 100644 Editor Resources/Icons/paint tool On.png.meta create mode 100644 Editor Resources/Icons/paint tool.png create mode 100644 Editor Resources/Icons/paint tool.png.meta create mode 100644 Editor Resources/Icons/paint_fill On.png create mode 100644 Editor Resources/Icons/paint_fill On.png.meta create mode 100644 Editor Resources/Icons/paint_fill On@2x.png create mode 100644 Editor Resources/Icons/paint_fill On@2x.png.meta create mode 100644 Editor Resources/Icons/paint_fill.png create mode 100644 Editor Resources/Icons/paint_fill.png.meta create mode 100644 Editor Resources/Icons/paint_fill@2x.png create mode 100644 Editor Resources/Icons/paint_fill@2x.png.meta diff --git a/Components/Components/Paint/ChiselPaintBrush.cs b/Components/Components/Paint/ChiselPaintBrush.cs index f5cc6839..b6c4d929 100644 --- a/Components/Components/Paint/ChiselPaintBrush.cs +++ b/Components/Components/Paint/ChiselPaintBrush.cs @@ -17,7 +17,9 @@ public enum ChiselPaintOperation : byte /// Towards the average of the neighbouring grid points Smooth = 3, /// Back towards the mode's default - Erase = 4 + Erase = 4, + /// The whole surface set to the brush's color or value + Fill = 5 } /// @@ -215,6 +217,11 @@ public static bool Dab(ChiselSurfacePaint paint, in ChiselPaintMode mode, Chisel { if (paint == null || !paint.IsValid) return false; + if (operation == ChiselPaintOperation.Fill) + { + Fill(paint, mode, target, group); + return true; + } var normal = Vector3.Cross(paint.cellU, paint.cellV).normalized; var before = operation == ChiselPaintOperation.Smooth ? (Vector4[])paint.values.Clone() : null; var changed = false; diff --git a/Components/Tests/ChiselPaintTests.cs b/Components/Tests/ChiselPaintTests.cs index c92a9048..31d86b2a 100644 --- a/Components/Tests/ChiselPaintTests.cs +++ b/Components/Tests/ChiselPaintTests.cs @@ -396,6 +396,20 @@ public void ChangingThePaintSpacing_ResamplesThePaint() Assert.That(box.SurfacePaint.Count, Is.EqualTo(1)); } + // A fill sets the whole surface, wherever it is clicked and whatever the brush's radius + [Test] + public void AFill_SetsTheWholeSurface() + { + var model = Model(); + var box = Box(model, Vector3.zero); + var top = SurfaceFacing(box, new float3(0, 1, 0)); + var mode = ChiselPaintMode.Color("Color"); + var paint = ChiselPaintBrush.PaintFor(box, top, mode); + Assert.That(ChiselPaintBrush.Dab(paint, mode, ChiselPaintOperation.Fill, new Vector4(0, 1, 0, 1), new Vector3(0.1f, 1, 0.1f), 0.01f, 0.1f, 0), Is.True); + foreach (var value in paint.values) + Assert.That(value, Is.EqualTo(new Vector4(0, 1, 0, 1))); + } + // The grid moves with its generator [Test] public void Paint_MovesWithItsGenerator() diff --git a/Editor Resources/Icons/d_paint tool On.png b/Editor Resources/Icons/d_paint tool On.png new file mode 100644 index 0000000000000000000000000000000000000000..affb6cf7af16319d8f3f47ea0ba0d1478585a006 GIT binary patch literal 599 zcmV-d0;v6oP)y zJ!=&~6o#Lf-S~lpAVgvWL9tS>38V-_kR)g!DAi$AA}te*;E=i;99TGr*UCbHGpF zh&*4=7F0eH{0DFlcrEWg=-b+7f*%6j0JFe*lbHC7@EKs!e8(^a3 z2QD>*M~|9VAxJ1D^MZG^6_wT(fy*iI=v9+%0_-914!Ej>D{NN-J_6e&M;e*mXMIHQ zd%#`bJ}|C|<}~7IxUQ}j=dA^d06PLd&~p&;xJN{Y&}U#$jV6j}C7=-Srb1r{6m#K` zyq!~RzorCC3V%NVF@dfCPxM1?LSC3iT+moHN0c@KoPZ(3PkY|m=iN>QOaPy>T}htr zC7=e5>hfT-9L;wG47Z%yCL#k_+2?&Fk7~08g~SB-8gRoztRA)i*VFByg@oGx?f_p> z3Qnp3bZrW-SHKFes$&+wm`>QrT%TTWK;!FQ*1SSKaM+tf;FpFsTVZozXjbmA787i! lz?4#4|4VQa0sI#N{s3K(sbXTzzzhHY002ovPDHLkV1i+@0lEMH literal 0 HcmV?d00001 diff --git a/Editor Resources/Icons/d_paint tool On.png.meta b/Editor Resources/Icons/d_paint tool On.png.meta new file mode 100644 index 00000000..86560671 --- /dev/null +++ b/Editor Resources/Icons/d_paint tool On.png.meta @@ -0,0 +1,119 @@ +fileFormatVersion: 2 +guid: 98d399f8e9b9efc45a3a615db41a0b11 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationMethod: 0 + spriteTessellationDetail: -1 + spriteGeometrySubdivision: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor Resources/Icons/d_paint tool.png b/Editor Resources/Icons/d_paint tool.png new file mode 100644 index 0000000000000000000000000000000000000000..5fcf7710de6af7ec40fe6d00ca475e8140824fcc GIT binary patch literal 601 zcmV-f0;c_mP)y z&r4N76vsbz?v;!dLSU#tP(+IYH$saRA(+@ANN^)YD|c=CFQRS#fG*tVvLecjytN2v z9BKFp=Qhz!%^Vu;C>pCL(+U`034~P5~Xks|qkx;Z+0} zmD8jOyi)A`-ifp8xH)imm;3II^X}ZqRDcX<8i5>WRM?NuRSM_>mVq}>@Cw)j zepNJhfSNe0?x|nYm+FW*sqU%^5mSq^g{Zyiw)(yz;Hx^VW@=tlAP44vb6^YT4q>yv z39twZg#7HOCwy1uqhekCp`NQ#!85fd6l7HjYCs)$4IBV3fKlKA_y!CCOEKtLm4X>{ zBRZuwC*P{t@E+;>t@ek478QiF=juE4Sl#gff7L%>e~hHA_5zgnA%d?Xxm@H%9Z;9m zR}r5ViAh#>*lC~%x4{BwCkEA1^;4mSHU)*el~UrRx)%Po;X6{FrN&e&N1^1iy01=@ z?$%3%$Es#fuzl;A-PCdQQ2iN=^5!S`4?6yh>9P~j`j(SZeDeLvy8{MFDPOyPAtp$ai4hr)f$|6w#f;ogn3#w%lSiTy zd6QDeBd>e!PfKgpviqNV&pB7V+Uw}G+2zg_?8?Z1O_V%SP(eh$X*mZK4(Bmq6@w};9 z1%;%UIEuBT&ymP>xQ&S(p2cOVRBF6a>?zhF>9M30 zlD_>+$ttJ*~PVwMzQzuP4K5x3w<3DQmunl$CU;w1)&tM=v6(V~IO0gwx&@hWEhe zJPfKMQxl1`ABSC%yMde7M4x}|a2k)~O<-QwU3#<}^KpCP8E$zF zR2n59zl^Njm&v~$7#N5qdF?OybqG;XZGKF2H9(*UBu@F2yrkshL;~;J`Hv8WrU@uK zJX^!Lj=RpuSWZM-&O=cAVbMIQy?i~T50*8OIwU=mbX3xh_?cHUNx%#k`dS>nCri=N ujQ2Vm$9dI2xI154^fLtgMy;#(zu`BS{-W}WSKsph0000YoBu~S(Yh@3hE{AP0?CNXu}iq2X>0D_;y)w zPEf}!yux>^;sAH>0ypvLG9|6Jfmy6!yMX<{F8*GpU^F_W;uf6aAtup@pVtxeVKcrH z!8C?3AA57y3gV`y#*=LH;tjg+8i$yQ`8*E))iMQ}7{dfU<2|0@Wnpm7uz}-7f=ZTU zDXAUzFcU+&A9yVYlC@>5go;|xLLEog!G|b5i=>2C!v8or@F<%77f>yzX^BfSAJ%dO z)xdtl`Wz!=1m}_b1@8(2KEV>+;x>BlAjoQYr#w=}1gyvO2e5#>zz1W!i*D>U668vf z@5KgtY5yaJ@C(0FUOI)c`*GoNklbnnI20j6;Kwn@u&;Tq2<^a2a_mz@}l!P`p>~0hA3OFp_LN*H6qOj}$ zzEdk6e2+uwCH63zvW&z)B@& z#U4m0p>DceTKtX3d|(aGA9xKM2c84NfZ4!Y;Ch1RN+K{)?=5cEwoo;&9+xEd0he_iEJOEhh z6fcO;2?-IZ`whTMwLA4RDvYh=BxI~ox(&<#CITB3me0U>Ww_a7jgJth*M%U{i-D7r zVJR^^qXer_B?QYj@U*gho4~W0YbT<)eV+2hbfjb{MoRcjIT1vu4tORnG|4-i4lv)c zB62y1&tWK9$T9%OYz$C3ztq|5GY?MN~G9~<=ld&z~a7vnEc@QMJO8qm`*HFv@ z`DQWC=#cUrO8PbuP)OW8(vQdi*GogX%ugjOmP8`l1CO|}^@~`3gx^!ZK2=jBx7TQ_ z%86I1R05vFJ&BCfyggypFMz$kh2Jy&^yPF&*&0YEx>QC?X=XrIr29+U*hS#16xGOO zbwt(#;a=ln$r~z%iYO6iCw8dCGF+uMa4b1Okp#9ky$q7m>6iTv}GU zP(K7-`Y^<0!FMH+?WQzMOF~Y3lqR(3(vmAC#l32AFJRcck|b}%y+EMbf6}Gsz<;%0 XWDoA;vn_$W00000NkvXXu0mjf^bemk literal 0 HcmV?d00001 diff --git a/Editor Resources/Icons/d_paint_fill@2x.png.meta b/Editor Resources/Icons/d_paint_fill@2x.png.meta new file mode 100644 index 00000000..8c4961d4 --- /dev/null +++ b/Editor Resources/Icons/d_paint_fill@2x.png.meta @@ -0,0 +1,108 @@ +fileFormatVersion: 2 +guid: 86ffe53716764a13b6ca64b212cc383e +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: -100 + wrapU: 1 + wrapV: 1 + wrapW: 1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 1 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 32 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor Resources/Icons/paint tool On.png b/Editor Resources/Icons/paint tool On.png new file mode 100644 index 0000000000000000000000000000000000000000..253ece0ac6ea16b6e3338dc9ffbbdd8ff90c6400 GIT binary patch literal 602 zcmV-g0;Ty z&r4N76vsbz?v;!dLSU#tP(+IYH$saRA(+@ANN^)YD|c=CFQRS#fG*tVvLecjySEV` zja)>nH2eWVMy2n1W#l_BJoI_z-Ro>RFuXf==H1WtoH=u5l18IJBuju4I8r;YEF!!B znk{`y1b8agHDC(3rG6440v4VF{lF4%8`uUO0N-o?Oayoz@KNyJKrir6Q7~Bq_#$u@ z_y=5(=kK)yxrqe-3tR%$)z)&B!r4uOXX$h38afd$|y@LXd} z!M}#UokwLX6~q>kcY-&y6uHJf0AnF==T(_G7WM}C0X$K{rT(b~Yymr7j+8QU&UlyL zFM$`pD_}qsEm3p3VO*!4>sJ=g1sn?eTF+L@Y>NmRp*`Tf4jLz_l7LjeNrgTa$mPO} zyd70-f2srw3jbLGTmn4?=JY}1g*^2lF{rlWZBeQSNCdPYo^N^YKcAdrz!0#jy{SMp#_D}hbQOG+EdpiNn4%Cdd6n;+BW?7pt omteI5LrQURk>D}{_%8$;1J)R&pZ}VYvj6}907*qoM6N<$f+9o%>i_@% literal 0 HcmV?d00001 diff --git a/Editor Resources/Icons/paint tool On.png.meta b/Editor Resources/Icons/paint tool On.png.meta new file mode 100644 index 00000000..084880b2 --- /dev/null +++ b/Editor Resources/Icons/paint tool On.png.meta @@ -0,0 +1,119 @@ +fileFormatVersion: 2 +guid: 2356d3f8a7d43464a85dcc1739f9a05b +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationMethod: 0 + spriteTessellationDetail: -1 + spriteGeometrySubdivision: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor Resources/Icons/paint tool.png b/Editor Resources/Icons/paint tool.png new file mode 100644 index 0000000000000000000000000000000000000000..d63b4c864a61a6f5eb41df674e3360ad0f44a8a5 GIT binary patch literal 602 zcmV-g0;Ty zPfJxn7>A!T=ZH!RK`<&oFv3Mb8=*yu2uyMz7{moBTD5E2w}`fV0bR7IWkr;m`vF8K zMHf;lmHvRBlIA_082Jqhm%N?1=QtZ*819)fbI0 zUkiadkIGmoh%F}X1aE06a*clg?uEdeS7qi{*h}CWFr$P^{ZkG24*c|Tq?DQejQ0rs z40sAW2d=21C2DRr+}Ek+`jrLr07nA9(6be@&?dr0Xa~5dgT{%fBp?-VPNB~Qa=9=s zZ>Ln-XO)0K;Xh4)OQ31sl|E>^kf&ZGZm2cAElL#uiGVi5uiM`H&nG7tFaiA1as_$b zNk9P%=yohvV*(vXHc~p2ykV=e&e*qqN5i9BzkhGn4&TPW115be6 zkb)B`09~6bY#CSrK5LsodS;a`xO`i_Wto7qmXwU_D%wv{ZTXCQusMhn`Irw oT!PgK3@OFMS%S+5;9Lm!3+x7@L#X1xwEzGB07*qoM6N<$g4aL-X#fBK literal 0 HcmV?d00001 diff --git a/Editor Resources/Icons/paint tool.png.meta b/Editor Resources/Icons/paint tool.png.meta new file mode 100644 index 00000000..a2555fec --- /dev/null +++ b/Editor Resources/Icons/paint tool.png.meta @@ -0,0 +1,119 @@ +fileFormatVersion: 2 +guid: da1863fda0d08fd46843f82920853093 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationMethod: 0 + spriteTessellationDetail: -1 + spriteGeometrySubdivision: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 4 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 4 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor Resources/Icons/paint_fill On.png b/Editor Resources/Icons/paint_fill On.png new file mode 100644 index 0000000000000000000000000000000000000000..b177fc08e7f8452024c11215a508db3f7526eec6 GIT binary patch literal 419 zcmV;U0bKrxP)A`-ifp8xH)imm;3II^X}ZqRDcX<8i5>WRM?NuRSM_>mVq}>@Cw)j zepNJhfSNe0?x|nYm+FW*sqU%^5mSq^g{Zyiw)(yz;Hx^VW@=tlAP44vb6^YT4q>yv z39twZg#7HOCwy1uqhekCp`NQ#!85fd6l7HjYCs)$4IBV3fKlKA_y!CCOEKtLm4X>{ zBRZuwC*P{t@E+;>t@ek478QiF=juE4Sl#gff7L%>e~hHA_5zgnA%d?Xxm@H%9Z;9m zR}r5ViAh#>*lC~%x4{BwCkEA1^;4mSHU)*el~UrRx)%Po;X6{FrN&e&N1^1iy01=@ z?$%3%$Es#fuzl;A-PCdQQ2iN=^5!S`4?6yh>9P~j`j(SZeDeLvy8{MFDPOyPAtp$ai4hr)f$|6w#f;ogn3#w%lSiTy zd6QDeBd>e!PfKgpviqNV&pB7V+Uw}G+2zg_?8?Z1O_V%SP(eh$X*mZK4(Bmq6@w};9 z1%;%UIEuBT&ymP>xQ&S(p2cOVRBF6a>?zhF>9M30 zlD_>+$ttJ*~PVwMzQzuP4K5x3w<3DQmunl$CU;w1)&tM=v6(V~IO0gwx&@hWEhe zJPfKMQxl1`ABSC%yMde7M4x}|a2k)~O<-QwU3#<}^KpCP8E$zF zR2n59zl^Njm&v~$7#N5qdF?OybqG;XZGKF2H9(*UBu@F2yrkshL;~;J`Hv8WrU@uK zJX^!Lj=RpuSWZM-&O=cAVbMIQy?i~T50*8OIwU=mbX3xh_?cHUNx%#k`dS>nCri=N ujQ2Vm$9dI2xI154^fLtgMy;#(zu`BS{-W}WSKsph000079 zw>1s^B4}X%YuLnd9AFsFa2YG;LbHzGVkW%6yGrakKH|rD6BKxW{VeXu^Sk(p8C=QY z#{VRYVLktYU=jE5Fwa)ghvT|Qiqv)iH}L^|7{no7p&yUZtFAf;>zKd>-s3*z@C75; z@kyH5?3B_d%d*JeD3-8?TN!WV3aL-Ako(_N0u@YF^j6{rpN7kig?i`N&`d2 zGO(g%y#MgM7hQ&I{qL3BiUdu_NNizzn2yM~jWx79}H?W=Y z+xfkYA?$V%1Y(kyESzs6y2bf2?%*@NMP402+x_ZQhpB-b+~|A<=ON7PB#X1tx)VUp VPxt!8%JcvL002ovPDHLkV1mH0&Ar!RSL>LDWJevFJk$=^`(p%!@44yb!-Yyif_r(8LS9 z2(Kg-?4pPwq9RE8z!EeSQ4vE>@6z=5EBoEG?mGkd_m$jCM(AJckwss3R59|VF z0RM;gr6iZ)Xy8lW46sDbE4mVwq`l^R4sZ~-2~5`eSf+&~>4dKU2agB_LMUJ1>iQY2Ur1I1KPtil!`*qsVt$sQpi2j zerkK(2Y!`vtN(exTHtG74De2fP$@~klluOTM_ z$jSofl9ARxzvlc+)%haeUtlq?3b-!amB3EzwIu$qKT$})MJdgcfyaOYz#)aVQ*>-q zg6R1R<-<_vn%-*dua-&3q*D4^3}|=zJMga9(2{H^a7p`L1b)+6H6*G57zV5>%Lhry zn{~%i#cn3YL`)_vz*dn|o5EfRjywv>l{bZ{IhT=(V#3qP39Io}fuH1fBQQ6xVto_K z6p_K&|8`LTnR-EFF;jd5_*RX`#o4hhaSpJ3PXnhyQuiq-C`ZIMM9J3o5%Dz3Za;)w z*S0!BGWL8l?7I&DA-rBnmsNt7WVA@XuL!J_?0lVmysmIsn6!E%^#Sm-yXUw8se2cFZr;o=WsKBXoAA_!x~HDQ49MwP3Z&eEusiPrC^96dsa z;G8oG10dY(r@#(5O?o~CW{W|cA8cqD1iY@_EsEMyC1X#|$SE!kV!b&3c~P}>M8X6W z%giI;>!yE?q!IvX!O(laGji^1S`GUQ%zwZq^3f{iHb49s2`VoEz>TA%y4}m%=;9sG za^kEw;iTq@d#WI7^53v8aRbf9ucm`)-XHHJ=03j;2kb5UfdNoL3N@pLCU*gK0P@zB S?zGte0000 s_Filled = new(); + static void Dab(Hit hit) { var index = ModeIndex(hit.profile); if (index < 0) return; var mode = hit.profile.modes[index]; + if (s_Operation == ChiselPaintOperation.Fill) + { + // a stroke fills each surface it goes over once + if (!s_Filled.Add((hit.generator, hit.surfaceIndex))) + return; + } Undo.RecordObject(hit.generator, "Chisel Paint"); var paint = ChiselPaintBrush.PaintFor(hit.generator, hit.surfaceIndex, mode); if (paint == null) @@ -374,6 +385,7 @@ static int Toggles(int chosen, GUIContent[] contents, bool[] disabled) static readonly (ChiselPaintOperation operation, string icon, string name, string tooltip)[] kOperations = { (ChiselPaintOperation.Paint, "paint_paint", "Paint", "Towards the color or value"), + (ChiselPaintOperation.Fill, "paint_fill", "Fill", "The whole surface clicked on, to the color or value"), (ChiselPaintOperation.Add, "paint_add", "Add", "Raises the value"), (ChiselPaintOperation.Subtract, "paint_subtract", "Subtract", "Lowers the value"), (ChiselPaintOperation.Smooth, "paint_smooth", "Smooth", "Towards the neighbouring paint"), @@ -445,7 +457,7 @@ static Texture2D Falloff() // neighbours share their border const float kSwatchGap = -1; static List s_Swatches; - static readonly GUIContent kAddSwatchContent = new(string.Empty, "Adds the current color as a swatch, and picks its color (middle-click a swatch to remove it)"); + static readonly GUIContent kAddSwatchContent = new(string.Empty, "Adds the current color as a swatch, and picks its color (middle-click or right-click a swatch to remove it)"); // the swatches, kept per user static List Swatches @@ -457,7 +469,7 @@ static List Swatches s_Swatches = new List(); foreach (var hex in EditorPrefs.GetString(kSwatchesPref, string.Empty).Split(';')) { - if (UnityEngine.ColorUtility.TryParseHtmlString("#" + hex, out var color)) + if (UnityEngine.ColorUtility.TryParseHtmlString("#" + hex, out var color) && !s_Swatches.Contains(color)) s_Swatches.Add(color); } if (s_Swatches.Count == 0 && !EditorPrefs.HasKey(kSwatchesPref)) @@ -474,11 +486,16 @@ static void SaveSwatches() EditorPrefs.SetString(kSwatchesPref, string.Join(";", hexes)); } - static int ChosenSwatch() + static int ChosenSwatch() => SwatchOf(s_Color); + + // The swatch of a color, as the swatches are kept (8 bits a component); -1 when it has none + static int SwatchOf(Color color) { + Color32 wanted = color; for (int i = 0; i < Swatches.Count; i++) { - if (Swatches[i] == s_Color) + Color32 swatch = Swatches[i]; + if (swatch.r == wanted.r && swatch.g == wanted.g && swatch.b == wanted.b && swatch.a == wanted.a) return i; } return -1; @@ -502,6 +519,42 @@ static void PickColor(Color color, System.Action changed) // a drag reorders them, a middle-click removes one static int s_DraggedSwatch = -1; + static void RemoveSwatch(int index) + { + if (index < 0 || index >= Swatches.Count) + return; + Swatches.RemoveAt(index); + SaveSwatches(); + SceneView.RepaintAll(); + } + + // The current color as a new swatch, then Unity's color picker on it; a color some other swatch has already + // isn't added twice, that swatch is chosen instead + static void AddSwatch() + { + var index = Swatches.Count; + Swatches.Add(s_Color); + SaveSwatches(); + PickColor(s_Color, color => + { + s_Color = color; + var existing = SwatchOf(color); + if (existing >= 0 && existing != index) + { + if (index >= 0 && index < Swatches.Count) + Swatches.RemoveAt(index); + index = -1; + } else if (index < 0) + { + index = Swatches.Count; + Swatches.Add(color); + } else if (index < Swatches.Count) + Swatches[index] = color; + SaveSwatches(); + SceneView.RepaintAll(); + }); + } + static void SwatchesGUI() { var swatches = Swatches; @@ -515,7 +568,7 @@ static void SwatchesGUI() area.xMin += indent; var evt = Event.current; var id = GUIUtility.GetControlID(FocusType.Passive); - var border = EditorGUIUtility.isProSkin ? new Color(0.6f, 0.6f, 0.6f) : new Color(0.3f, 0.3f, 0.3f); + var border = EditorGUIUtility.isProSkin ? new Color(0.36f, 0.36f, 0.36f) : new Color(0.58f, 0.58f, 0.58f); var highlight = new Color(0.25f, 0.6f, 1f); Rect CellOf(int i) => new(area.x + (i % perRow) * (kSwatchSize + kSwatchGap), area.y + (i / perRow) * (kSwatchSize + kSwatchGap) + 1, kSwatchSize, kSwatchSize); @@ -560,19 +613,13 @@ static void SwatchesGUI() EditorGUI.DrawRect(inner, EditorGUIUtility.isProSkin ? new Color(0.22f, 0.22f, 0.22f) : new Color(0.76f, 0.76f, 0.76f)); EditorGUI.DrawRect(new Rect(cell.x + 3, cell.y + 3, cell.width - 6, cell.height - 6), s_Color); } - if (GUI.Button(cell, kAddSwatchContent, GUIStyle.none)) + GUI.Label(cell, kAddSwatchContent, GUIStyle.none); + if (evt.type == EventType.MouseDown && evt.button == 0 && cell.Contains(evt.mousePosition)) { - var index = swatches.Count; - swatches.Add(s_Color); - SaveSwatches(); - PickColor(s_Color, color => - { - if (index < Swatches.Count) - Swatches[index] = color; - s_Color = color; - SaveSwatches(); - SceneView.RepaintAll(); - }); + evt.Use(); + // a color that has a swatch already is chosen there + if (chosen < 0) + AddSwatch(); } continue; } @@ -602,10 +649,17 @@ static void SwatchesGUI() evt.Use(); } else if (evt.button == 2) { - swatches.RemoveAt(i); - SaveSwatches(); + RemoveSwatch(i); evt.Use(); GUIUtility.ExitGUI(); + } else if (evt.button == 1) + { + // for those without a middle mouse button + var index = i; + var menu = new GenericMenu(); + menu.AddItem(new GUIContent("Delete"), false, () => RemoveSwatch(index)); + menu.ShowAsContext(); + evt.Use(); } } } @@ -630,26 +684,25 @@ internal static void PanelGUI() else s_Value = EditorGUILayout.Slider(new GUIContent(s_ModeName), s_Value, chosen.range.x, chosen.range.y); } - s_Radius = Mathf.Max(0.01f, EditorGUILayout.FloatField(kRadiusContent, s_Radius)); - using (new EditorGUILayout.HorizontalScope()) + // a fill takes the whole surface: the brush doesn't count + using (new EditorGUI.DisabledScope(s_Operation == ChiselPaintOperation.Fill)) + using (new EditorGUILayout.VerticalScope()) { - using (new EditorGUILayout.VerticalScope()) + s_Radius = Mathf.Max(0.01f, EditorGUILayout.FloatField(kRadiusContent, s_Radius)); + using (new EditorGUILayout.HorizontalScope()) { - s_Strength = EditorGUILayout.Slider(kStrengthContent, s_Strength, 0, 1); - s_Hardness = EditorGUILayout.Slider(kHardnessContent, s_Hardness, 0, 1); + using (new EditorGUILayout.VerticalScope()) + { + s_Strength = EditorGUILayout.Slider(kStrengthContent, s_Strength, 0, 1); + s_Hardness = EditorGUILayout.Slider(kHardnessContent, s_Hardness, 0, 1); + } + var falloff = GUILayoutUtility.GetRect(kFalloffSize, kFalloffSize, GUILayout.Width(kFalloffSize), GUILayout.Height(kFalloffSize)); + if (Event.current.type == EventType.Repaint) + GUI.DrawTexture(falloff, Falloff(), ScaleMode.ScaleToFit); } - var falloff = GUILayoutUtility.GetRect(kFalloffSize, kFalloffSize, GUILayout.Width(kFalloffSize), GUILayout.Height(kFalloffSize)); - if (Event.current.type == EventType.Repaint) - GUI.DrawTexture(falloff, Falloff(), ScaleMode.ScaleToFit); } using (new EditorGUI.DisabledScope(!Paintable(s_Last))) { - if (GUILayout.Button(kFillContent) && LastPaint(out var paint, out var mode)) - { - var target = mode.kind == ChiselPaintKind.Color ? (Vector4)s_Color : Vector4.one * s_Value; - ChiselPaintBrush.Fill(paint, mode, target, s_Last.profile.GroupOf(ModeIndex(s_Last.profile))); - Changed(); - } s_ShowChannels = EditorGUILayout.Foldout(s_ShowChannels, kChannelsContent, true); if (s_ShowChannels) { From cc65b34961f877e9959c0b740f5f59384fa9913a Mon Sep 17 00:00:00 2001 From: Sander van Rossen Date: Mon, 26 Oct 2026 09:35:41 +0100 Subject: [PATCH 17/27] Remove duplicate paint tool icons The icon script wrote them again under the old name with spaces; the tool loads paint_tool. --- Editor Resources/Icons/d_paint tool On.png | Bin 599 -> 0 bytes .../Icons/d_paint tool On.png.meta | 119 ------------------ Editor Resources/Icons/d_paint tool.png | Bin 601 -> 0 bytes Editor Resources/Icons/d_paint tool.png.meta | 119 ------------------ Editor Resources/Icons/paint tool On.png | Bin 602 -> 0 bytes Editor Resources/Icons/paint tool On.png.meta | 119 ------------------ Editor Resources/Icons/paint tool.png | Bin 602 -> 0 bytes Editor Resources/Icons/paint tool.png.meta | 119 ------------------ 8 files changed, 476 deletions(-) delete mode 100644 Editor Resources/Icons/d_paint tool On.png delete mode 100644 Editor Resources/Icons/d_paint tool On.png.meta delete mode 100644 Editor Resources/Icons/d_paint tool.png delete mode 100644 Editor Resources/Icons/d_paint tool.png.meta delete mode 100644 Editor Resources/Icons/paint tool On.png delete mode 100644 Editor Resources/Icons/paint tool On.png.meta delete mode 100644 Editor Resources/Icons/paint tool.png delete mode 100644 Editor Resources/Icons/paint tool.png.meta diff --git a/Editor Resources/Icons/d_paint tool On.png b/Editor Resources/Icons/d_paint tool On.png deleted file mode 100644 index affb6cf7af16319d8f3f47ea0ba0d1478585a006..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 599 zcmV-d0;v6oP)y zJ!=&~6o#Lf-S~lpAVgvWL9tS>38V-_kR)g!DAi$AA}te*;E=i;99TGr*UCbHGpF zh&*4=7F0eH{0DFlcrEWg=-b+7f*%6j0JFe*lbHC7@EKs!e8(^a3 z2QD>*M~|9VAxJ1D^MZG^6_wT(fy*iI=v9+%0_-914!Ej>D{NN-J_6e&M;e*mXMIHQ zd%#`bJ}|C|<}~7IxUQ}j=dA^d06PLd&~p&;xJN{Y&}U#$jV6j}C7=-Srb1r{6m#K` zyq!~RzorCC3V%NVF@dfCPxM1?LSC3iT+moHN0c@KoPZ(3PkY|m=iN>QOaPy>T}htr zC7=e5>hfT-9L;wG47Z%yCL#k_+2?&Fk7~08g~SB-8gRoztRA)i*VFByg@oGx?f_p> z3Qnp3bZrW-SHKFes$&+wm`>QrT%TTWK;!FQ*1SSKaM+tf;FpFsTVZozXjbmA787i! lz?4#4|4VQa0sI#N{s3K(sbXTzzzhHY002ovPDHLkV1i+@0lEMH diff --git a/Editor Resources/Icons/d_paint tool On.png.meta b/Editor Resources/Icons/d_paint tool On.png.meta deleted file mode 100644 index 86560671..00000000 --- a/Editor Resources/Icons/d_paint tool On.png.meta +++ /dev/null @@ -1,119 +0,0 @@ -fileFormatVersion: 2 -guid: 98d399f8e9b9efc45a3a615db41a0b11 -TextureImporter: - internalIDToNameTable: [] - externalObjects: {} - serializedVersion: 13 - mipmaps: - mipMapMode: 0 - enableMipMap: 1 - sRGBTexture: 1 - linearTexture: 0 - fadeOut: 0 - borderMipMap: 0 - mipMapsPreserveCoverage: 0 - alphaTestReferenceValue: 0.5 - mipMapFadeDistanceStart: 1 - mipMapFadeDistanceEnd: 3 - bumpmap: - convertToNormalMap: 0 - externalNormalMap: 0 - heightScale: 0.25 - normalMapFilter: 0 - flipGreenChannel: 0 - isReadable: 0 - streamingMipmaps: 0 - streamingMipmapsPriority: 0 - vTOnly: 0 - ignoreMipmapLimit: 0 - grayScaleToAlpha: 0 - generateCubemap: 6 - cubemapConvolution: 0 - seamlessCubemap: 0 - textureFormat: 1 - maxTextureSize: 2048 - textureSettings: - serializedVersion: 2 - filterMode: 1 - aniso: 1 - mipBias: 0 - wrapU: 0 - wrapV: 0 - wrapW: 0 - nPOTScale: 1 - lightmap: 0 - compressionQuality: 50 - spriteMode: 0 - spriteExtrude: 1 - spriteMeshType: 1 - alignment: 0 - spritePivot: {x: 0.5, y: 0.5} - spritePixelsToUnits: 100 - spriteBorder: {x: 0, y: 0, z: 0, w: 0} - spriteGenerateFallbackPhysicsShape: 1 - alphaUsage: 1 - alphaIsTransparency: 0 - spriteTessellationMethod: 0 - spriteTessellationDetail: -1 - spriteGeometrySubdivision: -1 - textureType: 0 - textureShape: 1 - singleChannelComponent: 0 - flipbookRows: 1 - flipbookColumns: 1 - maxTextureSizeSet: 0 - compressionQualitySet: 0 - textureFormatSet: 0 - ignorePngGamma: 0 - applyGammaDecoding: 0 - swizzle: 50462976 - cookieLightType: 0 - platformSettings: - - serializedVersion: 4 - buildTarget: DefaultTexturePlatform - maxTextureSize: 2048 - resizeAlgorithm: 0 - textureFormat: -1 - textureCompression: 1 - compressionQuality: 50 - crunchedCompression: 0 - allowsAlphaSplitting: 0 - overridden: 0 - ignorePlatformSupport: 0 - androidETC2FallbackOverride: 0 - forceMaximumCompressionQuality_BC6H_BC7: 0 - - serializedVersion: 4 - buildTarget: Standalone - maxTextureSize: 2048 - resizeAlgorithm: 0 - textureFormat: -1 - textureCompression: 1 - compressionQuality: 50 - crunchedCompression: 0 - allowsAlphaSplitting: 0 - overridden: 0 - ignorePlatformSupport: 0 - androidETC2FallbackOverride: 0 - forceMaximumCompressionQuality_BC6H_BC7: 0 - spriteSheet: - serializedVersion: 2 - sprites: [] - outline: [] - customData: - physicsShape: [] - bones: [] - spriteID: - internalID: 0 - vertices: [] - indices: - edges: [] - weights: [] - secondaryTextures: [] - spriteCustomMetadata: - entries: [] - nameFileIdTable: {} - mipmapLimitGroupName: - pSDRemoveMatte: 0 - userData: - assetBundleName: - assetBundleVariant: diff --git a/Editor Resources/Icons/d_paint tool.png b/Editor Resources/Icons/d_paint tool.png deleted file mode 100644 index 5fcf7710de6af7ec40fe6d00ca475e8140824fcc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 601 zcmV-f0;c_mP)y z&r4N76vsbz?v;!dLSU#tP(+IYH$saRA(+@ANN^)YD|c=CFQRS#fG*tVvLecjytN2v z9BKFp=Qhz!%^Vu;C>pCL(+U`034~P5~Xks|qkx;Z+0} zmD8jOyi)y z&r4N76vsbz?v;!dLSU#tP(+IYH$saRA(+@ANN^)YD|c=CFQRS#fG*tVvLecjySEV` zja)>nH2eWVMy2n1W#l_BJoI_z-Ro>RFuXf==H1WtoH=u5l18IJBuju4I8r;YEF!!B znk{`y1b8agHDC(3rG6440v4VF{lF4%8`uUO0N-o?Oayoz@KNyJKrir6Q7~Bq_#$u@ z_y=5(=kK)yxrqe-3tR%$)z)&B!r4uOXX$h38afd$|y@LXd} z!M}#UokwLX6~q>kcY-&y6uHJf0AnF==T(_G7WM}C0X$K{rT(b~Yymr7j+8QU&UlyL zFM$`pD_}qsEm3p3VO*!4>sJ=g1sn?eTF+L@Y>NmRp*`Tf4jLz_l7LjeNrgTa$mPO} zyd70-f2srw3jbLGTmn4?=JY}1g*^2lF{rlWZBeQSNCdPYo^N^YKcAdrz!0#jy{SMp#_D}hbQOG+EdpiNn4%Cdd6n;+BW?7pt omteI5LrQURk>D}{_%8$;1J)R&pZ}VYvj6}907*qoM6N<$f+9o%>i_@% diff --git a/Editor Resources/Icons/paint tool On.png.meta b/Editor Resources/Icons/paint tool On.png.meta deleted file mode 100644 index 084880b2..00000000 --- a/Editor Resources/Icons/paint tool On.png.meta +++ /dev/null @@ -1,119 +0,0 @@ -fileFormatVersion: 2 -guid: 2356d3f8a7d43464a85dcc1739f9a05b -TextureImporter: - internalIDToNameTable: [] - externalObjects: {} - serializedVersion: 13 - mipmaps: - mipMapMode: 0 - enableMipMap: 1 - sRGBTexture: 1 - linearTexture: 0 - fadeOut: 0 - borderMipMap: 0 - mipMapsPreserveCoverage: 0 - alphaTestReferenceValue: 0.5 - mipMapFadeDistanceStart: 1 - mipMapFadeDistanceEnd: 3 - bumpmap: - convertToNormalMap: 0 - externalNormalMap: 0 - heightScale: 0.25 - normalMapFilter: 0 - flipGreenChannel: 0 - isReadable: 0 - streamingMipmaps: 0 - streamingMipmapsPriority: 0 - vTOnly: 0 - ignoreMipmapLimit: 0 - grayScaleToAlpha: 0 - generateCubemap: 6 - cubemapConvolution: 0 - seamlessCubemap: 0 - textureFormat: 1 - maxTextureSize: 2048 - textureSettings: - serializedVersion: 2 - filterMode: 1 - aniso: 1 - mipBias: 0 - wrapU: 0 - wrapV: 0 - wrapW: 0 - nPOTScale: 1 - lightmap: 0 - compressionQuality: 50 - spriteMode: 0 - spriteExtrude: 1 - spriteMeshType: 1 - alignment: 0 - spritePivot: {x: 0.5, y: 0.5} - spritePixelsToUnits: 100 - spriteBorder: {x: 0, y: 0, z: 0, w: 0} - spriteGenerateFallbackPhysicsShape: 1 - alphaUsage: 1 - alphaIsTransparency: 0 - spriteTessellationMethod: 0 - spriteTessellationDetail: -1 - spriteGeometrySubdivision: -1 - textureType: 0 - textureShape: 1 - singleChannelComponent: 0 - flipbookRows: 1 - flipbookColumns: 1 - maxTextureSizeSet: 0 - compressionQualitySet: 0 - textureFormatSet: 0 - ignorePngGamma: 0 - applyGammaDecoding: 0 - swizzle: 50462976 - cookieLightType: 0 - platformSettings: - - serializedVersion: 4 - buildTarget: DefaultTexturePlatform - maxTextureSize: 2048 - resizeAlgorithm: 0 - textureFormat: -1 - textureCompression: 1 - compressionQuality: 50 - crunchedCompression: 0 - allowsAlphaSplitting: 0 - overridden: 0 - ignorePlatformSupport: 0 - androidETC2FallbackOverride: 0 - forceMaximumCompressionQuality_BC6H_BC7: 0 - - serializedVersion: 4 - buildTarget: Standalone - maxTextureSize: 2048 - resizeAlgorithm: 0 - textureFormat: -1 - textureCompression: 1 - compressionQuality: 50 - crunchedCompression: 0 - allowsAlphaSplitting: 0 - overridden: 0 - ignorePlatformSupport: 0 - androidETC2FallbackOverride: 0 - forceMaximumCompressionQuality_BC6H_BC7: 0 - spriteSheet: - serializedVersion: 2 - sprites: [] - outline: [] - customData: - physicsShape: [] - bones: [] - spriteID: - internalID: 0 - vertices: [] - indices: - edges: [] - weights: [] - secondaryTextures: [] - spriteCustomMetadata: - entries: [] - nameFileIdTable: {} - mipmapLimitGroupName: - pSDRemoveMatte: 0 - userData: - assetBundleName: - assetBundleVariant: diff --git a/Editor Resources/Icons/paint tool.png b/Editor Resources/Icons/paint tool.png deleted file mode 100644 index d63b4c864a61a6f5eb41df674e3360ad0f44a8a5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 602 zcmV-g0;Ty zPfJxn7>A!T=ZH!RK`<&oFv3Mb8=*yu2uyMz7{moBTD5E2w}`fV0bR7IWkr;m`vF8K zMHf;lmHvRBlIA_082Jqhm%N?1=QtZ*819)fbI0 zUkiadkIGmoh%F}X1aE06a*clg?uEdeS7qi{*h}CWFr$P^{ZkG24*c|Tq?DQejQ0rs z40sAW2d=21C2DRr+}Ek+`jrLr07nA9(6be@&?dr0Xa~5dgT{%fBp?-VPNB~Qa=9=s zZ>Ln-XO)0K;Xh4)OQ31sl|E>^kf&ZGZm2cAElL#uiGVi5uiM`H&nG7tFaiA1as_$b zNk9P%=yohvV*(vXHc~p2ykV=e&e*qqN5i9BzkhGn4&TPW115be6 zkb)B`09~6bY#CSrK5LsodS;a`xO`i_Wto7qmXwU_D%wv{ZTXCQusMhn`Irw oT!PgK3@OFMS%S+5;9Lm!3+x7@L#X1xwEzGB07*qoM6N<$g4aL-X#fBK diff --git a/Editor Resources/Icons/paint tool.png.meta b/Editor Resources/Icons/paint tool.png.meta deleted file mode 100644 index a2555fec..00000000 --- a/Editor Resources/Icons/paint tool.png.meta +++ /dev/null @@ -1,119 +0,0 @@ -fileFormatVersion: 2 -guid: da1863fda0d08fd46843f82920853093 -TextureImporter: - internalIDToNameTable: [] - externalObjects: {} - serializedVersion: 13 - mipmaps: - mipMapMode: 0 - enableMipMap: 1 - sRGBTexture: 1 - linearTexture: 0 - fadeOut: 0 - borderMipMap: 0 - mipMapsPreserveCoverage: 0 - alphaTestReferenceValue: 0.5 - mipMapFadeDistanceStart: 1 - mipMapFadeDistanceEnd: 3 - bumpmap: - convertToNormalMap: 0 - externalNormalMap: 0 - heightScale: 0.25 - normalMapFilter: 0 - flipGreenChannel: 0 - isReadable: 0 - streamingMipmaps: 0 - streamingMipmapsPriority: 0 - vTOnly: 0 - ignoreMipmapLimit: 0 - grayScaleToAlpha: 0 - generateCubemap: 6 - cubemapConvolution: 0 - seamlessCubemap: 0 - textureFormat: 1 - maxTextureSize: 2048 - textureSettings: - serializedVersion: 2 - filterMode: 1 - aniso: 1 - mipBias: 0 - wrapU: 0 - wrapV: 0 - wrapW: 0 - nPOTScale: 1 - lightmap: 0 - compressionQuality: 50 - spriteMode: 0 - spriteExtrude: 1 - spriteMeshType: 1 - alignment: 0 - spritePivot: {x: 0.5, y: 0.5} - spritePixelsToUnits: 100 - spriteBorder: {x: 0, y: 0, z: 0, w: 0} - spriteGenerateFallbackPhysicsShape: 1 - alphaUsage: 1 - alphaIsTransparency: 0 - spriteTessellationMethod: 0 - spriteTessellationDetail: -1 - spriteGeometrySubdivision: -1 - textureType: 0 - textureShape: 1 - singleChannelComponent: 0 - flipbookRows: 1 - flipbookColumns: 1 - maxTextureSizeSet: 0 - compressionQualitySet: 0 - textureFormatSet: 0 - ignorePngGamma: 0 - applyGammaDecoding: 0 - swizzle: 50462976 - cookieLightType: 0 - platformSettings: - - serializedVersion: 4 - buildTarget: DefaultTexturePlatform - maxTextureSize: 2048 - resizeAlgorithm: 0 - textureFormat: -1 - textureCompression: 1 - compressionQuality: 50 - crunchedCompression: 0 - allowsAlphaSplitting: 0 - overridden: 0 - ignorePlatformSupport: 0 - androidETC2FallbackOverride: 0 - forceMaximumCompressionQuality_BC6H_BC7: 0 - - serializedVersion: 4 - buildTarget: Standalone - maxTextureSize: 2048 - resizeAlgorithm: 0 - textureFormat: -1 - textureCompression: 1 - compressionQuality: 50 - crunchedCompression: 0 - allowsAlphaSplitting: 0 - overridden: 0 - ignorePlatformSupport: 0 - androidETC2FallbackOverride: 0 - forceMaximumCompressionQuality_BC6H_BC7: 0 - spriteSheet: - serializedVersion: 2 - sprites: [] - outline: [] - customData: - physicsShape: [] - bones: [] - spriteID: - internalID: 0 - vertices: [] - indices: - edges: [] - weights: [] - secondaryTextures: [] - spriteCustomMetadata: - entries: [] - nameFileIdTable: {} - mipmapLimitGroupName: - pSDRemoveMatte: 0 - userData: - assetBundleName: - assetBundleVariant: From c2ed793175608b7cf4bc87999400534ebec90f4b Mon Sep 17 00:00:00 2001 From: Sander van Rossen Date: Mon, 26 Oct 2026 09:36:39 +0100 Subject: [PATCH 18/27] Paint spacing is drawn once, with the surface details The surface's own paintSpacing field showed again under the details drawer (and for surfaces that can't be painted). --- Core/1.Input/Surfaces/ChiselSurface.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Core/1.Input/Surfaces/ChiselSurface.cs b/Core/1.Input/Surfaces/ChiselSurface.cs index a04e770e..5e47b621 100644 --- a/Core/1.Input/Surfaces/ChiselSurface.cs +++ b/Core/1.Input/Surfaces/ChiselSurface.cs @@ -71,8 +71,9 @@ public sealed class ChiselSurface internal ChiselMaterial chiselMaterial = new(); public SurfaceDetails surfaceDetails = SurfaceDetails.Default; // How far apart the points the surface is painted at (and cut at) are, in metres; 0 for the default. Only painting - // reads it, so it stays out of the details that go into every brush polygon. - public float paintSpacing; + // reads it, so it stays out of the details that go into every brush polygon. Drawn with the details, and only for + // a shader that takes paint. + [HideInInspector] public float paintSpacing; public const string kPaintSpacingName = nameof(paintSpacing); public const float kDefaultPaintSpacing = 0.25f; From e71d5b8881e81ce2265671a485a4913512e45c16 Mon Sep 17 00:00:00 2001 From: Sander van Rossen Date: Mon, 26 Oct 2026 09:46:07 +0100 Subject: [PATCH 19/27] Paint: the brush's entity once per brush buffer, shorter comments A surface render buffer kept the brush's entity with its painted surface index; the entity is now the brush buffer's, once, and a surface keeps only its index (-1 without paint). The paint lookup says whether there is any paint with a flag set when it is made, rather than asking its hash map for every surface. Comments that described removed parts (the Fill Surface button, the cell size, the profile assets) are brought up to date. --- Components/Components/Paint/ChiselPaintProfiles.cs | 4 ++-- Components/Managers/ChiselPaintManager.cs | 3 +-- Components/Tests/ChiselPaintTests.cs | 4 ++-- .../2.Processing/Jobs/GenerateSurfaceTrianglesJob.cs | 2 +- .../Jobs/JobData/ChiselBrushRenderBuffer.cs | 12 +++++------- Core/2.Processing/Paint/ChiselPaintStore.cs | 6 +++--- Core/2.Processing/Paint/PaintSubdivision.cs | 12 +++++------- Core/3.Output/OutputMeshes/ChiselOutputRenderable.cs | 12 +++++++----- Core/3.Output/OutputMeshes/OutputHotspotSections.cs | 2 +- Core/3.Output/OutputMeshes/OutputModelWeld.cs | 2 +- Core/Tests/BrushMeshBlobLayoutTests.cs | 2 +- Editor/EditorTools/ChiselPaintTool.cs | 2 +- .../Materials/URP/Paint/ChiselPaintColor.shader | 2 +- .../Materials/URP/Paint/ChiselPaintValue.shader | 3 +-- 14 files changed, 32 insertions(+), 36 deletions(-) diff --git a/Components/Components/Paint/ChiselPaintProfiles.cs b/Components/Components/Paint/ChiselPaintProfiles.cs index da4ebfca..bfc4914f 100644 --- a/Components/Components/Paint/ChiselPaintProfiles.cs +++ b/Components/Components/Paint/ChiselPaintProfiles.cs @@ -8,7 +8,7 @@ namespace Chisel.Components public static class ChiselPaintProfiles { static Dictionary s_ByShader; - // profiles made in code (not assets), which come first + // profiles registered in code, which come first static readonly List s_Registered = new(); public static void Register(ChiselPaintProfile profile) @@ -40,7 +40,7 @@ public static ChiselPaintProfile Find(Shader shader) return s_ByShader.TryGetValue(shader, out var found) ? found : null; } - /// Every profile: those made in code, then the project's (but for a shader one made in code is for) + /// Every profile: those registered in code, then the project's (unless one in code is for its shader) public static IEnumerable All { get diff --git a/Components/Managers/ChiselPaintManager.cs b/Components/Managers/ChiselPaintManager.cs index e97873cd..6b1b14aa 100644 --- a/Components/Managers/ChiselPaintManager.cs +++ b/Components/Managers/ChiselPaintManager.cs @@ -153,8 +153,7 @@ static void SetPaint(ChiselModelComponent model, List } } - // Paint is drawn while its surface's material's shader takes paint in its attribute; otherwise it is kept on the - // generator, but doesn't subdivide the surface or reach its mesh + // Whether the surface's shader takes paint in this attribute; paint it doesn't take is kept, but not drawn static bool Drawn(ChiselGeneratorComponent generator, ChiselSurfacePaint paint) { if (paint == null || !paint.IsValid) diff --git a/Components/Tests/ChiselPaintTests.cs b/Components/Tests/ChiselPaintTests.cs index 31d86b2a..5e88d5a6 100644 --- a/Components/Tests/ChiselPaintTests.cs +++ b/Components/Tests/ChiselPaintTests.cs @@ -227,8 +227,8 @@ public void APaintedSurface_IsSubdividedAtItsGridPointsInsideIt() AssertValuesAt(mesh, VertexAttribute.Color, new Vector3(0.5f, 1, 0.5f), new Vector4(0, 0, 0, 1), "the middle point's own value"); } - // Painted for the first time, a surface gets a grid over it of the brush's cell size; a dab in its middle paints - // there, and the mesh shows it + // Painted for the first time, a surface gets a grid of its paint spacing; a dab in its middle paints there, and + // the mesh shows it [Test] public void TheBrush_GivesASurfaceAGrid_AndPaintsWhereItDabs() { diff --git a/Core/2.Processing/Jobs/GenerateSurfaceTrianglesJob.cs b/Core/2.Processing/Jobs/GenerateSurfaceTrianglesJob.cs index ef7833cb..b0d6f048 100644 --- a/Core/2.Processing/Jobs/GenerateSurfaceTrianglesJob.cs +++ b/Core/2.Processing/Jobs/GenerateSurfaceTrianglesJob.cs @@ -316,7 +316,6 @@ unsafe void FinishSurface(int surf, int brushNodeOrder, ulong entityID, ref Blob var paintSurface = baseSurfaces[surf].descriptionIndex; if (paint.TryGetRange(entityID, paintSurface, out var paintRange)) { - buf.paintEntityID = entityID; buf.paintSurface = paintSurface; buf.surfaceHashValue = math.hash(new uint2(buf.surfaceHashValue, paintRange.hash)); } @@ -674,6 +673,7 @@ public unsafe void Execute(int index) FinishSurface(surf, brushNodeOrder, entityID, ref baseSurfaces, treeToPlane, transform.nodeToTree, finalFaceNormal, surfaceIndexList, ref uniqueVertexMapper, builder, surfaceBuffers, ref decalBuilder, decalCandidates, decalTargeted, decalCandidateStart, layout); } + root.entityID = entityID; FinishBrush(brushIndexOrder, brushNodeOrder, builder, ref root, surfaceBuffers, decalCandidateStart, layout); } finally diff --git a/Core/2.Processing/Jobs/JobData/ChiselBrushRenderBuffer.cs b/Core/2.Processing/Jobs/JobData/ChiselBrushRenderBuffer.cs index 1cf0cff7..984487d2 100644 --- a/Core/2.Processing/Jobs/JobData/ChiselBrushRenderBuffer.cs +++ b/Core/2.Processing/Jobs/JobData/ChiselBrushRenderBuffer.cs @@ -19,9 +19,7 @@ internal struct ChiselSurfaceRenderBuffer public int decalStack; // a transparent decal's piece that fades (DecalVolume.Fades): its mesh is its decal's own public bool decalFades; - // a painted surface: its brush's entity and its surface index, which its paint is found by (ChiselPaintLookup); - // 0 for a surface without paint - public ulong paintEntityID; + // a painted surface's index, which its paint is found by with its brush's entity (ChiselPaintLookup); -1 without public int paintSurface; public SurfaceDestinationFlags destinationFlags; public SurfaceDestinationParameters destinationParameters; @@ -202,8 +200,7 @@ public void Construct(BlobBuilder builder, this.decalEntityID = 0; this.decalStack = 0; this.decalFades = false; - this.paintEntityID = 0; - this.paintSurface = 0; + this.paintSurface = -1; this.destinationFlags = destinationFlags; this.destinationParameters = destinationParameters; this.outputFlags = outputFlags; @@ -230,8 +227,7 @@ public void Construct(BlobBuilder builder, this.decalEntityID = 0; this.decalStack = 0; this.decalFades = false; - this.paintEntityID = 0; - this.paintSurface = 0; + this.paintSurface = -1; this.destinationFlags = destinationFlags; this.destinationParameters = destinationParameters; @@ -314,6 +310,8 @@ internal struct ChiselBrushRenderBuffer { public BlobArray surfaces; public BlobArray querySurfaces; + // the node the brush was created with + public ulong entityID; public int surfaceOffset; public int surfaceCount; }; diff --git a/Core/2.Processing/Paint/ChiselPaintStore.cs b/Core/2.Processing/Paint/ChiselPaintStore.cs index 2630ad69..612dcaa6 100644 --- a/Core/2.Processing/Paint/ChiselPaintStore.cs +++ b/Core/2.Processing/Paint/ChiselPaintStore.cs @@ -31,13 +31,12 @@ internal struct ChiselPaintLookup [ReadOnly] public NativeParallelHashMap ranges; [ReadOnly] public NativeArray layers; [ReadOnly] public NativeArray values; - - public readonly bool IsEmpty => !ranges.IsCreated || ranges.IsEmpty; + public bool hasPaint; public readonly bool TryGetRange(ulong entityID, int surfaceIndex, out ChiselPaintRange range) { range = default; - return entityID != 0 && !IsEmpty && ranges.TryGetValue(new ChiselPaintKey { entityID = entityID, surfaceIndex = surfaceIndex }, out range); + return hasPaint && ranges.TryGetValue(new ChiselPaintKey { entityID = entityID, surfaceIndex = surfaceIndex }, out range); } public static ChiselPaintLookup Create(NativeList layers, NativeList values, Allocator allocator) @@ -51,6 +50,7 @@ public static ChiselPaintLookup Create(NativeList layers, Nati }; if (count == 0) return lookup; + lookup.hasPaint = true; NativeArray.Copy(layers.AsArray(), lookup.layers, count); NativeArray.Copy(values.AsArray(), lookup.values, values.Length); // the layers are sorted by surface (ChiselPaintStore.SetPaint) diff --git a/Core/2.Processing/Paint/PaintSubdivision.cs b/Core/2.Processing/Paint/PaintSubdivision.cs index 576f3673..ba873662 100644 --- a/Core/2.Processing/Paint/PaintSubdivision.cs +++ b/Core/2.Processing/Paint/PaintSubdivision.cs @@ -3,11 +3,9 @@ namespace Chisel.Core { - // A painted surface is subdivided at its paint's grid, so the paint has vertices to land on: each grid point in the - // surface becomes a vertex, and so does each place a grid line crosses its outline, inserted as in an incremental - // Delaunay triangulation (it splits the triangle it is in, or the triangles of the edge it is on, and the edges round - // it are flipped until the triangles round it are Delaunay), so no sliver is left for the paint to streak across. - // The outline is cut too: the surfaces on the other side of it don't get those vertices (T-junctions). + // Subdivides a painted surface at its paint's grid: every grid point inside it, and every place a grid line + // crosses its outline, is inserted as in an incremental Delaunay triangulation. Cutting the outline leaves + // T-junctions with the neighbouring surfaces. internal static class PaintSubdivision { // Most points one surface gets; a grid of tiny cells on a big surface stops here @@ -160,8 +158,8 @@ public void Insert(ref UniqueVertexMapper mapper, float3 position) if (math.distance(point, a) < snap || math.distance(point, b) < snap || math.distance(point, c) < snap) return; // there is a vertex already - // the nearest edge: a point near the outline is put on it (or float noise would leave a sliver along - // it, which can't be flipped), one near an edge inside only when it is on it (the flips mend the rest) + // snapped onto the outline when near it (a sliver there can't be flipped away), onto an inner edge + // only when on it var nearest = distance.x <= distance.y && distance.x <= distance.z ? 0 : distance.y <= distance.z ? 1 : 2; int opposite = nearest == 0 ? i0 : nearest == 1 ? i1 : i2; int e0 = nearest == 0 ? i1 : nearest == 1 ? i2 : i0; diff --git a/Core/3.Output/OutputMeshes/ChiselOutputRenderable.cs b/Core/3.Output/OutputMeshes/ChiselOutputRenderable.cs index 698a9d82..86e781f1 100644 --- a/Core/3.Output/OutputMeshes/ChiselOutputRenderable.cs +++ b/Core/3.Output/OutputMeshes/ChiselOutputRenderable.cs @@ -129,8 +129,7 @@ public static unsafe void Prepare([NoAlias, ReadOnly] NativeArray(); for (int slot = 0; slot < ChiselPaintOutput.kSlotCount; slot++) @@ -141,8 +140,9 @@ public static unsafe void Prepare([NoAlias, ReadOnly] NativeArray= 0) + subMeshSource.paint.TryGetRange(subMeshSurface.brushRenderBuffer.Value.entityID, sourceBuffer.paintSurface, out paintRange); ranges.Add(new RenderCopyRange { paint = paintPointer, diff --git a/Core/3.Output/OutputMeshes/OutputHotspotSections.cs b/Core/3.Output/OutputMeshes/OutputHotspotSections.cs index 577ed867..1a41cf76 100644 --- a/Core/3.Output/OutputMeshes/OutputHotspotSections.cs +++ b/Core/3.Output/OutputMeshes/OutputHotspotSections.cs @@ -492,6 +492,7 @@ static BlobAssetReference Cut(BlobAssetReference(); + root.entityID = original.entityID; var surfaces = builder.Allocate(ref root.surfaces, original.surfaces.Length); for (int s = 0; s < original.surfaces.Length; s++) { @@ -502,7 +503,6 @@ static BlobAssetReference Cut(BlobAssetReference Copy(BlobAssetReferenc { ref var original = ref source.Value; ref var root = ref builder.ConstructRoot(); + root.entityID = original.entityID; var surfaces = builder.Allocate(ref root.surfaces, original.surfaces.Length); for (int s = 0; s < original.surfaces.Length; s++) { @@ -1248,7 +1249,6 @@ static unsafe BlobAssetReference Copy(BlobAssetReferenc to.decalEntityID = from.decalEntityID; to.decalStack = from.decalStack; to.decalFades = from.decalFades; - to.paintEntityID = from.paintEntityID; to.paintSurface = from.paintSurface; to.destinationFlags = from.destinationFlags; to.destinationParameters = from.destinationParameters; diff --git a/Core/Tests/BrushMeshBlobLayoutTests.cs b/Core/Tests/BrushMeshBlobLayoutTests.cs index b7a979b3..27a9e1f1 100644 --- a/Core/Tests/BrushMeshBlobLayoutTests.cs +++ b/Core/Tests/BrushMeshBlobLayoutTests.cs @@ -32,7 +32,7 @@ public void Measure_ReadsTheManagedStructs() Assert.That(layout.surfaceDetails + layout.detailsSize, Is.LessThanOrEqualTo(layout.surfaceParameters)); } - // The 10-26 case: old code wrote 120 byte polygons of 104 byte surfaces, read at 128 and 112 + // A stale layout: polygons and surfaces 8 bytes short [Test] public void Check_LogsEveryFieldThatDiffers() { diff --git a/Editor/EditorTools/ChiselPaintTool.cs b/Editor/EditorTools/ChiselPaintTool.cs index 23648d45..fdc3a5e6 100644 --- a/Editor/EditorTools/ChiselPaintTool.cs +++ b/Editor/EditorTools/ChiselPaintTool.cs @@ -36,7 +36,7 @@ sealed class ChiselPaintTool : EditorTool static bool s_ShowChannels; static int s_ChannelFrom, s_ChannelTo = 1; - // the surface last under the mouse: what Fill and the channel buttons work on + // the surface last under the mouse: what the channel buttons work on static Hit s_Last; static readonly List s_Intersections = new(); diff --git a/Package Resources/Materials/URP/Paint/ChiselPaintColor.shader b/Package Resources/Materials/URP/Paint/ChiselPaintColor.shader index d639d3c4..49a2c647 100644 --- a/Package Resources/Materials/URP/Paint/ChiselPaintColor.shader +++ b/Package Resources/Materials/URP/Paint/ChiselPaintColor.shader @@ -1,4 +1,4 @@ -// Shows a Color paint mode: the base color times the vertex color (Chisel's "Paint Color" test profile) +// Test shader for a Color paint mode: the base color times the vertex color Shader "Chisel/Test/Paint Color" { Properties diff --git a/Package Resources/Materials/URP/Paint/ChiselPaintValue.shader b/Package Resources/Materials/URP/Paint/ChiselPaintValue.shader index ce5d182c..0d5d7099 100644 --- a/Package Resources/Materials/URP/Paint/ChiselPaintValue.shader +++ b/Package Resources/Materials/URP/Paint/ChiselPaintValue.shader @@ -1,5 +1,4 @@ -// Shows a Value paint mode: TEXCOORD2.x from 0 to 1, from the base color to the value color (Chisel's "Paint Value" -// test profile) +// Test shader for a Value paint mode: TEXCOORD2.x from 0 to 1, from the base color to the value color Shader "Chisel/Test/Paint Value" { Properties From efb1f18ce9df7d3f20b642d03e8c58b9c7071b1f Mon Sep 17 00:00:00 2001 From: Sander van Rossen Date: Mon, 26 Oct 2026 09:53:57 +0100 Subject: [PATCH 20/27] Paint: a paintable shader's attributes are always in its meshes A render mesh drawn with a material whose shader has a paint profile gets the profile's attributes, at their modes' defaults where nothing is painted, so the shader never reads an attribute the mesh lacks. Materials are registered as surfaces take them (ChiselPaintMaterials, as the hotspot store does); what their shaders take comes from the paint profiles, and a change to it rebuilds every brush once, before the next build. A surface's hash takes its material's paint attributes, so their meshes are written again. --- .../Components/Paint/ChiselPaintProfiles.cs | 36 +++++- Components/Managers/ChiselPaintManager.cs | 1 + Components/Tests/ChiselPaintTests.cs | 27 +++- Core/1.Input/Paint/ChiselPaintMaterials.cs | 120 ++++++++++++++++++ .../Paint/ChiselPaintMaterials.cs.meta | 2 + .../1.Input/Surfaces/InternalChiselSurface.cs | 1 + .../Jobs/GenerateSurfaceTrianglesJob.cs | 3 + .../Managers/CSGManager.UpdateTreeMeshes.cs | 6 +- Core/2.Processing/Paint/ChiselPaintStore.cs | 40 +++++- .../OutputMeshes/ChiselOutputRenderable.cs | 10 ++ 10 files changed, 234 insertions(+), 12 deletions(-) create mode 100644 Core/1.Input/Paint/ChiselPaintMaterials.cs create mode 100644 Core/1.Input/Paint/ChiselPaintMaterials.cs.meta diff --git a/Components/Components/Paint/ChiselPaintProfiles.cs b/Components/Components/Paint/ChiselPaintProfiles.cs index bfc4914f..96ab10c0 100644 --- a/Components/Components/Paint/ChiselPaintProfiles.cs +++ b/Components/Components/Paint/ChiselPaintProfiles.cs @@ -15,17 +15,50 @@ public static void Register(ChiselPaintProfile profile) { if (profile != null && !s_Registered.Contains(profile)) s_Registered.Add(profile); + SetDirty(); ChiselPaintManager.SetDirty(); } public static void Unregister(ChiselPaintProfile profile) { s_Registered.Remove(profile); + SetDirty(); ChiselPaintManager.SetDirty(); } /// Called when profiles may have been added, removed or changed - public static void SetDirty() => s_ByShader = null; + public static void SetDirty() + { + s_ByShader = null; + ChiselPaintMaterials.Changed(); + } + + // The attributes a material's shader takes paint in, each with the defaults of its modes + static void Resolve(Material material, List slots) + { + var profile = Find(material.shader); + if (profile == null) + return; + foreach (var mode in profile.modes) + { + var index = slots.FindIndex(slot => slot.attribute == mode.attribute); + if (index < 0) + { + slots.Add(new ChiselPaintMaterialSlot { attribute = mode.attribute }); + index = slots.Count - 1; + } + var slot = slots[index]; + slot.defaultValue = ChiselPaintLayer.Apply(mode.defaultValue, slot.defaultValue, mode.components); + slots[index] = slot; + } + } + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + static void Hook() + { + ChiselPaintMaterials.Resolver = Resolve; + ChiselPaintMaterials.Changed(); + } public static ChiselPaintProfile Find(Shader shader) { @@ -95,6 +128,7 @@ public static bool TakesPaint(Material material, ChiselPaintAttribute attribute) [UnityEditor.InitializeOnLoadMethod] static void Initialize() { + Hook(); UnityEditor.EditorApplication.projectChanged += () => { SetDirty(); diff --git a/Components/Managers/ChiselPaintManager.cs b/Components/Managers/ChiselPaintManager.cs index 6b1b14aa..b2e7b7c4 100644 --- a/Components/Managers/ChiselPaintManager.cs +++ b/Components/Managers/ChiselPaintManager.cs @@ -30,6 +30,7 @@ static Matrix4x4 SpaceOf(ChiselGeneratorComponent generator, ChiselModelComponen public static void Update() { + ChiselPaintMaterials.Update(); if (!s_Dirty) { foreach (var generator in s_Painted) diff --git a/Components/Tests/ChiselPaintTests.cs b/Components/Tests/ChiselPaintTests.cs index 5e88d5a6..777b5a41 100644 --- a/Components/Tests/ChiselPaintTests.cs +++ b/Components/Tests/ChiselPaintTests.cs @@ -148,7 +148,7 @@ public void PaintedColors_AreTheVertexColorsOfTheirSurface() var model = Model(); var box = Box(model, new Vector3(2, 0, 3)); Update(); - Assert.That(RenderMesh(model).HasVertexAttribute(VertexAttribute.Color), Is.False, "nothing painted: no colors"); + AssertValuesAt(RenderMesh(model), VertexAttribute.Color, new Vector3(2, 1, 3), Vector4.one, "nothing painted: the default"); PaintTop(box, ChiselPaintAttribute.Color, ChiselPaintComponents.XYZW); Update(); @@ -167,7 +167,7 @@ public void PaintedColors_AreTheVertexColorsOfTheirSurface() box.RemoveSurfacePaint(SurfaceFacing(box, new float3(0, 1, 0)), ChiselPaintAttribute.Color); Update(); - Assert.That(RenderMesh(model).HasVertexAttribute(VertexAttribute.Color), Is.False, "the paint removed"); + AssertValuesAt(RenderMesh(model), VertexAttribute.Color, new Vector3(2, 1, 3), Vector4.one, "the paint removed: the default"); } // Paint in one component of a texture coordinate leaves the others at the default @@ -180,15 +180,15 @@ public void PaintInOneComponent_LeavesTheOthersAtTheDefault() Update(); var mesh = RenderMesh(model); Assert.That(mesh.HasVertexAttribute(VertexAttribute.TexCoord3), Is.True); - Assert.That(mesh.HasVertexAttribute(VertexAttribute.Color), Is.False); - AssertValuesAt(mesh, VertexAttribute.TexCoord3, new Vector3(1, 1, 0), new Vector4(1, 1, 1, 1), "the green corner's y, the default elsewhere"); - AssertValuesAt(mesh, VertexAttribute.TexCoord3, new Vector3(0, 1, 0), new Vector4(1, 0, 1, 1), "the red corner's y"); + // the other components take the profile's default (the mask mode's 0) + AssertValuesAt(mesh, VertexAttribute.TexCoord3, new Vector3(1, 1, 0), new Vector4(0, 1, 0, 0), "the green corner's y, the default elsewhere"); + AssertValuesAt(mesh, VertexAttribute.TexCoord3, new Vector3(0, 1, 0), new Vector4(0, 0, 0, 0), "the red corner's y"); // a value changed in place shows at the next update paint[0, 0] = new Vector4(0.25f, 0.25f, 0.25f, 0.25f); box.PaintChanged(); Update(); - AssertValuesAt(RenderMesh(model), VertexAttribute.TexCoord3, new Vector3(0, 1, 0), new Vector4(1, 0.25f, 1, 1), "changed"); + AssertValuesAt(RenderMesh(model), VertexAttribute.TexCoord3, new Vector3(0, 1, 0), new Vector4(0, 0.25f, 0, 0), "changed"); } // A grid of quarter-metre cells on the top: all of its 25 points are vertices, the top is the grid's 32 half cells, @@ -410,6 +410,21 @@ public void AFill_SetsTheWholeSurface() Assert.That(value, Is.EqualTo(new Vector4(0, 1, 0, 1))); } + // A surface under a shader that takes paint has its attributes before it is painted, at their modes' defaults + [Test] + public void AnUnpaintedSurface_HasItsShadersAttributesAtTheirDefaults() + { + var model = Model(); + Box(model, Vector3.zero); + Update(); + var mesh = RenderMesh(model); + Assert.That(mesh.HasVertexAttribute(VertexAttribute.Color), Is.True); + Assert.That(mesh.HasVertexAttribute(VertexAttribute.TexCoord2), Is.True); + Assert.That(mesh.HasVertexAttribute(VertexAttribute.TexCoord3), Is.True); + AssertValuesAt(mesh, VertexAttribute.Color, new Vector3(0, 1, 0), new Vector4(1, 1, 1, 1), "the color mode's default"); + AssertValuesAt(mesh, VertexAttribute.TexCoord2, new Vector3(0, 1, 0), Vector4.zero, "the value mode's default"); + } + // The grid moves with its generator [Test] public void Paint_MovesWithItsGenerator() diff --git a/Core/1.Input/Paint/ChiselPaintMaterials.cs b/Core/1.Input/Paint/ChiselPaintMaterials.cs new file mode 100644 index 00000000..0a2ba014 --- /dev/null +++ b/Core/1.Input/Paint/ChiselPaintMaterials.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using Unity.Collections; +using Unity.Mathematics; +using UnityEngine; + +namespace Chisel.Core +{ + /// An attribute a material's shader takes paint in, and what it has where nothing is painted + public struct ChiselPaintMaterialSlot + { + public ulong material; + public ChiselPaintAttribute attribute; + public float4 defaultValue; + } + + /// + /// The paint attributes of the materials surfaces are drawn with: a render mesh with such a material has those + /// attributes, at their defaults where nothing is painted. Materials are registered as surfaces take them; what + /// their shaders take comes from . + /// + public static class ChiselPaintMaterials + { + /// Adds the attributes a material's shader takes paint in, with their defaults + public static Action> Resolver; + + static readonly Dictionary s_Materials = new(); + static readonly List s_Slots = new(); + static readonly List s_Resolved = new(); + static readonly List s_Scratch = new(); + static readonly List s_Gone = new(); + static bool s_Dirty; + static bool s_Changed; + + public static void Register(Material material) + { + if (!material) + return; + if (s_Materials.TryAdd(EntityId.ToULong(material.GetEntityId()), material)) + s_Dirty = true; + } + + /// What shaders take may have changed; looked into at the next + public static void Changed() => s_Changed = true; + + /// Before the trees are built: every brush is built again when a registered material's attributes changed + public static void Update() + { + if (!s_Changed) + return; + s_Changed = false; + s_Dirty = true; + var before = s_Slots.Count; + s_Scratch.Clear(); + s_Scratch.AddRange(s_Slots); + Resolve(); + var same = before == s_Slots.Count; + for (int i = 0; same && i < before; i++) + { + same = s_Scratch[i].material == s_Slots[i].material && s_Scratch[i].attribute == s_Slots[i].attribute && + s_Scratch[i].defaultValue.Equals(s_Slots[i].defaultValue); + } + if (same) + return; + using var trees = new NativeList(Allocator.Temp); + CompactHierarchyManager.GetAllTrees(trees); + foreach (var tree in trees) + { + for (int i = 0; tree.Valid && i < tree.Count; i++) + SetBrushesDirty(tree[i]); + } + } + + static void SetBrushesDirty(CSGTreeNode node) + { + if (!node.Valid) + return; + if (node.Type == CSGNodeType.Brush) + { + node.SetDirty(); + return; + } + for (int i = 0; i < node.Count; i++) + SetBrushesDirty(node[i]); + } + + // the slots of every registered material, its slots together + static void Resolve() + { + if (!s_Dirty) + return; + s_Dirty = false; + s_Slots.Clear(); + s_Gone.Clear(); + foreach (var entry in s_Materials) + { + if (!entry.Value) + { + s_Gone.Add(entry.Key); + continue; + } + if (Resolver == null) + continue; + s_Resolved.Clear(); + Resolver(entry.Value, s_Resolved); + foreach (var slot in s_Resolved) + s_Slots.Add(new ChiselPaintMaterialSlot { material = entry.Key, attribute = slot.attribute, defaultValue = slot.defaultValue }); + } + foreach (var key in s_Gone) + s_Materials.Remove(key); + } + + internal static void Gather(NativeList slots) + { + Resolve(); + foreach (var slot in s_Slots) + slots.Add(slot); + } + } +} diff --git a/Core/1.Input/Paint/ChiselPaintMaterials.cs.meta b/Core/1.Input/Paint/ChiselPaintMaterials.cs.meta new file mode 100644 index 00000000..500f1e01 --- /dev/null +++ b/Core/1.Input/Paint/ChiselPaintMaterials.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: cd8264abae7685049b5303f70a86f97b \ No newline at end of file diff --git a/Core/1.Input/Surfaces/InternalChiselSurface.cs b/Core/1.Input/Surfaces/InternalChiselSurface.cs index e99991ab..6dbff92b 100644 --- a/Core/1.Input/Surfaces/InternalChiselSurface.cs +++ b/Core/1.Input/Surfaces/InternalChiselSurface.cs @@ -26,6 +26,7 @@ public struct InternalChiselSurface [MethodImpl(MethodImplOptions.AggressiveInlining)] public static InternalChiselSurface Convert(ChiselSurface surface) { + ChiselPaintMaterials.Register(surface.RenderMaterial); return new InternalChiselSurface { details = surface.surfaceDetails, diff --git a/Core/2.Processing/Jobs/GenerateSurfaceTrianglesJob.cs b/Core/2.Processing/Jobs/GenerateSurfaceTrianglesJob.cs index b0d6f048..7b2f52bd 100644 --- a/Core/2.Processing/Jobs/GenerateSurfaceTrianglesJob.cs +++ b/Core/2.Processing/Jobs/GenerateSurfaceTrianglesJob.cs @@ -319,6 +319,9 @@ unsafe void FinishSurface(int surf, int brushNodeOrder, ulong entityID, ref Blob buf.paintSurface = paintSurface; buf.surfaceHashValue = math.hash(new uint2(buf.surfaceHashValue, paintRange.hash)); } + // the attributes its material's shader takes paint in are in its mesh, painted or not + if (paint.TryGetMaterial(parms.parameter1, out var materialRange)) + buf.surfaceHashValue = math.hash(new uint2(buf.surfaceHashValue, materialRange.hash)); if (HasBackFace(flags, outputFlags)) FinishBackFace(surf, entityID, flags, parms, outputFlags, surfaceIndexList, ref uniqueVertexMapper, builder, surfaceBuffers, slots); diff --git a/Core/2.Processing/Managers/CSGManager.UpdateTreeMeshes.cs b/Core/2.Processing/Managers/CSGManager.UpdateTreeMeshes.cs index 9979327a..38e55f35 100644 --- a/Core/2.Processing/Managers/CSGManager.UpdateTreeMeshes.cs +++ b/Core/2.Processing/Managers/CSGManager.UpdateTreeMeshes.cs @@ -564,7 +564,11 @@ public void Initialize() Temporaries.hotspotTileSides = new NativeArray(hotspotTileSides.AsArray(), defaultAllocator); Temporaries.hotspotVariants = new NativeArray(hotspotVariants.AsArray(), defaultAllocator); Temporaries.hotspotLinks = new NativeArray(chiselLookupValues.hotspotLinks.AsArray(), defaultAllocator); - Temporaries.paint = ChiselPaintLookup.Create(chiselLookupValues.paintLayers, chiselLookupValues.paintValues, defaultAllocator); + using (var materialSlots = new NativeList(Allocator.Temp)) + { + ChiselPaintMaterials.Gather(materialSlots); + Temporaries.paint = ChiselPaintLookup.Create(chiselLookupValues.paintLayers, chiselLookupValues.paintValues, materialSlots, defaultAllocator); + } } Temporaries.subMeshDescriptions = new NativeList(defaultAllocator); diff --git a/Core/2.Processing/Paint/ChiselPaintStore.cs b/Core/2.Processing/Paint/ChiselPaintStore.cs index 612dcaa6..06adbbc2 100644 --- a/Core/2.Processing/Paint/ChiselPaintStore.cs +++ b/Core/2.Processing/Paint/ChiselPaintStore.cs @@ -32,6 +32,10 @@ internal struct ChiselPaintLookup [ReadOnly] public NativeArray layers; [ReadOnly] public NativeArray values; public bool hasPaint; + // the attributes of the materials whose shaders take paint, by material (ChiselPaintMaterials) + [ReadOnly] public NativeParallelHashMap materials; + [ReadOnly] public NativeArray materialSlots; + public bool hasMaterials; public readonly bool TryGetRange(ulong entityID, int surfaceIndex, out ChiselPaintRange range) { @@ -39,15 +43,41 @@ public readonly bool TryGetRange(ulong entityID, int surfaceIndex, out ChiselPai return hasPaint && ranges.TryGetValue(new ChiselPaintKey { entityID = entityID, surfaceIndex = surfaceIndex }, out range); } - public static ChiselPaintLookup Create(NativeList layers, NativeList values, Allocator allocator) + public readonly bool TryGetMaterial(ulong material, out ChiselPaintRange range) + { + range = default; + return hasMaterials && materials.TryGetValue(material, out range); + } + + public static ChiselPaintLookup Create(NativeList layers, NativeList values, + NativeList materialSlots, Allocator allocator) { var count = layers.IsCreated ? layers.Length : 0; + var slots = materialSlots.IsCreated ? materialSlots.Length : 0; var lookup = new ChiselPaintLookup { - ranges = new NativeParallelHashMap(math.max(1, count), allocator), - layers = new NativeArray(math.max(1, count), allocator), - values = new NativeArray(math.max(1, values.IsCreated ? values.Length : 0), allocator) + ranges = new NativeParallelHashMap(math.max(1, count), allocator), + layers = new NativeArray(math.max(1, count), allocator), + values = new NativeArray(math.max(1, values.IsCreated ? values.Length : 0), allocator), + materials = new NativeParallelHashMap(math.max(1, slots), allocator), + materialSlots = new NativeArray(math.max(1, slots), allocator) }; + if (slots > 0) + { + lookup.hasMaterials = true; + NativeArray.Copy(materialSlots.AsArray(), lookup.materialSlots, slots); + // a material's slots are together (ChiselPaintMaterials) + for (int start = 0; start < slots;) + { + var material = materialSlots[start].material; + var end = start; + var hash = 0u; + for (; end < slots && materialSlots[end].material == material; end++) + hash = math.hash(new uint3(hash, (uint)materialSlots[end].attribute, math.hash(math.asuint(materialSlots[end].defaultValue)))); + lookup.materials.TryAdd(material, new ChiselPaintRange { start = start, count = end - start, hash = hash }); + start = end; + } + } if (count == 0) return lookup; lookup.hasPaint = true; @@ -75,6 +105,8 @@ public JobHandle Dispose(JobHandle dependency) if (ranges.IsCreated) handle = JobHandle.CombineDependencies(handle, ranges.Dispose(dependency)); if (layers.IsCreated) handle = JobHandle.CombineDependencies(handle, layers.Dispose(dependency)); if (values.IsCreated) handle = JobHandle.CombineDependencies(handle, values.Dispose(dependency)); + if (materials.IsCreated) handle = JobHandle.CombineDependencies(handle, materials.Dispose(dependency)); + if (materialSlots.IsCreated) handle = JobHandle.CombineDependencies(handle, materialSlots.Dispose(dependency)); this = default; return handle; } diff --git a/Core/3.Output/OutputMeshes/ChiselOutputRenderable.cs b/Core/3.Output/OutputMeshes/ChiselOutputRenderable.cs index 86e781f1..da30e494 100644 --- a/Core/3.Output/OutputMeshes/ChiselOutputRenderable.cs +++ b/Core/3.Output/OutputMeshes/ChiselOutputRenderable.cs @@ -142,6 +142,16 @@ public static unsafe void Prepare([NoAlias, ReadOnly] NativeArray Date: Mon, 26 Oct 2026 10:03:08 +0100 Subject: [PATCH 21/27] Paint tool: no channels section Copying, swapping and inverting components of a surface's paint is gone from the overlay, and with it from the paint brush. --- .../Components/Paint/ChiselPaintBrush.cs | 25 -------- Components/Tests/ChiselPaintTests.cs | 11 +--- Editor/EditorTools/ChiselPaintTool.cs | 59 +------------------ .../PropertyDrawers/ChiselPaintModeDrawer.cs | 2 +- 4 files changed, 4 insertions(+), 93 deletions(-) diff --git a/Components/Components/Paint/ChiselPaintBrush.cs b/Components/Components/Paint/ChiselPaintBrush.cs index b6c4d929..2a50399c 100644 --- a/Components/Components/Paint/ChiselPaintBrush.cs +++ b/Components/Components/Paint/ChiselPaintBrush.cs @@ -270,31 +270,6 @@ public static void Fill(ChiselSurfacePaint paint, in ChiselPaintMode mode, Vecto } } - /// One component's paint copied to another (0 x, 1 y, 2 z, 3 w) - public static void CopyComponent(ChiselSurfacePaint paint, int from, int to) - { - for (int i = 0; paint != null && i < paint.values.Length; i++) - paint.values[i][to] = paint.values[i][from]; - } - - /// Two components' paint swapped - public static void SwapComponents(ChiselSurfacePaint paint, int a, int b) - { - for (int i = 0; paint != null && i < paint.values.Length; i++) - { - var value = paint.values[i]; - (value[a], value[b]) = (value[b], value[a]); - paint.values[i] = value; - } - } - - /// A component's paint turned over within a range: its lowest becomes its highest - public static void InvertComponent(ChiselSurfacePaint paint, int component, Vector2 range) - { - for (int i = 0; paint != null && i < paint.values.Length; i++) - paint.values[i][component] = range.x + range.y - paint.values[i][component]; - } - // The weights of a group made to sum to one again after the painted components changed: the others share what is // left, in the proportion they had (equally when they were all 0) static Vector4 Normalize(Vector4 value, ChiselPaintComponents painted, ChiselPaintComponents others) diff --git a/Components/Tests/ChiselPaintTests.cs b/Components/Tests/ChiselPaintTests.cs index 777b5a41..3f5aa9d3 100644 --- a/Components/Tests/ChiselPaintTests.cs +++ b/Components/Tests/ChiselPaintTests.cs @@ -321,9 +321,9 @@ public void TheWeightsOfAGroup_AlwaysSumToOne() Assert.That(Vector3.Distance(paint[2, 2], new Vector3(0.25f, 0.25f, 0.5f)), Is.LessThan(1e-5f), "half sand: rock and moss give it alike"); } - // Sampling takes the paint between grid points; the channel buttons copy, swap and invert components + // Sampling takes the paint between grid points [Test] - public void Sampling_AndTheChannelUtilities() + public void Sampling_TakesThePaintBetweenGridPoints() { var model = Model(); var box = Box(model, Vector3.zero); @@ -334,13 +334,6 @@ public void Sampling_AndTheChannelUtilities() paint.values = new[] { new Vector4(0, 0, 0, 1), new Vector4(1, 0, 0, 1), new Vector4(0, 1, 0, 1), new Vector4(1, 1, 0, 1) }; var middle = paint.Sample(paint.origin + (paint.cellU + paint.cellV) * 0.5f + Vector3.up * 3); Assert.That(Vector4.Distance(middle, new Vector4(0.5f, 0.5f, 0, 1)), Is.LessThan(1e-5f), "between the four, the grid projected along its normal"); - - ChiselPaintBrush.CopyComponent(paint, 0, 2); - Assert.That(paint.values[1], Is.EqualTo(new Vector4(1, 0, 1, 1))); - ChiselPaintBrush.SwapComponents(paint, 0, 1); - Assert.That(paint.values[1], Is.EqualTo(new Vector4(0, 1, 1, 1))); - ChiselPaintBrush.InvertComponent(paint, 3, new Vector2(0, 1)); - Assert.That(paint.values[1].w, Is.EqualTo(0)); } // Paint under a shader that doesn't take it (its profile gone, or another material) is kept, but neither drawn nor diff --git a/Editor/EditorTools/ChiselPaintTool.cs b/Editor/EditorTools/ChiselPaintTool.cs index fdc3a5e6..0d99f02f 100644 --- a/Editor/EditorTools/ChiselPaintTool.cs +++ b/Editor/EditorTools/ChiselPaintTool.cs @@ -19,11 +19,6 @@ sealed class ChiselPaintTool : EditorTool static readonly GUIContent kRadiusContent = new("Radius", "The brush's radius, in metres."); static readonly GUIContent kStrengthContent = new("Strength", "How much a dab moves the paint towards what it paints."); static readonly GUIContent kHardnessContent = new("Hardness", "How much of the radius is painted at full strength; beyond it the brush falls off smoothly."); - static readonly GUIContent kChannelsContent = new("Channels", "Works on the paint of the last surface under the mouse, in the mode's attribute."); - static readonly GUIContent kCopyContent = new("Copy", "Copies the first component's paint to the second."); - static readonly GUIContent kSwapContent = new("Swap", "Swaps the two components' paint."); - static readonly GUIContent kInvertContent = new("Invert", "Turns the first component's paint over within the mode's range."); - static readonly string[] kComponentNames = { "X (R)", "Y (G)", "Z (B)", "W (A)" }; // the mode painted in, by name: a surface takes it when its shader's profile has a mode of that name static string s_ModeName; @@ -34,9 +29,7 @@ sealed class ChiselPaintTool : EditorTool static float s_Strength = 0.5f; static float s_Hardness = 0.5f; - static bool s_ShowChannels; - static int s_ChannelFrom, s_ChannelTo = 1; - // the surface last under the mouse: what the channel buttons work on + // the surface last under the mouse, whose mode's color or range the panel shows static Hit s_Last; static readonly List s_Intersections = new(); @@ -217,26 +210,6 @@ static void Sample(Hit hit) } } - // The paint of the last surface under the mouse in the mode's attribute, made when it has none - static bool LastPaint(out ChiselSurfacePaint paint, out ChiselPaintMode mode) - { - paint = null; - mode = default; - var index = ModeIndex(s_Last.profile); - if (!s_Last.valid || !s_Last.generator || index < 0) - return false; - mode = s_Last.profile.modes[index]; - Undo.RecordObject(s_Last.generator, "Chisel Paint"); - paint = ChiselPaintBrush.PaintFor(s_Last.generator, s_Last.surfaceIndex, mode); - return paint != null; - } - - static void Changed() - { - s_Last.generator.PaintChanged(); - EditorUtility.SetDirty(s_Last.generator); - } - static readonly HashSet<(ChiselGeneratorComponent, int)> s_Filled = new(); static void Dab(Hit hit) @@ -701,36 +674,6 @@ internal static void PanelGUI() GUI.DrawTexture(falloff, Falloff(), ScaleMode.ScaleToFit); } } - using (new EditorGUI.DisabledScope(!Paintable(s_Last))) - { - s_ShowChannels = EditorGUILayout.Foldout(s_ShowChannels, kChannelsContent, true); - if (s_ShowChannels) - { - using (new EditorGUILayout.HorizontalScope()) - { - s_ChannelFrom = EditorGUILayout.Popup(s_ChannelFrom, kComponentNames); - s_ChannelTo = EditorGUILayout.Popup(s_ChannelTo, kComponentNames); - } - using (new EditorGUILayout.HorizontalScope()) - { - if (GUILayout.Button(kCopyContent) && LastPaint(out var copyPaint, out _)) - { - ChiselPaintBrush.CopyComponent(copyPaint, s_ChannelFrom, s_ChannelTo); - Changed(); - } - if (GUILayout.Button(kSwapContent) && LastPaint(out var swapPaint, out _)) - { - ChiselPaintBrush.SwapComponents(swapPaint, s_ChannelFrom, s_ChannelTo); - Changed(); - } - if (GUILayout.Button(kInvertContent) && LastPaint(out var invertPaint, out var invertMode)) - { - ChiselPaintBrush.InvertComponent(invertPaint, s_ChannelFrom, invertMode.range); - Changed(); - } - } - } - } EditorGUILayout.HelpBox("Control-click takes the paint under the mouse.", MessageType.None); } EditorGUIUtility.labelWidth = labelWidth; diff --git a/Editor/PropertyDrawers/ChiselPaintModeDrawer.cs b/Editor/PropertyDrawers/ChiselPaintModeDrawer.cs index 76bd0c6b..737451b3 100644 --- a/Editor/PropertyDrawers/ChiselPaintModeDrawer.cs +++ b/Editor/PropertyDrawers/ChiselPaintModeDrawer.cs @@ -16,7 +16,7 @@ sealed class ChiselPaintModeDrawer : PropertyDrawer static readonly GUIContent kComponentsContent = new("Components", "The components of the attribute the mode writes; the others keep their default."); static readonly GUIContent kDefaultColor = new("Default", "What an unpainted surface has."); static readonly GUIContent kDefaultValue = new("Default", "What an unpainted surface has, in each of the mode's components."); - static readonly GUIContent kRangeContent = new("Range", "The lowest and highest value: the brush keeps paint between them, and Invert turns it over within them."); + static readonly GUIContent kRangeContent = new("Range", "The lowest and highest value: the brush keeps paint between them."); static readonly GUIContent kGroupContent = new("Group", "Value modes with the same group (above 0), in one attribute, are weights that always sum to one: painting one takes from the others. 0 is no group."); static readonly string[] kXYZW = { "X", "Y", "Z", "W" }; static readonly string[] kRGBA = { "R", "G", "B", "A" }; From 67cc2c03d8677d4750b985e92e1554b502d5ce67 Mon Sep 17 00:00:00 2001 From: Sander van Rossen Date: Mon, 26 Oct 2026 10:13:59 +0100 Subject: [PATCH 22/27] Paint: kept by the tree between updates, replaced per entity The tree keeps its paint with the ranges and hashes an update looks up, made when the paint changes rather than copied and hashed at every update; an update reads views of it. Paint is replaced one entity at a time: only that entity's layers are hashed, its old values are left behind until they are half of all values, then compacted. The materials' paint attributes are copied to the tree only when they change. --- Core/1.Input/Paint/ChiselPaintMaterials.cs | 9 + Core/2.Processing/Cache/ChiselLookup.cs | 26 +- .../Managers/CSGManager.UpdateTreeMeshes.cs | 9 +- Core/2.Processing/Paint/ChiselPaintStore.cs | 276 +++++++++++------- Core/Tests/ChiselPaintStoreTests.cs | 76 +++++ Core/Tests/ChiselPaintStoreTests.cs.meta | 2 + 6 files changed, 277 insertions(+), 121 deletions(-) create mode 100644 Core/Tests/ChiselPaintStoreTests.cs create mode 100644 Core/Tests/ChiselPaintStoreTests.cs.meta diff --git a/Core/1.Input/Paint/ChiselPaintMaterials.cs b/Core/1.Input/Paint/ChiselPaintMaterials.cs index 0a2ba014..696cbeaa 100644 --- a/Core/1.Input/Paint/ChiselPaintMaterials.cs +++ b/Core/1.Input/Paint/ChiselPaintMaterials.cs @@ -30,6 +30,7 @@ public static class ChiselPaintMaterials static readonly List s_Scratch = new(); static readonly List s_Gone = new(); static bool s_Dirty; + static int s_Version; static bool s_Changed; public static void Register(Material material) @@ -90,6 +91,7 @@ static void Resolve() if (!s_Dirty) return; s_Dirty = false; + s_Version++; s_Slots.Clear(); s_Gone.Clear(); foreach (var entry in s_Materials) @@ -110,6 +112,13 @@ static void Resolve() s_Materials.Remove(key); } + // The slots as they are now, and a number that changes when they do + internal static int Refresh() + { + Resolve(); + return s_Version; + } + internal static void Gather(NativeList slots) { Resolve(); diff --git a/Core/2.Processing/Cache/ChiselLookup.cs b/Core/2.Processing/Cache/ChiselLookup.cs index 95ee3121..3d510f20 100644 --- a/Core/2.Processing/Cache/ChiselLookup.cs +++ b/Core/2.Processing/Cache/ChiselLookup.cs @@ -39,9 +39,16 @@ public class Data public NativeList changedDecalBounds; // The surfaces connected or disconnected by hand (ChiselHotspotLinkStore) public NativeList hotspotLinks; - // The painted surfaces' layers, sorted by surface, and their grids' values (ChiselPaintStore) + // The painted surfaces' layers and their grids' values, and what an update looks up in them (ChiselPaintStore) public NativeList paintLayers; + public NativeList paintLayerHashes; public NativeList paintValues; + public int paintUnusedValues; + public NativeParallelHashMap paintEntityHashes; + public NativeParallelHashMap paintRanges; + public NativeList paintMaterialSlots; + public NativeParallelHashMap paintMaterials; + public int paintMaterialsVersion = -1; public NativeParallelHashMap> brushRenderBufferLookup; @@ -84,7 +91,13 @@ internal void Initialize() decalVolumes = new NativeList(16, Allocator.Persistent); hotspotLinks = new NativeList(4, Allocator.Persistent); paintLayers = new NativeList(4, Allocator.Persistent); + paintLayerHashes = new NativeList(4, Allocator.Persistent); paintValues = new NativeList(16, Allocator.Persistent); + paintEntityHashes = new NativeParallelHashMap(4, Allocator.Persistent); + paintRanges = new NativeParallelHashMap(4, Allocator.Persistent); + paintMaterialSlots = new NativeList(4, Allocator.Persistent); + paintMaterials = new NativeParallelHashMap(4, Allocator.Persistent); + paintMaterialsVersion = -1; hotspotLayoutHashes = new NativeList(4, Allocator.Persistent); collapseMoves = new NativeList(4, Allocator.Persistent); hotspotCutCache = new NativeParallelHashMap(16, Allocator.Persistent); @@ -242,6 +255,17 @@ internal void Dispose() paintLayers = default; if (paintValues.IsCreated) paintValues.Dispose(); paintValues = default; + if (paintLayerHashes.IsCreated) paintLayerHashes.Dispose(); + paintLayerHashes = default; + if (paintEntityHashes.IsCreated) paintEntityHashes.Dispose(); + paintEntityHashes = default; + if (paintRanges.IsCreated) paintRanges.Dispose(); + paintRanges = default; + if (paintMaterialSlots.IsCreated) paintMaterialSlots.Dispose(); + paintMaterialSlots = default; + if (paintMaterials.IsCreated) paintMaterials.Dispose(); + paintMaterials = default; + paintUnusedValues = 0; if (hotspotLayoutHashes.IsCreated) hotspotLayoutHashes.Dispose(); hotspotLayoutHashes = default; if (collapseMoves.IsCreated) collapseMoves.Dispose(); diff --git a/Core/2.Processing/Managers/CSGManager.UpdateTreeMeshes.cs b/Core/2.Processing/Managers/CSGManager.UpdateTreeMeshes.cs index 38e55f35..60f93bde 100644 --- a/Core/2.Processing/Managers/CSGManager.UpdateTreeMeshes.cs +++ b/Core/2.Processing/Managers/CSGManager.UpdateTreeMeshes.cs @@ -564,11 +564,8 @@ public void Initialize() Temporaries.hotspotTileSides = new NativeArray(hotspotTileSides.AsArray(), defaultAllocator); Temporaries.hotspotVariants = new NativeArray(hotspotVariants.AsArray(), defaultAllocator); Temporaries.hotspotLinks = new NativeArray(chiselLookupValues.hotspotLinks.AsArray(), defaultAllocator); - using (var materialSlots = new NativeList(Allocator.Temp)) - { - ChiselPaintMaterials.Gather(materialSlots); - Temporaries.paint = ChiselPaintLookup.Create(chiselLookupValues.paintLayers, chiselLookupValues.paintValues, materialSlots, defaultAllocator); - } + ChiselPaintStore.UpdateMaterials(chiselLookupValues); + Temporaries.paint = ChiselPaintLookup.Of(chiselLookupValues); } Temporaries.subMeshDescriptions = new NativeList(defaultAllocator); @@ -2705,8 +2702,6 @@ public JobHandle PreMeshUpdateDispose() lastJobHandle.AddDependency(Temporaries.hotspotTiles .SafeDispose(JobHandles[JobHandleType.decalVolumesJobHandle].readWriteBarrier)); lastJobHandle.AddDependency(Temporaries.hotspotLayouts .SafeDispose(JobHandles[JobHandleType.decalVolumesJobHandle].readWriteBarrier)); lastJobHandle.AddDependency(Temporaries.hotspotLinks .SafeDispose(JobHandles[JobHandleType.decalVolumesJobHandle].readWriteBarrier)); - lastJobHandle.AddDependency(Temporaries.paint.Dispose(JobHandle.CombineDependencies(JobHandles[JobHandleType.brushRenderBufferCacheJobHandle].readWriteBarrier, - JobHandles[JobHandleType.vertexBufferContents_meshesJobHandle].readWriteBarrier))); lastJobHandle.AddDependency(Temporaries.hotspotTileSides .SafeDispose(JobHandles[JobHandleType.decalVolumesJobHandle].readWriteBarrier)); lastJobHandle.AddDependency(Temporaries.hotspotVariants .SafeDispose(JobHandles[JobHandleType.decalVolumesJobHandle].readWriteBarrier)); lastJobHandle.AddDependency(Temporaries.changedHotspotLayouts .SafeDispose(JobHandles[JobHandleType.decalVolumesJobHandle].readWriteBarrier)); diff --git a/Core/2.Processing/Paint/ChiselPaintStore.cs b/Core/2.Processing/Paint/ChiselPaintStore.cs index 06adbbc2..88ce8b80 100644 --- a/Core/2.Processing/Paint/ChiselPaintStore.cs +++ b/Core/2.Processing/Paint/ChiselPaintStore.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; -using Unity.Jobs; using Unity.Mathematics; namespace Chisel.Core @@ -17,7 +16,7 @@ internal struct ChiselPaintKey : IEquatable public override readonly int GetHashCode() => (int)math.hash(new uint3((uint)entityID, (uint)(entityID >> 32), (uint)surfaceIndex)); } - // A surface's layers in the update's layer array, and a hash of them and their values + // A surface's layers in the tree's layer list, and a hash of them and their values internal struct ChiselPaintRange { public int start; @@ -25,17 +24,17 @@ internal struct ChiselPaintRange public uint hash; } - // The tree's paint as an update reads it: each painted surface's layers, found by its key + // The tree's paint as an update reads it: views of what the tree keeps (ChiselPaintStore) internal struct ChiselPaintLookup { [ReadOnly] public NativeParallelHashMap ranges; - [ReadOnly] public NativeArray layers; - [ReadOnly] public NativeArray values; - public bool hasPaint; + [ReadOnly] public NativeArray layers; + [ReadOnly] public NativeArray values; + public bool hasPaint; // the attributes of the materials whose shaders take paint, by material (ChiselPaintMaterials) - [ReadOnly] public NativeParallelHashMap materials; - [ReadOnly] public NativeArray materialSlots; - public bool hasMaterials; + [ReadOnly] public NativeParallelHashMap materials; + [ReadOnly] public NativeArray materialSlots; + public bool hasMaterials; public readonly bool TryGetRange(ulong entityID, int surfaceIndex, out ChiselPaintRange range) { @@ -49,160 +48,211 @@ public readonly bool TryGetMaterial(ulong material, out ChiselPaintRange range) return hasMaterials && materials.TryGetValue(material, out range); } - public static ChiselPaintLookup Create(NativeList layers, NativeList values, - NativeList materialSlots, Allocator allocator) + public static ChiselPaintLookup Of(ChiselTreeLookup.Data data) { - var count = layers.IsCreated ? layers.Length : 0; - var slots = materialSlots.IsCreated ? materialSlots.Length : 0; - var lookup = new ChiselPaintLookup + return new ChiselPaintLookup { - ranges = new NativeParallelHashMap(math.max(1, count), allocator), - layers = new NativeArray(math.max(1, count), allocator), - values = new NativeArray(math.max(1, values.IsCreated ? values.Length : 0), allocator), - materials = new NativeParallelHashMap(math.max(1, slots), allocator), - materialSlots = new NativeArray(math.max(1, slots), allocator) + ranges = data.paintRanges, + layers = data.paintLayers.AsArray(), + values = data.paintValues.AsArray(), + hasPaint = data.paintLayers.Length > 0, + materials = data.paintMaterials, + materialSlots = data.paintMaterialSlots.AsArray(), + hasMaterials = data.paintMaterialSlots.Length > 0 }; - if (slots > 0) - { - lookup.hasMaterials = true; - NativeArray.Copy(materialSlots.AsArray(), lookup.materialSlots, slots); - // a material's slots are together (ChiselPaintMaterials) - for (int start = 0; start < slots;) - { - var material = materialSlots[start].material; - var end = start; - var hash = 0u; - for (; end < slots && materialSlots[end].material == material; end++) - hash = math.hash(new uint3(hash, (uint)materialSlots[end].attribute, math.hash(math.asuint(materialSlots[end].defaultValue)))); - lookup.materials.TryAdd(material, new ChiselPaintRange { start = start, count = end - start, hash = hash }); - start = end; - } - } - if (count == 0) - return lookup; - lookup.hasPaint = true; - NativeArray.Copy(layers.AsArray(), lookup.layers, count); - NativeArray.Copy(values.AsArray(), lookup.values, values.Length); - // the layers are sorted by surface (ChiselPaintStore.SetPaint) - for (int start = 0; start < count;) - { - var key = new ChiselPaintKey { entityID = layers[start].entityID, surfaceIndex = layers[start].surfaceIndex }; - var end = start + 1; - while (end < count && layers[end].entityID == key.entityID && layers[end].surfaceIndex == key.surfaceIndex) - end++; - var hash = 0u; - for (int l = start; l < end; l++) - hash = math.hash(new uint2(hash, ChiselPaintStore.HashOf(layers[l], values))); - lookup.ranges.TryAdd(key, new ChiselPaintRange { start = start, count = end - start, hash = hash }); - start = end; - } - return lookup; - } - - public JobHandle Dispose(JobHandle dependency) - { - var handle = dependency; - if (ranges.IsCreated) handle = JobHandle.CombineDependencies(handle, ranges.Dispose(dependency)); - if (layers.IsCreated) handle = JobHandle.CombineDependencies(handle, layers.Dispose(dependency)); - if (values.IsCreated) handle = JobHandle.CombineDependencies(handle, values.Dispose(dependency)); - if (materials.IsCreated) handle = JobHandle.CombineDependencies(handle, materials.Dispose(dependency)); - if (materialSlots.IsCreated) handle = JobHandle.CombineDependencies(handle, materialSlots.Dispose(dependency)); - this = default; - return handle; } } + // The tree keeps its paint between updates: each entity's layers together, their values appended as they come (the + // values of replaced layers are left until they are half of them), and the ranges and hashes an update looks up internal static class ChiselPaintStore { struct BySurface : IComparer { public int Compare(ChiselPaintLayer x, ChiselPaintLayer y) { - if (x.entityID != y.entityID) return x.entityID.CompareTo(y.entityID); if (x.surfaceIndex != y.surfaceIndex) return x.surfaceIndex.CompareTo(y.surfaceIndex); return ((int)x.attribute).CompareTo((int)y.attribute); } } // A layer and its values - public static unsafe uint HashOf(in ChiselPaintLayer layer, NativeList values) + public static unsafe uint HashOf(in ChiselPaintLayer layer, float4* values) { var copy = layer; copy.valueStart = 0; var hash = math.hash(©, UnsafeUtility.SizeOf()); - var count = math.min(layer.ValueCount, values.Length - layer.valueStart); - if (count > 0) - hash = math.hash(new uint2(hash, math.hash((float4*)values.GetUnsafeReadOnlyPtr() + layer.valueStart, count * UnsafeUtility.SizeOf()))); - return hash; - } - - // Each entity's paint, hashed - static void HashByEntity(NativeList layers, NativeList values, NativeParallelHashMap hashes) - { - for (int l = 0; l < layers.Length; l++) - { - var entityID = layers[l].entityID; - hashes.TryGetValue(entityID, out var hash); - hashes[entityID] = math.hash(new uint2(hash, HashOf(layers[l], values))); - } + return math.hash(new uint2(hash, math.hash(values + layer.valueStart, layer.ValueCount * UnsafeUtility.SizeOf()))); } - public static bool SetPaint(CSGTree tree, NativeArray layers, NativeArray values, NativeList changedEntities) + /// Replaces the paint of one entity: its layers, and the values their grids take from. True when it changed; + /// its brushes then have to be rebuilt to show it. + public static unsafe bool SetPaint(CSGTree tree, ulong entityID, NativeArray layers, NativeArray values) { - if (changedEntities.IsCreated) - changedEntities.Clear(); if (!tree.Valid) return false; var data = ChiselTreeLookup.Value[tree]; - // the new paint, its layers in their surfaces' order and each with its own values - var count = layers.IsCreated ? layers.Length : 0; NativeList sorted; - using var _sorted = sorted = new NativeList(math.max(1, count), Allocator.Temp); - NativeList packed; - using var _packed = packed = new NativeList(math.max(1, values.IsCreated ? values.Length : 0), Allocator.Temp); - for (int l = 0; l < count; l++) + using var _sorted = sorted = new NativeList(math.max(1, layers.IsCreated ? layers.Length : 0), Allocator.Temp); + for (int l = 0; layers.IsCreated && l < layers.Length; l++) { var layer = layers[l]; if (layer.ValueCount <= 0 || layer.valueStart < 0 || layer.valueStart + layer.ValueCount > values.Length) continue; + layer.entityID = entityID; sorted.Add(layer); } sorted.Sort(new BySurface()); + + NativeList hashes; + using var _hashes = hashes = new NativeList(math.max(1, sorted.Length), Allocator.Temp); + var entityHash = 0u; + var valuesPointer = values.IsCreated ? (float4*)values.GetUnsafeReadOnlyPtr() : null; + for (int l = 0; l < sorted.Length; l++) + { + hashes.Add(HashOf(sorted[l], valuesPointer)); + entityHash = math.hash(new uint2(entityHash, hashes[l])); + } + data.paintEntityHashes.TryGetValue(entityID, out var previous); + if (sorted.Length == 0 ? !data.paintEntityHashes.ContainsKey(entityID) : previous == entityHash && data.paintEntityHashes.ContainsKey(entityID)) + return false; + + data.lastJobHandle.Complete(); + + // its old layers out, their values left behind + var first = 0; + while (first < data.paintLayers.Length && data.paintLayers[first].entityID != entityID) + first++; + var last = first; + while (last < data.paintLayers.Length && data.paintLayers[last].entityID == entityID) + { + data.paintUnusedValues += data.paintLayers[last].ValueCount; + last++; + } + data.paintLayers.RemoveRange(first, last - first); + data.paintLayerHashes.RemoveRange(first, last - first); + + // its new layers at the end, their values after the others for (int l = 0; l < sorted.Length; l++) { var layer = sorted[l]; - var start = packed.Length; - for (int v = 0; v < layer.ValueCount; v++) - packed.Add(values[layer.valueStart + v]); + var start = data.paintValues.Length; + data.paintValues.AddRange(valuesPointer + layer.valueStart, layer.ValueCount); layer.valueStart = start; - sorted[l] = layer; + data.paintLayers.Add(layer); + data.paintLayerHashes.Add(hashes[l]); } + if (sorted.Length == 0) + data.paintEntityHashes.Remove(entityID); + else + data.paintEntityHashes[entityID] = entityHash; - // which entities' paint changed: theirs is new, gone, or other - using var before = new NativeParallelHashMap(math.max(1, data.paintLayers.Length), Allocator.Temp); - using var after = new NativeParallelHashMap(math.max(1, sorted.Length), Allocator.Temp); - HashByEntity(data.paintLayers, data.paintValues, before); - HashByEntity(sorted, packed, after); - foreach (var entry in after) + if (data.paintUnusedValues > 0 && data.paintUnusedValues * 2 >= data.paintValues.Length) + Compact(data); + UpdateRanges(data); + return true; + } + + // The values of the layers only, in their order + static void Compact(ChiselTreeLookup.Data data) + { + using var compacted = new NativeList(math.max(1, data.paintValues.Length - data.paintUnusedValues), Allocator.Temp); + for (int l = 0; l < data.paintLayers.Length; l++) { - if (!before.TryGetValue(entry.Key, out var hash) || hash != entry.Value) - changedEntities.Add(entry.Key); + var layer = data.paintLayers[l]; + var start = compacted.Length; + compacted.AddRange(data.paintValues.AsArray().GetSubArray(layer.valueStart, layer.ValueCount)); + layer.valueStart = start; + data.paintLayers[l] = layer; } - foreach (var entry in before) + data.paintValues.Clear(); + data.paintValues.AddRange(compacted.AsArray()); + data.paintUnusedValues = 0; + } + + // Each surface's layers (an entity's are together, by surface) and their hash + static void UpdateRanges(ChiselTreeLookup.Data data) + { + var layers = data.paintLayers; + data.paintRanges.Clear(); + if (data.paintRanges.Capacity < layers.Length) + data.paintRanges.Capacity = layers.Length; + for (int start = 0; start < layers.Length;) { - if (!after.ContainsKey(entry.Key)) - changedEntities.Add(entry.Key); + var key = new ChiselPaintKey { entityID = layers[start].entityID, surfaceIndex = layers[start].surfaceIndex }; + var end = start; + var hash = 0u; + for (; end < layers.Length && layers[end].entityID == key.entityID && layers[end].surfaceIndex == key.surfaceIndex; end++) + hash = math.hash(new uint2(hash, data.paintLayerHashes[end])); + data.paintRanges.TryAdd(key, new ChiselPaintRange { start = start, count = end - start, hash = hash }); + start = end; } - if (changedEntities.Length == 0) + } + + /// Replaces all paint of the tree: the entities in the layers get theirs, every other entity loses its paint. + /// Returns the entities whose paint changed. + public static bool SetPaint(CSGTree tree, NativeArray layers, NativeArray values, NativeList changedEntities) + { + if (changedEntities.IsCreated) + changedEntities.Clear(); + if (!tree.Valid) return false; + var data = ChiselTreeLookup.Value[tree]; + var entities = new List(); + for (int l = 0; layers.IsCreated && l < layers.Length; l++) + { + if (!entities.Contains(layers[l].entityID)) + entities.Add(layers[l].entityID); + } + using var gone = data.paintEntityHashes.GetKeyArray(Allocator.Temp); + foreach (var entityID in gone) + { + if (!entities.Contains(entityID) && SetPaint(tree, entityID, default, default) && changedEntities.IsCreated) + changedEntities.Add(entityID); + } + + using var own = new NativeList(Allocator.Temp); + foreach (var entityID in entities) + { + own.Clear(); + for (int l = 0; l < layers.Length; l++) + { + if (layers[l].entityID == entityID) + own.Add(layers[l]); + } + if (SetPaint(tree, entityID, own.AsArray(), values) && changedEntities.IsCreated) + changedEntities.Add(entityID); + } + return changedEntities.IsCreated && changedEntities.Length > 0; + } + + // The tree's copy of the materials' paint attributes, made again when they changed + public static void UpdateMaterials(ChiselTreeLookup.Data data) + { + var version = ChiselPaintMaterials.Refresh(); + if (data.paintMaterialsVersion == version) + return; data.lastJobHandle.Complete(); - data.paintLayers.Clear(); - data.paintLayers.AddRange(sorted.AsArray()); - data.paintValues.Clear(); - data.paintValues.AddRange(packed.AsArray()); - return true; + data.paintMaterialsVersion = version; + var slots = data.paintMaterialSlots; + slots.Clear(); + ChiselPaintMaterials.Gather(slots); + data.paintMaterials.Clear(); + if (data.paintMaterials.Capacity < slots.Length) + data.paintMaterials.Capacity = slots.Length; + // a material's slots are together (ChiselPaintMaterials) + for (int start = 0; start < slots.Length;) + { + var material = slots[start].material; + var end = start; + var hash = 0u; + for (; end < slots.Length && slots[end].material == material; end++) + hash = math.hash(new uint3(hash, (uint)slots[end].attribute, math.hash(math.asuint(slots[end].defaultValue)))); + data.paintMaterials.TryAdd(material, new ChiselPaintRange { start = start, count = end - start, hash = hash }); + start = end; + } } } } diff --git a/Core/Tests/ChiselPaintStoreTests.cs b/Core/Tests/ChiselPaintStoreTests.cs new file mode 100644 index 00000000..5a1b36c4 --- /dev/null +++ b/Core/Tests/ChiselPaintStoreTests.cs @@ -0,0 +1,76 @@ +using NUnit.Framework; +using Unity.Collections; +using Unity.Mathematics; + +namespace Chisel.Core.Tests +{ + // The paint a tree keeps between updates (ChiselPaintStore): replaced an entity at a time + [TestFixture] + public sealed class ChiselPaintStoreTests + { + CSGTree tree; + + [SetUp] + public void SetUp() { tree = CSGTree.Create(default(UnityEngine.EntityId)); } + + [TearDown] + public void TearDown() + { + if (tree.Valid) + { + ChiselTreeLookup.Value[tree].lastJobHandle.Complete(); + tree.Destroy(); + } + } + + // A 2 x 2 grid on a surface, every point at value + static bool Paint(CSGTree tree, ulong entityID, int surfaceIndex, float value) + { + NativeArray layers; + using var _layers = layers = new NativeArray(1, Allocator.Temp); + NativeArray values; + using var _values = values = new NativeArray(4, Allocator.Temp); + layers[0] = new ChiselPaintLayer { surfaceIndex = surfaceIndex, attribute = ChiselPaintAttribute.Color, components = ChiselPaintComponents.XYZW, + cellU = new float3(1, 0, 0), cellV = new float3(0, 0, 1), resolution = new int2(2, 2) }; + for (int i = 0; i < 4; i++) + values[i] = value; + return ChiselPaintStore.SetPaint(tree, entityID, layers, values); + } + + static float ValueOf(CSGTree tree, ulong entityID, int surfaceIndex) + { + var lookup = ChiselPaintLookup.Of(ChiselTreeLookup.Value[tree]); + Assert.That(lookup.TryGetRange(entityID, surfaceIndex, out var range), Is.True, $"surface {surfaceIndex} of {entityID}"); + return lookup.values[lookup.layers[range.start].valueStart].x; + } + + [Test] + public void AnEntitysPaint_IsReplacedWithoutTouchingTheOthers() + { + Assert.That(Paint(tree, 1, 0, 0.25f), Is.True); + Assert.That(Paint(tree, 2, 3, 0.5f), Is.True); + Assert.That(Paint(tree, 1, 0, 0.25f), Is.False, "the same paint: no change"); + + Assert.That(Paint(tree, 1, 0, 0.75f), Is.True); + Assert.That(ValueOf(tree, 1, 0), Is.EqualTo(0.75f)); + Assert.That(ValueOf(tree, 2, 3), Is.EqualTo(0.5f)); + + using var none = new NativeArray(0, Allocator.Temp); + Assert.That(ChiselPaintStore.SetPaint(tree, 1, none, default), Is.True, "its paint removed"); + Assert.That(ChiselPaintLookup.Of(ChiselTreeLookup.Value[tree]).TryGetRange(1, 0, out _), Is.False); + Assert.That(ValueOf(tree, 2, 3), Is.EqualTo(0.5f)); + } + + // Values of replaced layers are dropped once they are half of all of them + [Test] + public void ReplacedValues_AreCompactedAway() + { + Paint(tree, 1, 0, 0); + for (int i = 1; i <= 10; i++) + Paint(tree, 1, 0, i); + var data = ChiselTreeLookup.Value[tree]; + Assert.That(data.paintValues.Length, Is.LessThanOrEqualTo(8)); + Assert.That(ValueOf(tree, 1, 0), Is.EqualTo(10)); + } + } +} diff --git a/Core/Tests/ChiselPaintStoreTests.cs.meta b/Core/Tests/ChiselPaintStoreTests.cs.meta new file mode 100644 index 00000000..638bba9d --- /dev/null +++ b/Core/Tests/ChiselPaintStoreTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: afdab3d64f8a8414f8f45eee2c3a4b16 \ No newline at end of file From fcee7b1c19fde76c7a10ff45da4f77863bafdabc Mon Sep 17 00:00:00 2001 From: Sander van Rossen Date: Mon, 26 Oct 2026 10:17:06 +0100 Subject: [PATCH 23/27] Paint subdivision: edges looked up, points located by walking The triangle across an edge comes from a map of edges kept with the triangles, and a point is found by walking from the triangle the last point went into (the grid's points come row by row), rather than by scanning every triangle for each; a point the walk doesn't find is still looked for in every triangle. A grid at the most points a surface gets is tested too. --- Core/2.Processing/Paint/PaintSubdivision.cs | 197 +++++++++++++------- Core/Tests/PaintSubdivisionTests.cs | 23 ++- 2 files changed, 145 insertions(+), 75 deletions(-) diff --git a/Core/2.Processing/Paint/PaintSubdivision.cs b/Core/2.Processing/Paint/PaintSubdivision.cs index ba873662..029f6403 100644 --- a/Core/2.Processing/Paint/PaintSubdivision.cs +++ b/Core/2.Processing/Paint/PaintSubdivision.cs @@ -114,101 +114,165 @@ static double2 GridCoordinates(in ChiselPaintLayer layer, float3 point) struct Triangulation { + enum Result { NotHere, Inserted, Existing } + NativeList indices; NativeList flat; // every vertex in the surface's plane NativeList pending; // edges (a, b) of triangles (a, b, v) round a new vertex v, to be made Delaunay + // the triangle each edge (a, b) is in, in its winding + NativeParallelHashMap edges; double3 axisU, axisV; double snap; + int last; // where the last point went: the next is usually near it public Triangulation(NativeList indices, ref UniqueVertexMapper mapper, float3 normal, double snap) { this.indices = indices; this.snap = snap; + last = 0; axisU = math.normalizesafe(math.cross((double3)normal, math.abs(normal.x) < 0.9f ? new double3(1, 0, 0) : new double3(0, 1, 0))); axisV = math.normalizesafe(math.cross((double3)normal, axisU)); flat = new NativeList(mapper.surfaceColliderVertices.Length, Allocator.Temp); pending = new NativeList(16, Allocator.Temp); + edges = new NativeParallelHashMap(math.max(16, indices.Length * 4), Allocator.Temp); for (int i = 0; i < mapper.surfaceColliderVertices.Length; i++) flat.Add(Flat(mapper.surfaceColliderVertices[i])); + for (int t = 0; t + 2 < indices.Length; t += 3) + { + for (int k = 0; k < 3; k++) + edges[new int2(indices[t + k], indices[t + (k + 1) % 3])] = t; + } } public void Dispose() { flat.Dispose(); pending.Dispose(); + edges.Dispose(); } readonly double2 Flat(float3 position) => new double2(math.dot((double3)position, axisU), math.dot((double3)position, axisV)); - public void Insert(ref UniqueVertexMapper mapper, float3 position) + void SetTriangle(int t, int a, int b, int c) { - var point = Flat(position); - for (int t = 0; t + 2 < indices.Length; t += 3) + for (int k = 0; k < 3; k++) + { + var edge = new int2(indices[t + k], indices[t + (k + 1) % 3]); + if (edges.TryGetValue(edge, out var owner) && owner == t) + edges.Remove(edge); + } + indices[t] = a; indices[t + 1] = b; indices[t + 2] = c; + edges[new int2(a, b)] = t; + edges[new int2(b, c)] = t; + edges[new int2(c, a)] = t; + } + + void AddTriangle(int a, int b, int c) + { + var t = indices.Length; + indices.Add(a); indices.Add(b); indices.Add(c); + edges[new int2(a, b)] = t; + edges[new int2(b, c)] = t; + edges[new int2(c, a)] = t; + } + + // The triangle the point is in, walking from where the last point went; -1 when the walk leaves the surface + readonly int Locate(double2 point) + { + var t = last + 2 < indices.Length ? last : 0; + for (int steps = 0; steps <= indices.Length / 3; steps++) { int i0 = indices[t], i1 = indices[t + 1], i2 = indices[t + 2]; var a = flat[i0]; var b = flat[i1]; var c = flat[i2]; var twiceArea = Cross(a, b, c); if (!(math.abs(twiceArea) > 1e-18)) - continue; - // the point's weights, and how far it is from each edge (the one opposite each corner) + return -1; var w = new double3(Cross(point, b, c), Cross(a, point, c), Cross(a, b, point)) / twiceArea; - var distance = w * math.abs(twiceArea) / new double3(math.distance(b, c), math.distance(c, a), math.distance(a, b)); - if (math.cmin(distance) < -snap) - continue; - if (math.distance(point, a) < snap || math.distance(point, b) < snap || math.distance(point, c) < snap) - return; // there is a vertex already - - // snapped onto the outline when near it (a sliver there can't be flipped away), onto an inner edge - // only when on it - var nearest = distance.x <= distance.y && distance.x <= distance.z ? 0 : distance.y <= distance.z ? 1 : 2; - int opposite = nearest == 0 ? i0 : nearest == 1 ? i1 : i2; - int e0 = nearest == 0 ? i1 : nearest == 1 ? i2 : i0; - int e1 = nearest == 0 ? i2 : nearest == 1 ? i0 : i1; - var other = FindEdge(e1, e0, out var otherOpposite); - var onEdge = other < 0 ? snap : snap * 1e-3; - if (distance[nearest] < -onEdge) - continue; // across an edge inside: in the triangle on its other side + // on to the triangle across the edge it is furthest beyond + var k = w.x <= w.y && w.x <= w.z ? 0 : w.y <= w.z ? 1 : 2; + if (w[k] >= 0) + return t; + int e0 = k == 0 ? i1 : k == 1 ? i2 : i0; + int e1 = k == 0 ? i2 : k == 1 ? i0 : i1; + if (!edges.TryGetValue(new int2(e1, e0), out t)) + return -1; + } + return -1; + } - if (distance[nearest] >= onEdge) - { - // inside: three triangles round it - var v = AddVertex(ref mapper, i0, i1, i2, w); - indices[t + 2] = v; - indices.Add(i1); indices.Add(i2); indices.Add(v); - indices.Add(i2); indices.Add(i0); indices.Add(v); - pending.Add(new int2(i0, i1)); - pending.Add(new int2(i1, i2)); - pending.Add(new int2(i2, i0)); - Legalize(v); + public void Insert(ref UniqueVertexMapper mapper, float3 position) + { + var point = Flat(position); + var found = Locate(point); + if (found >= 0 && TryInsert(ref mapper, point, found) != Result.NotHere) + return; + // near the outline, or where the walk stopped: every triangle + for (int t = 0; t + 2 < indices.Length; t += 3) + { + if (t != found && TryInsert(ref mapper, point, t) != Result.NotHere) return; - } + } + } - // on the edge: it splits both of its triangles, or the one on the surface's outline - // exactly on the edge - w = math.max(w, 0); - w[nearest] = 0; - w /= math.csum(w); - var vertex = AddVertex(ref mapper, i0, i1, i2, w); - if (other < 0) - { - indices[t] = opposite; indices[t + 1] = e0; indices[t + 2] = vertex; - indices.Add(opposite); indices.Add(vertex); indices.Add(e1); - pending.Add(new int2(opposite, e0)); - pending.Add(new int2(e1, opposite)); - Legalize(vertex); - return; - } - indices[t] = opposite; indices[t + 1] = e0; indices[t + 2] = vertex; - indices.Add(opposite); indices.Add(vertex); indices.Add(e1); - indices[other] = otherOpposite; indices[other + 1] = e1; indices[other + 2] = vertex; - indices.Add(otherOpposite); indices.Add(vertex); indices.Add(e0); - pending.Add(new int2(opposite, e0)); - pending.Add(new int2(e1, opposite)); + Result TryInsert(ref UniqueVertexMapper mapper, double2 point, int t) + { + int i0 = indices[t], i1 = indices[t + 1], i2 = indices[t + 2]; + var a = flat[i0]; var b = flat[i1]; var c = flat[i2]; + var twiceArea = Cross(a, b, c); + if (!(math.abs(twiceArea) > 1e-18)) + return Result.NotHere; + // the point's weights, and how far it is from each edge (the one opposite each corner) + var w = new double3(Cross(point, b, c), Cross(a, point, c), Cross(a, b, point)) / twiceArea; + var distance = w * math.abs(twiceArea) / new double3(math.distance(b, c), math.distance(c, a), math.distance(a, b)); + if (math.cmin(distance) < -snap) + return Result.NotHere; + if (math.distance(point, a) < snap || math.distance(point, b) < snap || math.distance(point, c) < snap) + return Result.Existing; + + // snapped onto the outline when near it (a sliver there can't be flipped away), onto an inner edge + // only when on it + var nearest = distance.x <= distance.y && distance.x <= distance.z ? 0 : distance.y <= distance.z ? 1 : 2; + int opposite = nearest == 0 ? i0 : nearest == 1 ? i1 : i2; + int e0 = nearest == 0 ? i1 : nearest == 1 ? i2 : i0; + int e1 = nearest == 0 ? i2 : nearest == 1 ? i0 : i1; + var other = FindEdge(e1, e0, out var otherOpposite); + var onEdge = other < 0 ? snap : snap * 1e-3; + if (distance[nearest] < -onEdge) + return Result.NotHere; // across an edge inside: in the triangle on its other side + + last = t; + if (distance[nearest] >= onEdge) + { + // inside: three triangles round it + var v = AddVertex(ref mapper, i0, i1, i2, w); + SetTriangle(t, i0, i1, v); + AddTriangle(i1, i2, v); + AddTriangle(i2, i0, v); + pending.Add(new int2(i0, i1)); + pending.Add(new int2(i1, i2)); + pending.Add(new int2(i2, i0)); + Legalize(v); + return Result.Inserted; + } + + // on the edge: it splits both of its triangles, or the one on the surface's outline + w = math.max(w, 0); + w[nearest] = 0; + w /= math.csum(w); + var vertex = AddVertex(ref mapper, i0, i1, i2, w); + SetTriangle(t, opposite, e0, vertex); + AddTriangle(opposite, vertex, e1); + pending.Add(new int2(opposite, e0)); + pending.Add(new int2(e1, opposite)); + if (other >= 0) + { + SetTriangle(other, otherOpposite, e1, vertex); + AddTriangle(otherOpposite, vertex, e0); pending.Add(new int2(otherOpposite, e1)); pending.Add(new int2(e0, otherOpposite)); - Legalize(vertex); - return; } + Legalize(vertex); + return Result.Inserted; } // Each edge (a, b) of a triangle (a, b, v) is flipped when the far corner d of the triangle across it is in @@ -229,8 +293,8 @@ void Legalize(int v) if (!InCircle(flat[a], flat[b], flat[v], flat[d]) || !Convex(flat[a], flat[d], flat[b], flat[v])) continue; // (a, b, v) and (b, a, d) become (v, a, d) and (d, b, v) - indices[t] = v; indices[t + 1] = a; indices[t + 2] = d; - indices[u] = d; indices[u + 1] = b; indices[u + 2] = v; + SetTriangle(t, v, a, d); + SetTriangle(u, d, b, v); pending.Add(new int2(a, d)); pending.Add(new int2(d, b)); } @@ -239,19 +303,14 @@ void Legalize(int v) // The triangle with the edge from a to b (in its winding), and its third corner; -1 when there is none readonly int FindEdge(int a, int b, out int opposite) { - for (int t = 0; t + 2 < indices.Length; t += 3) + if (!edges.TryGetValue(new int2(a, b), out var t)) { - for (int k = 0; k < 3; k++) - { - if (indices[t + k] == a && indices[t + (k + 1) % 3] == b) - { - opposite = indices[t + (k + 2) % 3]; - return t; - } - } + opposite = -1; + return -1; } - opposite = -1; - return -1; + var k = indices[t] == a ? 0 : indices[t + 1] == a ? 1 : 2; + opposite = indices[t + (k + 2) % 3]; + return t; } // A vertex where the weights put it in the triangle, every stream interpolated diff --git a/Core/Tests/PaintSubdivisionTests.cs b/Core/Tests/PaintSubdivisionTests.cs index e597a1f0..1d051e5a 100644 --- a/Core/Tests/PaintSubdivisionTests.cs +++ b/Core/Tests/PaintSubdivisionTests.cs @@ -8,8 +8,8 @@ namespace Chisel.Core.Tests // the grid meets it, and no sliver left by the flips public sealed class PaintSubdivisionTests { - // A square of side 2 in the xz plane at offset, two triangles, and a grid of 0.1 cells over it exactly - static void Subdivide(float3 offset, bool otherDiagonal, out int vertexCount, out int triangleCount, out float minAngle, out string worst, out string offGrid) + // A square of side 2 in the xz plane at offset, two triangles, and a grid of cells x cells over it exactly + static void Subdivide(float3 offset, bool otherDiagonal, int cells, out int vertexCount, out int triangleCount, out float minAngle, out string worst, out string offGrid) { var mapper = new UniqueVertexMapper { @@ -30,8 +30,9 @@ static void Subdivide(float3 offset, bool otherDiagonal, out int vertexCount, ou NativeArray layers; using var _layers = layers = new NativeArray(1, Allocator.Temp); - using var values = new NativeArray(21 * 21, Allocator.Temp); - layers[0] = new ChiselPaintLayer { origin = offset, cellU = new float3(0.1f, 0, 0), cellV = new float3(0, 0, 0.1f), resolution = new int2(21, 21) }; + var size = 2f / cells; + using var values = new NativeArray((cells + 1) * (cells + 1), Allocator.Temp); + layers[0] = new ChiselPaintLayer { origin = offset, cellU = new float3(size, 0, 0), cellV = new float3(0, 0, size), resolution = new int2(cells + 1, cells + 1) }; var lookup = new ChiselPaintLookup { layers = layers, values = values }; PaintSubdivision.Subdivide(indices, ref mapper, new float3(0, 1, 0), lookup, new ChiselPaintRange { start = 0, count = 1 }); @@ -39,7 +40,7 @@ static void Subdivide(float3 offset, bool otherDiagonal, out int vertexCount, ou offGrid = ""; for (int v = 0; v < vertexCount; v++) { - var cell = (mapper.surfaceColliderVertices[v] - offset) / 0.1f; + var cell = (mapper.surfaceColliderVertices[v] - offset) / size; if (math.any(math.abs(cell - math.round(cell)) > 0.01f)) offGrid += " " + (mapper.surfaceColliderVertices[v] - offset); } @@ -70,10 +71,20 @@ static void Subdivide(float3 offset, bool otherDiagonal, out int vertexCount, ou [Test] public void EveryGridPointInside_IsAVertex_AndNoTriangleIsASliver([Values(false, true)] bool otherDiagonal, [Values(0, 5000)] float offset) { - Subdivide(new float3(offset, offset * 0.5f, -offset), otherDiagonal, out var vertices, out var triangles, out var minAngle, out var worst, out var offGrid); + Subdivide(new float3(offset, offset * 0.5f, -offset), otherDiagonal, 20, out var vertices, out var triangles, out var minAngle, out var worst, out var offGrid); Assert.That(vertices, Is.EqualTo(21 * 21), "not on a grid point:" + offGrid); Assert.That(triangles, Is.EqualTo(2 * 20 * 20)); Assert.That(minAngle, Is.GreaterThan(30), "no sliver; the thinnest: " + worst); } + + // A grid of as many points as a surface gets (64 x 64): every one a vertex, still the grid's half cells + [Test] + public void AGridOfTheMostPoints_IsSubdividedAsASmallOneIs() + { + Subdivide(float3.zero, false, 63, out var vertices, out var triangles, out var minAngle, out var worst, out var offGrid); + Assert.That(vertices, Is.EqualTo(PaintSubdivision.kMaxPoints), "not on a grid point:" + offGrid); + Assert.That(triangles, Is.EqualTo(2 * 63 * 63)); + Assert.That(minAngle, Is.GreaterThan(30), "no sliver; the thinnest: " + worst); + } } } From 871070107f7f4000178647d21ce30f78d164754f Mon Sep 17 00:00:00 2001 From: Sander van Rossen Date: Mon, 26 Oct 2026 10:21:05 +0100 Subject: [PATCH 24/27] Paint: a paint change builds the painted brush only Paint doesn't change a brush's shape, so its brushes are marked with a new surfaces-only flag (SetSurfacesDirty) instead of as reshaped: the brush is built again, the brushes touching it aren't. --- Components/Managers/ChiselPaintManager.cs | 2 +- .../CompactHierarchy.Internal.cs | 14 +++++++++++++ Core/1.Input/Nodes/CSGTreeNode.cs | 4 ++++ Core/1.Input/Paint/ChiselPaintMaterials.cs | 2 +- .../Managers/CompactHierarchyManager.cs | 12 +++++++++++ Core/Tests/Contents/SetStateTests.cs | 20 +++++++++++++++++++ 6 files changed, 52 insertions(+), 2 deletions(-) diff --git a/Components/Managers/ChiselPaintManager.cs b/Components/Managers/ChiselPaintManager.cs index b2e7b7c4..1f1eb027 100644 --- a/Components/Managers/ChiselPaintManager.cs +++ b/Components/Managers/ChiselPaintManager.cs @@ -171,7 +171,7 @@ static void SetBrushesDirty(CSGTreeNode node) return; if (node.Type == CSGNodeType.Brush) { - node.SetDirty(); + node.SetSurfacesDirty(); return; } for (int i = 0; i < node.Count; i++) diff --git a/Core/1.Input/CompactHierarchy/CompactHierarchy.Internal.cs b/Core/1.Input/CompactHierarchy/CompactHierarchy.Internal.cs index 0b69dec2..0a7af7b5 100644 --- a/Core/1.Input/CompactHierarchy/CompactHierarchy.Internal.cs +++ b/Core/1.Input/CompactHierarchy/CompactHierarchy.Internal.cs @@ -1409,6 +1409,20 @@ internal void SetBrushesBeforeIntersectingNodeDirty(CompactNodeID compactNodeID) SetBrushesBeforeDirty(compactNodeID); } + // A brush whose surfaces change but not its shape: only it is built again, not the brushes touching it + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal bool SetSurfacesDirty(CompactNodeID compactNodeID) + { + if (!IsValidCompactNodeID(compactNodeID) || RootID == compactNodeID) + return false; + ref var node = ref GetChildRef(compactNodeID); + if (node.brushMeshHash == Int32.MaxValue) + return false; + node.flags |= NodeStatusFlags.OutlineModified; + GetChildRef(RootID).flags |= NodeStatusFlags.TreeNeedsUpdate; + return true; + } + // This method might be removed/renamed in the future [MethodImpl(MethodImplOptions.AggressiveInlining)] internal bool SetDirty(CompactNodeID compactNodeID) diff --git a/Core/1.Input/Nodes/CSGTreeNode.cs b/Core/1.Input/Nodes/CSGTreeNode.cs index 79c36b9c..2fa12525 100644 --- a/Core/1.Input/Nodes/CSGTreeNode.cs +++ b/Core/1.Input/Nodes/CSGTreeNode.cs @@ -105,6 +105,10 @@ internal static CSGTreeNode Encapsulate(NodeID nodeID) /// Force set the dirty flag of the . public void SetDirty () { CompactHierarchyManager.SetDirty(nodeID); } + /// Builds a brush's surfaces again without its shape having changed (its paint did): the brushes + /// touching it are left as they are. + public void SetSurfacesDirty() { CompactHierarchyManager.SetSurfacesDirty(nodeID); } + /// Destroy this . Sets the state to invalid. /// true on success, false on failure public bool Destroy () { var prevNodeID = nodeID; this = Invalid; return CompactHierarchyManager.DestroyNode(prevNodeID); } diff --git a/Core/1.Input/Paint/ChiselPaintMaterials.cs b/Core/1.Input/Paint/ChiselPaintMaterials.cs index 696cbeaa..26d62a1f 100644 --- a/Core/1.Input/Paint/ChiselPaintMaterials.cs +++ b/Core/1.Input/Paint/ChiselPaintMaterials.cs @@ -78,7 +78,7 @@ static void SetBrushesDirty(CSGTreeNode node) return; if (node.Type == CSGNodeType.Brush) { - node.SetDirty(); + node.SetSurfacesDirty(); return; } for (int i = 0; i < node.Count; i++) diff --git a/Core/2.Processing/Managers/CompactHierarchyManager.cs b/Core/2.Processing/Managers/CompactHierarchyManager.cs index e608c3bd..fa57724a 100644 --- a/Core/2.Processing/Managers/CompactHierarchyManager.cs +++ b/Core/2.Processing/Managers/CompactHierarchyManager.cs @@ -792,6 +792,15 @@ internal bool SetDirty(NodeID nodeID) return SetDirty(nodes[index]); } + [return: MarshalAs(UnmanagedType.U1)] + internal bool SetSurfacesDirty(NodeID nodeID) + { + if (!IsValidNodeID(nodeID, out var index) || !IsValidCompactNodeID(nodes[index])) + return false; + ref var hierarchy = ref GetHierarchy(nodes[index]); + return hierarchy.SetSurfacesDirty(nodes[index]); + } + // Do not use. This method might be removed/renamed in the future [EditorBrowsable(EditorBrowsableState.Never)] [return: MarshalAs(UnmanagedType.U1)] @@ -2311,6 +2320,9 @@ public static bool IsBrushSelectable(NodeID nodeID) [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static bool SetDirty(NodeID nodeID) { return instance.SetDirty(nodeID); } + [return: MarshalAs(UnmanagedType.U1)] + internal static bool SetSurfacesDirty(NodeID nodeID) { return instance.SetSurfacesDirty(nodeID); } + // Do not use. This method might be removed/renamed in the future [EditorBrowsable(EditorBrowsableState.Never)] [return: MarshalAs(UnmanagedType.U1)] diff --git a/Core/Tests/Contents/SetStateTests.cs b/Core/Tests/Contents/SetStateTests.cs index 832f2779..143b4fb5 100644 --- a/Core/Tests/Contents/SetStateTests.cs +++ b/Core/Tests/Contents/SetStateTests.cs @@ -73,5 +73,25 @@ public void SetState_WithAMovedBrush_FlagsItAndWhatItTouches() Assert.That(hierarchy.GetLocalTransformation(brushID), Is.EqualTo(moved)); } } + + // Surfaces built again without a change of shape (paint): the brush alone, not what it touches + [Test] + public void SetSurfacesDirty_FlagsTheBrushOnly() + { + using (var harness = BuildAndUpdate()) + { + var brush = (CSGTreeBrush)harness.Tree[0]; + var brushID = brush.CompactNodeID; + ref var hierarchy = ref CompactHierarchyManager.GetHierarchy(brushID); + + ((CSGTreeNode)brush).SetSurfacesDirty(); + + Assert.That(hierarchy.IsStatusFlagSet(brushID, NodeStatusFlags.OutlineModified), Is.True); + Assert.That(hierarchy.IsStatusFlagSet(brushID, NodeStatusFlags.NeedAllTouchingUpdated), Is.False); + Assert.That(hierarchy.IsStatusFlagSet(brushID, NodeStatusFlags.ShapeModified), Is.False); + Assert.That(harness.Update(), Is.True, "the CSG update did not run"); + Assert.That(hierarchy.IsAnyStatusFlagSet(brushID), Is.False, "the update left flags on the brush"); + } + } } } From dee2800d5d4f2fb0776dd1dc221be89501599147 Mon Sep 17 00:00:00 2001 From: Sander van Rossen Date: Mon, 26 Oct 2026 10:25:20 +0100 Subject: [PATCH 25/27] Paint manager: only the generators whose paint changed A generator says its paint changed (PaintChanged, its paint set or removed, enabled, disabled, validated) and only those, and the painted generators that moved, are given to their trees again, one entity at a time (CSGTree.SetPaint with an entity). Every generator is looked at only when what shaders take may have changed. A generator that loses all of its paint, or is disabled, has its paint taken from the tree and its brushes built again. --- .../Base/ChiselGeneratorComponent.cs | 15 +- Components/Managers/ChiselPaintManager.cs | 200 ++++++++---------- Components/Tests/ChiselPaintTests.cs | 22 ++ Core/1.Input/Paint/ChiselPaint.cs | 9 + 4 files changed, 130 insertions(+), 116 deletions(-) diff --git a/Components/Components/Base/ChiselGeneratorComponent.cs b/Components/Components/Base/ChiselGeneratorComponent.cs index ab328c03..c3155cfe 100644 --- a/Components/Components/Base/ChiselGeneratorComponent.cs +++ b/Components/Components/Base/ChiselGeneratorComponent.cs @@ -208,17 +208,17 @@ public void SetSurfacePaint(ChiselSurfacePaint paint) return; surfacePaint.RemoveAll(other => other == null || (other.surfaceIndex == paint.surfaceIndex && other.attribute == paint.attribute)); surfacePaint.Add(paint); - ChiselPaintManager.SetDirty(); + ChiselPaintManager.SetDirty(this); } public void RemoveSurfacePaint(int surfaceIndex, ChiselPaintAttribute attribute) { if (surfacePaint.RemoveAll(other => other == null || (other.surfaceIndex == surfaceIndex && other.attribute == attribute)) > 0) - ChiselPaintManager.SetDirty(); + ChiselPaintManager.SetDirty(this); } /// Called after the values of a surface's paint were changed in place - public void PaintChanged() => ChiselPaintManager.SetDirty(); + public void PaintChanged() => ChiselPaintManager.SetDirty(this); public override CSGTreeNode TopTreeNode { @@ -450,16 +450,14 @@ protected override void OnValidateState() if (TopTreeNode.Dirty) ChiselNodeHierarchyManager.NotifyContentsModified(this); - if (surfacePaint.Count > 0) - ChiselPaintManager.SetDirty(); + ChiselPaintManager.SetDirty(this); base.OnValidateState(); } protected override void OnEnable() { base.OnEnable(); - if (surfacePaint.Count > 0) - ChiselPaintManager.SetDirty(); + ChiselPaintManager.SetDirty(this); } public override void UpdateTransformation() @@ -487,8 +485,7 @@ protected override void OnDisable() { base.OnDisable(); DestroyChildTreeNodes(); - if (surfacePaint.Count > 0) - ChiselPaintManager.SetDirty(); + ChiselPaintManager.SetDirty(this); } [HideInInspector] int prevMaterialHash; diff --git a/Components/Managers/ChiselPaintManager.cs b/Components/Managers/ChiselPaintManager.cs index 1f1eb027..cb84872f 100644 --- a/Components/Managers/ChiselPaintManager.cs +++ b/Components/Managers/ChiselPaintManager.cs @@ -8,20 +8,33 @@ namespace Chisel.Components { /// /// Gives each model's tree the paint of its generators' surfaces (), - /// in the model's space, and rebuilds the brushes whose paint changed. Called before the trees are built. + /// in the model's space, and rebuilds the brushes whose paint changed. Called before the trees are built. Only the + /// generators that said their paint changed, or that moved, are looked at. /// public static class ChiselPaintManager { - static bool s_Dirty = true; - static readonly List s_Painted = new(); - // each painted generator's space in its model's, as its paint was last given in - static readonly Dictionary s_Spaces = new(); - static readonly HashSet s_PaintedModels = new(); - static readonly Dictionary> s_ByModel = new(); - // every generator paint was given for, by entity: one that lost all of its paint is rebuilt without it too - static readonly Dictionary s_Known = new(); + // a generator whose paint a tree has: which tree, and the generator's space in its model's as the paint was given in + struct Given + { + public ulong entityID; + public CSGTree tree; + public Matrix4x4 space; + } + + static bool s_All = true; + static readonly HashSet s_Changed = new(); + static readonly Dictionary s_Given = new(); + static readonly List s_Work = new(); - public static void SetDirty() => s_Dirty = true; + /// Every generator's paint is looked at again (what shaders take may have changed) + public static void SetDirty() => s_All = true; + + /// A generator's paint, or what it is drawn with, may have changed + public static void SetDirty(ChiselGeneratorComponent generator) + { + if (generator) + s_Changed.Add(generator); + } static Matrix4x4 SpaceOf(ChiselGeneratorComponent generator, ChiselModelComponent model) { @@ -31,80 +44,69 @@ static Matrix4x4 SpaceOf(ChiselGeneratorComponent generator, ChiselModelComponen public static void Update() { ChiselPaintMaterials.Update(); - if (!s_Dirty) - { - foreach (var generator in s_Painted) - { - var model = generator ? generator.hierarchyItem?.Model : null; - if (!generator || !model || !s_Spaces.TryGetValue(generator, out var space) || space != SpaceOf(generator, model)) - { - s_Dirty = true; - break; - } - } - if (!s_Dirty) - return; - } - s_Dirty = false; - - s_Painted.Clear(); - s_Spaces.Clear(); - foreach (var list in s_ByModel.Values) - list.Clear(); - foreach (var generator in Object.FindObjectsByType(FindObjectsInactive.Exclude)) - { - var model = generator.hierarchyItem?.Model; - if (!generator.isActiveAndEnabled || generator.SurfacePaint.Count == 0 || !model) - continue; - ChiselPaintBrush.Respace(generator); - s_Painted.Add(generator); - s_Spaces[generator] = SpaceOf(generator, model); - if (!s_ByModel.TryGetValue(model, out var generators)) - s_ByModel[model] = generators = new List(); - generators.Add(generator); - } - // every model with paint, and every model that had paint (which loses it) - var models = new HashSet(s_PaintedModels); - foreach (var entry in s_ByModel) + s_Work.Clear(); + // the painted generators that moved, or are gone + foreach (var entry in s_Given) { - if (entry.Value.Count > 0) - models.Add(entry.Key); + var generator = entry.Key; + var model = generator ? generator.hierarchyItem?.Model : null; + if (s_All || !generator || !model || entry.Value.space != SpaceOf(generator, model)) + s_Work.Add(generator); } - s_PaintedModels.Clear(); - using var changed = new NativeList(16, Allocator.Temp); - foreach (var model in models) + if (s_All) { - if (!model || !model.Node.Valid) - continue; - s_ByModel.TryGetValue(model, out var generators); - if (generators != null && generators.Count > 0) - s_PaintedModels.Add(model); - SetPaint(model, generators, changed); + foreach (var generator in Object.FindObjectsByType(FindObjectsInactive.Exclude)) + { + if (generator.SurfacePaint.Count > 0 && !s_Given.ContainsKey(generator)) + s_Work.Add(generator); + } + s_All = false; } - foreach (var model in new List(s_ByModel.Keys)) + foreach (var generator in s_Changed) { - if (!model) - s_ByModel.Remove(model); + if (!s_Work.Contains(generator)) + s_Work.Add(generator); } + s_Changed.Clear(); + + foreach (var generator in s_Work) + Give(generator); } - static void SetPaint(ChiselModelComponent model, List generators, NativeList changed) + // The generator's paint given to its model's tree, and taken from the tree it was in before + static void Give(ChiselGeneratorComponent generator) { - var layerCount = 0; - var valueCount = 0; - if (generators != null) + s_Given.TryGetValue(generator, out var before); + var given = s_Given.ContainsKey(generator); + var model = generator ? generator.hierarchyItem?.Model : null; + if (!generator || !generator.isActiveAndEnabled || !model || !model.Node.Valid || generator.SurfacePaint.Count == 0) { - foreach (var generator in generators) + if (given) { - foreach (var paint in generator.SurfacePaint) - { - if (!Drawn(generator, paint)) - continue; - layerCount++; - valueCount += paint.values.Length; - } + // built again without it, if it is still there + if (before.tree.Valid && before.tree.SetPaint(before.entityID, default, default) && generator) + SetBrushesDirty(generator.TopTreeNode); + s_Given.Remove(generator); } + return; + } + + var entityID = EntityId.ToULong(generator.GetEntityId()); + var tree = model.Node; + if (given && (before.tree != tree || before.entityID != entityID) && before.tree.Valid) + before.tree.SetPaint(before.entityID, default, default); + + ChiselPaintBrush.Respace(generator); + var space = SpaceOf(generator, model); + var layerCount = 0; + var valueCount = 0; + foreach (var paint in generator.SurfacePaint) + { + if (!Drawn(generator, paint)) + continue; + layerCount++; + valueCount += paint.values.Length; } NativeArray layers; @@ -112,46 +114,30 @@ static void SetPaint(ChiselModelComponent model, List NativeArray values; using var _values = values = new NativeArray(valueCount, Allocator.Temp); int l = 0, v = 0; - if (generators != null) + foreach (var paint in generator.SurfacePaint) { - foreach (var generator in generators) + if (!Drawn(generator, paint)) + continue; + layers[l++] = new ChiselPaintLayer { - var entityID = EntityId.ToULong(generator.GetEntityId()); - s_Known[entityID] = generator; - var space = s_Spaces[generator]; - foreach (var paint in generator.SurfacePaint) - { - if (!Drawn(generator, paint)) - continue; - layers[l++] = new ChiselPaintLayer - { - entityID = entityID, - surfaceIndex = paint.surfaceIndex, - attribute = paint.attribute, - components = paint.components, - defaultValue = paint.defaultValue, - origin = space.MultiplyPoint3x4(paint.origin), - cellU = space.MultiplyVector(paint.cellU), - cellV = space.MultiplyVector(paint.cellV), - resolution = new int2(paint.resolution.x, paint.resolution.y), - valueStart = v - }; - foreach (var value in paint.values) - values[v++] = value; - } - } + entityID = entityID, + surfaceIndex = paint.surfaceIndex, + attribute = paint.attribute, + components = paint.components, + defaultValue = paint.defaultValue, + origin = space.MultiplyPoint3x4(paint.origin), + cellU = space.MultiplyVector(paint.cellU), + cellV = space.MultiplyVector(paint.cellV), + resolution = new int2(paint.resolution.x, paint.resolution.y), + valueStart = v + }; + foreach (var value in paint.values) + values[v++] = value; } - - if (!model.Node.SetPaint(layers, values, changed)) - return; + s_Given[generator] = new Given { entityID = entityID, tree = tree, space = space }; // the brushes of a generator whose paint changed are built again, with it - foreach (var entityID in changed) - { - if (s_Known.TryGetValue(entityID, out var generator) && generator) - SetBrushesDirty(generator.TopTreeNode); - else - s_Known.Remove(entityID); - } + if (tree.SetPaint(entityID, layers, values)) + SetBrushesDirty(generator.TopTreeNode); } // Whether the surface's shader takes paint in this attribute; paint it doesn't take is kept, but not drawn diff --git a/Components/Tests/ChiselPaintTests.cs b/Components/Tests/ChiselPaintTests.cs index 3f5aa9d3..b94b6a64 100644 --- a/Components/Tests/ChiselPaintTests.cs +++ b/Components/Tests/ChiselPaintTests.cs @@ -418,6 +418,28 @@ public void AnUnpaintedSurface_HasItsShadersAttributesAtTheirDefaults() AssertValuesAt(mesh, VertexAttribute.TexCoord2, new Vector3(0, 1, 0), Vector4.zero, "the value mode's default"); } + // A disabled generator's paint leaves its tree, and comes back with it + [Test] + public void ADisabledGenerator_TakesItsPaintWithIt() + { + var model = Model(); + var box = Box(model, Vector3.zero); + var top = SurfaceFacing(box, new float3(0, 1, 0)); + PaintTop(box, ChiselPaintAttribute.Color, ChiselPaintComponents.XYZW); + Update(); + var entityID = EntityId.ToULong(box.GetEntityId()); + bool InTree() => ChiselPaintLookup.Of(ChiselTreeLookup.Value[model.Node]).TryGetRange(entityID, top, out _); + Assert.That(InTree(), Is.True); + + box.enabled = false; + Update(); + Assert.That(InTree(), Is.False, "disabled"); + + box.enabled = true; + Update(); + Assert.That(InTree(), Is.True, "enabled again"); + } + // The grid moves with its generator [Test] public void Paint_MovesWithItsGenerator() diff --git a/Core/1.Input/Paint/ChiselPaint.cs b/Core/1.Input/Paint/ChiselPaint.cs index 351afad3..a61d3713 100644 --- a/Core/1.Input/Paint/ChiselPaint.cs +++ b/Core/1.Input/Paint/ChiselPaint.cs @@ -130,5 +130,14 @@ public static bool SetPaint(this CSGTree tree, NativeArray lay { return ChiselPaintStore.SetPaint(tree, layers, values, changedEntities); } + + /// + /// Replaces the paint of one entity: its layers, and the values their grids take from. True when it changed; the + /// entity's brushes then have to be rebuilt to show it (). + /// + public static bool SetPaint(this CSGTree tree, ulong entityID, NativeArray layers, NativeArray values) + { + return ChiselPaintStore.SetPaint(tree, entityID, layers, values); + } } } From 71fef56520943ecdc690e42ac4f38daa6ae0040d Mon Sep 17 00:00:00 2001 From: Sander van Rossen Date: Mon, 26 Oct 2026 10:40:32 +0100 Subject: [PATCH 26/27] Paint: values and mesh streams packed A grid point keeps only the components its paint has, in the generator's grid, in the tree's values and in what a vertex samples (x, y, z, w in that order), so the generator's values go to the tree as they are. In the render mesh the color is 4 bytes (UNorm8) and a texture coordinate has floats up to its last painted component; the materials' paint attributes carry their components for that. --- .../Components/Paint/ChiselPaintBrush.cs | 19 +++-- .../Components/Paint/ChiselPaintProfiles.cs | 1 + .../Components/Paint/ChiselSurfacePaint.cs | 79 +++++++++++++++---- Components/Managers/ChiselPaintManager.cs | 8 +- Components/Tests/ChiselPaintTests.cs | 6 +- Core/1.Input/Paint/ChiselPaint.cs | 36 ++++++--- Core/1.Input/Paint/ChiselPaintMaterials.cs | 11 +-- Core/2.Processing/Cache/ChiselLookup.cs | 4 +- Core/2.Processing/Paint/ChiselPaintStore.cs | 16 ++-- .../OutputMeshes/ChiselOutputRenderable.cs | 75 +++++++++++++----- Core/Tests/ChiselPaintStoreTests.cs | 10 +-- Core/Tests/PaintSubdivisionTests.cs | 2 +- 12 files changed, 186 insertions(+), 81 deletions(-) diff --git a/Components/Components/Paint/ChiselPaintBrush.cs b/Components/Components/Paint/ChiselPaintBrush.cs index 2a50399c..3f1e45af 100644 --- a/Components/Components/Paint/ChiselPaintBrush.cs +++ b/Components/Components/Paint/ChiselPaintBrush.cs @@ -184,10 +184,7 @@ public static ChiselSurfacePaint PaintFor(ChiselGeneratorComponent generator, in if (added != ChiselPaintComponents.None) { // another mode of the attribute painted it already: its components stay, the mode's start at its default - paint.defaultValue = ChiselPaintLayer.Apply(mode.defaultValue, paint.defaultValue, added); - for (int i = 0; i < paint.values.Length; i++) - paint.values[i] = ChiselPaintLayer.Apply(mode.defaultValue, paint.values[i], added); - paint.components |= added; + paint.AddComponents(added, mode.defaultValue); generator.PaintChanged(); } return paint; @@ -223,7 +220,13 @@ public static bool Dab(ChiselSurfacePaint paint, in ChiselPaintMode mode, Chisel return true; } var normal = Vector3.Cross(paint.cellU, paint.cellV).normalized; - var before = operation == ChiselPaintOperation.Smooth ? (Vector4[])paint.values.Clone() : null; + Vector4[] before = null; + if (operation == ChiselPaintOperation.Smooth) + { + before = new Vector4[paint.PointCount]; + for (int p = 0; p < before.Length; p++) + before[p] = paint[p]; + } var changed = false; for (int j = 0; j < paint.resolution.y; j++) { @@ -261,12 +264,12 @@ public static void Fill(ChiselSurfacePaint paint, in ChiselPaintMode mode, Vecto { if (paint == null || !paint.IsValid) return; - for (int i = 0; i < paint.values.Length; i++) + for (int i = 0; i < paint.PointCount; i++) { - Vector4 result = ChiselPaintLayer.Apply(target, paint.values[i], mode.components); + Vector4 result = ChiselPaintLayer.Apply(target, paint[i], mode.components); if (group != ChiselPaintComponents.None) result = Normalize(result, mode.components, group); - paint.values[i] = result; + paint[i] = result; } } diff --git a/Components/Components/Paint/ChiselPaintProfiles.cs b/Components/Components/Paint/ChiselPaintProfiles.cs index 96ab10c0..38e08970 100644 --- a/Components/Components/Paint/ChiselPaintProfiles.cs +++ b/Components/Components/Paint/ChiselPaintProfiles.cs @@ -49,6 +49,7 @@ static void Resolve(Material material, List slots) } var slot = slots[index]; slot.defaultValue = ChiselPaintLayer.Apply(mode.defaultValue, slot.defaultValue, mode.components); + slot.components |= mode.components; slots[index] = slot; } } diff --git a/Components/Components/Paint/ChiselSurfacePaint.cs b/Components/Components/Paint/ChiselSurfacePaint.cs index 3a5fbde1..e6f613d6 100644 --- a/Components/Components/Paint/ChiselSurfacePaint.cs +++ b/Components/Components/Paint/ChiselSurfacePaint.cs @@ -6,8 +6,9 @@ namespace Chisel.Components { /// /// The paint of one surface of a generator in one vertex attribute: a grid of values in the surface's plane, in the - /// generator's own space. Grid point (i, j) is at origin + i * cellU + j * cellV; holds them - /// row by row along cellU. The cells are also how finely the surface is cut to show the paint. + /// generator's own space. Grid point (i, j) is at origin + i * cellU + j * cellV; holds + /// them row by row along cellU, each as only the components the paint has (x, y, z, w in that order). The cells are + /// also how finely the surface is cut to show the paint. /// [Serializable] public sealed class ChiselSurfacePaint @@ -24,17 +25,14 @@ public sealed class ChiselSurfacePaint /// The surface's paint spacing the grid was made for (its cells are larger when that would have been /// too many points); 0 when not known public float spacing; - public Vector4[] values = Array.Empty(); + public float[] values = Array.Empty(); /// A grid of resolution points from origin along cellU and cellV, every point at the default public static ChiselSurfacePaint Create(int surfaceIndex, ChiselPaintAttribute attribute, ChiselPaintComponents components, Vector4 defaultValue, Vector3 origin, Vector3 cellU, Vector3 cellV, Vector2Int resolution) { resolution = Vector2Int.Max(resolution, Vector2Int.one); - var values = new Vector4[resolution.x * resolution.y]; - for (int i = 0; i < values.Length; i++) - values[i] = defaultValue; - return new ChiselSurfacePaint + var paint = new ChiselSurfacePaint { surfaceIndex = surfaceIndex, attribute = attribute, @@ -43,12 +41,67 @@ public static ChiselSurfacePaint Create(int surfaceIndex, ChiselPaintAttribute a origin = origin, cellU = cellU, cellV = cellV, - resolution = resolution, - values = values + resolution = resolution }; + paint.values = new float[paint.PointCount * paint.Stride]; + for (int i = 0; i < paint.PointCount; i++) + paint[i] = defaultValue; + return paint; } - public bool IsValid => resolution.x > 0 && resolution.y > 0 && values != null && values.Length == resolution.x * resolution.y; + /// How many values each grid point has: one per component + public int Stride => Unity.Mathematics.math.countbits((int)components & 15); + + public int PointCount => Mathf.Max(resolution.x, 0) * Mathf.Max(resolution.y, 0); + + public bool IsValid => resolution.x > 0 && resolution.y > 0 && Stride > 0 && values != null && values.Length == PointCount * Stride; + + /// A grid point's value, the default in the components the paint doesn't have + public Vector4 this[int point] + { + get + { + var value = defaultValue; + var at = point * Stride; + for (int c = 0; c < 4; c++) + { + if (((int)components & (1 << c)) != 0) + value[c] = values[at++]; + } + return value; + } + set + { + var at = point * Stride; + for (int c = 0; c < 4; c++) + { + if (((int)components & (1 << c)) != 0) + values[at++] = value[c]; + } + } + } + + public Vector4 this[int i, int j] + { + get => this[j * resolution.x + i]; + set => this[j * resolution.x + i] = value; + } + + /// The paint takes more components, each at value at every grid point + public void AddComponents(ChiselPaintComponents added, Vector4 value) + { + added &= ~components; + if (added == ChiselPaintComponents.None) + return; + var points = new Vector4[PointCount]; + for (int i = 0; i < points.Length; i++) + points[i] = this[i]; + defaultValue = ChiselPaintLayer.Apply(value, defaultValue, added); + components |= added; + values = new float[PointCount * Stride]; + for (int i = 0; i < points.Length; i++) + this[i] = ChiselPaintLayer.Apply(value, points[i], added); + } /// The paint at a point (in the generator's space), between the four grid points round it; the grid is /// projected along its plane's normal, and a point beyond it takes the nearest edge's paint @@ -75,11 +128,5 @@ public Vector4 Sample(Vector3 point) var top = Vector4.Lerp(this[i, j1], this[i1, j1], x - i); return Vector4.Lerp(bottom, top, y - j); } - - public Vector4 this[int i, int j] - { - get => values[j * resolution.x + i]; - set => values[j * resolution.x + i] = value; - } } } diff --git a/Components/Managers/ChiselPaintManager.cs b/Components/Managers/ChiselPaintManager.cs index cb84872f..4440cb91 100644 --- a/Components/Managers/ChiselPaintManager.cs +++ b/Components/Managers/ChiselPaintManager.cs @@ -111,8 +111,8 @@ static void Give(ChiselGeneratorComponent generator) NativeArray layers; using var _layers = layers = new NativeArray(layerCount, Allocator.Temp); - NativeArray values; - using var _values = values = new NativeArray(valueCount, Allocator.Temp); + NativeArray values; + using var _values = values = new NativeArray(valueCount, Allocator.Temp); int l = 0, v = 0; foreach (var paint in generator.SurfacePaint) { @@ -131,8 +131,8 @@ static void Give(ChiselGeneratorComponent generator) resolution = new int2(paint.resolution.x, paint.resolution.y), valueStart = v }; - foreach (var value in paint.values) - values[v++] = value; + NativeArray.Copy(paint.values, 0, values, v, paint.values.Length); + v += paint.values.Length; } s_Given[generator] = new Given { entityID = entityID, tree = tree, space = space }; // the brushes of a generator whose paint changed are built again, with it diff --git a/Components/Tests/ChiselPaintTests.cs b/Components/Tests/ChiselPaintTests.cs index b94b6a64..6d8d7539 100644 --- a/Components/Tests/ChiselPaintTests.cs +++ b/Components/Tests/ChiselPaintTests.cs @@ -331,7 +331,7 @@ public void Sampling_TakesThePaintBetweenGridPoints() var mode = ChiselPaintMode.Color("Color"); box.SurfaceDefinition.surfaces[top].paintSpacing = 1; var paint = ChiselPaintBrush.PaintFor(box, top, mode); - paint.values = new[] { new Vector4(0, 0, 0, 1), new Vector4(1, 0, 0, 1), new Vector4(0, 1, 0, 1), new Vector4(1, 1, 0, 1) }; + paint[0] = new Vector4(0, 0, 0, 1); paint[1] = new Vector4(1, 0, 0, 1); paint[2] = new Vector4(0, 1, 0, 1); paint[3] = new Vector4(1, 1, 0, 1); var middle = paint.Sample(paint.origin + (paint.cellU + paint.cellV) * 0.5f + Vector3.up * 3); Assert.That(Vector4.Distance(middle, new Vector4(0.5f, 0.5f, 0, 1)), Is.LessThan(1e-5f), "between the four, the grid projected along its normal"); } @@ -399,8 +399,8 @@ public void AFill_SetsTheWholeSurface() var mode = ChiselPaintMode.Color("Color"); var paint = ChiselPaintBrush.PaintFor(box, top, mode); Assert.That(ChiselPaintBrush.Dab(paint, mode, ChiselPaintOperation.Fill, new Vector4(0, 1, 0, 1), new Vector3(0.1f, 1, 0.1f), 0.01f, 0.1f, 0), Is.True); - foreach (var value in paint.values) - Assert.That(value, Is.EqualTo(new Vector4(0, 1, 0, 1))); + for (int p = 0; p < paint.PointCount; p++) + Assert.That(paint[p], Is.EqualTo(new Vector4(0, 1, 0, 1))); } // A surface under a shader that takes paint has its attributes before it is painted, at their modes' defaults diff --git a/Core/1.Input/Paint/ChiselPaint.cs b/Core/1.Input/Paint/ChiselPaint.cs index a61d3713..07279bd4 100644 --- a/Core/1.Input/Paint/ChiselPaint.cs +++ b/Core/1.Input/Paint/ChiselPaint.cs @@ -50,10 +50,14 @@ public struct ChiselPaintLayer : IEquatable public float3 cellV; /// How many grid points there are along cellU and cellV public int2 resolution; - /// Where the grid's values start in the values given with the layers, row by row along cellU + /// Where the grid's values start in the values given with the layers: row by row along cellU, each point + /// as only its components (x, y, z, w in that order) public int valueStart; - public readonly int ValueCount => math.max(resolution.x, 0) * math.max(resolution.y, 0); + /// How many values each grid point has: one per component + public readonly int Stride => math.countbits((int)components & 15); + public readonly int PointCount => math.max(resolution.x, 0) * math.max(resolution.y, 0); + public readonly int ValueCount => PointCount * Stride; public readonly bool Equals(ChiselPaintLayer other) { @@ -68,23 +72,35 @@ public readonly bool Equals(ChiselPaintLayer other) public override readonly int GetHashCode() => (int)math.hash(new uint4((uint)entityID, (uint)surfaceIndex, (uint)attribute, math.hash(origin))); /// The layer's value at a point, in all four components (its grid projected along the plane's normal) - public readonly unsafe float4 Sample(NativeArray values, float3 point) + public readonly unsafe float4 Sample(NativeArray values, float3 point) { - return Sample((float4*)Unity.Collections.LowLevel.Unsafe.NativeArrayUnsafeUtility.GetUnsafeReadOnlyPtr(values), point); + return Sample((float*)Unity.Collections.LowLevel.Unsafe.NativeArrayUnsafeUtility.GetUnsafeReadOnlyPtr(values), point); } - internal readonly unsafe float4 Sample(float4* values, float3 point) + internal readonly unsafe float4 Sample(float* values, float3 point) { if (!GridPosition(point, out var cell, out var t)) return defaultValue; - var row0 = valueStart + cell.y * resolution.x + cell.x; + var row0 = cell.y * resolution.x + cell.x; var row1 = row0 + (resolution.y > 1 ? resolution.x : 0); var next = resolution.x > 1 ? 1 : 0; - var bottom = math.lerp(values[row0], values[row0 + next], t.x); - var top = math.lerp(values[row1], values[row1 + next], t.x); + var bottom = math.lerp(Load(values, row0), Load(values, row0 + next), t.x); + var top = math.lerp(Load(values, row1), Load(values, row1 + next), t.x); return math.lerp(bottom, top, t.y); } + // A grid point's value, the default in the components the layer doesn't have + readonly unsafe float4 Load(float* values, int point) + { + var value = defaultValue; + var at = values + valueStart + point * Stride; + if ((components & ChiselPaintComponents.X) != 0) value.x = *at++; + if ((components & ChiselPaintComponents.Y) != 0) value.y = *at++; + if ((components & ChiselPaintComponents.Z) != 0) value.z = *at++; + if ((components & ChiselPaintComponents.W) != 0) value.w = *at; + return value; + } + // Where a point is on the grid: the cell (its lower grid point) and how far across it, clamped to the grid readonly bool GridPosition(float3 point, out int2 cell, out float2 t) { @@ -126,7 +142,7 @@ public static class CSGTreePaintExtensions /// ). Returns the entities whose paint changed; their brushes have to be /// rebuilt to show it (the caller marks them dirty). /// - public static bool SetPaint(this CSGTree tree, NativeArray layers, NativeArray values, NativeList changedEntities) + public static bool SetPaint(this CSGTree tree, NativeArray layers, NativeArray values, NativeList changedEntities) { return ChiselPaintStore.SetPaint(tree, layers, values, changedEntities); } @@ -135,7 +151,7 @@ public static bool SetPaint(this CSGTree tree, NativeArray lay /// Replaces the paint of one entity: its layers, and the values their grids take from. True when it changed; the /// entity's brushes then have to be rebuilt to show it (). /// - public static bool SetPaint(this CSGTree tree, ulong entityID, NativeArray layers, NativeArray values) + public static bool SetPaint(this CSGTree tree, ulong entityID, NativeArray layers, NativeArray values) { return ChiselPaintStore.SetPaint(tree, entityID, layers, values); } diff --git a/Core/1.Input/Paint/ChiselPaintMaterials.cs b/Core/1.Input/Paint/ChiselPaintMaterials.cs index 26d62a1f..b719039a 100644 --- a/Core/1.Input/Paint/ChiselPaintMaterials.cs +++ b/Core/1.Input/Paint/ChiselPaintMaterials.cs @@ -9,9 +9,10 @@ namespace Chisel.Core /// An attribute a material's shader takes paint in, and what it has where nothing is painted public struct ChiselPaintMaterialSlot { - public ulong material; - public ChiselPaintAttribute attribute; - public float4 defaultValue; + public ulong material; + public ChiselPaintAttribute attribute; + public ChiselPaintComponents components; + public float4 defaultValue; } /// @@ -59,7 +60,7 @@ public static void Update() for (int i = 0; same && i < before; i++) { same = s_Scratch[i].material == s_Slots[i].material && s_Scratch[i].attribute == s_Slots[i].attribute && - s_Scratch[i].defaultValue.Equals(s_Slots[i].defaultValue); + s_Scratch[i].components == s_Slots[i].components && s_Scratch[i].defaultValue.Equals(s_Slots[i].defaultValue); } if (same) return; @@ -106,7 +107,7 @@ static void Resolve() s_Resolved.Clear(); Resolver(entry.Value, s_Resolved); foreach (var slot in s_Resolved) - s_Slots.Add(new ChiselPaintMaterialSlot { material = entry.Key, attribute = slot.attribute, defaultValue = slot.defaultValue }); + s_Slots.Add(new ChiselPaintMaterialSlot { material = entry.Key, attribute = slot.attribute, components = slot.components, defaultValue = slot.defaultValue }); } foreach (var key in s_Gone) s_Materials.Remove(key); diff --git a/Core/2.Processing/Cache/ChiselLookup.cs b/Core/2.Processing/Cache/ChiselLookup.cs index 3d510f20..06b32056 100644 --- a/Core/2.Processing/Cache/ChiselLookup.cs +++ b/Core/2.Processing/Cache/ChiselLookup.cs @@ -42,7 +42,7 @@ public class Data // The painted surfaces' layers and their grids' values, and what an update looks up in them (ChiselPaintStore) public NativeList paintLayers; public NativeList paintLayerHashes; - public NativeList paintValues; + public NativeList paintValues; public int paintUnusedValues; public NativeParallelHashMap paintEntityHashes; public NativeParallelHashMap paintRanges; @@ -92,7 +92,7 @@ internal void Initialize() hotspotLinks = new NativeList(4, Allocator.Persistent); paintLayers = new NativeList(4, Allocator.Persistent); paintLayerHashes = new NativeList(4, Allocator.Persistent); - paintValues = new NativeList(16, Allocator.Persistent); + paintValues = new NativeList(64, Allocator.Persistent); paintEntityHashes = new NativeParallelHashMap(4, Allocator.Persistent); paintRanges = new NativeParallelHashMap(4, Allocator.Persistent); paintMaterialSlots = new NativeList(4, Allocator.Persistent); diff --git a/Core/2.Processing/Paint/ChiselPaintStore.cs b/Core/2.Processing/Paint/ChiselPaintStore.cs index 88ce8b80..d4e8247e 100644 --- a/Core/2.Processing/Paint/ChiselPaintStore.cs +++ b/Core/2.Processing/Paint/ChiselPaintStore.cs @@ -29,7 +29,7 @@ internal struct ChiselPaintLookup { [ReadOnly] public NativeParallelHashMap ranges; [ReadOnly] public NativeArray layers; - [ReadOnly] public NativeArray values; + [ReadOnly] public NativeArray values; public bool hasPaint; // the attributes of the materials whose shaders take paint, by material (ChiselPaintMaterials) [ReadOnly] public NativeParallelHashMap materials; @@ -77,17 +77,17 @@ public int Compare(ChiselPaintLayer x, ChiselPaintLayer y) } // A layer and its values - public static unsafe uint HashOf(in ChiselPaintLayer layer, float4* values) + public static unsafe uint HashOf(in ChiselPaintLayer layer, float* values) { var copy = layer; copy.valueStart = 0; var hash = math.hash(©, UnsafeUtility.SizeOf()); - return math.hash(new uint2(hash, math.hash(values + layer.valueStart, layer.ValueCount * UnsafeUtility.SizeOf()))); + return math.hash(new uint2(hash, math.hash(values + layer.valueStart, layer.ValueCount * sizeof(float)))); } /// Replaces the paint of one entity: its layers, and the values their grids take from. True when it changed; /// its brushes then have to be rebuilt to show it. - public static unsafe bool SetPaint(CSGTree tree, ulong entityID, NativeArray layers, NativeArray values) + public static unsafe bool SetPaint(CSGTree tree, ulong entityID, NativeArray layers, NativeArray values) { if (!tree.Valid) return false; @@ -108,7 +108,7 @@ public static unsafe bool SetPaint(CSGTree tree, ulong entityID, NativeArray hashes; using var _hashes = hashes = new NativeList(math.max(1, sorted.Length), Allocator.Temp); var entityHash = 0u; - var valuesPointer = values.IsCreated ? (float4*)values.GetUnsafeReadOnlyPtr() : null; + var valuesPointer = values.IsCreated ? (float*)values.GetUnsafeReadOnlyPtr() : null; for (int l = 0; l < sorted.Length; l++) { hashes.Add(HashOf(sorted[l], valuesPointer)); @@ -157,7 +157,7 @@ public static unsafe bool SetPaint(CSGTree tree, ulong entityID, NativeArray(math.max(1, data.paintValues.Length - data.paintUnusedValues), Allocator.Temp); + using var compacted = new NativeList(math.max(1, data.paintValues.Length - data.paintUnusedValues), Allocator.Temp); for (int l = 0; l < data.paintLayers.Length; l++) { var layer = data.paintLayers[l]; @@ -192,7 +192,7 @@ static void UpdateRanges(ChiselTreeLookup.Data data) /// Replaces all paint of the tree: the entities in the layers get theirs, every other entity loses its paint. /// Returns the entities whose paint changed. - public static bool SetPaint(CSGTree tree, NativeArray layers, NativeArray values, NativeList changedEntities) + public static bool SetPaint(CSGTree tree, NativeArray layers, NativeArray values, NativeList changedEntities) { if (changedEntities.IsCreated) changedEntities.Clear(); @@ -249,7 +249,7 @@ public static void UpdateMaterials(ChiselTreeLookup.Data data) var end = start; var hash = 0u; for (; end < slots.Length && slots[end].material == material; end++) - hash = math.hash(new uint3(hash, (uint)slots[end].attribute, math.hash(math.asuint(slots[end].defaultValue)))); + hash = math.hash(new uint4(hash, (uint)slots[end].attribute, (uint)slots[end].components, math.hash(math.asuint(slots[end].defaultValue)))); data.paintMaterials.TryAdd(material, new ChiselPaintRange { start = start, count = end - start, hash = hash }); start = end; } diff --git a/Core/3.Output/OutputMeshes/ChiselOutputRenderable.cs b/Core/3.Output/OutputMeshes/ChiselOutputRenderable.cs index da30e494..b0835389 100644 --- a/Core/3.Output/OutputMeshes/ChiselOutputRenderable.cs +++ b/Core/3.Output/OutputMeshes/ChiselOutputRenderable.cs @@ -129,8 +129,9 @@ public static unsafe void Prepare([NoAlias, ReadOnly] NativeArray(); for (int slot = 0; slot < ChiselPaintOutput.kSlotCount; slot++) paintDefaults.Add(float4.zero); @@ -149,7 +150,8 @@ public static unsafe void Prepare([NoAlias, ReadOnly] NativeArray(); var vertices = meshData.GetVertexData(stream: 0); - var paintPointer = paintSlots != 0 ? (float4*)NativeArrayUnsafeUtility.GetUnsafePtr(meshData.GetVertexData(stream: 1)) : null; + var paintPointer = paintSlots != 0 ? (byte*)NativeArrayUnsafeUtility.GetUnsafePtr(meshData.GetVertexData(stream: 1)) : null; var indexBufferLength = indices.Length; var vertexBufferLength = vertices.Length; // the ranges write through these, so no copy asks the mesh for its buffers again @@ -268,11 +271,12 @@ public static unsafe void Prepare([NoAlias, ReadOnly] NativeArray 0 ? (ChiselPaintLayer*)subMeshSource.paint.layers.GetUnsafeReadOnlyPtr() + paintRange.start : null, paintLayerCount = paintRange.count, - paintValues = paintRange.count > 0 ? (float4*)subMeshSource.paint.values.GetUnsafeReadOnlyPtr() : null, + paintValues = paintRange.count > 0 ? (float*)subMeshSource.paint.values.GetUnsafeReadOnlyPtr() : null, meshIndex = meshIndex, indices = indexPointer, vertices = vertexPointer, @@ -357,40 +361,62 @@ internal unsafe struct RenderCopyRange public LightmapChartPlacement placement; public float4 chartRect; public float lightmapScale; - // The mesh's paint stream (null without one): the slots it has, a float4 each, their defaults, and the surface's layers - [NativeDisableUnsafePtrRestriction] public float4* paint; + // The mesh's paint stream (null without one): the slots it has, their components (4 bits a slot), a vertex's bytes, + // the slots' defaults, and the surface's layers + [NativeDisableUnsafePtrRestriction] public byte* paint; public int paintSlots; + public int paintComponents; public int paintStride; public FixedList128Bytes paintDefaults; [NativeDisableUnsafePtrRestriction] public ChiselPaintLayer* paintLayers; public int paintLayerCount; - [NativeDisableUnsafePtrRestriction] public float4* paintValues; + [NativeDisableUnsafePtrRestriction] public float* paintValues; } - // How paint goes into a render mesh: a float4 in a second vertex stream for every vertex attribute painted on its - // surfaces (a slot), in the order of Unity's attributes + // How paint goes into a render mesh: a second vertex stream with every vertex attribute painted on its surfaces (a + // slot), in the order of Unity's attributes: the color as 4 bytes, a texture coordinate as floats up to its last + // painted component internal static unsafe class ChiselPaintOutput { public const int kSlotCount = 7; + const int kColorBytes = 4; public static int SlotOf(ChiselPaintAttribute attribute) => attribute == ChiselPaintAttribute.Color ? 0 : (int)attribute - 1; static VertexAttribute AttributeOf(int slot) => slot == 0 ? VertexAttribute.Color : VertexAttribute.TexCoord0 + (slot + 1); + // How many floats a texture coordinate slot has: up to its last painted component + static int DimensionOf(int components, int slot) + { + var mask = (components >> (slot * 4)) & 15; + return mask == 0 ? 0 : 32 - math.lzcnt(mask); + } + + public static int StrideOf(int slots, int components) + { + var stride = (slots & 1) != 0 ? kColorBytes : 0; + for (int slot = 1; slot < kSlotCount; slot++) + { + if ((slots & (1 << slot)) != 0) + stride += DimensionOf(components, slot) * sizeof(float); + } + return stride; + } + // The mesh's attributes with the painted ones: Color after Tangent, the texture coordinates at the end - public static NativeArray Descriptors(NativeArray descriptors, int slots) + public static NativeArray Descriptors(NativeArray descriptors, int slots, int components) { var result = new NativeList(descriptors.Length + kSlotCount, Allocator.Temp); for (int i = 0; i < descriptors.Length; i++) { result.Add(descriptors[i]); if (descriptors[i].attribute == VertexAttribute.Tangent && (slots & 1) != 0) - result.Add(new VertexAttributeDescriptor(VertexAttribute.Color, VertexAttributeFormat.Float32, 4, stream: 1)); + result.Add(new VertexAttributeDescriptor(VertexAttribute.Color, VertexAttributeFormat.UNorm8, 4, stream: 1)); } for (int slot = 1; slot < kSlotCount; slot++) { if ((slots & (1 << slot)) != 0) - result.Add(new VertexAttributeDescriptor(AttributeOf(slot), VertexAttributeFormat.Float32, 4, stream: 1)); + result.Add(new VertexAttributeDescriptor(AttributeOf(slot), VertexAttributeFormat.Float32, DimensionOf(components, slot), stream: 1)); } var array = new NativeArray(result.AsArray(), Allocator.Temp); result.Dispose(); @@ -398,9 +424,9 @@ public static NativeArray Descriptors(NativeArray layers; using var _layers = layers = new NativeArray(1, Allocator.Temp); - NativeArray values; - using var _values = values = new NativeArray(4, Allocator.Temp); + NativeArray values; + using var _values = values = new NativeArray(4 * 4, Allocator.Temp); layers[0] = new ChiselPaintLayer { surfaceIndex = surfaceIndex, attribute = ChiselPaintAttribute.Color, components = ChiselPaintComponents.XYZW, cellU = new float3(1, 0, 0), cellV = new float3(0, 0, 1), resolution = new int2(2, 2) }; - for (int i = 0; i < 4; i++) + for (int i = 0; i < values.Length; i++) values[i] = value; return ChiselPaintStore.SetPaint(tree, entityID, layers, values); } @@ -41,7 +41,7 @@ static float ValueOf(CSGTree tree, ulong entityID, int surfaceIndex) { var lookup = ChiselPaintLookup.Of(ChiselTreeLookup.Value[tree]); Assert.That(lookup.TryGetRange(entityID, surfaceIndex, out var range), Is.True, $"surface {surfaceIndex} of {entityID}"); - return lookup.values[lookup.layers[range.start].valueStart].x; + return lookup.values[lookup.layers[range.start].valueStart]; } [Test] @@ -69,7 +69,7 @@ public void ReplacedValues_AreCompactedAway() for (int i = 1; i <= 10; i++) Paint(tree, 1, 0, i); var data = ChiselTreeLookup.Value[tree]; - Assert.That(data.paintValues.Length, Is.LessThanOrEqualTo(8)); + Assert.That(data.paintValues.Length, Is.LessThanOrEqualTo(2 * 4 * 4)); Assert.That(ValueOf(tree, 1, 0), Is.EqualTo(10)); } } diff --git a/Core/Tests/PaintSubdivisionTests.cs b/Core/Tests/PaintSubdivisionTests.cs index 1d051e5a..f1ae378b 100644 --- a/Core/Tests/PaintSubdivisionTests.cs +++ b/Core/Tests/PaintSubdivisionTests.cs @@ -31,7 +31,7 @@ static void Subdivide(float3 offset, bool otherDiagonal, int cells, out int vert NativeArray layers; using var _layers = layers = new NativeArray(1, Allocator.Temp); var size = 2f / cells; - using var values = new NativeArray((cells + 1) * (cells + 1), Allocator.Temp); + using var values = new NativeArray(1, Allocator.Temp); layers[0] = new ChiselPaintLayer { origin = offset, cellU = new float3(size, 0, 0), cellV = new float3(0, 0, size), resolution = new int2(cells + 1, cells + 1) }; var lookup = new ChiselPaintLookup { layers = layers, values = values }; PaintSubdivision.Subdivide(indices, ref mapper, new float3(0, 1, 0), lookup, new ChiselPaintRange { start = 0, count = 1 }); From c8cee145e25416d5b6a97d719175cec5e5e480b1 Mon Sep 17 00:00:00 2001 From: Sander van Rossen Date: Mon, 26 Oct 2026 10:47:19 +0100 Subject: [PATCH 27/27] Paint: a broken grid starts again; only painted generators held A surface whose grid isn't whole (its values didn't load) gets a new grid when it is painted, rather than the brush stopping at it. The paint manager only keeps the generators with paint, or whose paint a tree has, so a destroyed model's meshes aren't kept loaded through the others. --- .../Components/Paint/ChiselPaintBrush.cs | 3 ++- Components/Managers/ChiselPaintManager.cs | 3 ++- Components/Tests/ChiselPaintTests.cs | 18 ++++++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/Components/Components/Paint/ChiselPaintBrush.cs b/Components/Components/Paint/ChiselPaintBrush.cs index 3f1e45af..d062629e 100644 --- a/Components/Components/Paint/ChiselPaintBrush.cs +++ b/Components/Components/Paint/ChiselPaintBrush.cs @@ -173,7 +173,8 @@ public static ChiselSurfacePaint PaintFor(ChiselGeneratorComponent generator, in { Respace(generator); var paint = generator.GetSurfacePaint(surfaceIndex, mode.attribute); - if (paint == null) + // a grid that isn't whole (its values didn't load) starts again + if (paint == null || !paint.IsValid) { paint = CreateGrid(generator, surfaceIndex, mode.attribute, mode.components, mode.defaultValue, SpacingOf(generator, surfaceIndex)); if (paint != null) diff --git a/Components/Managers/ChiselPaintManager.cs b/Components/Managers/ChiselPaintManager.cs index 4440cb91..ac1cae02 100644 --- a/Components/Managers/ChiselPaintManager.cs +++ b/Components/Managers/ChiselPaintManager.cs @@ -32,7 +32,8 @@ struct Given /// A generator's paint, or what it is drawn with, may have changed public static void SetDirty(ChiselGeneratorComponent generator) { - if (generator) + // only one with paint, or whose paint a tree has (it may have lost it): no other generator is held on to + if (generator && (generator.SurfacePaint.Count > 0 || s_Given.ContainsKey(generator))) s_Changed.Add(generator); } diff --git a/Components/Tests/ChiselPaintTests.cs b/Components/Tests/ChiselPaintTests.cs index 6d8d7539..e58f643b 100644 --- a/Components/Tests/ChiselPaintTests.cs +++ b/Components/Tests/ChiselPaintTests.cs @@ -440,6 +440,24 @@ public void ADisabledGenerator_TakesItsPaintWithIt() Assert.That(InTree(), Is.True, "enabled again"); } + // A grid whose values didn't load is made again when the surface is painted + [Test] + public void AGridWithoutItsValues_IsMadeAgainWhenPainted() + { + var model = Model(); + var box = Box(model, Vector3.zero); + var top = SurfaceFacing(box, new float3(0, 1, 0)); + var mode = ChiselPaintMode.Color("Color"); + var broken = ChiselPaintBrush.PaintFor(box, top, mode); + broken.values = System.Array.Empty(); + Assert.That(broken.IsValid, Is.False); + + var paint = ChiselPaintBrush.PaintFor(box, top, mode); + Assert.That(paint, Is.Not.SameAs(broken)); + Assert.That(paint.IsValid, Is.True); + Assert.That(box.GetSurfacePaint(top, ChiselPaintAttribute.Color), Is.SameAs(paint)); + } + // The grid moves with its generator [Test] public void Paint_MovesWithItsGenerator()