Cg 编程/Unity/镜子
本教程介绍了平面镜的渲染。
它不需要任何着色器编程(除非你想将其用于立体渲染),但它确实需要对“顶点变换”部分和“纹理球体”部分中讨论的纹理化有所了解。
在计算机图形学中,有各种渲染平面镜的方法。如果可以渲染到纹理,最常见的方法包括以下步骤:
- 在镜子上镜像主摄像机的位置,并将“镜子摄像机”放置在镜像位置的镜子后面。
- 从镜子摄像机的视角渲染场景,使用镜面作为视平面。将此图像渲染到渲染纹理中。
- 在使用主摄像机渲染场景时,将渲染纹理用作镜子的纹理。
这是基本方法。让我们来实现它。
第一个问题是如何获得主摄像机的位置。对于单眼摄像机,这仅仅是摄像机的transform.position
。对于立体摄像机,我们必须决定使用右眼摄像机的位置还是左眼摄像机的位置。我们可以使用以下代码获取mainCamera
的右眼视图矩阵:
Matrix4x4 viewMatrix = mainCamera.GetStereoViewMatrix (Camera.StereoscopicEye.Right);
以及左眼视图矩阵:
Matrix4x4 viewMatrix = mainCamera.GetStereoViewMatrix (Camera.StereoscopicEye.Left);
视图矩阵的逆矩阵将摄像机坐标系原点转换为第四列的向量(如果从 0 开始计数,则为第三列)。由于摄像机坐标系原点是摄像机的位置,并且视图矩阵的逆矩阵将摄像机坐标转换为世界坐标,因此视图矩阵的逆矩阵的第四列指定了摄像机在世界坐标系中的位置。如果视图矩阵存储在viewMatrix
中,我们可以使用以下代码获得此向量:
mainCameraPosition = viewMatrix.inverse.GetColumn (3);
可以通过多种方式在平面上镜像主摄像机的位置。在 Unity 中,一种简单的方法是将主摄像机的位置转换为四边形游戏对象的对象坐标系。然后,可以通过更改其坐标的符号来轻松镜像此位置。然后将此镜像位置转换回世界空间。
以下是一个实现此过程的 C# 代码:
// This script should be called "SetMirroredPosition"
// and should be attached to a Camera object
// in Unity which acts as a mirror camera behind a
// mirror. Once a Quad object is specified as the
// "mirrorQuad" and a "mainCamera" is set, the script
// computes the mirrored position of the "mainCamera"
// and places the script's camera at that position.
using UnityEngine;
[ExecuteInEditMode]
public class SetMirroredPosition : MonoBehaviour {
public GameObject mirrorQuad;
public Camera mainCamera;
public bool isMainCameraStereo;
public bool useRightEye;
void LateUpdate () {
if (null != mirrorQuad && null != mainCamera &&
null != mainCamera.GetComponent<Camera> ()) {
Vector3 mainCameraPosition;
if (!isMainCameraStereo) {
mainCameraPosition = mainCamera.transform.position;
} else {
Matrix4x4 viewMatrix = mainCamera.GetStereoViewMatrix (
useRightEye ? Camera.StereoscopicEye.Right :
Camera.StereoscopicEye.Left);
mainCameraPosition = viewMatrix.inverse.GetColumn (3);
}
Vector3 positionInMirrorSpace =
mirrorQuad.transform.InverseTransformPoint (mainCameraPosition);
positionInMirrorSpace.z = -positionInMirrorSpace.z;
transform.position =
mirrorQuad.transform.TransformPoint (
positionInMirrorSpace);
}
}
}
该脚本应附加到一个新的摄像机对象(在主菜单中选择游戏对象 > 摄像机)并命名为SetMirroredPosition.cs
。mirrorQuad
应设置为一个表示镜子的四边形游戏对象,mainCamera
应设置为主游戏摄像机。
为了使用mirrorQuad
作为视平面,我们可以使用“虚拟现实投影”部分中的脚本,该脚本应附加到我们新的镜子摄像机。在该脚本中包含[ExecuteInEditMode]
行,使其在编辑器中运行。确保选中setNearClipPlane
,以便进入镜子的物体被裁剪。如果在物体与镜面相交处存在伪影,请减小参数nearClipDistanceOffset
,直到这些伪影消失。
为了将镜子摄像机的渲染图像存储在渲染纹理中,请通过在项目窗口中选择创建 > 渲染纹理来创建一个新的渲染纹理。在检查器窗口中,确保渲染纹理的大小不要太小(否则镜子中的图像将显得像素化)。获得渲染纹理后,选择镜子摄像机,并在检查器窗口中将目标纹理设置为新的渲染纹理。现在,你应该能够在渲染纹理的检查器窗口中看到镜子中的图像。
为了将渲染纹理用作镜子的纹理图像,请将带有纹理的着色器应用于镜子四边形,例如,标准着色器或 Unlit/Texture 着色器。像使用任何其他纹理对象一样使用渲染纹理进行纹理化。请注意,你可能需要旋转镜子四边形,使其正面朝向主摄像机可见。默认情况下,纹理图像将水平镜像。但是,使用纹理的着色器属性有一个简单的解决方法:将平铺的X坐标设置为-1
,将偏移的X坐标设置为1
。
对于立体渲染,我们需要两个镜像摄像机:一个用于左眼,一个用于右眼。我们还需要每个眼睛一个渲染纹理。镜子的纹理化必须确保每个眼睛访问其对应的渲染纹理。为此,Unity 提供了一个内置的着色器变量unity_StereoEyeIndex
,它对于左眼为 0,对于右眼为 1。
一个基本的着色器,它接受两种纹理并返回当前渲染眼睛的纹理颜色,可能看起来像这样:
Shader "BasicStereoTexture"
{
Properties
{
_LeftTex ("Left Texture", 2D) = "white" {}
_RightTex ("Right Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct vertexInput
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct vertexOutput
{
float2 uvLeft : TEXCOORD0;
float2 uvRight : TEXCOORD1;
float4 vertex : SV_POSITION;
};
sampler2D _LeftTex;
uniform float4 _LeftTex_ST;
sampler2D _RightTex;
uniform float4 _RightTex_ST;
vertexOutput vert (vertexInput i)
{
vertexOutput o;
o.vertex = UnityObjectToClipPos(i.vertex);
o.uvLeft = TRANSFORM_TEX(i.uv, _LeftTex);
o.uvRight = TRANSFORM_TEX(i.uv, _RightTex);
return o;
}
float4 frag (vertexOutput i) : SV_Target
{
return lerp(tex2D(_LeftTex, i.uvLeft),
tex2D(_RightTex, i.uvRight),
unity_StereoEyeIndex);
}
ENDCG
}
}
FallBack "Diffuse"
}
Shader "StereoTexture"
{
Properties
{
_LeftTex ("Left Texture", 2D) = "white" {}
_RightTex ("Right Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
CGPROGRAM
#pragma surface surf Standard
uniform sampler2D _LeftTex;
uniform sampler2D _RightTex;
struct Input
{
float2 uv_LeftTex;
float2 uv_RightTex;
};
void surf (Input IN, inout SurfaceOutputStandard o)
{
fixed4 c = lerp(tex2D(_LeftTex, IN.uv_LeftTex),
tex2D(_RightTex, IN.uv_RightTex),
unity_StereoEyeIndex);
o.Emission = c.rgb;
}
ENDCG
}
FallBack "Diffuse"
}
此实现有几个我们尚未解决的局限性。例如:
- 多个镜子(你可能需要为所有镜子共享相同的渲染纹理)
- 多个镜子中的多次反射(这很复杂,因为你需要呈指数增长的镜子摄像机数量)
- 镜面中的光线反射(每个光源都应该有一个镜像伴侣,以考虑首先在镜面中反射然后照亮物体的光线)
- 不均匀的镜子(例如,使用法线贴图)
- 等等。
恭喜!干得好。我们已经了解了一些内容:
- 如何通过将位置转换为平面的对象坐标系并更改对象坐标的符号来在平面上镜像位置。
- 如何将摄像机视图渲染到渲染纹理中。
- 如何使用镜像的渲染纹理进行纹理化。
如果你想了解更多
- 有关使用模板缓冲区渲染镜子的信息,可以阅读 Tom McReynolds 组织的 SIGGRAPH '98 课程“使用 OpenGL 的高级图形编程技术”的第 9.3.1 节,该课程可以在网上获得。