
之前做游戏 demo 的时候最常见的反馈是“玩法太普通”“一眼就能猜到下一关”。后来办公室桌上正好有一副红蓝 3D 眼镜我突发奇想如果做一款只有戴上红蓝 3D 眼镜才能正常看清画面层次的游戏会不会更有意思于是就有了这篇教程的主角——一个基于 Unity 的 2D 躲避小游戏 demo。这篇文章不只讲玩法创意更重要的是完整拆解红蓝 3D 游戏背后的实现原理左右眼视差怎么产生、红色/青色通道如何分离、RenderTexture 双相机渲染怎么做、Canvas 叠加显示如何配置。所有代码都是可直接复制运行的 Unity 脚本和 Shader适合 Unity 初学者、游戏开发爱好者以及想在个人项目中加入立体视觉效果的开发者。读者跟完整个流程后可以掌握以下内容红蓝 3D 成像的基本原理以及和偏振 3D、快门 3D 的区别。在 Unity 中搭建“双相机 RenderTexture 自定义 Shader”的立体渲染框架。用几个简单脚本实现玩家控制、障碍生成、计分与游戏结束。处理红蓝重影、亮度异常、手机性能等常见问题。1. 背景与核心概念1.1 红蓝 3D 眼镜是怎么工作的先理解一个最基本的生理常识人的两只眼睛之间大约有 6 厘米距离所以左眼和右眼观察同一个场景时看到的画面并不是完全相同的。两只眼睛分别捕捉到两幅带有轻微角度差异的画面大脑再把它们融合成一个具有深度感的立体图像这就是“双目视差”。普通显示器和普通游戏画面只提供“一个视角”无论你左眼还是右眼看到的都是同一张图片大脑无法从单张图片中提取可靠的深度信息所以看起来就是平的。红蓝 3D 眼镜采用一种非常古老的“分色法”。它的工作方式可以概括为两步屏幕同时显示两张画面左眼画面被着色成红色右眼画面被着色成青色。眼镜的左镜片是一块红色滤光片它会过滤掉青色内容只让红色画面进入左眼右镜片是青色滤光片只让青色画面进入右眼。这样两只眼睛虽然同时在看同一个屏幕但每只眼睛实际收到的图像却是不同的。大脑再进行视差融合立体感就出现了。这套方案不需要专门的 3D 显示器也不需要红外同步只需要一副十几块钱的红蓝纸框眼镜因此在低成本 demo、线下展台互动、老式 3D 电影还原等场景中非常实用。1.2 为什么“只有戴上 3D 眼镜才能玩”这里有一个很容易被忽略的点红蓝 3D 游戏在不戴眼镜时的画面是“红色左眼视图”和“青色右眼视图”直接叠加在一起的效果。不戴眼镜时你会看到画面上同时存在红、青两种颜色的重影。物体边缘出现明显的红边和青边。场景层次看起来是错位的甚至有点眩晕。玩家和障碍物的碰撞位置很难目测准确。换言之不戴眼镜时游戏是“不可玩”的。一旦戴上红蓝眼镜左眼和右眼各看各的画面大脑会把两层图像合成重影消失场景的远近关系立刻变得清晰。这种“戴上才正常”的体验就构成了游戏的核心乐趣和话题点。1.3 红蓝 3D 和其他 3D 方案的差异很多读者会联想到电影院里的偏振 3D、电视上的快门式 3D、还有 VR 头盔。简单对比一下方案原理硬件成本游戏适配难度长时间使用体验红蓝分色 3D用颜色通道分离左右眼画面很低一副纸眼镜即可低普通屏幕即可运行画面偏色明显容易疲劳偏振 3D两束偏振光分别进入左右眼需要偏振屏幕和眼镜较高普通显示器不支持色彩还原好但亮度会降低快门式 3D左右眼交替显示液晶眼镜交替遮挡需要 120Hz 显示器和同步眼镜较高需要精确同步刷新率不足时闪烁明显VR 头显两块独立屏幕分别显示左右眼画面很高需要完整的 VR 渲染管线沉浸感强但硬件门槛高红蓝 3D 的缺点很明显色彩失真、长期佩戴不适。但它的优点也很直接开发门槛低、运行环境要求低、分享传播方便。对于一个小型创意 demo 来说红蓝 3D 是最容易实现的立体视觉方案。2. 环境准备与版本说明2.1 运行环境这次 demo 我以 Unity 2021.3 LTS 为例使用内置渲染管线Built-in Render Pipeline。如果你使用 URP 或 HDRPShader 的语法和坐标空间会有差异需要额外适配。为了减少弯路建议新建工程时选择3D 模板而不是“通用渲染管线模板”。系统环境方面Windows、macOS 都可以只要能运行 Unity 编辑器就行。最终打包发布到 Windows 可执行程序或 Android 手机都可以。下面的代码不依赖平台特性。2.2 准备红蓝 3D 眼镜你不需要买很贵的设备某宝或者拼多多上搜索“红蓝 3D 眼镜”选择红色左镜片、青色右镜片的普通纸框眼镜即可。这里有一个容易踩坑的地方部分便宜眼镜的红色滤片颜色很深青色滤片偏蓝不同批次之间的透光率差别很大。后面调试颜色通道时如果发现左右眼画面亮度差异明显不要怀疑代码先换一副眼镜试一下。2.3 示例项目结构为了便于理解这个 demo 的项目结构非常简单Assets/ |-- Scenes/ | |-- Main.unity |-- Scripts/ | |-- GameController.cs | |-- GameView.cs | |-- PlayerController.cs | |-- EnemySpawner.cs | |-- Enemy.cs |-- Shaders/ | |-- RedEye.shader | |-- CyanEye.shader |-- Materials/ | |-- RedEyeMat.mat | |-- CyanEyeMat.mat |-- Prefabs/ | |-- Enemy.prefab实际开发时可以继续拆分目录这里保持最小结构方便复制脚本和对照路径。3. 核心原理与方案选型3.1 从“双眼视差”到“红蓝分离”在 Unity 中实现红蓝 3D 有两个问题需要解决如何生成左右眼两幅不同视角的画面。如何把两幅画面分别染成红色和青色并叠加到同一个屏幕上。第一个问题最简单的方式是使用两个 Camera左眼 Camera 放在主相机位置左侧一点右眼 Camera 放在主相机位置右侧一点它们分别渲染到两张 RenderTexture。位置偏移量通常在 0.1 到 0.3 之间模拟人眼的瞳距。第二个问题可以通过自定义 Shader 解决。左眼画面只保留红色通道右眼画面只保留绿色和蓝色通道。在 Shader 中核心代码只有一行// 左眼只保留红色通道 fixed4 c tex2D(_MainTex, i.uv); return fixed4(c.r, 0, 0, 1);// 右眼只保留绿色和蓝色通道也就是青色 fixed4 c tex2D(_MainTex, i.uv); return fixed4(0, c.g, c.b, 1);当这两层画面叠加在屏幕上时红蓝眼镜就会自动完成左右眼的画面分离。3.2 两条实现路线对比在开始写代码之前我先对比两条常用路线路线做法优点缺点单相机后处理只用一个相机渲染完成后在屏幕后处理 Shader 里做红蓝染色性能开销小实现代码少只有一个视角没有真正的双眼视差双相机 RenderTexture用两个相机分别代表左右眼各自渲染到纹理再叠加到 Canvas 上符合人眼视差原理立体感真实需要两倍场景渲染开销代码量稍多这个 demo 必须选择双相机 RenderTexture路线否则戴不戴 3D 眼镜只是画面颜色不同没有真实的纵深层次也就没有“戴上才能玩”的效果。3.3 整体架构整个游戏运行逻辑可以用一句话概括主相机只作为“参考相机”提供位置和参数左右眼相机负责把场景渲染到两张纹理上Canvas 上的两个 RawImage 分别显示这两张纹理并通过自定义材质分离红、青通道。层次关系如下GameController ├── Player 玩家可交互对象z0 ├── EnemySpawner 障碍生成器 ├── Main Camera 参考相机运行时禁用渲染 ├── Canvas │ ├── LeftEyeImage 红色画面 │ ├── RightEyeImage 青色画面 │ ├── ScoreText 得分文本 │ └── StateText 状态提示文本 └── Background 背景装饰多层 z4. 完整实战制作一个红蓝 3D 躲避小游戏4.1 创建项目与场景打开 Unity 编辑器新建一个 3D 项目。场景中默认会有一个 Main Camera把它的位置设置为(0, 0, -10)Rotation 保持(0, 0, 0)。这个相机在运行时会被禁用渲染只负责提供“参考视角”的位置和参数。左右眼 Camera 会自动创建不需要手动摆放 Camera 到场景中。然后创建核心物体物体类型位置说明GameController空物体(0, 0, 0)挂载游戏控制脚本PlayerSprite(0, -3, 0)玩家角色挂载碰撞体EnemySpawner空物体(0, 0, 0)挂载障碍生成脚本CanvasUI(0, 0, 0)用于叠加左右眼画面和 UI 文字4.2 创建玩家与障碍预制体玩家可以用一个简单的正方形 Sprite 表示。切换到 2D 项目不需要这里我们直接在 3D 场景中使用 SpriteRenderer。选择菜单GameObject - 2D Object - Sprite创建两个 Sprite一个作为 Player一个作为 Enemy 预制体。Player 需要挂载以下组件Rigidbody2DGravity Scale 设为 0。CircleCollider2D勾选 Is Trigger。PlayerController脚本。Tag 设置为Player。Enemy 预制体需要挂载Rigidbody2DGravity Scale 设为 0Body Type 设置为 Dynamic。BoxCollider2D勾选 Is Trigger。Enemy脚本。Tag 设置为Enemy。4.3 编写两个红蓝分离 Shader在Assets下新建Shaders文件夹然后创建RedEye.shader。Shader Custom/RedEye { Properties { _MainTex (Texture, 2D) white {} } SubShader { Tags { QueueTransparent RenderTypeTransparent } Pass { Blend One One ZWrite Off CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; sampler2D _MainTex; v2f vert (appdata v) { v2f o; o.vertex UnityObjectToClipPos(v.vertex); o.uv v.uv; return o; } fixed4 frag (v2f i) : SV_Target { fixed4 c tex2D(_MainTex, i.uv); return fixed4(c.r, 0, 0, 1); } ENDCG } } }再创建CyanEye.shader片元着色器部分改为fixed4 frag (v2f i) : SV_Target { fixed4 c tex2D(_MainTex, i.uv); return fixed4(0, c.g, c.b, 1); }这里有一个细节需要注意两个 RawImage 是上下重叠显示的所以 Shader 的混合模式必须使用Blend One One也就是加法混合。如果使用默认的Blend One Zero后绘制的青色图会完全覆盖红色图最终只能看到一种颜色。4.4 创建红蓝显示材质在Assets下新建Materials文件夹创建两个 MaterialRedEyeMatShader 选择Custom/RedEye。CyanEyeMatShader 选择Custom/CyanEye。这两个材质分别赋值给 Canvas 下的两个 RawImage。Canvas 结构如下Canvas (Screen Space - Overlay) ├── LeftEyeImage (RawImage) ├── RightEyeImage (RawImage) ├── ScoreText (Text) └── StateText (Text)两个 RawImage 的属性设置一致Anchor 设置为拉伸模式四个角都设置为(0, 0)和(1, 1)让图片铺满整个 Canvas。LeftEyeImage 的 Material 设置为RedEyeMat。RightEyeImage 的 Material 设置为CyanEyeMat。ScoreText 和 StateText 放在最后保证显示在最上层。所有 UI 组件的 Raycast Target 可以取消勾选避免遮挡后续交互。4.5 编写相机与 RenderTexture 管理脚本这是整个红蓝 3D 效果最核心的部分。新建GameView.cs并挂载到 GameController 上。using UnityEngine; using UnityEngine.UI; public class GameView : MonoBehaviour { [Header(参考相机)] public Camera sceneCamera; [Header(左右眼画面)] public RawImage leftEyeImage; public RawImage rightEyeImage; [Header(参数)] public float eyeDistance 0.2f; private Camera leftCamera; private Camera rightCamera; private RenderTexture leftRT; private RenderTexture rightRT; void Start() { if (sceneCamera null) { sceneCamera Camera.main; } CreateEyeCamera(ref leftCamera, ref leftRT, LeftEyeCamera); CreateEyeCamera(ref rightCamera, ref rightRT, RightEyeCamera); // 主相机只保留位置参数不再直接渲染到屏幕 sceneCamera.enabled false; } void LateUpdate() { // 左右眼相机沿场景相机右侧方向偏移 Vector3 leftPos sceneCamera.transform.position - sceneCamera.transform.right * (eyeDistance * 0.5f); Vector3 rightPos sceneCamera.transform.position sceneCamera.transform.right * (eyeDistance * 0.5f); leftCamera.transform.position leftPos; rightCamera.transform.position rightPos; leftCamera.transform.rotation sceneCamera.transform.rotation; rightCamera.transform.rotation sceneCamera.transform.rotation; // 把渲染纹理绑定到 RawImage if (leftEyeImage ! null leftEyeImage.texture ! leftRT) { leftEyeImage.texture leftRT; } if (rightEyeImage ! null rightEyeImage.texture ! rightRT) { rightEyeImage.texture rightRT; } } void CreateEyeCamera(ref Camera cam, ref RenderTexture rt, string goName) { GameObject go new GameObject(goName); cam go.AddComponentCamera(); cam.clearFlags CameraClearFlags.SolidColor; cam.backgroundColor Color.black; cam.depth -10; cam.cullingMask sceneCamera.cullingMask; // 复制参考相机的投影参数 if (sceneCamera.orthographic) { cam.orthographic true; cam.orthographicSize sceneCamera.orthographicSize; } else { cam.orthographic false; cam.fieldOfView sceneCamera.fieldOfView; } int w Mathf.Max(2, Screen.width / 2); int h Mathf.Max(2, Screen.height / 2); rt new RenderTexture(w, h, 24); rt.name goName _RT; cam.targetTexture rt; } void OnDestroy() { if (leftRT ! null) { leftRT.Release(); } if (rightRT ! null) { rightRT.Release(); } } }这里的重点有四个左右眼相机是运行时动态创建的不需要手动在场景里摆放。eyeDistance模拟瞳距数值越大立体感越强但太大容易导致重影。RenderTexture 分辨率取屏幕的一半用于平衡清晰度和性能。主相机被禁用但它的transform仍然可以作为左右眼相机的基准位置。如果你希望立体感更明显可以把eyeDistance调到0.3到0.5。如果画面看起来太晕就调回0.1。4.6 编写玩家控制脚本新建PlayerController.cs挂载到 Player 物体上。using UnityEngine; public class PlayerController : MonoBehaviour { public float moveSpeed 6f; public float xLimit 2.5f; private bool isGameOver false; void Update() { if (isGameOver) { return; } float horizontal Input.GetAxis(Horizontal); Vector3 pos transform.position; pos.x horizontal * moveSpeed * Time.deltaTime; pos.x Mathf.Clamp(pos.x, -xLimit, xLimit); transform.position pos; } public void SetGameOver(bool value) { isGameOver value; } }这个脚本控制玩家左右移动。这里没有使用Rigidbody2D.velocity是因为 2D 躲避小游戏的移动逻辑比较简单直接修改transform.position更直观。需要注意的是这里只限制 X 轴方向Y 轴和 Z 轴位置保持不变这样玩家的碰撞判定始终固定在z 0平面避免左右眼视差导致“视觉位置”和“碰撞位置”不一致。4.7 编写障碍生成脚本新建EnemySpawner.cs挂载到 EnemySpawner 空物体上。using System.Collections; using UnityEngine; public class EnemySpawner : MonoBehaviour { public GameObject enemyPrefab; public float spawnInterval 1f; public float fallSpeed 3f; private bool isSpawning true; void Start() { StartCoroutine(SpawnLoop()); } IEnumerator SpawnLoop() { while (isSpawning) { SpawnOne(); yield return new WaitForSeconds(spawnInterval); } } void SpawnOne() { GameObject go Instantiate(enemyPrefab, transform); float randomX Random.Range(-2.2f, 2.2f); go.transform.position new Vector3(randomX, 5f, 0f); go.GetComponentRigidbody2D().velocity new Vector2(0, -fallSpeed); } public void StopSpawn() { isSpawning false; } }这里的障碍物体 z 轴固定为0和玩家一致。所有可碰撞对象都在同一个平面上2D 碰撞体才能正常工作。如果想增加视觉层次可以把背景装饰物放在不同的 z 轴上比如远处背景放在z 8近处贴图放在z -3。这样戴上眼镜后背景和游戏层会有明显的远近分离。4.8 编写障碍与计分逻辑新建Enemy.cs挂载到 Enemy 预制体上。using UnityEngine; public class Enemy : MonoBehaviour { private bool scored false; void OnTriggerEnter2D(Collider2D other) { if (other.CompareTag(Player)) { GameController.Instance.EndGame(); } } void Update() { if (transform.position.y -6f !scored) { scored true; GameController.Instance.AddScore(); Destroy(gameObject); } } }新建GameController.cs挂载到 GameController 空物体上。using UnityEngine; using UnityEngine.UI; public class GameController : MonoBehaviour { public static GameController Instance { get; private set; } public PlayerController player; public EnemySpawner spawner; public Text scoreText; public Text stateText; private int score; void Awake() { Instance this; score 0; UpdateScore(); } public void AddScore() { score; UpdateScore(); } public void EndGame() { spawner.StopSpawn(); player.SetGameOver(true); stateText.text Game Over重新运行场景再来一局; } void UpdateScore() { if (scoreText ! null) { scoreText.text 得分: score; } } }运行场景后玩家可以按 A/D 或方向键左右移动躲避从上方掉落的方块。掉落物到底部会自动加分碰到玩家则游戏结束。4.9 运行与验证运行场景后从 Game 视图观察画面不戴眼镜时画面是红、青叠加的重影明显。戴上红蓝眼镜后场景会变成比较正常的分层彩色画面。背景层和游戏层之间有明显纵深得分文字显示在顶层。如果画面看起来全红或全青只需要检查两个 RawImage 的材质是否分别使用了RedEyeMat和CyanEyeMat。5. 常见问题与排查思路5.1 常见现象表问题现象常见原因解决思路画面只有红色或只有青色两个 RawImage 的材质被设置成了同一个检查 LeftEyeImage 和 RightEyeImage 的 Material 是否分别对应红、青画面黑色没有任何游戏内容左右眼 Camera 没有渲染或主相机被禁用后场景无输出检查左右眼 Camera 的 enable 状态和 RenderTexture 是否创建成功戴上眼镜后重影很大eyeDistance数值太大把eyeDistance降到 0.10.3游戏层之间没有立体感所有物体都在同一个 z 平面上把背景、装饰、可碰撞层分层放置UI 文字模糊或拉伸RawImage 的 RenderTexture 分辨率过低提高 RenderTexture 分辨率或按屏幕宽高比创建手机帧率低双相机渲染两张全屏 RT开销较大降低 RT 分辨率限制物体数量使用对象池颜色偏差严重青画面很暗某些红蓝眼镜滤片浓度不一致换一副标准红蓝眼镜或在 Shader 里微调颜色权重5.2 黑屏问题排查顺序如果运行后看不到任何内容按照下面的顺序排查确认sceneCamera在 GameView 组件中被正确引用。确认两个 RawImage 的 Texture 在运行时被赋值。确认两个 RawImage 的材质不是默认材质。确认左右眼 Camera 的cullingMask包含了游戏物体所在的 Layer。把sceneCamera.enabled false;这一行注释掉临时观察主相机是否能看到场景。5.3 颜色校准调整不同厂家生产的红蓝眼镜透光率差异很大。如果戴上去后左眼和右眼看到的亮度不一致可以在 Shader 中增加权重参数。例如红色通道保留时额外保留一点绿色return fixed4(c.r * 1.0, c.g * 0.05, c.b * 0.05, 1.0);青色通道保留时也可以根据实际情况增加一点红色补偿。这类微调没有标准答案需要结合你手头眼镜的实际透光效果来测试。6. 最佳实践与工程建议6.1 视差参数管理不要把eyeDistance写死在代码里最好做成 GameView 上的可序列化字段方便在 Inspector 中调试。设计上还要区分“近距离物体”和“远距离物体”可交互物体固定在同一个 z 平面避免 2D 碰撞位置与视觉重影冲突。背景、装饰、粒子效果放在不同 z 平面产生景深。不要给 UI 文字添加视差UI 始终作为最顶层平面。立体感的强弱不是只看相机偏移量还和场景中物体的 z 值分布有关。在透视相机下距离相机越近的物体左右眼视差越大距离越远的物体视差越小。合理拉开 z 值能让场景层次更明显。6.2 UI 和文字处理红蓝画面叠加之后如果文字直接放在 RawImage 之下会被红青色调干扰。建议所有 UI 元素放在 Canvas 下、两个 RawImage 之后保证绘制顺序在最上层。文字使用白色或纯色不使用半透明材质。避免给 Text 添加阴影或描边效果红蓝眼镜下这些效果容易产生伪影。用GraphicRaycaster时取消 RawImage 的 Raycast Target防止画面层拦截点击事件。6.3 性能优化方向双相机意味着场景会被渲染两次虽然 demo 中物体数量少看不出来性能问题但正式项目必须注意RenderTexture 分辨率不必等于屏幕分辨率使用一半分辨率往往就够了。左右眼 Camera 只渲染必要的 LayerUI、透明特效不要进入 cullingMask。障碍物和生产对象使用对象池避免频繁 Instantiate 和 Destroy 造成 GC 压力。如果目标平台是手机可以限制同屏物体数量关闭抗锯齿或降低阴影质量。不要盲目使用Camera.Render()手动渲染让它自动渲染即可手动调用会导致一帧内重复渲染。6.4 玩法和安全体验红蓝 3D 游戏不适合长时间游玩长时间佩戴容易出现眼睛疲劳。作为游戏设计者应该在游戏开始前给出提示建议每玩 15 分钟休息一次。遇到头晕、眼睛干涩时立即停止。儿童需要在成年人陪同下使用。红绿色弱用户可能无法正常体验可以考虑增加“普通模式”开关。这些不只是一句文案更是游戏内容里的基本体验保障。7. 总结与学习路线这篇教程实现了一个完整的红蓝 3D 小游戏 demo代码和配置都可以直接运行。核心收获有三点理解红蓝 3D 眼镜的颜色分离原理知道“左右眼看到不同图像”才是立体感的来源。掌握 Unity 中双相机渲染、RenderTexture、RawImage 叠加显示这套组合用法。能自己通过调整相机偏移和场景 z 轴深度控制立体感的强弱。如果你想继续深入可以尝试以下几个方向把 2D 玩家和障碍层改成真正的 3D 模型让障碍物本身拥有立体厚度。在 Shader 中增加颜色校准参数适配不同品牌的 3D 眼镜。加入对偏振 3D 或者 VR 设备的适配把同一套视差渲染逻辑迁移到其他平台。做一个简单的立体拼图或找茬游戏利用左右眼视觉差设计核心玩法。红蓝 3D 虽然不是什么新技术但在个人项目和创意游戏 demo 里依然是一个很有性价比的亮点。戴上眼镜的那一刻看到场景真实地“分开”再“合上”那种直观的沉浸感是普通 2D 游戏很难带来的。如果你也按照今天的步骤做出来了自己的红蓝 3D 小游戏可以试试把眼距调到 0.3再对比一下和 0.1 时的立体感差异。参数调校的过程本身就是对双目视差最好的理解方式。