Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 12 additions & 1 deletion Components/Components/Decals/ChiselDecalComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,18 @@ void OnDrawGizmosSelected()
void DrawVolume()
{
Gizmos.color = kGizmoColor;
Gizmos.matrix = transform.localToWorldMatrix;
if (settings.projection == ChiselDecalProjection.Omni)
{
// how far it reaches; the image lies on each surface it reaches
Gizmos.matrix = transform.localToWorldMatrix;
var reach = math.abs(settings.size.z);
if (settings.reach == ChiselDecalReach.Cube)
Gizmos.DrawWireCube(settings.center, new Vector3(reach, reach, reach));
else
Gizmos.DrawWireSphere(settings.center, reach * 0.5f);
return;
}
Gizmos.matrix = transform.localToWorldMatrix * (Matrix4x4)ChiselDecalSettings.SkewMatrix(settings.skew);
GetCorners(s_Corners);
for (int i = 0; i < 4; i++)
{
Expand Down
63 changes: 63 additions & 0 deletions Components/Components/Decals/ChiselDecalFades.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using System.Collections.Generic;
using Chisel.Core;
using Unity.Mathematics;
using UnityEngine;

namespace Chisel.Components
{
/// <summary>
/// Transparent decals that fade with distance (<see cref="ChiselDecalSettings.fadeDistance"/>). Each one is a mesh
/// of its own (<see cref="ChiselDecalMeshes.FadeChunkOf"/>), and its renderer is cross-faded by a LODGroup: whole
/// from the camera up to the start distance, gone past the end distance. A LODGroup goes by the size the renderer
/// takes up on screen, so the distances hold at <see cref="kFieldOfView"/> and scale with the camera's field of view.
/// The material's shader has to support LOD cross-fading, as URP's Lit does, with LOD Cross Fade on in the pipeline asset.
/// </summary>
public static class ChiselDecalFades
{
public const float kFieldOfView = 60.0f;

// the fade distances of the fading decals, by the chunk of their mesh
static readonly Dictionary<uint, float2> s_Distances = new();

internal static void Register(in ChiselDecalInstance decal)
{
if (decal.settings.Fades)
s_Distances[ChiselDecalMeshes.FadeChunkOf(decal.entityID)] = decal.settings.fadeDistance;
}

public static bool TryGetDistances(uint chunk, out float2 distances) => s_Distances.TryGetValue(chunk, out distances);

/// <summary>The height on screen, relative to the screen's, at which something this big this far away is</summary>
public static float ScreenHeightAt(float size, float distance)
{
return size / (2.0f * math.max(distance, 1e-4f) * math.tan(math.radians(kFieldOfView) * 0.5f));
}

internal static void Apply(ChiselRenderObjects renderable)
{
if (renderable == null || renderable.surfaceParameter != ChiselDecalMeshes.kTransparentDecals ||
!s_Distances.TryGetValue(renderable.chunk, out var distances) || !renderable.HasGeometry || !renderable.meshRenderer)
return;

var bounds = renderable.sharedMesh.bounds;
var scale = renderable.container.transform.lossyScale;
var size = math.max(math.cmax(bounds.size), 1e-4f);
var world = size * math.cmax(math.abs((float3)scale));
var end = math.max(distances.y, 1e-3f);
var start = math.clamp(distances.x, 0, end);
var gone = math.min(ScreenHeightAt(world, end), 0.999f);
var whole = math.min(ScreenHeightAt(world, start), 1.0f);
// the cross-fade runs over this part of the way from the LOD's height up to the whole screen
var width = gone < 1 ? math.saturate((whole - gone) / (1 - gone)) : 0;

var group = renderable.container.GetComponent<LODGroup>();
if (group == null)
group = renderable.container.AddComponent<LODGroup>();
group.fadeMode = LODFadeMode.CrossFade;
group.animateCrossFading = false;
group.localReferencePoint = bounds.center;
group.size = size;
group.SetLODs(new[] { new LOD(gone, new Renderer[] { renderable.meshRenderer }) { fadeTransitionWidth = width } });
}
}
}
2 changes: 2 additions & 0 deletions Components/Components/Decals/ChiselDecalFades.cs.meta

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

6 changes: 6 additions & 0 deletions Components/Components/Generated/ChiselGeneratedObjects.cs
Original file line number Diff line number Diff line change
Expand Up @@ -817,6 +817,12 @@ public int FinishMeshUpdates(ChiselModelComponent model,
changedColliders[i].sharedMesh.RecalculateBounds();
Profiler.EndSample();

// a fading decal's renderer is cross-faded by a LODGroup, sized to its mesh
Profiler.BeginSample("DecalFades");
foreach (var renderable in renderables)
ChiselDecalFades.Apply(renderable);
Profiler.EndSample();

Profiler.BeginSample("Schedule Collider bake");
if (changedColliders.Length > 0)
ChiselColliderObjects.ScheduleColliderBake(model, changedColliders);
Expand Down
6 changes: 5 additions & 1 deletion Components/Managers/ChiselDecalManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,10 @@ public static void Update()
try
{
for (int i = 0; i < state.decals.Count; i++)
{
decals[i] = state.decals[i];
ChiselDecalFades.Register(state.decals[i]);
}
for (int i = 0; i < state.targets.Count; i++)
targets[i] = state.targets[i];
state.tree.SetDecals(decals, targets);
Expand Down Expand Up @@ -458,7 +461,8 @@ static void Describe(ChiselDecalComponent decal, DecalState decalState, Matrix4x
renderMaterial = decalState.renderMaterial,
destinationFlags = decalState.destinationFlags
};
var perspective = settings.projection == ChiselDecalProjection.Perspective;
// a perspective or omni-directional decal's image isn't a box to cut into tiles: it is drawn once
var perspective = settings.projection != ChiselDecalProjection.Orthographic;
if (!sheet || perspective)
{
// uv = offset + scale * (rect.position + rect.size * repeat * p)
Expand Down
114 changes: 114 additions & 0 deletions Components/Tests/ChiselDecalComponentTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,120 @@ public void Decal_IsDrawnOnTheFloors()
Assert.That(other, Is.EqualTo(1.0).Within(kTolerance), "the opaque decal replaces a square metre of the two floors");
}

// Skewed 45 degrees, the square metre leans into a parallelogram twice as wide, of the same area
[Test]
public void ASkewedDecal_IsAParallelogramOnTheFloors()
{
var model = Floors(out _, out _, out var decal);
var settings = decal.Settings;
settings.skew = new Unity.Mathematics.float2(45, 0);
decal.Settings = settings;
Update();
Assert.That(UpAreas(model).decal, Is.EqualTo(1.0).Within(kTolerance));
float minX = float.PositiveInfinity, maxX = float.NegativeInfinity;
foreach (var r in model.generated.renderables)
{
if (r == null || !r.Valid || r.sharedMesh == null || r.renderMaterials == null)
continue;
var vertices = r.sharedMesh.vertices;
for (int s = 0; s < r.sharedMesh.subMeshCount && s < r.renderMaterials.Length; s++)
{
if (r.renderMaterials[s] != decalMaterial)
continue;
foreach (var index in r.sharedMesh.GetTriangles(s))
{
minX = Mathf.Min(minX, vertices[index].x);
maxX = Mathf.Max(maxX, vertices[index].x);
}
}
}
Assert.That(minX, Is.EqualTo(-1).Within(kTolerance));
Assert.That(maxX, Is.EqualTo(1).Within(kTolerance));
}

// The area drawn with the decal's material on triangles facing this way
double DecalAreaFacing(ChiselModelComponent model, Vector3 direction)
{
double area = 0;
foreach (var r in model.generated.renderables)
{
if (r == null || !r.Valid || r.sharedMesh == null || r.renderMaterials == null)
continue;
var vertices = r.sharedMesh.vertices;
for (int s = 0; s < r.sharedMesh.subMeshCount && s < r.renderMaterials.Length; s++)
{
if (r.renderMaterials[s] != decalMaterial)
continue;
var triangles = r.sharedMesh.GetTriangles(s);
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]]);
if (Vector3.Dot(normal.normalized, direction) > 0.99f)
area += normal.magnitude * 0.5;
}
}
}
return area;
}

