
简介这份资源面向Unity开发者与游戏视觉设计学习者提供在Unity引擎中实现照片墙效果的完整工程参考。内容围绕图片素材组织、平面几何体搭建、C#脚本动态切换与淡入淡出过渡、自定义材质纹理贴图、环境光与聚光灯等灯光布置以及点击触发、缩放拖动等交互功能展开适合具备一定Unity编辑器与脚本基础、希望提升场景视觉表现力的中高级开发者。压缩包共310个文件约1.54MB以png图片素材、meta资源索引、asset配置、sample示例、dll插件库及少量cs脚本、unity场景文件为主另含ProjectSettings等工程配置目录结构便于按模块查阅。目前已有525人学习下载。借助该工程读者可对照实现图片轮播、材质调优与灯光氛围营造理解资源目录组织与插件集成思路快速将照片墙效果迁移到自己的项目场景中。1. 从一堆散装资源到可跑的照片墙这套 Unity 工程到底给了什么如果你手里正好有一份 Unity 照片墙工程打开后看到的不是满屏截图而是DOTweenTextMeshPro.cs.addon、DOTweenTk2d.cs.addon、ProjectSettings.asset、InputManager.asset、QualitySettings.asset、GraphicsSettings.asset、NavMeshAreas.asset、TagManager.asset、Physics2DSettings.asset、DynamicsManager.asset这一串文件第一反应大概率是这到底是个能直接跑的项目还是别人拆剩下的配置残骸我一开始也这么想。照片墙效果本身不复杂难的是把它做成一个可复现、可扩展、不依赖特定版本玄学环境的工程。这套资源的价值不在“照片墙”三个字而在于它把 Unity 项目里最容易被忽略、又最容易导致翻车的底层配置和补丁文件一起打包了。适合谁适合已经会拖 UI、会写简单 C# 脚本但一遇到工程迁移、插件冲突、输入系统错乱就头大的从业者。你拿到的不是一张效果图而是一套带配置上下文的实现底子。2. 照片墙的渲染与布局从 Plane 排布到相机参数怎么定2.1 为什么优先用 Quad 而不是 Plane 做照片单元很多人做照片墙第一反应是GameObject 3D Object Plane因为名字听起来就像“平面”。但真到排几十上百张图的时候Plane 的面数默认 121 个顶点和默认尺寸10x10 单位会带来两个麻烦一是缩放时纹理容易糊二是批量生成时内存和 Draw Call 都不划算。我一般会换成 Quad默认 1x1 单位、4 个顶点贴图比例好控制配合Material的Unlit/Texture或Unlit/Transparent就能出干净的照片效果。具体做法是写一个编辑器脚本或运行时脚本按行列生成 Quad 网格。下面这段是运行时生成照片墙骨架的常用写法using UnityEngine; public class PhotoWallBuilder : MonoBehaviour { public int columns 5; // 每行照片数 public int rows 4; // 行数 public float spacing 1.2f; // 照片间距单位是世界单位 public GameObject photoPrefab; // 带 Quad 和 Material 的预制体 void Start() { BuildWall(); } void BuildWall() { // 计算整体偏移让照片墙中心对齐父物体原点 float offsetX (columns - 1) * spacing * 0.5f; float offsetY (rows - 1) * spacing * 0.5f; for (int y 0; y rows; y) { for (int x 0; x columns; x) { Vector3 pos new Vector3( x * spacing - offsetX, y * spacing - offsetY, 0f ); GameObject photo Instantiate(photoPrefab, transform); photo.transform.localPosition pos; photo.transform.localRotation Quaternion.identity; // 给每张照片一个唯一名字方便后续换图或做交互 photo.name $Photo_{x}_{y}; } } } }逻辑说明columns和rows决定网格密度spacing控制照片之间的呼吸感。offsetX和offsetY是为了让整面墙的中心落在父物体原点不然后面做相机对准或旋转时你会找不到中心。photoPrefab建议做成一个 Quad 加一个带纹理的材质材质 Shader 选Unlit/Texture这样不受场景灯光影响照片颜色最接近原图。参数怎么改如果照片是 16:9把 Quad 的localScale设成(1.6f, 0.9f, 1f)这类比例而不是直接拉伸贴图。间距spacing一般取照片宽度的 1.1 到 1.3 倍太密会显得挤太疏就散了。2.2 相机选透视还是正交取决于你要不要景深和旋转照片墙如果只是正面平铺正交相机Orthographic最省事不会出现近大远小的透视变形排布整齐。但如果你想要那种“照片墙微微倾斜、有纵深感”的效果就得用透视相机Perspective并把Field of View控制在 40 到 60 之间。FOV 太大边缘会拉伸太小又显得扁平。我一般会这样设相机位置放在(0, 0, -10)看向(0, 0, 0)透视模式 FOV 50。如果照片墙宽高比是 5:4相机的Aspect在 Game 视图里设成对应比例或者用脚本动态算正交尺寸using UnityEngine; [RequireComponent(typeof(Camera))] public class PhotoWallCameraFit : MonoBehaviour { public float wallWidth 6f; // 照片墙总宽度 public float wallHeight 4.8f; // 照片墙总高度 void Start() { Camera cam GetComponentCamera(); if (cam.orthographic) { // 正交相机根据墙高和宽高比算 orthographicSize float screenAspect (float)Screen.width / Screen.height; float wallAspect wallWidth / wallHeight; if (screenAspect wallAspect) { // 屏幕更宽以高度为准 cam.orthographicSize wallHeight * 0.5f; } else { // 屏幕更窄以宽度为准反推高度 cam.orthographicSize (wallWidth / screenAspect) * 0.5f; } } } }这段脚本解决的是“照片墙在 PC 和手机上都完整显示”的问题。wallWidth和wallHeight按你实际排布算比如 5 列 4 行、间距 1.2那宽度大约是(5-1)*1.2 1 5.8高度是(4-1)*1.2 1 4.6取整到 6 和 4.8 留点边距。正交尺寸算完后相机不管屏幕怎么变墙都在视野内。透视相机就没这么简单得靠调距离或 FOV常见做法是保持 FOV 不变、移动相机 Z 值来适配。2.3 材质与纹理设置别让照片变成紫红色Unity 里材质变紫红基本等于 Shader 丢了或纹理没赋值。照片墙最稳的材质配置是新建 MaterialShader 选Unlit/Texture把照片纹理拖到Texture槽。纹理导入设置里Texture Type选Sprite (2D and UI)或Default都行但Non-Power of 2要设成None不然 Unity 会强行缩放纹理导致模糊。Filter Mode选BilinearAniso Level拉到 2 到 4斜看时照片边缘更干净。如果照片数量多不要每张照片一个 Material那样 Draw Call 会爆炸。常见做法是做一个 Material 数组或图集用MaterialPropertyBlock改每张 Quad 的纹理using UnityEngine; public class PhotoTextureAssigner : MonoBehaviour { public Texture2D[] photos; // 所有照片纹理 public Renderer[] photoQuads; // 所有 Quad 的 Renderer void Start() { // 用 MaterialPropertyBlock 避免为每张照片生成新材质实例 MaterialPropertyBlock block new MaterialPropertyBlock(); for (int i 0; i photoQuads.Length; i) { if (i photos.Length) break; block.SetTexture(_MainTex, photos[i]); photoQuads[i].SetPropertyBlock(block); } } }_MainTex是Unlit/Texture的标准纹理属性名。MaterialPropertyBlock的好处是不破坏合批Unity 仍然可以把这些 Quad 合在一起渲染。注意photos数组长度要和photoQuads对齐不然会漏图或报空引用。如果照片尺寸差异大建议在导入前统一裁成相同比例不然墙上会参差不齐。3. 交互与动画点击、缩放、拖拽怎么接才不打架3.1 用射线检测做点击换图或弹窗照片墙的交互第一步是“点中哪张”。3D 物体用Physics.RaycastUI 用EventSystem但照片墙如果是 Quad 加 Collider走物理射线最直接。给每个 Quad 加BoxCollider然后相机发射线using UnityEngine; public class PhotoClickHandler : MonoBehaviour { public Camera mainCamera; public float maxDistance 100f; void Update() { // 鼠标左键或触摸 if (Input.GetMouseButtonDown(0)) { Ray ray mainCamera.ScreenPointToRay(Input.mousePosition); if (Physics.Raycast(ray, out RaycastHit hit, maxDistance)) { // 只处理带 PhotoTag 的物体 if (hit.collider.CompareTag(Photo)) { Debug.Log($点击了照片{hit.collider.gameObject.name}); // 这里可以触发弹窗、换图或放大逻辑 } } } } }maxDistance限制射线距离避免点到相机后面的东西。CompareTag(Photo)要求你在TagManager.asset里提前加好Photo标签这也解释了为什么资源包里带了TagManager.asset——标签和层不是代码里写个字符串就完事得在工程设置里注册。如果点不中先检查 Quad 有没有 Collider、相机是不是mainCamera、标签有没有拼错。3.2 DOTween 做淡入淡出和缩放过渡资源包里出现了DOTweenTextMeshPro.cs.addon和DOTweenTk2d.cs.addon说明这套工程用了 DOTween 做补间动画。照片墙常见的过渡是点击某张照片它放大并淡入到前景其他照片变暗。用 DOTween 写起来很顺手using UnityEngine; using DG.Tweening; public class PhotoFocusEffect : MonoBehaviour { public Transform targetPhoto; public float focusScale 1.5f; public float duration 0.3f; public void FocusOnPhoto() { // 先杀掉该物体上可能存在的旧补间避免叠加 targetPhoto.DOKill(); // 缩放过渡 targetPhoto.DOScale(Vector3.one * focusScale, duration) .SetEase(Ease.OutBack); // 位置微调让它往相机方向靠一点 targetPhoto.DOLocalMoveZ(-0.5f, duration) .SetEase(Ease.OutQuad); } public void ResetPhoto() { targetPhoto.DOKill(); targetPhoto.DOScale(Vector3.one, duration).SetEase(Ease.OutQuad); targetPhoto.DOLocalMoveZ(0f, duration).SetEase(Ease.OutQuad); } }DOKill()是血泪经验如果不杀旧补间快速连点会导致缩放叠加照片越点越大或者抖成筛子。SetEase(Ease.OutBack)给一点回弹感适合照片聚焦。DOLocalMoveZ往相机方向拉近配合透视相机有纵深感。注意 DOTween 需要初始化一般在第一个场景的脚本里调DOTween.Init()或者直接依赖它的自动初始化。如果报DOTween命名空间找不到检查DOTweenTextMeshPro.cs.addon这类补丁文件有没有正确导入以及Modules文件夹是否完整。3.3 拖拽与缩放把输入管理交给 InputManager.asset资源包里的InputManager.asset是 Unity 旧输入系统的配置文件。如果你要做拖拽照片墙用Input.GetAxis(Mouse X)和Input.GetAxis(Mouse Y)最省事但这些轴必须在InputManager.asset里存在。默认工程有Mouse X和Mouse Y但如果你迁移工程时把这个文件覆盖错了轴就没了代码直接报错。拖拽逻辑可以这样写using UnityEngine; public class PhotoWallDrag : MonoBehaviour { public float dragSpeed 0.5f; public float minX -3f; public float maxX 3f; public float minY -2f; public float maxY 2f; void Update() { // 按住鼠标左键或单指触摸时拖拽 if (Input.GetMouseButton(0)) { float deltaX Input.GetAxis(Mouse X) * dragSpeed; float deltaY Input.GetAxis(Mouse Y) * dragSpeed; Vector3 newPos transform.position new Vector3(-deltaX, -deltaY, 0f); // 限制范围别让墙拖出视野 newPos.x Mathf.Clamp(newPos.x, minX, maxX); newPos.y Mathf.Clamp(newPos.y, minY, maxY); transform.position newPos; } } }dragSpeed控制拖拽灵敏度一般 0.3 到 0.8 之间。minX/maxX/minY/maxY是边界防止照片墙被拖到看不见的地方。注意这里用的是旧输入系统如果工程升级到新 Input SystemInput.GetAxis会直接抛异常得改用InputAction。资源包里带InputManager.asset说明它默认走旧输入迁移时要么保留这个文件要么把代码改成新输入系统。4. 工程配置与资源管理那些 .asset 文件到底在管什么4.1 ProjectSettings 下的关键配置文件对照很多人拿到工程只关心脚本和预制体结果一换机器就各种报错。资源包里那串.asset文件其实是 Unity 工程的“黑匣子”管着输入、质量、图形、标签、物理等底层设置。下面这张表是我整理的核心文件作用与常见翻车点文件名作用常见翻车点ProjectSettings.asset产品名、包名、默认分辨率、API 兼容级别迁移后包名冲突Android 打包失败InputManager.asset旧输入系统的轴和按键映射覆盖后Mouse X丢失拖拽代码报错QualitySettings.asset各质量等级的抗锯齿、阴影、纹理限制照片墙在高画质下纹理被限制成半分辨率GraphicsSettings.asset渲染管线、Shader 包含、预加载升级管线后 Shader 丢失材质变紫红TagManager.asset标签和层代码里CompareTag(Photo)找不到标签Physics2DSettings.asset2D 物理重力、碰撞矩阵如果照片墙用 2D 碰撞重力没关会往下掉DynamicsManager.asset3D 物理重力、碰撞矩阵射线检测层冲突点不中照片NavMeshAreas.asset寻路区域照片墙工程一般用不到但迁移时缺失会报错这些文件不需要你手动改但要知道它们的存在。迁移工程时最稳的做法是整体拷贝ProjectSettings文件夹而不是只拷Assets。如果只拷Assets新工程会用默认设置标签、输入轴、质量等级全丢照片墙可能跑不起来。4.2 照片资源导入与图集策略照片墙的照片数量一多导入设置就决定性能。单张照片如果按默认Default类型导入Unity 会生成 Mipmap远处看会糊近处看又浪费内存。我一般会这样设Texture Type选Sprite (2D and UI)Sprite Mode选SinglePacking Tag留空或统一填PhotoWallMax Size按实际需要设 1024 或 2048Compression选Normal Quality。如果照片是 UI 上显示勾Generate Mip Maps反而会让缩略图变糊可以关掉。图集策略上如果照片数量在 20 张以内直接每张一个 Material 也能跑。超过 50 张建议用Sprite Atlas或手动打成一张大图然后用 UV 偏移取子区域。资源包里没有现成图集说明它默认走的是每张照片独立纹理的路线适合中小规模照片墙。如果你要扩展到几百张得自己加图集或分页加载不然内存和 Draw Call 都会成为瓶颈。4.3 用 TagManager 和 Layer 做交互过滤照片墙的交互经常需要区分“可点击的照片”和“背景装饰”。最省事的做法是在TagManager.asset里加一个Photo标签给所有照片 Quad 设上。层Layer则用来控制射线检测范围比如把照片放在Default层背景放在Background层射线只打Defaultusing UnityEngine; public class PhotoRayFilter : MonoBehaviour { public Camera mainCamera; public LayerMask photoLayer; // 在 Inspector 里选 Default 或自定义层 void Update() { if (Input.GetMouseButtonDown(0)) { Ray ray mainCamera.ScreenPointToRay(Input.mousePosition); // 只检测 photoLayer 上的碰撞体 if (Physics.Raycast(ray, out RaycastHit hit, 100f, photoLayer)) { Debug.Log($命中{hit.collider.name}); } } } }photoLayer是LayerMask类型在 Inspector 里勾选对应层即可。这样即使背景有 Collider也不会被射线打中。注意LayerMask的索引和TagManager.asset里的层顺序对应迁移工程时如果层顺序变了photoLayer的数值可能错位需要重新勾选。5. 避坑与排查照片墙从能跑到稳跑的五个坎5.1 照片全变紫红材质球一片洋红现象场景里所有 Quad 都是紫红色材质 Inspector 里 Shader 显示Hidden/InternalErrorShader。原因Shader 丢失或渲染管线不匹配常见于从 Built-in 管线迁移到 URP/HDRP 后没换 Shader。解决把材质 Shader 改成当前管线支持的Universal Render Pipeline/Unlit或Unlit/Texture检查GraphicsSettings.asset里的渲染管线资产是否指向正确。如果只是照片墙用Unlit/Texture最稳不依赖管线。5.2 点击没反应射线打不中现象鼠标点照片没任何输出Debug.Log不打印。原因Quad 没加 Collider或者相机不是mainCamera或者标签/层过滤把照片排除了。解决给 Quad 加BoxCollider确认mainCamera在 Inspector 里赋值检查CompareTag的标签是否在TagManager.asset里注册检查LayerMask是否包含照片所在层。如果用的是 UI 照片墙得走EventSystem而不是Physics.Raycast。5.3 DOTween 动画叠加照片越点越大现象快速连点同一张照片缩放不断累加照片最后大到出屏。原因每次点击都新建补间旧补间没杀。解决在每次DOScale前调targetPhoto.DOKill()或者用DOTween.Kill(targetPhoto)统一清理。如果多个补间同时作用在同一个属性上还可以用SetId加DOTween.Kill(id)精确控制。5.4 迁移工程后输入轴丢失拖拽报错现象代码里Input.GetAxis(Mouse X)抛异常ArgumentException: Input Axis Mouse X is not setup。原因只拷了Assets文件夹没拷ProjectSettings/InputManager.asset新工程没有Mouse X轴。解决把原工程的InputManager.asset覆盖到新工程ProjectSettings下或者手动在Edit Project Settings Input Manager里加回Mouse X和Mouse Y。如果已经升级到新 Input System改用InputAction读取鼠标 delta。5.5 照片纹理模糊边缘发虚现象照片在墙上显示模糊放大后更明显。原因纹理导入时Max Size太小或者Non-Power of 2被设成ToNearest导致缩放或者 Mipmap 在近处反而拉低清晰度。解决把Max Size提到 2048Non-Power of 2设NoneFilter Mode用Bilinear如果照片只在近处看就关掉Generate Mip Maps。另外检查 Quad 的localScale是否把纹理拉伸变形保持宽高比一致。6. 进阶把照片墙做成可配置的数据驱动模块照片墙做到后面最烦的不是效果而是每换一批照片就要改代码或拖预制体。我后来习惯把它做成数据驱动用一个ScriptableObject存照片列表和布局参数运行时读配置生成。这样美术换图不用碰脚本策划调间距也不用重新编译。using UnityEngine; [CreateAssetMenu(fileName PhotoWallConfig, menuName PhotoWall/Config)] public class PhotoWallConfig : ScriptableObject { public Texture2D[] photos; // 照片纹理列表 public int columns 5; // 列数 public int rows 4; // 行数 public float spacing 1.2f; // 间距 public float focusScale 1.5f; // 聚焦缩放倍数 public float animationDuration 0.3f; // 动画时长 }然后在PhotoWallBuilder里把columns、rows、spacing换成从PhotoWallConfig读取。photos数组直接喂给PhotoTextureAssigner。这样一套配置对应一面墙多面墙就建多个ScriptableObject资产。验证方法很简单在 Editor 里改配置运行后看照片数量、间距、点击缩放是否跟着变。如果没变检查PhotoWallBuilder有没有在Awake或Start里引用配置以及配置资产有没有正确赋值。另一个进阶点是性能验证。打开Window Analysis Profiler看Rendering里的Draw Call和SetPass Call。照片墙如果每张照片一个 MaterialDraw Call 会等于照片数用了MaterialPropertyBlock或图集后Draw Call 应该明显下降。如果没降检查 Quad 之间有没有被其他材质或层级打断合批。我一般会强制自己照片超过 30 张就必须看一次 Profiler不然上线后帧率掉到 30 以下再回头查成本翻倍。从那以后我每次拿到类似工程都强制走一遍“配置检查 → 材质验证 → 射线测试 → Profiler 看 Draw Call”的流程不再凭感觉拖拽。希望帮到你。本文还有配套的精品资源点击获取