PictureFrameBuilder: normal/height maps, strengths, crop, subdivisions
- Optional Picture Normal Map + Height Map fields on the per-frame material instance (_BumpMap/_NORMALMAP keyword, _HeightMap; height doubles as occlusion map when the material has none) - Sliders: Normal Map Strength, Occlusion Strength, Tessellation Strength (0-3, default 1), Occlusion Contrast (-1-3), Brightness (0-3) - Picture Crop left/right/top/bottom via UV window remap; aspect follows the cropped pixel dimensions - Picture Subdivisions (1-64): picture surface as triangle grid for tessellation displacement Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d0d7028ebb
commit
defd970cf8
1 file changed
+127
-13
@@ -13,7 +13,8 @@ namespace Chameleon.Components
|
||||
// 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.
|
||||
// instance (the shared material asset is never modified) and drives the aspect ratio; an optional
|
||||
// PictureNormalMap rides on the same instance as its bump map.
|
||||
//
|
||||
// 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
|
||||
@@ -63,6 +64,32 @@ namespace Chameleon.Components
|
||||
[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;
|
||||
[Tooltip("Optional normal map for the picture: applied as the picture material's bump map via the same per-frame material instance.")]
|
||||
[SerializeField] Texture _pictureNormalMap;
|
||||
[Tooltip("Intensity of the picture normal map (URP _BumpScale). 1 = as authored.")]
|
||||
[SerializeField, Range(0f, 3f)] float _normalMapStrength = 1f;
|
||||
[Tooltip("Optional height map for the picture: set as _HeightMap on the per-frame material instance (tessellation shader); on plain URP Lit it falls back to the parallax map.")]
|
||||
[SerializeField] Texture _pictureHeightMap;
|
||||
[Tooltip("Scales the material's occlusion (_OcclusionStrength). 1 = as authored.")]
|
||||
[SerializeField, Range(0f, 3f)] float _occlusionStrength = 1f;
|
||||
[Tooltip("Contrast of the occlusion map around mid-gray (_OcclusionContrast). 0 = unchanged, -1 = flat, 3 = strongly exaggerated.")]
|
||||
[SerializeField, Range(-1f, 3f)] float _occlusionContrast;
|
||||
[Tooltip("Brightness multiplier on the picture base map (_Brightness) to compensate the darkening from occlusion. 1 = unchanged.")]
|
||||
[SerializeField, Range(0f, 3f)] float _brightness = 1f;
|
||||
[Tooltip("Scales the material's tessellation displacement (_TessellationStrength). 1 = as authored.")]
|
||||
[SerializeField, Range(0f, 10f)] float _tessellationStrength = 1f;
|
||||
[Tooltip("Triangle grid resolution of the picture plane (cells along its longer side); raise it so displacement/tessellation has vertices to work with. 1 = plain quad.")]
|
||||
[SerializeField, Range(1, 10)] int _pictureSubdivisions = 1;
|
||||
|
||||
[Header("Picture Crop")]
|
||||
[Tooltip("Cut away this fraction of the picture from the left edge; the frame aspect follows the cropped region.")]
|
||||
[SerializeField, Range(0f, 0.9f)] float _cropLeft;
|
||||
[Tooltip("Cut away this fraction of the picture from the right edge.")]
|
||||
[SerializeField, Range(0f, 0.9f)] float _cropRight;
|
||||
[Tooltip("Cut away this fraction of the picture from the top edge.")]
|
||||
[SerializeField, Range(0f, 0.9f)] float _cropTop;
|
||||
[Tooltip("Cut away this fraction of the picture from the bottom edge.")]
|
||||
[SerializeField, Range(0f, 0.9f)] float _cropBottom;
|
||||
|
||||
[Header("Materials")]
|
||||
[SerializeField] Material _pictureMaterial;
|
||||
@@ -164,12 +191,29 @@ namespace Chameleon.Components
|
||||
_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 plane, subdivided into a grid so displacement/tessellation shaders have
|
||||
// vertices to work with; cell counts follow the aspect so cells stay roughly square.
|
||||
// The UVs sample only the cropped sub-rect of the texture (full texture when no crop);
|
||||
// normal/height maps ride the same UV set, so they crop along automatically.
|
||||
CropSpans(out float cu0, out float cu1, out float cv0, out float cv1);
|
||||
int cellsX = Mathf.Max(1, Mathf.RoundToInt(_pictureSubdivisions * _aspect.x));
|
||||
int cellsY = Mathf.Max(1, Mathf.RoundToInt(_pictureSubdivisions * _aspect.y));
|
||||
for (int cy = 0; cy < cellsY; cy++)
|
||||
for (int cx = 0; cx < cellsX; cx++)
|
||||
{
|
||||
float u0 = (float)cx / cellsX;
|
||||
float u1 = (float)(cx + 1) / cellsX;
|
||||
float v0 = (float)cy / cellsY;
|
||||
float v1 = (float)(cy + 1) / cellsY;
|
||||
Quad(SubPicture,
|
||||
new Vector3(pw * (u0 - 0.5f), ph * (v0 - 0.5f), pz), new Vector3(pw * (u1 - 0.5f), ph * (v0 - 0.5f), pz),
|
||||
new Vector3(pw * (u1 - 0.5f), ph * (v1 - 0.5f), pz), new Vector3(pw * (u0 - 0.5f), ph * (v1 - 0.5f), pz),
|
||||
Vector3.back,
|
||||
new Vector2(Mathf.Lerp(cu0, cu1, u0), Mathf.Lerp(cv0, cv1, v0)),
|
||||
new Vector2(Mathf.Lerp(cu0, cu1, u1), Mathf.Lerp(cv0, cv1, v0)),
|
||||
new Vector2(Mathf.Lerp(cu0, cu1, u1), Mathf.Lerp(cv0, cv1, v1)),
|
||||
new Vector2(Mathf.Lerp(cu0, cu1, u0), Mathf.Lerp(cv0, cv1, v1)));
|
||||
}
|
||||
|
||||
// Picture side walls when the plane sits off the inner floor: sunken = inward-facing recess,
|
||||
// raised = outward-facing plinth.
|
||||
@@ -328,7 +372,10 @@ namespace Chameleon.Components
|
||||
// mesh, the instance is never saved and is recreated in OnEnable after scene load.
|
||||
Material ResolvePictureMaterial()
|
||||
{
|
||||
if (!_pictureTexture || !_pictureMaterial)
|
||||
bool wantsInstance = _pictureTexture || _pictureNormalMap || _pictureHeightMap
|
||||
|| _occlusionStrength != 1f || _tessellationStrength != 1f
|
||||
|| _occlusionContrast != 0f || _brightness != 1f;
|
||||
if (!wantsInstance || !_pictureMaterial)
|
||||
{
|
||||
if (_pictureMaterialInstance)
|
||||
{
|
||||
@@ -349,13 +396,74 @@ namespace Chameleon.Components
|
||||
_pictureMaterialInstance.CopyPropertiesFromMaterial(_pictureMaterial);
|
||||
}
|
||||
_pictureMaterialInstance.name = _pictureMaterial.name + " (Picture)";
|
||||
if (_pictureMaterialInstance.HasProperty("_BaseMap"))
|
||||
_pictureMaterialInstance.SetTexture("_BaseMap", _pictureTexture);
|
||||
else
|
||||
_pictureMaterialInstance.mainTexture = _pictureTexture;
|
||||
if (_pictureTexture)
|
||||
{
|
||||
if (_pictureMaterialInstance.HasProperty("_BaseMap"))
|
||||
_pictureMaterialInstance.SetTexture("_BaseMap", _pictureTexture);
|
||||
else
|
||||
_pictureMaterialInstance.mainTexture = _pictureTexture;
|
||||
}
|
||||
if (_pictureNormalMap && _pictureMaterialInstance.HasProperty("_BumpMap"))
|
||||
{
|
||||
_pictureMaterialInstance.SetTexture("_BumpMap", _pictureNormalMap);
|
||||
// URP Lit only samples _BumpMap with the keyword on; CopyPropertiesFromMaterial has
|
||||
// just reset the keywords to the source material's (which has no bump map).
|
||||
_pictureMaterialInstance.EnableKeyword("_NORMALMAP");
|
||||
if (_pictureMaterialInstance.HasProperty("_BumpScale"))
|
||||
_pictureMaterialInstance.SetFloat("_BumpScale", _normalMapStrength);
|
||||
}
|
||||
if (_pictureHeightMap)
|
||||
{
|
||||
if (_pictureMaterialInstance.HasProperty("_HeightMap"))
|
||||
_pictureMaterialInstance.SetTexture("_HeightMap", _pictureHeightMap);
|
||||
else if (_pictureMaterialInstance.HasProperty("_ParallaxMap"))
|
||||
{
|
||||
// Plain URP Lit: no displacement support, use the height map as parallax.
|
||||
_pictureMaterialInstance.SetTexture("_ParallaxMap", _pictureHeightMap);
|
||||
_pictureMaterialInstance.EnableKeyword("_PARALLAXMAP");
|
||||
}
|
||||
|
||||
// The height map doubles as the occlusion source (crevices darken) unless the
|
||||
// material has its own authored occlusion map - a white default map makes the
|
||||
// occlusion strength slider a mathematical no-op.
|
||||
if (_pictureMaterialInstance.HasProperty("_OcclusionMap")
|
||||
&& !_pictureMaterial.GetTexture("_OcclusionMap"))
|
||||
_pictureMaterialInstance.SetTexture("_OcclusionMap", _pictureHeightMap);
|
||||
}
|
||||
if (_pictureMaterialInstance.HasProperty("_OcclusionStrength"))
|
||||
_pictureMaterialInstance.SetFloat("_OcclusionStrength", _occlusionStrength);
|
||||
if (_pictureMaterialInstance.HasProperty("_TessellationStrength"))
|
||||
_pictureMaterialInstance.SetFloat("_TessellationStrength", _tessellationStrength);
|
||||
if (_pictureMaterialInstance.HasProperty("_OcclusionContrast"))
|
||||
_pictureMaterialInstance.SetFloat("_OcclusionContrast", _occlusionContrast);
|
||||
if (_pictureMaterialInstance.HasProperty("_Brightness"))
|
||||
_pictureMaterialInstance.SetFloat("_Brightness", _brightness);
|
||||
return _pictureMaterialInstance;
|
||||
}
|
||||
|
||||
// Remaining UV window after the per-side crops (u: left->right, v: bottom->top). Opposing
|
||||
// crops that would meet or invert collapse to a minimal centered span instead of degenerating.
|
||||
void CropSpans(out float u0, out float u1, out float v0, out float v1)
|
||||
{
|
||||
u0 = Mathf.Clamp01(_cropLeft);
|
||||
u1 = 1f - Mathf.Clamp01(_cropRight);
|
||||
v0 = Mathf.Clamp01(_cropBottom);
|
||||
v1 = 1f - Mathf.Clamp01(_cropTop);
|
||||
const float minSpan = 0.01f;
|
||||
if (u1 - u0 < minSpan)
|
||||
{
|
||||
float mid = (u0 + u1) * 0.5f;
|
||||
u0 = mid - minSpan * 0.5f;
|
||||
u1 = mid + minSpan * 0.5f;
|
||||
}
|
||||
if (v1 - v0 < minSpan)
|
||||
{
|
||||
float mid = (v0 + v1) * 0.5f;
|
||||
v0 = mid - minSpan * 0.5f;
|
||||
v1 = mid + minSpan * 0.5f;
|
||||
}
|
||||
}
|
||||
|
||||
#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).
|
||||
@@ -397,7 +505,13 @@ namespace Chameleon.Components
|
||||
}
|
||||
|
||||
if (w > 0 && h > 0)
|
||||
aspect = w >= h ? new Vector2(1f, (float)h / w) : new Vector2((float)w / h, 1f);
|
||||
{
|
||||
// The frame follows the CROPPED region's proportions, not the full image's.
|
||||
CropSpans(out float u0, out float u1, out float v0, out float v1);
|
||||
float cw = w * (u1 - u0);
|
||||
float ch = h * (v1 - v0);
|
||||
aspect = cw >= ch ? new Vector2(1f, ch / cw) : new Vector2(cw / ch, 1f);
|
||||
}
|
||||
}
|
||||
|
||||
if ((aspect - _aspect).sqrMagnitude < 1e-12f) return;
|
||||
|
||||
Reference in new issue
Block a user