diff --git a/Assets/Scripts/Components/PictureFrameBuilder.cs b/Assets/Scripts/Components/PictureFrameBuilder.cs index 3c6ca99..de11e85 100644 --- a/Assets/Scripts/Components/PictureFrameBuilder.cs +++ b/Assets/Scripts/Components/PictureFrameBuilder.cs @@ -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;