diff --git a/Assets/Materials/Frame Picture.mat b/Assets/Materials/Frame Picture.mat new file mode 100644 index 0000000..b9d4523 --- /dev/null +++ b/Assets/Materials/Frame Picture.mat @@ -0,0 +1,136 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Frame Picture + m_Shader: {fileID: 4800000, guid: 023c98a32f6cc00478e50479839530c5, type: 3} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: [] + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: + RenderType: Opaque + disabledShaderPasses: [] + m_LockedProperties: + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BaseMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _SpecGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_Lightmaps: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_LightmapsInd: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_ShadowMasks: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Ints: [] + m_Floats: + - _AddPrecomputedVelocity: 0 + - _AlphaClip: 0 + - _AlphaToMask: 0 + - _Blend: 0 + - _BlendModePreserveSpecular: 1 + - _BumpScale: 1 + - _ClearCoatMask: 0 + - _ClearCoatSmoothness: 0 + - _Cull: 2 + - _Cutoff: 0.5 + - _DetailAlbedoMapScale: 1 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _DstBlendAlpha: 0 + - _EnvironmentReflections: 1 + - _GlossMapScale: 0 + - _Glossiness: 0 + - _GlossyReflections: 0 + - _Metallic: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.005 + - _QueueOffset: 0 + - _ReceiveShadows: 1 + - _Smoothness: 0 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _SrcBlendAlpha: 1 + - _Surface: 0 + - _WorkflowMode: 1 + - _XRMotionVectorsPass: 1 + - _ZWrite: 1 + m_Colors: + - _BaseColor: {r: 1, g: 1, b: 1, a: 1} + - _Color: {r: 1, g: 1, b: 1, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1} + m_BuildTextureStacks: [] + m_AllowLocking: 1 +--- !u!114 &6905797306856702536 +MonoBehaviour: + m_ObjectHideFlags: 11 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} + m_Name: + m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion + version: 10 diff --git a/Assets/Materials/Frame Picture.mat.meta b/Assets/Materials/Frame Picture.mat.meta new file mode 100644 index 0000000..140ca75 --- /dev/null +++ b/Assets/Materials/Frame Picture.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9c88ebf613f4b1c49af2bb5a621d2dee +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Components/PictureFrameBuilder.cs b/Assets/Scripts/Components/PictureFrameBuilder.cs index 3db8e1f..3c6ca99 100644 --- a/Assets/Scripts/Components/PictureFrameBuilder.cs +++ b/Assets/Scripts/Components/PictureFrameBuilder.cs @@ -11,7 +11,9 @@ namespace Chameleon.Components // 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. + // 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 @@ -59,6 +61,8 @@ namespace Chameleon.Components [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; @@ -82,6 +86,7 @@ namespace Chameleon.Components }; Mesh _mesh; + Material _pictureMaterialInstance; bool _rebuildQueued; bool _rebuilding; @@ -110,6 +115,11 @@ namespace Chameleon.Components void OnDestroy() { + if (_pictureMaterialInstance) + { + DestroyOwned(_pictureMaterialInstance); + _pictureMaterialInstance = null; + } if (!_mesh) return; if (Application.isPlaying) Destroy(_mesh); else DestroyImmediate(_mesh); @@ -301,13 +311,49 @@ namespace Chameleon.Components void ApplyMaterials() { + var picture = ResolvePictureMaterial(); var meshRenderer = GetComponent(); var current = meshRenderer.sharedMaterials; bool dirty = current.Length != 4 - || current[0] != _pictureMaterial || current[1] != _innerMaterial + || current[0] != picture || current[1] != _innerMaterial || current[2] != _frameMaterial || current[3] != _backMaterial; if (dirty) - meshRenderer.sharedMaterials = new[] { _pictureMaterial, _innerMaterial, _frameMaterial, _backMaterial }; + 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 @@ -318,14 +364,14 @@ namespace Chameleon.Components 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. + // 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 = null; - if (_pictureMaterial) + Texture tex = _pictureTexture; + if (!tex && _pictureMaterial) { if (_pictureMaterial.HasProperty("_BaseMap")) tex = _pictureMaterial.GetTexture("_BaseMap"); if (!tex) tex = _pictureMaterial.mainTexture; diff --git a/Assets/Scripts/Components/PictureFrameBuilder.cs.meta b/Assets/Scripts/Components/PictureFrameBuilder.cs.meta index e1f753e..a917c28 100644 --- a/Assets/Scripts/Components/PictureFrameBuilder.cs.meta +++ b/Assets/Scripts/Components/PictureFrameBuilder.cs.meta @@ -4,7 +4,8 @@ MonoImporter: externalObjects: {} serializedVersion: 2 defaultReferences: - - _pictureMaterial: {instanceID: 0} + - _pictureTexture: {instanceID: 0} + - _pictureMaterial: {fileID: 2100000, guid: 9c88ebf613f4b1c49af2bb5a621d2dee, type: 2} - _innerMaterial: {fileID: 2100000, guid: bc0ac45503ad5c44b8b966da0d0a40d3, type: 2} - _frameMaterial: {fileID: 2100000, guid: a7794896ef878a64399cca929db5c43c, type: 2} - _backMaterial: {fileID: 2100000, guid: a7794896ef878a64399cca929db5c43c, type: 2}