Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
b11378b
Vertex painting: paint profiles, surface paint and the paint stream
LogicalError Oct 25, 2026
a3b3108
Vertex painting: painted surfaces subdivided at their grid
LogicalError Oct 25, 2026
fa066de
Vertex painting: the paint tool, fill, sample, channels, weight groups
LogicalError Oct 25, 2026
51dfda8
Paint tool: pick only on mouse events, panel without a GUI window
LogicalError Oct 26, 2026
de70835
Paint tool: its settings in a scene view overlay
LogicalError Oct 26, 2026
abd5917
Paint profiles: an inspector for their modes
LogicalError Oct 26, 2026
852eed3
Paint: the outline cut at the grid too, and a not-paintable cursor
LogicalError Oct 26, 2026
58ecbdd
Paint: drawn only under a shader that takes it
LogicalError Oct 26, 2026
e67ae74
Paint: spacing per surface, modes as toggles
LogicalError Oct 26, 2026
6f01c4f
Paint: one settings asset, a tool among the Chisel tools
LogicalError Oct 26, 2026
8f42409
Paint overlay: swatches, icon operations, modes as a list
LogicalError Oct 26, 2026
24263d9
Paint overlay: the add-swatch cell is the current color, inset
LogicalError Oct 26, 2026
cadf173
Paint overlay: smaller swatches without gaps
LogicalError Oct 26, 2026
eb4e402
Paint overlay: swatches start where the labels' text does
LogicalError Oct 26, 2026
3674bfb
Check that compiled code lays out brush blobs as managed code does
LogicalError Oct 26, 2026
3e7695e
Paint: fill as an operation; swatches without duplicates
LogicalError Oct 26, 2026
cc65b34
Remove duplicate paint tool icons
LogicalError Oct 26, 2026
c2ed793
Paint spacing is drawn once, with the surface details
LogicalError Oct 26, 2026
e71d5b8
Paint: the brush's entity once per brush buffer, shorter comments
LogicalError Oct 26, 2026
efb1f18
Paint: a paintable shader's attributes are always in its meshes
LogicalError Oct 26, 2026
94b9bd9
Paint tool: no channels section
LogicalError Oct 26, 2026
67cc2c0
Paint: kept by the tree between updates, replaced per entity
LogicalError Oct 26, 2026
fcee7b1
Paint subdivision: edges looked up, points located by walking
LogicalError Oct 26, 2026
8710701
Paint: a paint change builds the painted brush only
LogicalError Oct 26, 2026
dee2800
Paint manager: only the generators whose paint changed
LogicalError Oct 26, 2026
71fef56
Paint: values and mesh streams packed
LogicalError Oct 26, 2026
c8cee14
Paint: a broken grid starts again; only painted generators held
LogicalError Oct 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions Components/Components/Base/ChiselGeneratorComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ChiselSurfacePaint> surfacePaint = new();

public System.Collections.Generic.IReadOnlyList<ChiselSurfacePaint> SurfacePaint => surfacePaint;

/// <summary>The paint of a surface in an attribute, or null</summary>
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;
}

/// <summary>Gives a surface this paint in its attribute, in place of what it had</summary>
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(this);
}

public void RemoveSurfacePaint(int surfaceIndex, ChiselPaintAttribute attribute)
{
if (surfacePaint.RemoveAll(other => other == null || (other.surfaceIndex == surfaceIndex && other.attribute == attribute)) > 0)
ChiselPaintManager.SetDirty(this);
}

/// <summary>Called after the values of a surface's paint were changed in place</summary>
public void PaintChanged() => ChiselPaintManager.SetDirty(this);

public override CSGTreeNode TopTreeNode
{
Expand Down Expand Up @@ -416,9 +450,16 @@ protected override void OnValidateState()

if (TopTreeNode.Dirty)
ChiselNodeHierarchyManager.NotifyContentsModified(this);
ChiselPaintManager.SetDirty(this);
base.OnValidateState();
}

protected override void OnEnable()
{
base.OnEnable();
ChiselPaintManager.SetDirty(this);
}

public override void UpdateTransformation()
{
// TODO: recalculate transformation based on hierarchy up to (but not including) model
Expand All @@ -444,6 +485,7 @@ protected override void OnDisable()
{
base.OnDisable();
DestroyChildTreeNodes();
ChiselPaintManager.SetDirty(this);
}

[HideInInspector] int prevMaterialHash;
Expand Down
8 changes: 8 additions & 0 deletions Components/Components/Paint.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

321 changes: 321 additions & 0 deletions Components/Components/Paint/ChiselPaintBrush.cs

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Components/Components/Paint/ChiselPaintBrush.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

89 changes: 89 additions & 0 deletions Components/Components/Paint/ChiselPaintProfile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using System;
using System.Collections.Generic;
using Chisel.Core;
using UnityEngine;

namespace Chisel.Components
{
/// <summary>What a paint mode writes: a color the brush paints with, or a value per component</summary>
public enum ChiselPaintKind : byte
{
/// <summary>A color, written to the mode's components (rgba, or rgb)</summary>
Color = 0,
/// <summary>A number (a height, wetness, a layer's weight): painted up or down with strength and falloff</summary>
Value = 1
}

/// <summary>One thing a shader takes from paint: what it is called, and where it goes in the vertices</summary>
[Serializable]
public struct ChiselPaintMode
{
public string name;
public ChiselPaintKind kind;
public ChiselPaintAttribute attribute;
public ChiselPaintComponents components;
/// <summary>What an unpainted surface has, in all four components</summary>
public Vector4 defaultValue;
/// <summary>A value mode's lowest and highest value</summary>
public Vector2 range;
/// <summary>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)</summary>
public int group;

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) };
}
}

