Derive picture frame size from picture texture aspect ratio

Replace Width/Height with a single Size (longer side); the shorter side
follows the picture material's base map source-image aspect (editor-
resolved, serialized for builds, 1:1 fallback). Add Range attributes on
all frame parameters and default material references (new Frame.mat).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Fable 5 committed 2026-07-09 20:33:18 +02:00
1 parent bac32a87a9
commit 850c92bcce
4 files changed
+231 -16

No files matched your search

@@ -5,8 +5,9 @@ namespace Chameleon.Components
{
// Procedural picture frame: a box-profile frame with a recessed inner floor (FrameInnerDepth) and a
// picture plane offset relative to it (PictureDepth: + sunken, - raised plinth), generated live in the
// editor (and at runtime) from the inspector parameters - resize via Width/Height, NOT via transform
// scale, so the frame profile never stretches (9-slice behaviour). Four submeshes with their own
// editor (and at runtime) from the inspector parameters - resize via Size (width/height follow the
// picture texture's aspect ratio), NOT via transform scale, so the frame profile never stretches
// (9-slice behaviour). 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.
//
@@ -27,28 +28,31 @@ namespace Chameleon.Components
const int SubBack = 3;
[Header("Size")]
[Tooltip("Outer width of the frame in meters.")]
[SerializeField, Min(0.01f)] float _width = 1f;
[Tooltip("Outer height of the frame in meters.")]
[SerializeField, Min(0.01f)] float _height = 1f;
[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 profile in the picture plane.")]
[SerializeField, Min(0.001f)] float _frameThickness = 0.08f;
[SerializeField, Range(0.001f, 1.000f)] float _frameThickness = 0.08f;
[Tooltip("Depth of the frame, front face to back face.")]
[SerializeField, Min(0.002f)] float _frameDepth = 0.12f;
[SerializeField, Range(0.002f, 0.500f)] float _frameDepth = 0.12f;
[Tooltip("Rounding radius of the outer frame edges; 0 = sharp box. Clamped to thickness / half depth.")]
[SerializeField, Min(0f)] float _cornerRadius;
[SerializeField, Range(0.000f, 0.500f)] float _cornerRadius;
[Tooltip("Arc segments of the edge rounding.")]
[SerializeField, Range(1, 12)] int _bevelSegments = 4;
[SerializeField, Range(1, 8)] int _bevelSegments = 4;
[Tooltip("How deep behind the front face the inner floor (padding ring) sits.")]
[SerializeField, Min(0f)] float _frameInnerDepth = 0.06f;
[SerializeField, Range(0.000f, 0.498f)] float _frameInnerDepth = 0.06f;
[Header("Picture")]
[Tooltip("Gap between the frame's inner edge and the picture (visible inner-material ring).")]
[SerializeField, Min(0f)] float _padding = 0.02f;
[SerializeField, Range(0.000f, 1.000f)] float _padding = 0.02f;
[Tooltip("Picture plane relative to the inner floor: positive sinks it into the frame, negative raises it out like a plinth, 0 = flush.")]
[SerializeField] float _pictureDepth;
[SerializeField, Range(-0.500f, 0.500f)] float _pictureDepth;
[Header("Materials")]
[SerializeField] Material _pictureMaterial;
@@ -110,8 +114,11 @@ namespace Chameleon.Components
var filter = GetComponent<MeshFilter>();
EnsureMesh(filter);
float w = Mathf.Max(_width, 0.01f);
float h = Mathf.Max(_height, 0.01f);
#if UNITY_EDITOR
ResolveAspect();
#endif
float w = Mathf.Max(_size * _aspect.x, 0.01f);
float h = Mathf.Max(_size * _aspect.y, 0.01f);
float t = Mathf.Clamp(_frameThickness, 0.001f, Mathf.Min(w, h) * 0.5f - 0.001f);
float d = Mathf.Max(_frameDepth, 0.002f);
float r = Mathf.Min(_cornerRadius, Mathf.Min(t, d * 0.5f) - 0.0005f);
@@ -239,6 +246,56 @@ namespace Chameleon.Components
meshRenderer.sharedMaterials = new[] { _pictureMaterial, _innerMaterial, _frameMaterial, _backMaterial };
}
#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 picture material's base map and
// serialize it - the source image file is not available in a build. 1:1 whenever the
// material -> base map -> texture chain breaks.
void ResolveAspect()
{
var aspect = Vector2.one;
Texture tex = null;
if (_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)
{
@@ -1,2 +1,15 @@
fileFormatVersion: 2
guid: 86a75d0253697424fb4b506c394f13bc
guid: 86a75d0253697424fb4b506c394f13bc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences:
- _pictureMaterial: {instanceID: 0}
- _innerMaterial: {fileID: 2100000, guid: a7794896ef878a64399cca929db5c43c, type: 2}
- _frameMaterial: {fileID: 2100000, guid: a7794896ef878a64399cca929db5c43c, type: 2}
- _backMaterial: {fileID: 2100000, guid: a7794896ef878a64399cca929db5c43c, type: 2}
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: