简介:面向游戏开发者、虚拟现实/增强现实爱好者及仿真应用初学者的Unity3D立体效果演示包,围绕分屏式3D立体视觉的实现展开,重点解决左右眼双相机视角配置、屏幕分割与立体投影效果落地问题。资源整体6.63MB,共24个文件,以DLL动态库、config配置、Unity资源数据与可运行exe为主,另有少量辅助脚本和说明文档;DLL负责扩展渲染功能,配置类文件管理相机与显示参数,exe可直接运行预览,结构精简,便于对照学习。已有1614人学习下载,适合作为Unity立体渲染管线的入门参考。借助该资源,可直观理解平级相机摆放、视锥体调节、偏移参数设置以及后期立体增强等关键环节;尤其能看清左右眼相机如何模拟人眼视差,结合分屏输出与3D眼镜兼容格式,快速搭建试验场景并观察立体效果,为后续开发虚拟现实场景、3D影院式体验或仿真应用打下基础。
1. 一个标题,三种完全不同的立体需求
“unity3d立体效果”这六个字,在真实工单里能对应到三种完全不同的场景:第一类是把 UI 做出伪 3D 的视差分层感,第二类是让 3D 场景本身看起来更有立体感(靠光照、景深和模型细节),第三类是真正输出左右眼画面,给 VR 设备或立体显示器用。很多人一搜“立体效果”就直接去找双相机脚本,结果做 UI 的人用不上,做 VR 的人又嫌它太简陋——需求没对齐,抄代码必然翻车。
这篇文章按一线最常见的落地路径来拆:先用相机、光照和材质把场景的立体感底子打好,再用 Shader 做视差深度效果(这是 UI 和 2D 场景立体的主力方案),最后处理真正的左右眼立体渲染。每一章都有可抄的代码和参数,最后一章专门讲我调这些参数时踩过的坑。适合刚把项目从 2D 转到 3D 的开发者,也适合做 VR 原型但不想从零研究立体相机的人。
先说结论:90% 的“立体效果”需求,其实不需要双相机,而是需要深度和视差。搞懂这一点,能帮你省掉大量不必要的性能开销和代码复杂度。
2. 立体感的地基:相机、光照与材质三板斧
2.1 为什么你的场景“看着平”:核心在深度线索缺失
人眼判断立体感,靠的是遮挡关系、近大远小、光影明暗和双目视差。在单屏上,前三个线索完全由相机和光照决定,和模型精细度关系没那么大。很多新手把模型面数堆得很高,场景依然“看起来很平”,就是因为相机 FOV 太大、光照没有方向性、材质全是纯色漫反射——深度线索全被抹掉了。
我先说相机。FOV 是影响立体感最直观的参数,默认 60° 属于“上帝视角”的广角观感,近处和远处物体大小差异不明显,透视感弱。我一般做第三人称或展示类场景,会把 FOV 压到 35~45° 之间,近景物体会明显放大,纵深关系立刻出来。第一人称射击类游戏则保留 70° 以上,因为需要视野广度,代价就是立体感弱,要靠其他线索补。
光照方向决定了阴影能不能“画出”物体的体积。常见做法是用一个主平行光模拟太阳,角度别放在正前方,我习惯放在物体前方偏上 30~45° 的位置,让物体有一个明显的亮面和暗面。如果场景所有物体都被环境光均匀照亮,那材质再细腻也看不出凹凸。记住一个自查口诀:把场景截图转成灰度图,如果整个画面灰蒙蒙没有明显的明暗分界,光照就一定是平的。
2.2 相机参数速查表:FOV、近远裁剪面的相互制约
相机参数不能单独调,它们相互掣肘。近裁剪面和远裁剪面差距过大会导致深度精度不足,出现闪烁;FOV 太小又会让人感觉镜头“被卡住”。我给一个可直接套用的基础配置:
| 参数 | 推荐范围 | 说明 |
|---|---|---|
| FOV | 35°~45°(展示类);70°~90°(第一人称) | 越小透视越强,立体感越明显 |
| Near Clip Plane | 0.1~0.3 | 越小越能看清近处物体,但和远裁剪面差距过大会掉精度 |
| Far Clip Plane | 100~500 | 按场景实际大小定,不要盲目拉到 1000 |
| 相机位置 | 略高于物体中心,俯角 15°~30° | 俯视天然提供深度层次 |
| 背景雾效 | 开启 Fog,指数模式 | 远处物体变淡,拉开纵深 |
这套参数的真实作用是强制场景“分层”:近处清楚、远处模糊,大脑会自动把这种差异解读为深度。Far Clip 拉到 1000 是新手最常见的问题,场景里根本没有那么远的物体,结果就是深度缓冲区被无效范围占用,中景位置出现 z-fighting 闪烁。我把近裁剪面设为 0.15、远裁剪面设为 300,大部分场景已经足够。
2.3 让材质“鼓起来”:法线贴图与高光参数的实战设置
场景立体的秘密有一半在材质里。简单说,人眼判断一个物体是平面还是曲面,靠的是光在表面上的反射变化——法线贴图就是骗这个判断的。一个纯色立方体如果贴上法线贴图,它表面的光照会模拟出砖块凹凸的明暗变化,看起来就像真的有浮雕一样。
我在 Unity 里给建筑外墙和地面做立体感,标准做法是在 URP 管线下用 Lit 材质,法线贴图槽位贴一张带法线的纹理,然后把 Smoothness 调到 0.3~0.5 之间。Smoothness 太低会让表面像砂纸,没有高光反射,立体感会减弱;太高会让表面像塑料,产生大片高光反而掩盖细节。金属类物体把 Metallic 调到 0.7 以上,非金属一律留在 0~0.1。
另外一个常被忽略的参数是材质的 Detail Normal Map,也就是次级法线贴图。当相机靠近物体时,主法线贴图的分辨率不够,表面会变糊,这时叠加一张 Detail 法线能补回近距离的凹凸细节。我的习惯是主法线铺大结构、Detail 法线铺小纹理,两层配合,场景在近距离和远距离都有立体感。
3. 立体效果的主力实现:视差与深度分层 Shader
3.1 视差错觉:为什么 2D 的画面也能“往外凸”
很多“立体效果”需求其实发生在 UI 或 2D 背景上,并没有真正的 3D 模型。这时候要用的不是相机,而是视差(Parallax)原理——不同距离的图层在视角变化时移动速度不同,大脑会自动把它解读为深度。这是做 2.5D UI 和动态背景最便宜、效果最直观的手段。
我要写的 Shader 核心逻辑很直接:把同一张图拆成前景、中景、背景三层,按一个从相机传入的参数控制每层的偏移量。红色通道存前景的位移量,绿色通道存中景位移量,蓝色通道存背景位移量。相机视角变化时,三层以不同速度滑动——这不是真正的透视,但视觉上足够骗过人眼。
用这个方案去做 2D 场景的“立体效果”有个额外好处,它能和 UI 的点击热区完全解耦——图片在视觉上有前后层次,但每个图层的碰撞体位置不变,不会出现用户点不到按钮的问题。
3.2 写一个可用的视差 Shader:带 UV 偏移的描点采样
我直接用 URP 的 ShaderLab 写一个经过验证的版本,大家可以直接抄到工程里用:
Shader "Custom/ParallaxDepth" { Properties { _MainTex ("Base Texture", 2D) = "white" {} _DepthMap ("Depth Map (R/G/B channels)", 2D) = "white" {} _ParallaxScale ("Parallax Strength", Range(0, 0.1)) = 0.03 _DepthMapScale ("Depth Map Tiling", Float) = 1 } SubShader { Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" } Pass { HLSLPROGRAM #pragma vertex vert #pragma fragment frag #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; }; struct Varyings { float4 positionHCS : SV_POSITION; float2 uv : TEXCOORD0; float3 viewDirWS : TEXCOORD1; }; TEXTURE2D(_MainTex); SAMPLER(sampler_MainTex); TEXTURE2D(_DepthMap); SAMPLER(sampler_DepthMap); float _ParallaxScale; float _DepthMapScale; Varyings vert(Attributes input) { Varyings o; o.positionHCS = TransformObjectToHClip(input.positionOS.xyz); o.uv = input.uv; // 把相机方向转到世界空间,后续用它的视角偏移做视差 o.viewDirWS = normalize(GetWorldSpaceViewDir(TransformObjectToWorld(input.positionOS.xyz))); return o; } float4 frag(Varyings input) : SV_Target { // 对深度图做平铺采样,平铺能让视差细节更密 float2 depthUV = input.uv * _DepthMapScale; float4 depthSample = SAMPLE_TEXTURE2D(_DepthMap, sampler_DepthMap, depthUV); // 用视角方向和深度通道计算UV偏移量 float2 parallaxOffset = input.viewDirWS.xy / input.viewDirWS.z * _ParallaxScale; parallaxOffset *= depthSample.r * 0.5 + depthSample.g * 0.3; // 偏移后的UV对主纹理采样 float2 finalUV = input.uv - parallaxOffset; float4 color = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, finalUV); return color; } ENDHLSL } } }逻辑说明:顶点着色器拿到视角方向后传到片元阶段,像素着色器先从深度图采样,用红绿通道的占比决定偏移量大小,再把主纹理的 UV 按偏移量移动后采样。这比传统的高度视差(Parallax Mapping)简单,因为省略了多次迭代采样,适合在 UI 和移动端跑。_ParallaxScale 控制在 0.01~0.05,偏移量太大边缘会出现撕裂感。
参数说明:
- _ParallaxScale:整体视差强度,0.03 是安全起点,配大图可以降到 0.01
- _DepthMapScale:深度图平铺倍数,值越大视差细节越多,但会让大块凸起变碎
- depthSample.r 和 depthSample.g:红通道控制水平方向位移量,绿通道控制垂直方向位移量,各自为 0.5 左右效果最均衡
3.3 用 Renderer Feature 做描边立体:快速烘焙深度轮廓
还有一种常见的立体感需求,是给模型加深度描边——轮廓线附近提亮或变暗,模拟浮雕效果。Unity URP 里可以在 Renderer 上挂一个全屏 Renderer Feature,后处理渲染相机深度纹理,在深度不连续的地方画线。这么做的好处是自动识别模型边界,不用手动描模型轮廓。
我在做某产品的图标展示场景时,给所有模型统一挂了这个效果,立体感提升非常明显。关键设置是:深度阈值 Depth Threshold 设为 0.5 左右,法线阈值 Normal Threshold 设为 0.3,厚度参数控制在 1~2 个像素。阈值太小会让描边碎成噪点,太大则描边会漏掉细小物体。这个效果的代价是全屏后处理,移动端中低端机型慎重开启。
3.4 蒙版与半透明排序:UI 上做立体效应必须处理的排序问题
视差 Shader 在 UI 上落地,首先要解决的是渲染排序。UI 元素如果互相重叠,半透明物体的渲染顺序错了,就会出现前景挡住中景的穿帮。我的做法是把带视差效果的元素放到同一个 Sorting Layer,并把 Render Queue 设为 Transparent(3000 之后),然后在脚本里手动按纵深排序子物体的绘制顺序。
另外要提一个细节:Canvas 默认是 Screen Space Overlay,叠加 Shader 做视差时需要改成 Screen Space Camera,配合一个专门的 UI 相机,否则视差偏移会被 UI 网格的坐标空间限制住。我在这里浪费过一个下午,改了 Shader 怎么都没反应,最后发现是 Canvas 模式不对。
4. 真正的立体渲染:左右眼双相机配置与输出
4.1 双相机方案 vs 单相机后期:该走哪条路
到了真正的立体显示需求,VR 头盔、裸眼 3D 屏或红蓝分色眼镜,核心方案是渲染出左右眼两个略有差异的画面,让眼睛分别接收。Unity 里实现方式分两类:一类是开启 XR 支持,由引擎自动管理双眼渲染;另一类是纯手动搭建双相机,适用于裸眼 3D 显示器和传统红蓝立体。
我先给出选型判断标准:目标是 VR 头盔,直接用 XR 插件,手动配双相机纯属脱裤子放屁;目标是裸眼 3D 显示器,一般用双相机的 Render Texture 输出;目标是红蓝眼镜(虽然现在用的人少了),也需要手动分开渲染通道。我见过的很多教学帖直接把手动双相机脚本贴给所有人,但 VR 用户根本不该用这套。
4.2 手动搭建左右眼相机:基线距离与视差角度的调整
手动双相机的原理很简单:主相机左右各放一个子物体,作为左眼和右眼的位置锚点,两眼距离(基线距离)按真实人眼瞳距的 1/10 到 1/20 缩放。场景一米的单位,实际瞳距 6.5 厘米,在场景里取 0.2~0.5 之间的值,看场景大小微调。
每个相机单独渲染到一张 Render Texture,左眼看左纹理、右眼看右纹理,然后通过全屏 Quad 把两张纹理并列输出到屏幕上。关键点在于两个相机的投影矩阵要完全一致,只改变位置,不要动旋转。我的搭建脚本如下:
using UnityEngine; using UnityEngine.Rendering; public class StereoRig : MonoBehaviour { public Camera stereoCamera; public float interpupillaryDistance = 0.3f; public RenderTexture leftRT, rightRT; void Start() { // 把主相机拆成左右两个渲染目标 leftRT = CreateRT(); rightRT = CreateRT(); stereoCamera.targetTexture = leftRT; // 复制一份相机参数给右眼,但位置偏移 Camera rightCam = gameObject.AddComponent<Camera>(); rightCam.CopyFrom(stereoCamera); rightCam.targetTexture = rightRT; rightCam.transform.position = transform.position + transform.right * interpupillaryDistance; } RenderTexture CreateRT() { RenderTexture rt = new RenderTexture(1920, 1080, 24); rt.name = "StereoRT"; rt.Create(); return rt; } }逻辑说明:Start 里创建两张 Render Texture,左眼用主相机,右眼复制一份相机参数、水平偏移一个基线距离。这里只改位置不动旋转,保证左右眼画面的差异纯粹来自视差——旋转会让画面产生无法融合的扭曲。
参数说明:interpupillaryDistance 控制在 0.2~0.5,值越大立体感越强,但超过 0.5 后大脑难以融合,会产生晕眩。如果画面太立体就看 target 距离,立体感同时受相机到物体的距离影响——基线距离固定时,镜头离物体越近立体感越强,这正好是调节的另一个旋钮。
4.3 左右眼画面合并输出:用 Shader 做并排或交错
拿到两张 Render Texture 后,需要把它们合成到屏幕。常见做法是左右并排(Side-by-Side)给裸眼 3D 屏用,交错行(Interlaced)给偏光式 3D 显示器用。我写一个并排输出的后处理 Shader,它同时能处理左右顺序的开关:
Shader "Custom/StereoMerge" { Properties { _LeftTex ("Left Eye", 2D) = "black" {} _RightTex ("Right Eye", 2D) = "black" {} _FlipEyes ("Flip Left/Right", Float) = 0 } SubShader { Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" sampler2D _LeftTex; sampler2D _RightTex; float _FlipEyes; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; }; v2f vert(appdata_base v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv = v.texcoord; return o; } fixed4 frag(v2f i) : SV_Target { // 左半部分显示左眼,右半部分显示右眼 float side = step(0.5, i.uv.x); float2 luv = float2(i.uv.x * 2.0, i.uv.y); float2 ruv = float2((i.uv.x - 0.5) * 2.0, i.uv.y); fixed4 col = side ? tex2D(_RightTex, ruv) : tex2D(_LeftTex, luv); // 支持左右眼交换,应对不同显示设备的默认顺序 if (_FlipEyes > 0.5) { col = side ? tex2D(_LeftTex, luv) : tex2D(_RightTex, ruv); } return col; } ENDCG } } }逻辑说明:像素着色器按 UV 的 x 坐标切分屏幕,左半屏采样左眼纹理、右半屏采样右眼纹理。难点在于 UV 要重新映射——左半屏原来的 x 范围是 0~0.5,必须乘以 2 映射到完整纹理的 0~1,否则只取到纹理一半的内容。
参数说明:_FlipEyes 在有些设备上左右眼顺序是反的,开了这个参数可以一键交换,不用重新打包工程。做裸眼 3D 屏时这个开关救了我好几次,不同厂商的屏幕默认时序经常不一样。
4.4 红蓝分色立体渲染:旧方案但有教学价值
虽然现在做红蓝立体的人少了,但它是理解立体渲染原理最直观的入门方案。做法是在双相机渲染的基础上,左眼相机只输出红色通道,右眼相机只输出青蓝色通道,然后叠加到同一张纹理。这个方案的优点是任何普通屏幕都能看,不用特殊硬件。
我在 Unity 里实现时用的是替代 Shader:左眼相机的 Replacement Shader 把颜色输出设为纯红色乘原色,右眼相机设为青色。关键是 Shader 里要把 Alpha 设为 1,并关闭金属高光的白色输出——白色会同时刺激左右眼,产生鬼影。如果追求更好的效果,需要做色域空间转换,但作为学习 Demo 直接叠加已经完全够用。
5. 立体效果避坑:五个最容易翻车的地方
5.1 现象:视差 Shader 没反应,UV 偏移量算出来全是零
原因排查:最常见的情况是视角方向向量在顶点着色器里没有正确归一化。很多 Shader 里 viewDirWS 直接用了相机方向向量没 normalize,导致偏移量极其微小,肉眼看不出来。另一个原因是深度图没有正确导入,Unity 默认把灰度图导入成 sRGB,采样值会偏大,导致偏移过于猛烈或出现奇怪的色彩条纹。
解决:检查深度图的 Import Settings,确认 sRGB (Color Texture) 被取消勾选,并把 Wrap Mode 设为 Clamp(防止边缘采样黑边)。如果偏移量还是小,用 Debug 视图直接输出 parallaxOffset 到颜色值,看看是不是接近 0.01 数量级——太小就要加大 _ParallaxScale。
5.2 现象:左右眼画面看起来像错开的两张照片,完全无法融合
原因排查:双相机的投影矩阵不一致,或者两个相机的旋转角度有微小差异。特别是用了 CopyFrom 复制相机后,如果脚本里对右眼相机的 transform 做了旋转调整,即使只是 0.1° 的偏转,也会造成垂直视差,大脑完全无法融合,看一会儿就会头晕。
解决:右眼相机永远只改 position,绝不改 rotation。如果需要调整观看角度,只旋转父节点,让两个子相机同时跟着转。我用这个规则后,立体融合的成功率从“偶尔能看”变成了“每次都能融合”。另外检查一下相机的立体视差是否设置了反向,如果左眼看左图被调成右图,用 4.3 的 _FlipEyes 一键交换。
5.3 现象:立体效果开启后帧率暴跌,主相机渲染了两次
原因排查:手动双相机方案如果不加控制,两架相机会分别渲染整个场景,相当于把场景总光照和阴影计算量翻倍。很多教程没有提这一点,新手一挂脚本,GPU 时间直接翻倍。
解决:把两架相机放进同一个层(比如只包含主要立体物体的层),次要 UI 元素只留在主相机。另一个做法是给左眼相机加 Culling Mask,只渲染需要强立体感的模型,远处天空盒等背景只用左眼渲染,右眼用 Skybox 填充。实测能省掉 20%~30% 的渲染开销。
5.4 现象:视差图边缘出现锯齿和硬边,像贴了一层壳
原因排查:深度图用红绿通道做偏移时,通道边缘的采样值不连续,导致 UV 跳变。本质上是因为深度图的压缩格式把通道精度压低了,RGB 通道在移动端压缩纹理里经常只有 5-6 位有效精度。
解决:把深度图导入格式改为 RGBA32,避免压缩格式。另一个方案是_在片段着色器里对深度图采样加 blur,用周围像素平均来平滑不连续。深度图的分辨率跟不上显示分辨率时也会出现锯齿,我一般把深度图分辨率设为主纹理的 0.5~1 倍,太高不会增加明显效果,反而增加带宽压力。
5.5 现象:UI 立体效果在部分手机上闪烁,文字抖动
原因排查:UI 视差 Shader 的偏移让文字采样坐标发生了亚像素偏移,整个文字图像的采样点不再对齐整数像素,导致文字边缘发虚、闪烁。这是锐利的 UI 元素最常见的翻车点。
解决:UI 里的文字不要直接应用视差 Shader,分开处理——把视差效果只放在整块的背景装饰上,文字放在视差层之上,维持普通 UI 渲染。如果必须让文字也产生前后层次感,用纯粹的 UI 动画(位移)模拟视差,不要让文字纹理真的落到 GPU 做偏移采样。
6. 进阶技巧:用深度图做交互式立体触控
最后一个章分享一个我自己项目中效果很好的技巧:把视差效果从纯视觉扩展到交互。做法是在主纹理的同一张深度图上,额外存一份物体“可交互深度范围”。运行时用户滑动屏幕,画面按视差偏移,同时把深度值映射到 UI 响应参数,用来控制音效音量、动画进度、甚至物体本身的运动。
具体实现里,我在 Canvas 上挂一个 RawImage,把全屏视差渲染的结果作为显示源,然后监听 Drag 事件,把拖动方向映射为 _ParallaxScale 的实时值。向左滑动图片往左偏,向右滑往右偏,同时叠加一个缓动函数让偏移有惯性。这样做出来的交互感和 3D 场景里抓取物体非常接近,但不需要任何碰撞体。
更加进阶的玩法是把深度图后续交给物理系统——用户可以点按屏幕任意位置,通过深度值计算出一个伪 3D 坐标,物体在这个坐标上做浮空旋转。这套思路适合做产品展示,客户点按任意角度都能看到物体侧面,比传统录一段旋转视频强很多。代价是深度图的制作精度要求高,画面里多个物体必须严格分层。
我在实际项目里养成的一个习惯是:每次调整完视差参数,都会截图放到手机上用真实手指滑一遍,而不是只在编辑器里看。很多效果在编辑器里看着没问题,一到真机上就暴露了边缘撕裂或者闪烁的问题。移动端和编辑器的渲染精度差异,是我这几年做立体效果最大的教训,没有之一。
立体效果的坑大多不在代码逻辑,而在参数和硬件习惯差异。把相机基线、视差强度这类核心数值做成可在运行时调节的公共参数,上线前留一周专门真机调参,会让你的项目少走很多弯路。希望帮到你。
本文还有配套的精品资源,点击获取