/// <summary>
/// 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.
/// </summary>
[Serializable]
public sealed class ChiselPaintProfile
{
public Shader shader;
public List<ChiselPaintMode> modes = new();

/// <summary>The components of the other modes in a mode's group (in its attribute); None when it has no group</summary>
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;
}

/// <summary>The first two modes that write one component of one attribute; false when there are none</summary>
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;
}
}
}
2 changes: 2 additions & 0 deletions Components/Components/Paint/ChiselPaintProfile.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

141 changes: 141 additions & 0 deletions Components/Components/Paint/ChiselPaintProfiles.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
using System.Collections.Generic;
using Chisel.Core;
using UnityEngine;

namespace Chisel.Components
{
/// <summary>The project's paint profiles (<see cref="ChiselPaintSettings"/>), by the shader they are for</summary>
public static class ChiselPaintProfiles
{
static Dictionary<Shader, ChiselPaintProfile> s_ByShader;
// profiles registered in code, which come first
static readonly List<ChiselPaintProfile> s_Registered = new();

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();
}

/// <summary>Called when profiles may have been added, removed or changed</summary>
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<ChiselPaintMaterialSlot> 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);
slot.components |= mode.components;
slots[index] = slot;
}
}

[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
static void Hook()
{
ChiselPaintMaterials.Resolver = Resolve;
ChiselPaintMaterials.Changed();
}

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;
}
Load();
return s_ByShader.TryGetValue(shader, out var found) ? found : null;
}

/// <summary>Every profile: those registered in code, then the project's (unless one in code is for its shader)</summary>
public static IEnumerable<ChiselPaintProfile> 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<Shader, ChiselPaintProfile>();
var settings = ChiselPaintSettings.Project;
if (settings)
{
foreach (var profile in settings.profiles)
{
if (profile != null && profile.shader != null)
s_ByShader.TryAdd(profile.shader, profile);
}
}
}
}

/// <summary>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.</summary>
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()
{
Hook();
UnityEditor.EditorApplication.projectChanged += () =>
{
SetDirty();
ChiselPaintManager.SetDirty();
};
}
#endif
}
}
2 changes: 2 additions & 0 deletions Components/Components/Paint/ChiselPaintProfiles.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

76 changes: 76 additions & 0 deletions Components/Components/Paint/ChiselPaintSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using System.Collections.Generic;
using UnityEngine;

namespace Chisel.Components
{
/// <summary>
/// The project's paint profiles: one per shader whose surfaces can be painted, in one asset, edited in
/// Project Settings > Chisel > Paint
/// </summary>
public sealed class ChiselPaintSettings : ScriptableObject
{
public const string kAssetPath = "Assets/Chisel/ChiselPaintSettings.asset";

public List<ChiselPaintProfile> profiles = new();

static ChiselPaintSettings s_Project;

/// <summary>The project's paint settings, null when it has none</summary>
public static ChiselPaintSettings Project
{
get
{
#if UNITY_EDITOR
if (!s_Project)
{
s_Project = UnityEditor.AssetDatabase.LoadAssetAtPath<ChiselPaintSettings>(kAssetPath);
if (!s_Project)
{
foreach (var guid in UnityEditor.AssetDatabase.FindAssets("t:" + nameof(ChiselPaintSettings)))
{
s_Project = UnityEditor.AssetDatabase.LoadAssetAtPath<ChiselPaintSettings>(UnityEditor.AssetDatabase.GUIDToAssetPath(guid));
if (s_Project)
break;
}
}
}
#endif
return s_Project;
}
set
{
s_Project = value;
Changed();
}
}

/// <summary>The first profile for a shader, or null</summary>
public ChiselPaintProfile Find(Shader shader)
{
if (!shader)
return null;
foreach (var profile in profiles)
{
if (profile != null && profile.shader == shader)
return profile;
}
return null;
}

/// <summary>Called after the profiles changed</summary>
public static void Changed()
{
ChiselPaintProfiles.SetDirty();
ChiselPaintManager.SetDirty();
}

void OnEnable()
{
if (!s_Project)
s_Project = this;
Changed();
}

void OnValidate() => Changed();
}
}
Loading
Loading