// An omni-directional decal reaching half a metre: just above the floors, its whole image is on them; raised out of
// their reach, it is on the wall beside it instead
[Test]
public void AnOmniDecal_IsItsWholeImageOnEverySurfaceItReaches()
{
var model = Floors(out _, out _, out var decal);
var settings = decal.Settings;
settings.projection = ChiselDecalProjection.Omni;
decal.Settings = settings;
decal.Size = new Vector3(0.5f, 0.5f, 1);
decal.transform.localPosition = new Vector3(0, 0.1f, 0);
Update();
Assert.That(DecalAreaFacing(model, Vector3.up), Is.EqualTo(0.25).Within(kTolerance), "on both floors, round the point under it");

Box("Wall", model.transform, new Vector3(0.3f, 0, -0.5f));
decal.transform.localPosition = new Vector3(0, 0.6f, 0);
Update();
Assert.That(DecalAreaFacing(model, Vector3.up), Is.EqualTo(0).Within(kTolerance), "the floors are out of reach");
Assert.That(DecalAreaFacing(model, Vector3.left), Is.EqualTo(0.25).Within(kTolerance), "on the wall, 0.3 away");
}

// A transparent decal that fades is a renderer of its own, cross-faded by a LODGroup between its distances; one that
// doesn't fade stays in the decals' mesh, and an opaque decal never fades
[Test]
public void AFadingDecal_IsCrossFadedByALODGroup()
{
// both on B, in one of the decals' 32 m cells (x = 0 is the edge of one)
var model = Floors(out _, out _, out var decal);
decal.Transparent = true;
decal.transform.localPosition = new Vector3(0.5f, 0, 0);
var settings = decal.Settings;
settings.fadeDistance = new Unity.Mathematics.float2(5, 10);
decal.Settings = settings;
var still = Object.Instantiate(decal.gameObject, decal.transform.parent).GetComponent<ChiselDecalComponent>();
still.transform.localPosition = new Vector3(0.6f, 0, 0);
var stillSettings = still.Settings;
stillSettings.fadeDistance = Unity.Mathematics.float2.zero;
still.Settings = stillSettings;
Update();

var decalMeshes = System.Array.FindAll(model.generated.renderables, r => r != null && r.surfaceParameter == ChiselDecalMeshes.kTransparentDecals);
Assert.That(decalMeshes.Length, Is.EqualTo(2), "the fading decal's own, and the decals' mesh");
var fading = System.Array.Find(decalMeshes, r => r.container.GetComponent<LODGroup>() != null);
Assert.That(fading, Is.Not.Null);
var lods = fading.container.GetComponent<LODGroup>().GetLODs();
Assert.That(lods.Length, Is.EqualTo(1));
Assert.That(lods[0].renderers, Is.EqualTo(new Renderer[] { fading.meshRenderer }));
var size = fading.sharedMesh.bounds.size;
var expected = ChiselDecalFades.ScreenHeightAt(Mathf.Max(size.x, Mathf.Max(size.y, size.z)), 10);
Assert.That(lods[0].screenRelativeTransitionHeight, Is.EqualTo(expected).Within(1e-5f), "gone at 10");
Assert.That(lods[0].fadeTransitionWidth, Is.GreaterThan(0).And.LessThan(1), "fading from 5");

decal.Transparent = false;
Update();
Assert.That(System.Array.FindAll(model.generated.renderables, r => r != null && r.container.GetComponent<LODGroup>() != null), Is.Empty,
"an opaque decal doesn't fade");
}

