Assigned texture drives the picture material BaseMap via a per-frame material instance (asset untouched, frames sharing a material stay independent) and takes priority for aspect resolution. New default picture material Frame Picture.mat wired in the script defaults. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
563 lines
29 KiB
C#
563 lines
29 KiB
C#
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
namespace Chameleon.Components
|
|
{
|
|
// Procedural picture frame, generated live in the editor (and at runtime) from the inspector
|
|
// parameters. Sizing is inside-out: the picture keeps the texture's aspect ratio EXACTLY and the
|
|
// outer frame grows around it by padding + thickness (Size = longer outer side) - insetting a
|
|
// uniform border from an aspect-scaled outer rect would distort the picture. All shape parameters
|
|
// are relative fractions (thickness/padding of the picture's longer side, radius of the maximum
|
|
// possible rounding, depths of the frame depth), so a frame keeps its proportions when resized via
|
|
// Size - resize via Size, NOT via transform scale. Four submeshes with their own
|
|
// material slots: 0 picture, 1 inner cavity (recess walls + padding ring), 2 frame (front ring, outer
|
|
// sides, edge rounding), 3 back plate. Front faces -Z like Unity's built-in Quad. An assigned
|
|
// PictureTexture becomes the picture material's base map via a per-frame DontSave material
|
|
// instance (the shared material asset is never modified) and drives the aspect ratio.
|
|
//
|
|
// The generated mesh is a per-instance HideFlags.DontSave object (never saved into the scene file);
|
|
// it is rebuilt on scene load / domain reload via OnEnable. Duplicated frames get their own mesh
|
|
// (identified by instance id), so two frames never write into one shared mesh.
|
|
//
|
|
// GenerateColliders maintains matching BoxColliders (corner radius ignored): 4 border bars, a slab
|
|
// behind the inner floor, plus a plinth box when the picture is raised. Only colliders tracked in
|
|
// _colliders are touched - manually added ones are left alone. The boxes live on a dedicated
|
|
// "Colliders" child: adding/removing a component on the inspected GameObject itself would rebuild
|
|
// its inspector mid-drag and release the slider being held (Unity 6's UI Toolkit inspector never
|
|
// sets GUIUtility.hotControl, so the mouse state cannot be polled) - on a child they can appear
|
|
// and disappear live without ever disturbing the editing.
|
|
[ExecuteAlways]
|
|
[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
|
|
public sealed class PictureFrameBuilder : MonoBehaviour
|
|
{
|
|
const int SubPicture = 0;
|
|
const int SubInner = 1;
|
|
const int SubFrame = 2;
|
|
const int SubBack = 3;
|
|
|
|
[Header("Size")]
|
|
[Tooltip("Length of the frame's longer side in meters; the shorter side follows the picture's aspect ratio.")]
|
|
[SerializeField, Min(0.01f)] float _size = 1f;
|
|
// Auto-resolved in the editor from the picture material's base map texture (longer side = 1,
|
|
// 1:1 when no texture is found) and serialized, because the source image file/importer is not
|
|
// available in a build. Manual edits are overwritten on the next rebuild.
|
|
[Tooltip("Width : height factors derived from the picture texture (auto-set in the editor, longer side = 1).")]
|
|
[SerializeField] Vector2 _aspect = Vector2.one;
|
|
|
|
[Header("Frame")]
|
|
[Tooltip("Border width of the frame as a fraction of the picture's longer side.")]
|
|
[SerializeField, Range(0f, 1f)] float _frameThickness = 0.05f;
|
|
[Tooltip("Depth of the frame, front face to back face.")]
|
|
[SerializeField, Range(0.002f, 0.500f)] float _frameDepth = 0.05f;
|
|
[Tooltip("Rounding of the outer frame edges relative to the maximum possible radius (thickness / half depth); 0 = sharp box, 1 = fully rounded.")]
|
|
[SerializeField, Range(0f, 1f)] float _cornerRadius = 0.2f;
|
|
[Tooltip("Arc segments of the edge rounding.")]
|
|
[SerializeField, Range(1, 8)] int _bevelSegments = 4;
|
|
[Tooltip("Inner floor relative to the frame depth: -1 = fully sunken (on the back plate), 0 = flush with the front face, 1 = a full frame depth in front.")]
|
|
[SerializeField, Range(-1f, 1f)] float _frameInnerDepth = -0.1f;
|
|
|
|
[Header("Picture")]
|
|
[Tooltip("Gap between the frame's inner edge and the picture as a fraction of the picture's longer side.")]
|
|
[SerializeField, Range(0f, 1f)] float _padding = 0.01f;
|
|
[Tooltip("Picture plane: -1 = fully inside (on the back plate), 0 = flush with the inner floor, 1 = a full frame depth in front of the frame.")]
|
|
[SerializeField, Range(-1f, 1f)] float _pictureDepth = 0.1f;
|
|
[Tooltip("Photo for this frame: applied as the picture material's base map via a per-frame material instance (the material asset itself is never modified) and drives the aspect ratio. Empty = the material's own base map is shown.")]
|
|
[SerializeField] Texture _pictureTexture;
|
|
|
|
[Header("Materials")]
|
|
[SerializeField] Material _pictureMaterial;
|
|
[SerializeField] Material _innerMaterial;
|
|
[SerializeField] Material _frameMaterial;
|
|
[SerializeField] Material _backMaterial;
|
|
|
|
[Header("Colliders")]
|
|
[Tooltip("Automatically maintain BoxColliders matching the frame (corner radius is ignored).")]
|
|
[SerializeField] bool _generateColliders = true;
|
|
// Ownership list so rebuilds never touch colliders the user added by hand.
|
|
[SerializeField, HideInInspector] List<BoxCollider> _colliders = new List<BoxCollider>();
|
|
[SerializeField, HideInInspector] Transform _colliderRoot;
|
|
|
|
readonly List<Vector3> _verts = new List<Vector3>();
|
|
readonly List<Vector3> _normals = new List<Vector3>();
|
|
readonly List<Vector2> _uvs = new List<Vector2>();
|
|
readonly List<int>[] _tris =
|
|
{
|
|
new List<int>(), new List<int>(), new List<int>(), new List<int>()
|
|
};
|
|
|
|
Mesh _mesh;
|
|
Material _pictureMaterialInstance;
|
|
bool _rebuildQueued;
|
|
bool _rebuilding;
|
|
|
|
void OnEnable() => Rebuild();
|
|
|
|
// Mesh/renderer writes are not allowed during OnValidate, so it only flags; the actual
|
|
// rebuild runs in Update (ExecuteAlways covers edit mode).
|
|
void OnValidate()
|
|
{
|
|
_rebuildQueued = true;
|
|
#if UNITY_EDITOR
|
|
// In edit mode Update only ticks when the editor decides to pump the player loop;
|
|
// request one so the last inspector change never stays un-applied.
|
|
UnityEditor.EditorApplication.QueuePlayerLoopUpdate();
|
|
#endif
|
|
}
|
|
|
|
void Update()
|
|
{
|
|
if (!_rebuildQueued || _rebuilding) return;
|
|
_rebuildQueued = false;
|
|
_rebuilding = true;
|
|
try { Rebuild(); }
|
|
finally { _rebuilding = false; }
|
|
}
|
|
|
|
void OnDestroy()
|
|
{
|
|
if (_pictureMaterialInstance)
|
|
{
|
|
DestroyOwned(_pictureMaterialInstance);
|
|
_pictureMaterialInstance = null;
|
|
}
|
|
if (!_mesh) return;
|
|
if (Application.isPlaying) Destroy(_mesh);
|
|
else DestroyImmediate(_mesh);
|
|
}
|
|
|
|
[ContextMenu("Rebuild")]
|
|
public void Rebuild()
|
|
{
|
|
var filter = GetComponent<MeshFilter>();
|
|
EnsureMesh(filter);
|
|
|
|
#if UNITY_EDITOR
|
|
ResolveAspect();
|
|
#endif
|
|
// Inside-out sizing: the picture keeps the texture aspect exactly; thickness and padding
|
|
// are fractions of the picture's longer side, and Size stays the longer OUTER side:
|
|
// size = pLong + 2*(t + pad) = pLong * (1 + thickness + padding). No clamp dead zones -
|
|
// every 0..1 combination yields valid geometry.
|
|
float d = Mathf.Max(_frameDepth, 0.002f);
|
|
float pLong = Mathf.Max(_size, 0.01f) / (1f + _frameThickness + _padding);
|
|
float t = Mathf.Max(_frameThickness * pLong * 0.5f, 0.001f);
|
|
float pad = _padding * pLong * 0.5f;
|
|
float pw = Mathf.Max(pLong * _aspect.x, 0.001f);
|
|
float ph = Mathf.Max(pLong * _aspect.y, 0.001f);
|
|
float ow = pw + 2f * pad;
|
|
float oh = ph + 2f * pad;
|
|
float w = ow + 2f * t;
|
|
float h = oh + 2f * t;
|
|
float r = _cornerRadius * Mathf.Max(Mathf.Min(t, d * 0.5f) - 0.0005f, 0f);
|
|
if (r < 1e-4f) r = 0f;
|
|
// Depths are fractions of the frame depth. Inner floor: -1 = back plate .. +1 = a full
|
|
// frame depth in front of the face. Picture: interpolates from the back plate (-1) over
|
|
// the inner floor (0) to a full frame depth in front (+1) - endpoints are absolute so
|
|
// the extremes always mean "all the way in/out" regardless of where the floor sits.
|
|
float id = -_frameInnerDepth * d;
|
|
float pz = _pictureDepth < 0f
|
|
? Mathf.Lerp(id, d, -_pictureDepth)
|
|
: Mathf.Lerp(id, -d, _pictureDepth);
|
|
|
|
_verts.Clear();
|
|
_normals.Clear();
|
|
_uvs.Clear();
|
|
for (int i = 0; i < _tris.Length; i++) _tris[i].Clear();
|
|
|
|
// Picture plane (UV 0..1 so the assigned texture fills it exactly).
|
|
Quad(SubPicture,
|
|
new Vector3(-pw * 0.5f, -ph * 0.5f, pz), new Vector3(pw * 0.5f, -ph * 0.5f, pz),
|
|
new Vector3(pw * 0.5f, ph * 0.5f, pz), new Vector3(-pw * 0.5f, ph * 0.5f, pz),
|
|
Vector3.back,
|
|
new Vector2(0f, 0f), new Vector2(1f, 0f), new Vector2(1f, 1f), new Vector2(0f, 1f));
|
|
|
|
// Picture side walls when the plane sits off the inner floor: sunken = inward-facing recess,
|
|
// raised = outward-facing plinth.
|
|
if (Mathf.Abs(pz - id) > 1e-4f)
|
|
PerimeterWalls(SubInner, pw * 0.5f, ph * 0.5f, Mathf.Min(pz, id), Mathf.Max(pz, id), pz > id ? -1f : 1f);
|
|
|
|
// Inner cavity: padding ring around the picture + walls between the front face and the
|
|
// inner floor (inward recess when sunken, outward block sides when it protrudes).
|
|
Ring(SubInner, ow * 0.5f, oh * 0.5f, pw * 0.5f, ph * 0.5f, id);
|
|
if (Mathf.Abs(id) > 1e-4f)
|
|
PerimeterWalls(SubInner, ow * 0.5f, oh * 0.5f, Mathf.Min(id, 0f), Mathf.Max(id, 0f), id > 0f ? -1f : 1f);
|
|
|
|
// Frame: mitred front ring (from the rounded-edge inset to the opening), outer sides, bevels.
|
|
Ring(SubFrame, w * 0.5f - r, h * 0.5f - r, ow * 0.5f, oh * 0.5f, 0f);
|
|
OuterSides(w, h, d, r);
|
|
if (r > 0f) Bevels(w, h, d, r, Mathf.Max(1, _bevelSegments));
|
|
|
|
// Back plate.
|
|
Quad(SubBack,
|
|
new Vector3(-(w * 0.5f - r), -(h * 0.5f - r), d), new Vector3(w * 0.5f - r, -(h * 0.5f - r), d),
|
|
new Vector3(w * 0.5f - r, h * 0.5f - r, d), new Vector3(-(w * 0.5f - r), h * 0.5f - r, d),
|
|
Vector3.forward,
|
|
new Vector2(-(w * 0.5f - r), -(h * 0.5f - r)), new Vector2(w * 0.5f - r, -(h * 0.5f - r)),
|
|
new Vector2(w * 0.5f - r, h * 0.5f - r), new Vector2(-(w * 0.5f - r), h * 0.5f - r));
|
|
|
|
_mesh.Clear();
|
|
_mesh.SetVertices(_verts);
|
|
_mesh.SetNormals(_normals);
|
|
_mesh.SetUVs(0, _uvs);
|
|
_mesh.subMeshCount = _tris.Length;
|
|
for (int i = 0; i < _tris.Length; i++) _mesh.SetTriangles(_tris[i], i, false);
|
|
_mesh.RecalculateBounds();
|
|
_mesh.RecalculateTangents();
|
|
|
|
ApplyMaterials();
|
|
RebuildColliders(w, h, t, d, ow, oh, pw, ph, id, pz);
|
|
}
|
|
|
|
// Box approximation, corner radius intentionally ignored: 4 border bars (full depth), one slab
|
|
// filling the opening from the inner floor to the back, and a plinth box only when the picture
|
|
// is raised out of the floor. A sunken picture recess is NOT carved out - the slab stays full
|
|
// (colliders would need a per-side split for a cosmetic dent).
|
|
void RebuildColliders(float w, float h, float t, float d, float ow, float oh, float pw, float ph, float id, float pz)
|
|
{
|
|
_colliders.RemoveAll(c => !c);
|
|
|
|
// Migrate colliders older versions created directly on this GameObject: they must move to
|
|
// the child root, otherwise every structural change here would rebuild the inspector.
|
|
for (int i = _colliders.Count - 1; i >= 0; i--)
|
|
{
|
|
if (_colliderRoot && _colliders[i].transform == _colliderRoot) continue;
|
|
DestroyOwned(_colliders[i]);
|
|
_colliders.RemoveAt(i);
|
|
}
|
|
|
|
// The root child is fully owned, so any box that slipped out of the list (undo, aborted
|
|
// rebuild) is adopted back instead of leaking as an orphan.
|
|
if (_colliderRoot)
|
|
foreach (var box in _colliderRoot.GetComponents<BoxCollider>())
|
|
if (!_colliders.Contains(box)) _colliders.Add(box);
|
|
|
|
int count = 0;
|
|
if (_generateColliders)
|
|
{
|
|
SetBox(count++, new Vector3(0f, (h - t) * 0.5f, d * 0.5f), new Vector3(w, t, d));
|
|
SetBox(count++, new Vector3(0f, -(h - t) * 0.5f, d * 0.5f), new Vector3(w, t, d));
|
|
SetBox(count++, new Vector3((w - t) * 0.5f, 0f, d * 0.5f), new Vector3(t, oh, d));
|
|
SetBox(count++, new Vector3(-(w - t) * 0.5f, 0f, d * 0.5f), new Vector3(t, oh, d));
|
|
|
|
// Keep a minimal slab even when the inner floor sits on the back plate, so the opening
|
|
// is never backless.
|
|
float slabFront = Mathf.Min(id, d - 0.002f);
|
|
SetBox(count++, new Vector3(0f, 0f, (slabFront + d) * 0.5f), new Vector3(ow, oh, d - slabFront));
|
|
|
|
if (pz < id - 1e-4f)
|
|
SetBox(count++, new Vector3(0f, 0f, (pz + id) * 0.5f), new Vector3(pw, ph, id - pz));
|
|
}
|
|
|
|
for (int i = _colliders.Count - 1; i >= count; i--)
|
|
{
|
|
var extra = _colliders[i];
|
|
_colliders.RemoveAt(i);
|
|
if (extra) DestroyOwned(extra);
|
|
}
|
|
|
|
// Generation switched off: drop the root too, but only when nothing foreign lives on it.
|
|
if (!_generateColliders && _colliderRoot && _colliderRoot.childCount == 0 &&
|
|
_colliderRoot.GetComponents<Component>().Length == 1)
|
|
{
|
|
var go = _colliderRoot.gameObject;
|
|
_colliderRoot = null;
|
|
DestroyOwned(go);
|
|
}
|
|
}
|
|
|
|
void SetBox(int index, Vector3 center, Vector3 size)
|
|
{
|
|
while (_colliders.Count <= index)
|
|
_colliders.Add(EnsureColliderRoot().gameObject.AddComponent<BoxCollider>());
|
|
var box = _colliders[index];
|
|
if (box.center != center) box.center = center;
|
|
if (box.size != size) box.size = size;
|
|
}
|
|
|
|
Transform EnsureColliderRoot()
|
|
{
|
|
if (_colliderRoot) return _colliderRoot;
|
|
var existing = transform.Find("Colliders");
|
|
if (existing) _colliderRoot = existing;
|
|
else
|
|
{
|
|
var go = new GameObject("Colliders") { layer = gameObject.layer };
|
|
go.transform.SetParent(transform, false);
|
|
_colliderRoot = go.transform;
|
|
}
|
|
return _colliderRoot;
|
|
}
|
|
|
|
static void DestroyOwned(Object o)
|
|
{
|
|
if (Application.isPlaying) Destroy(o);
|
|
else DestroyImmediate(o);
|
|
}
|
|
|
|
void EnsureMesh(MeshFilter filter)
|
|
{
|
|
string meshName = "PictureFrame " + GetInstanceID();
|
|
if (!_mesh)
|
|
{
|
|
// Re-adopt our own mesh after a domain reload; a duplicated object has a different
|
|
// instance id and therefore gets a fresh mesh instead of fighting over the original's.
|
|
var existing = filter.sharedMesh;
|
|
_mesh = existing && existing.name == meshName
|
|
? existing
|
|
: new Mesh { name = meshName, hideFlags = HideFlags.DontSave };
|
|
}
|
|
if (filter.sharedMesh != _mesh) filter.sharedMesh = _mesh;
|
|
}
|
|
|
|
void ApplyMaterials()
|
|
{
|
|
var picture = ResolvePictureMaterial();
|
|
var meshRenderer = GetComponent<MeshRenderer>();
|
|
var current = meshRenderer.sharedMaterials;
|
|
bool dirty = current.Length != 4
|
|
|| current[0] != picture || current[1] != _innerMaterial
|
|
|| current[2] != _frameMaterial || current[3] != _backMaterial;
|
|
if (dirty)
|
|
meshRenderer.sharedMaterials = new[] { picture, _innerMaterial, _frameMaterial, _backMaterial };
|
|
}
|
|
|
|
// With a picture texture assigned, the picture slot uses a per-frame DontSave material
|
|
// instance whose base map is that texture - many frames can share ONE picture material asset
|
|
// with different photos, and the asset is never mutated. Properties are re-copied from the
|
|
// source material on every rebuild, so asset edits keep showing up on the instance. Like the
|
|
// mesh, the instance is never saved and is recreated in OnEnable after scene load.
|
|
Material ResolvePictureMaterial()
|
|
{
|
|
if (!_pictureTexture || !_pictureMaterial)
|
|
{
|
|
if (_pictureMaterialInstance)
|
|
{
|
|
DestroyOwned(_pictureMaterialInstance);
|
|
_pictureMaterialInstance = null;
|
|
}
|
|
return _pictureMaterial;
|
|
}
|
|
|
|
if (!_pictureMaterialInstance)
|
|
{
|
|
_pictureMaterialInstance = new Material(_pictureMaterial) { hideFlags = HideFlags.DontSave };
|
|
}
|
|
else
|
|
{
|
|
if (_pictureMaterialInstance.shader != _pictureMaterial.shader)
|
|
_pictureMaterialInstance.shader = _pictureMaterial.shader;
|
|
_pictureMaterialInstance.CopyPropertiesFromMaterial(_pictureMaterial);
|
|
}
|
|
_pictureMaterialInstance.name = _pictureMaterial.name + " (Picture)";
|
|
if (_pictureMaterialInstance.HasProperty("_BaseMap"))
|
|
_pictureMaterialInstance.SetTexture("_BaseMap", _pictureTexture);
|
|
else
|
|
_pictureMaterialInstance.mainTexture = _pictureTexture;
|
|
return _pictureMaterialInstance;
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
// Importer API for the source file's pixel size (public in Unity 6, internal in older
|
|
// versions - hence reflection; null-fallback is the imported texture size).
|
|
static readonly System.Reflection.MethodInfo s_sourceSize = typeof(UnityEditor.TextureImporter)
|
|
.GetMethod("GetSourceTextureWidthAndHeight",
|
|
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic
|
|
| System.Reflection.BindingFlags.Instance);
|
|
|
|
// Editor-only: derive _aspect (longer side = 1) from the assigned picture texture (falling
|
|
// back to the picture material's base map) and serialize it - the source image file is not
|
|
// available in a build. 1:1 whenever the whole chain breaks.
|
|
void ResolveAspect()
|
|
{
|
|
var aspect = Vector2.one;
|
|
Texture tex = _pictureTexture;
|
|
if (!tex && _pictureMaterial)
|
|
{
|
|
if (_pictureMaterial.HasProperty("_BaseMap")) tex = _pictureMaterial.GetTexture("_BaseMap");
|
|
if (!tex) tex = _pictureMaterial.mainTexture;
|
|
}
|
|
|
|
if (tex)
|
|
{
|
|
// Prefer the source file's pixel size: importer NPOT/max-size scaling can distort the
|
|
// imported texture's proportions (e.g. 4:3 photo -> power-of-two texture).
|
|
int w = tex.width;
|
|
int h = tex.height;
|
|
string path = UnityEditor.AssetDatabase.GetAssetPath(tex);
|
|
if (s_sourceSize != null && !string.IsNullOrEmpty(path)
|
|
&& UnityEditor.AssetImporter.GetAtPath(path) is UnityEditor.TextureImporter importer)
|
|
{
|
|
object[] size = { 0, 0 };
|
|
s_sourceSize.Invoke(importer, size);
|
|
if ((int)size[0] > 0 && (int)size[1] > 0)
|
|
{
|
|
w = (int)size[0];
|
|
h = (int)size[1];
|
|
}
|
|
}
|
|
|
|
if (w > 0 && h > 0)
|
|
aspect = w >= h ? new Vector2(1f, (float)h / w) : new Vector2((float)w / h, 1f);
|
|
}
|
|
|
|
if ((aspect - _aspect).sqrMagnitude < 1e-12f) return;
|
|
_aspect = aspect;
|
|
UnityEditor.EditorUtility.SetDirty(this);
|
|
}
|
|
#endif
|
|
|
|
// Flat ring between two concentric rects at depth z, facing the viewer (-Z), mitred corners.
|
|
void Ring(int sub, float ox, float oy, float ix, float iy, float z)
|
|
{
|
|
if (oy - iy > 1e-5f)
|
|
{
|
|
QuadXY(sub, new Vector3(-ix, iy, z), new Vector3(ix, iy, z), new Vector3(ox, oy, z), new Vector3(-ox, oy, z));
|
|
QuadXY(sub, new Vector3(-ox, -oy, z), new Vector3(ox, -oy, z), new Vector3(ix, -iy, z), new Vector3(-ix, -iy, z));
|
|
}
|
|
if (ox - ix > 1e-5f)
|
|
{
|
|
QuadXY(sub, new Vector3(ix, -iy, z), new Vector3(ox, -oy, z), new Vector3(ox, oy, z), new Vector3(ix, iy, z));
|
|
QuadXY(sub, new Vector3(-ox, -oy, z), new Vector3(-ix, -iy, z), new Vector3(-ix, iy, z), new Vector3(-ox, oy, z));
|
|
}
|
|
}
|
|
|
|
void QuadXY(int sub, Vector3 a, Vector3 b, Vector3 c, Vector3 d)
|
|
=> Quad(sub, a, b, c, d, Vector3.back,
|
|
new Vector2(a.x, a.y), new Vector2(b.x, b.y), new Vector2(c.x, c.y), new Vector2(d.x, d.y));
|
|
|
|
// Vertical walls around rect (hx, hy) spanning z0..z1; sign -1 = facing inward (recess),
|
|
// +1 = facing outward (plinth). Winding follows the normals automatically.
|
|
void PerimeterWalls(int sub, float hx, float hy, float z0, float z1, float sign)
|
|
{
|
|
Quad(sub, new Vector3(-hx, hy, z0), new Vector3(hx, hy, z0), new Vector3(hx, hy, z1), new Vector3(-hx, hy, z1),
|
|
sign * Vector3.up, new Vector2(-hx, z0), new Vector2(hx, z0), new Vector2(hx, z1), new Vector2(-hx, z1));
|
|
Quad(sub, new Vector3(-hx, -hy, z0), new Vector3(hx, -hy, z0), new Vector3(hx, -hy, z1), new Vector3(-hx, -hy, z1),
|
|
sign * Vector3.down, new Vector2(-hx, z0), new Vector2(hx, z0), new Vector2(hx, z1), new Vector2(-hx, z1));
|
|
Quad(sub, new Vector3(hx, -hy, z0), new Vector3(hx, hy, z0), new Vector3(hx, hy, z1), new Vector3(hx, -hy, z1),
|
|
sign * Vector3.right, new Vector2(-hy, z0), new Vector2(hy, z0), new Vector2(hy, z1), new Vector2(-hy, z1));
|
|
Quad(sub, new Vector3(-hx, -hy, z0), new Vector3(-hx, hy, z0), new Vector3(-hx, hy, z1), new Vector3(-hx, -hy, z1),
|
|
sign * Vector3.left, new Vector2(-hy, z0), new Vector2(hy, z0), new Vector2(hy, z1), new Vector2(-hy, z1));
|
|
}
|
|
|
|
// Flat outer side faces, shortened by the rounding radius on every border.
|
|
void OuterSides(float w, float h, float d, float r)
|
|
{
|
|
float hx = w * 0.5f;
|
|
float hy = h * 0.5f;
|
|
Quad(SubFrame, new Vector3(hx, -hy + r, r), new Vector3(hx, hy - r, r), new Vector3(hx, hy - r, d - r), new Vector3(hx, -hy + r, d - r),
|
|
Vector3.right, new Vector2(-hy + r, r), new Vector2(hy - r, r), new Vector2(hy - r, d - r), new Vector2(-hy + r, d - r));
|
|
Quad(SubFrame, new Vector3(-hx, -hy + r, r), new Vector3(-hx, hy - r, r), new Vector3(-hx, hy - r, d - r), new Vector3(-hx, -hy + r, d - r),
|
|
Vector3.left, new Vector2(-hy + r, r), new Vector2(hy - r, r), new Vector2(hy - r, d - r), new Vector2(-hy + r, d - r));
|
|
Quad(SubFrame, new Vector3(-hx + r, hy, r), new Vector3(hx - r, hy, r), new Vector3(hx - r, hy, d - r), new Vector3(-hx + r, hy, d - r),
|
|
Vector3.up, new Vector2(-hx + r, r), new Vector2(hx - r, r), new Vector2(hx - r, d - r), new Vector2(-hx + r, d - r));
|
|
Quad(SubFrame, new Vector3(-hx + r, -hy, r), new Vector3(hx - r, -hy, r), new Vector3(hx - r, -hy, d - r), new Vector3(-hx + r, -hy, d - r),
|
|
Vector3.down, new Vector2(-hx + r, r), new Vector2(hx - r, r), new Vector2(hx - r, d - r), new Vector2(-hx + r, d - r));
|
|
}
|
|
|
|
// Rounded outer edges: 8 quarter-cylinder edge strips (front + back), 4 vertical corner
|
|
// quarter-cylinders and 8 sphere-octant corner caps.
|
|
void Bevels(float w, float h, float d, float r, int segs)
|
|
{
|
|
float hx = w * 0.5f;
|
|
float hy = h * 0.5f;
|
|
Vector3 front = Vector3.back;
|
|
Vector3 back = Vector3.forward;
|
|
|
|
EdgeBevel(new Vector3(-hx + r, hy - r, r), new Vector3(hx - r, hy - r, r), front, Vector3.up, r, segs);
|
|
EdgeBevel(new Vector3(-hx + r, -hy + r, r), new Vector3(hx - r, -hy + r, r), front, Vector3.down, r, segs);
|
|
EdgeBevel(new Vector3(hx - r, -hy + r, r), new Vector3(hx - r, hy - r, r), front, Vector3.right, r, segs);
|
|
EdgeBevel(new Vector3(-hx + r, -hy + r, r), new Vector3(-hx + r, hy - r, r), front, Vector3.left, r, segs);
|
|
|
|
EdgeBevel(new Vector3(-hx + r, hy - r, d - r), new Vector3(hx - r, hy - r, d - r), back, Vector3.up, r, segs);
|
|
EdgeBevel(new Vector3(-hx + r, -hy + r, d - r), new Vector3(hx - r, -hy + r, d - r), back, Vector3.down, r, segs);
|
|
EdgeBevel(new Vector3(hx - r, -hy + r, d - r), new Vector3(hx - r, hy - r, d - r), back, Vector3.right, r, segs);
|
|
EdgeBevel(new Vector3(-hx + r, -hy + r, d - r), new Vector3(-hx + r, hy - r, d - r), back, Vector3.left, r, segs);
|
|
|
|
for (int sx = -1; sx <= 1; sx += 2)
|
|
for (int sy = -1; sy <= 1; sy += 2)
|
|
{
|
|
var corner = new Vector3(sx * (hx - r), sy * (hy - r), 0f);
|
|
EdgeBevel(corner + new Vector3(0f, 0f, r), corner + new Vector3(0f, 0f, d - r),
|
|
new Vector3(sx, 0f, 0f), new Vector3(0f, sy, 0f), r, segs);
|
|
CornerOctant(corner + new Vector3(0f, 0f, r), sx, sy, -1f, r, segs);
|
|
CornerOctant(corner + new Vector3(0f, 0f, d - r), sx, sy, 1f, r, segs);
|
|
}
|
|
}
|
|
|
|
// Quarter-circle sweep from dirA (angle 0) to dirB (angle 90) along the centerline c0->c1.
|
|
void EdgeBevel(Vector3 c0, Vector3 c1, Vector3 dirA, Vector3 dirB, float r, int segs)
|
|
{
|
|
float along = Vector3.Distance(c0, c1);
|
|
for (int s = 0; s < segs; s++)
|
|
{
|
|
float a0 = Mathf.PI * 0.5f * s / segs;
|
|
float a1 = Mathf.PI * 0.5f * (s + 1) / segs;
|
|
Vector3 d0 = Mathf.Cos(a0) * dirA + Mathf.Sin(a0) * dirB;
|
|
Vector3 d1 = Mathf.Cos(a1) * dirA + Mathf.Sin(a1) * dirB;
|
|
Quad(SubFrame,
|
|
c0 + r * d0, c1 + r * d0, c1 + r * d1, c0 + r * d1,
|
|
d0, d0, d1, d1,
|
|
new Vector2(0f, r * a0), new Vector2(along, r * a0),
|
|
new Vector2(along, r * a1), new Vector2(0f, r * a1));
|
|
}
|
|
}
|
|
|
|
// Eighth-sphere cap where two edge bevels and a vertical corner bevel meet.
|
|
void CornerOctant(Vector3 c, float sx, float sy, float sz, float r, int segs)
|
|
{
|
|
Vector3 Dir(float alpha, float beta) => new Vector3(
|
|
sx * Mathf.Sin(alpha) * Mathf.Cos(beta),
|
|
sy * Mathf.Sin(alpha) * Mathf.Sin(beta),
|
|
sz * Mathf.Cos(alpha));
|
|
|
|
for (int ia = 0; ia < segs; ia++)
|
|
for (int ib = 0; ib < segs; ib++)
|
|
{
|
|
float a0 = Mathf.PI * 0.5f * ia / segs;
|
|
float a1 = Mathf.PI * 0.5f * (ia + 1) / segs;
|
|
float b0 = Mathf.PI * 0.5f * ib / segs;
|
|
float b1 = Mathf.PI * 0.5f * (ib + 1) / segs;
|
|
Vector3 d00 = Dir(a0, b0);
|
|
Vector3 d10 = Dir(a1, b0);
|
|
Vector3 d11 = Dir(a1, b1);
|
|
Vector3 d01 = Dir(a0, b1);
|
|
Quad(SubFrame,
|
|
c + r * d00, c + r * d10, c + r * d11, c + r * d01,
|
|
d00, d10, d11, d01,
|
|
new Vector2(r * b0, r * a0), new Vector2(r * b0, r * a1),
|
|
new Vector2(r * b1, r * a1), new Vector2(r * b1, r * a0));
|
|
}
|
|
}
|
|
|
|
void Quad(int sub, Vector3 a, Vector3 b, Vector3 c, Vector3 d, Vector3 n,
|
|
Vector2 uva, Vector2 uvb, Vector2 uvc, Vector2 uvd)
|
|
=> Quad(sub, a, b, c, d, n, n, n, n, uva, uvb, uvc, uvd);
|
|
|
|
// Corners a..d must form a simple loop; winding is derived from the normals, so callers never
|
|
// have to reason about triangle order.
|
|
void Quad(int sub, Vector3 a, Vector3 b, Vector3 c, Vector3 d,
|
|
Vector3 na, Vector3 nb, Vector3 nc, Vector3 nd,
|
|
Vector2 uva, Vector2 uvb, Vector2 uvc, Vector2 uvd)
|
|
{
|
|
int i = _verts.Count;
|
|
_verts.Add(a); _verts.Add(b); _verts.Add(c); _verts.Add(d);
|
|
_normals.Add(na); _normals.Add(nb); _normals.Add(nc); _normals.Add(nd);
|
|
_uvs.Add(uva); _uvs.Add(uvb); _uvs.Add(uvc); _uvs.Add(uvd);
|
|
|
|
Vector3 winding = Vector3.Cross(b - a, c - a);
|
|
if (winding.sqrMagnitude < 1e-12f) winding = Vector3.Cross(c - a, d - a);
|
|
var tris = _tris[sub];
|
|
if (Vector3.Dot(winding, na + nb + nc + nd) >= 0f)
|
|
{
|
|
tris.Add(i); tris.Add(i + 1); tris.Add(i + 2);
|
|
tris.Add(i); tris.Add(i + 2); tris.Add(i + 3);
|
|
}
|
|
else
|
|
{
|
|
tris.Add(i); tris.Add(i + 2); tris.Add(i + 1);
|
|
tris.Add(i); tris.Add(i + 3); tris.Add(i + 2);
|
|
}
|
|
}
|
|
}
|
|
}
|