游戏内置相机模式与景深虚化算法:中秋合影功能实现
在中秋节日活动或大型社交版本上线期间,“拍照模式”(Photo Mode / 相机合影功能)是激发玩家社交传播、提高游戏活跃度的核心功能之一。玩家希望在明月中秋之夜,与帮派好友或 NPC 角色并肩而立,自由调节焦距、光圈、旋转构图,拍出具有单反级大光圈虚化(Bokeh Depth of Field)与节日氛围感的高清游戏截图。
很多项目的内置拍照功能往往只是简单地隐藏 UI 并调用ScreenCapture.CaptureScreenshot(),导致拍出来的照片缺乏层次感与光学真实感。在引擎渲染管线中实现一套高质量的实时景深虚化(DoF)与单反相机物理参数模拟系统,是提升游戏视觉质感的关键环节。
光学物理模型与弥散圆(Circle of Confusion, CoC)计算
真实单反相机的景深效果由薄透镜成像定律(Thin Lens Equation)决定。当物体不处于对焦平面(Focus Plane)时,物点在传感器上成像为一个圆形光斑,称为弥散圆(Circle of Confusion, CoC)。
CoC 的物理直径 $c$ 可通过焦距 $f$、光圈数 $N$(F-stop)、对焦距离 $d_{\text{focus}}$ 以及物体实际深度 $z$ 计算得出:
$$c = \frac{f^2}{N \cdot (d_{\text{focus}} - f)} \cdot \frac{|z - d_{\text{focus}}|}{z}$$
在实时着色器中,我们通常将该物理公式转化为屏幕归一化坐标下的 CoC 半径值,并将景深解耦为**近景虚化区(Near Field)与远景虚化区(Far Field)**分别计算,以防止前景物体的颜色错误溢出到背景深色区域(Foreground Bleeding Artifacts)。
// HLSL: 物理相机 CoC 计算 Pass (CameraCoC.hlsl) #ifndef PHYSICAL_CAMERA_COC_INCLUDED #define PHYSICAL_CAMERA_COC_INCLUDED #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" Texture2D _CameraDepthTexture; SamplerState sampler_CameraDepthTexture; CBUFFER_START(PhysicalCameraParams) float _FocalLength; // 镜头焦距 (米,如 0.05 代表 50mm) float _ApertureFStop; // 光圈 F 值 (如 1.4, 2.8) float _FocusDistance; // 对焦距离 (米) float _MaxCoCRadius; // 最大弥散圆像素半径 float4 _ZBufferParamsCopy; CBUFFER_END // 计算线性真实深度 (米) float GetLinearDepth(float rawDepth) { return LinearEyeDepth(rawDepth, _ZBufferParamsCopy); } // 片元着色器:输出 CoC 因子到 R16G16F 纹理 (x: 远景 CoC, y: 近景 CoC) float2 FragCalculateCoC(float2 uv : TEXCOORD0) : SV_Target { float rawDepth = _CameraDepthTexture.SampleLevel(sampler_CameraDepthTexture, uv, 0).r; float z = GetLinearDepth(rawDepth); // 计算透镜孔径直径 A = f / N float apertureDiameter = _FocalLength / _ApertureFStop; // 计算弥散圆直径 (薄透镜近似公式) float coc = (apertureDiameter * _FocalLength * abs(z - _FocusDistance)) / (_FocusDistance * (z - _FocalLength) + 1e-5); // 映射至屏幕像素空间 float pixelCoC = (coc / 0.036) * _ScreenParams.x; // 假设 36mm 全画幅传感器底片 pixelCoC = clamp(pixelCoC, 0.0, _MaxCoCRadius); float farCoC = 0.0; float nearCoC = 0.0; if (z > _FocusDistance) { // 远景虚化 farCoC = pixelCoC; } else { // 近景虚化 nearCoC = pixelCoC; } return float2(farCoC, nearCoC); } #endif多边形光斑模拟(Gather-based Circular / Hexagonal Bokeh Blur)
传统的双向高斯模糊虽然计算快速,但只能产生雾状的平滑模糊,完全失去了大光圈下明亮光源形成的晶莹剔透的多边形/圆形光斑(Bokeh)。
高质量景深通常采用基于收集(Gather-based)的圆盘或六边形泊松采样(Poisson Disk Sampling / Golden Spiral)。在片元着色器中,对采样点的 HDR 颜色进行亮度加权(Highlight Boost),使得高光区域能够放大为璀璨的节日光斑。
// HLSL: 高质量圆盘光斑收集模糊 Pass #define SAMPLE_COUNT 32 static const float2 GoldenSpiralSamples[SAMPLE_COUNT] = { float2(0.000, 0.000), float2(0.125, 0.216), float2(-0.216, 0.125), float2(0.245, -0.245), float2(-0.353, -0.204), float2(0.380, 0.285), // ... 黄金螺旋泊松圆盘采样点分布 float2(-0.707, 0.707), float2(0.866, -0.500), float2(-0.950, -0.312) }; Texture2D _MainTex; SamplerState sampler_MainTex; Texture2D _CoCTexture; SamplerState sampler_CoCTexture; float4 FragGatherBokeh(float2 uv : TEXCOORD0) : SV_Target { float2 centerCoC = _CoCTexture.SampleLevel(sampler_CoCTexture, uv, 0).xy; float effectiveCoC = max(centerCoC.x, centerCoC.y); if (effectiveCoC < 0.5) { // 焦点清晰区域,直接返回原图,跳过昂贵的循环采样 return _MainTex.SampleLevel(sampler_MainTex, uv, 0); } float4 accColor = 0; float totalWeight = 0; [unroll(SAMPLE_COUNT)] for (int i = 0; i < SAMPLE_COUNT; ++i) { float2 offset = GoldenSpiralSamples[i] * effectiveCoC * _ScreenParams.zw; float2 sampleUV = uv + offset; float4 color = _MainTex.SampleLevel(sampler_MainTex, sampleUV, 0); float2 sampleCoC = _CoCTexture.SampleLevel(sampler_CoCTexture, sampleUV, 0).xy; // 深度权重混合,抑制近景向远景的溢色瑕疵 float sampleRadius = max(sampleCoC.x, sampleCoC.y); float weight = saturate((sampleRadius - length(offset * _ScreenParams.xy)) + 1.0); // HDR 高光增益,突出中秋月光与灯笼的晶莹光斑 float luminance = dot(color.rgb, float3(0.2126, 0.7152, 0.0722)); float highlightFactor = 1.0 + pow(saturate(luminance - 0.8), 2.0) * 5.0; accColor += color * (weight * highlightFactor); totalWeight += weight * highlightFactor; } return accColor / (totalWeight + 1e-4); }相机控制器与高分辨率截屏管线设计
为了给玩家提供类似专业相机的操控体验,客户端封装了完整的对焦测距射线检测(Tap-to-Focus)与无 UI 纯净超清截屏管线。
using UnityEngine; using System.IO; public class GamePhotoStudioController : MonoBehaviour { [Header("相机镜头硬件参数")] [SerializeField] private Camera photoCamera; [Range(0.024f, 0.200f)] [SerializeField] private float focalLength = 0.085f; // 85mm 人像黄金焦段 [Range(1.2f, 16.0f)] [SerializeField] private float apertureFStop = 1.8f; // F1.8 大光圈 [SerializeField] private float focusDistance = 3.5f; [Header("滤镜与后处理")] [SerializeField] private Material dofMaterial; void Update() { // 玩家点击屏幕自动对焦 (Tap to Focus) if (Input.GetMouseButtonDown(0)) { Ray ray = photoCamera.ScreenPointToRay(Input.mousePosition); if (Physics.Raycast(ray, out RaycastHit hit, 100.0f)) { focusDistance = Vector3.Distance(photoCamera.transform.position, hit.point); UpdateCameraParameters(); } } } public void UpdateCameraParameters() { dofMaterial.SetFloat("_FocalLength", focalLength); dofMaterial.SetFloat("_ApertureFStop", apertureFStop); dofMaterial.SetFloat("_FocusDistance", focusDistance); } public void CaptureUltraHDPhoto(int superSamplingMultiplier, string saveFileName) { int width = Screen.width * superSamplingMultiplier; int height = Screen.height * superSamplingMultiplier; var rt = RenderTexture.GetTemporary(width, height, 24, RenderTextureFormat.ARGB32); photoCamera.targetTexture = rt; photoCamera.Render(); RenderTexture.active = rt; var screenshot = new Texture2D(width, height, TextureFormat.RGB24, false); screenshot.ReadPixels(new Rect(0, 0, width, height), 0, 0); screenshot.Apply(); photoCamera.targetTexture = null; RenderTexture.active = null; RenderTexture.ReleaseTemporary(rt); // 异步将截屏编码并保存到本地相册 byte[] bytes = screenshot.EncodeToPNG(); Destroy(screenshot); File.WriteAllBytes(saveFileName, bytes); Debug.Log($"[PhotoMode] UltraHD Photo saved to {saveFileName}"); } }在中秋活动场景中,通过将 85mm 人像长焦镜头模型、F1.8 大光圈虚化以及圆盘光斑算法深度集成,玩家拍摄中秋月下合影时,远处的满月与灯笼能够自然化作如梦似幻的圆形光斑,而近处的人物发丝与服饰细节清晰锐利,大幅提升了玩家在社交媒体上的分享欲望与画面口碑。