// Nothing tells the manager about a moved transform: it has to see it
[Test]
public void MovedDecal_MovesOnTheFloors()
Expand Down
59 changes: 56 additions & 3 deletions Core/1.Input/Decals/ChiselDecal.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,22 @@ public enum ChiselDecalProjection : byte

/// <summary>From a point behind the box, like a projector. The image has the decal's size at the center
/// of the box, and grows with depth.</summary>
Perspective = 1
Perspective = 1,

/// <summary>From the center out in every direction, like Source's infodecal: every surface the decal reaches
/// takes the whole image, laid flat on it round the point nearest the center, turned with the surface's
/// texture.</summary>
Omni = 2
}

/// <summary>Which surfaces an omni-directional decal reaches: those passing within the decal's reach of its center.</summary>
public enum ChiselDecalReach : byte
{
/// <summary>Within a sphere: a surface no further than the reach from the center.</summary>
Sphere = 0,

/// <summary>Within a cube turned with the decal: a surface passing through it.</summary>
Cube = 1
}

/// <summary>
Expand All @@ -26,16 +41,21 @@ public struct ChiselDecalSettings : IEquatable<ChiselDecalSettings>
public const float kMinFieldOfView = 0.1f;
public const float kMaxFieldOfView = 170.0f;
public const float kDefaultSurfaceOffset = 0.001f;
public const float kMaxSkew = 80.0f;

/// <summary>The center of the box.</summary>
public float3 center;

/// <summary>The size of the box. X and Y are the image's width and height (for a perspective decal: at the
/// center of the box), Z is how deep the decal reaches.</summary>
/// center of the box), Z is how deep the decal reaches (for an omni-directional decal: the diameter of its
/// sphere, or its cube's edge).</summary>
public float3 size;

public ChiselDecalProjection projection;

/// <summary>The shape an omni-directional decal reaches in.</summary>
public ChiselDecalReach reach;

/// <summary>The vertical field of view of a perspective decal, in degrees.</summary>
public float fieldOfView;

Expand Down Expand Up @@ -65,6 +85,27 @@ public struct ChiselDecalSettings : IEquatable<ChiselDecalSettings>
/// square ends. A trim decal's pieces are cut this way where they meet round a corner.</summary>
public float2 endSlant;

/// <summary>Where a transparent decal fades out, by its distance from the camera: x where it starts to fade, y where
/// it is gone (at a 60 degree field of view). Each fading decal is a renderer of its own, cross-faded by a
/// LODGroup. A y of 0 never fades; opaque decals never fade, since they replace the surface below.</summary>
public float2 fadeDistance;

public readonly bool Fades => transparent && fadeDistance.y > 0;

/// <summary>How far the decal is sheared, in degrees, so its image frame is a parallelogram: x leans the image's
/// sides (x moves with y), y leans its top and bottom (y moves with x). The shear is about the decal's origin.</summary>
public float2 skew;

/// <summary>The shear <see cref="skew"/> describes, from the decal's unsheared space to its space</summary>
public static float4x4 SkewMatrix(float2 skew)
{
var lean = math.tan(math.radians(math.clamp(skew, -kMaxSkew, kMaxSkew)));
return new float4x4(1, lean.x, 0, 0,
lean.y, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1);
}

/// <summary>Where in the image a point p of the decal (0 to 1 across it) looks, for an image turned this many quarter
/// turns counter-clockwise</summary>
public static float2 TurnImage(float2 p, int quarterTurns)
Expand Down Expand Up @@ -106,14 +147,17 @@ public readonly bool Equals(ChiselDecalSettings other)
return math.all(center == other.center) &&
math.all(size == other.size) &&
projection == other.projection &&
reach == other.reach &&
math.all(fadeDistance == other.fadeDistance) &&
fieldOfView == other.fieldOfView &&
maxAngle == other.maxAngle &&
transparent == other.transparent &&
surfaceOffset == other.surfaceOffset &&
math.all(uvScale == other.uvScale) &&
math.all(uvOffset == other.uvOffset) &&
(imageTurns & 3) == (other.imageTurns & 3) &&
math.all(endSlant == other.endSlant);
math.all(endSlant == other.endSlant) &&
math.all(skew == other.skew);
}

public override readonly bool Equals(object obj) => obj is ChiselDecalSettings other && Equals(other);
Expand All @@ -126,6 +170,8 @@ public override readonly int GetHashCode()
hash = math.hash(new uint2(hash, math.hash(new float3(fieldOfView, maxAngle, surfaceOffset))));
hash = math.hash(new uint3(hash, math.hash(uvScale), math.hash(uvOffset)));
hash = math.hash(new uint3(hash, math.hash(endSlant), (uint)(imageTurns & 3)));
hash = math.hash(new uint3(hash, math.hash(skew), (uint)reach));
hash = math.hash(new uint2(hash, math.hash(fadeDistance)));
return (int)hash;
}
}
Expand Down Expand Up @@ -213,6 +259,13 @@ public static class ChiselDecalMeshes
/// <summary>The surface parameter of a chunk's transparent decals' mesh: all their materials, one sub-mesh per
/// material, in the order the decals stack, so that they draw in that order</summary>
public const ulong kTransparentDecals = ulong.MaxValue;

/// <summary>The chunk of a fading decal's mesh (see <see cref="ChiselDecalSettings.fadeDistance"/>): its own, so
/// that it is a renderer of its own</summary>
public static uint FadeChunkOf(ulong decalEntityID)
{
return math.hash(new uint3((uint)decalEntityID, (uint)(decalEntityID >> 32), 0x46414445u));
}
}

public static class CSGTreeDecalExtensions
Expand Down
Loading
Loading