这篇博文是为了补充前面遗漏的细节技术,之前我们学习了图形流水线(渲染流程),我们在CG shader代码中常用的图形流程就处理到了MVP变换,也就是只处理了顶点源坐标变换到裁剪空间的过程,但是后续裁剪空间到各个不同显示设备空间的过程被忽略了,这是因为nvidiaCG觉得裁剪空间到全球各个厂商的屏幕设备空间这个变换过程是繁琐的,这个操作过程最好被封装于开发者实现着色器之后,让开发者尽可能专注于自己着色特效的研究。
但是特殊情况下,让我们不得不考虑这个问题,因为裁剪空间到当前屏幕空间这个变换能帮我们实现一些好玩的着色效果。
比如前面我写的实时反射,模拟一面镜面的技术实现,裁剪空间到屏幕空间的变换就是重要的实现环节之一,下面我来画图依次说明一下:
①.获取当前眼睛渲染场景纹理关于镜面的对称纹理。
原本正常渲染的场景画面经过对称变换后变成后面的纹理图案,当然树木超出屏幕长宽应该被截取掉的,我只是绘制保留了。
②.对三维裁剪空间中镜面网格顶点进行裁剪空间到屏幕设备空间的坐标变换,得到三维裁剪空间镜面网格顶点在二维屏幕设备中的坐标。
这就是正视图和侧视图下三维裁剪空间中镜面网格顶点到二维屏幕空间坐标点的示意图了,裁剪空间是一个2*2*2的立方体,裁剪空间变换到屏幕空间,xy[-1,1]坐标分量要处理到屏幕x[0,1920]y[0,1080]像素值的(假设我用的1080p的显示器),z[-1,1]则变为了depth[0,256]深度值。
③.获取到三维裁剪空间中镜面网格顶点在二维屏幕平面中的坐标后,根据坐标与屏幕分辨率的比例进行采样,采样目标就是①中的对称渲染纹理,那么我们就得到了镜面在屏幕平面中那块区域对对称纹理采样的图像,就完美的在三维裁剪空间中的镜面上显示出了对称纹理,就完成了我们的镜面反射技术。
根据镜面在二维平面的顶点坐标进行对称纹理采样,那么镜面采样的对称纹理局部纹理就和mainCamera主摄像机渲染的画面纹理相结合,就产生了镜面反射效果。
这就是之前真实反射(镜面反射)的实现原理,其中两个关键计算:
①.三维空间顶点在平面的对称点计算矩阵(前面我们推导过)
②.裁剪空间到屏幕空间的变换(这个也简单,上面第二张图就比较形象了,一个[-1,1]到[0,设备像素长度]的比例计算)
接下来就让我们看下unity CG如何帮我们处理裁剪空间到屏幕空间的变换,UnityCG.cginc和UnityShaderVariables.cginc内置代码如下:
来详细分解一下(暂时不考虑UNITY_SINGLE_PASS_STEREO,这是一种VR双屏渲染的情况,以后做VR着色实现再来讲解):
①._ProjectionParams.x = 1 or -1 (如果使用了翻转投影则为-1)
②.ComputeNonStereoScreenPos(float4 mvpPos);
float4 o = mvpPos* 0.5f; //首先mvpPos的(x,y,z)的齐次坐标形式<xw,yw,zw,w>,同时mvpPos(x,y,z)处于[-1,-1,-1]到[1,1,1]的2*2*2裁剪立方空间中,那么mvpPos的齐次坐标xw,yw,zw分量处于[-w]到[w]之间,在*0.5f得到新float4 o的x,y,z份量则处于[-0.5w]到[0.5w]之间,而o.w = 0.5*mvpPos.w。
o.xy = float2(o.x, o.y*_ProjectionParams.x) + o.w; //o.y首先要处理是否翻转的情况,然后o.x和o.y分别+o.w平移到[0,w]的区间。
o.zw = mvpPos.zw; //o.zw保持mvpPos.zw不变
这么看来,实际上ComputeScreenPos(float4 mvpPos);并没有跟我想象的一样直接计算出mvp变换后的裁剪空间顶点在屏幕设备空间中xy像素值,而是将xy归w化到[0,w]区间,z不变[-w,w],w不变。unity CG这么做的目的是保留w分量的同时可以/w进行归一化,方便我们进行采样等计算。
这里顺便再来看一下这个字段:
_ScreenParams的xy储存了当前显示分辨率,我们用这个参数处理一下ComputeScreenPos返回的值就能得到裁剪空间顶点在屏幕上真正的像素坐标了。
写到这里,我们基本上已经知道ComputeScreenPos这个函数的关键作用了,下面我们就来通过一个CG着色例子来看下能达到什么效果,如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TransparentEffect : MonoBehaviour
{
public MeshFilter mMeshFilter;
public MeshRenderer mMeshRender;
public Camera mEyeCamera;
private Mesh mMesh;
private int xCount = 60;
private int yCount = 30;
private float mCellLen = 1f;
void Start()
{
genPerfectRenderTarget();
genRectangleMesh();
}
//创建适合分辨率的eyeCamera渲染贴图
private void genPerfectRenderTarget()
{
RenderTexture rt = new RenderTexture(mEyeCamera.pixelWidth, mEyeCamera.pixelHeight, 24);
mEyeCamera.targetTexture = rt;
}
//创建一个rectange网格
//圆前面已经讲过,这里不在过多讲解
private void genRectangleMesh()
{
//构建一个任意单位长宽的长方形
mMesh = new Mesh();
int xPointCount = xCount + 1; //x轴网格点数量
int yPointCount = yCount + 1; //y轴网格点数量
int xyMeshPointCount = xPointCount * yPointCount; //网格顶点的数量
int triangleCount = xCount * yCount * 2; //三角面数量(小正方形的两倍)
//构建mesh网格的所有信息数组
Vector3[] vertices = new Vector3[xyMeshPointCount];
int[] triangles = new int[triangleCount * 3];
Vector2[] uvs = new Vector2[xyMeshPointCount];
//记录拓扑信息循环的间隔
int triangleIndex = 0;
for (int x = 0; x < xPointCount; x++)
{
for (int y = 0; y < yPointCount; y++)
{
int index = x + y * xPointCount;
vertices[index] = new Vector3((xCount - x) * mCellLen, (yCount - y) * mCellLen, 0);
if (x < xCount && y < yCount)
{
//这里就是拓扑信息的循环计算,结合绘画的拓扑信息图算一下
triangles[triangleIndex] = x + y * xPointCount;
triangles[triangleIndex + 1] = x + (y + 1) * xPointCount;
triangles[triangleIndex + 2] = x + (y + 1) * xPointCount + 1;
triangles[triangleIndex + 3] = x + y * xPointCount;
triangles[triangleIndex + 4] = x + (y + 1) * xPointCount + 1;
triangles[triangleIndex + 5] = x + y * xPointCount + 1;
triangleIndex += 6;
}
uvs[index] = new Vector2((float)x / (float)xCount, (float)y / (float)yCount);
}
}
mMesh.vertices = vertices;
mMesh.triangles = triangles;
mMesh.uv = uvs;
mMeshFilter.mesh = mMesh;
}
void Update()
{
//这个函数标识了反转剔除模式
//因为eyeCamera相机渲染被我用了对称矩阵修改了顶点
//但是法向量没有修改,所以在计算机图形处理中会导致错误
//只能反向剔除再render才能达到正确的现实
GL.invertCulling = true;
mEyeCamera.Render();
GL.invertCulling = false;
mMeshRender.sharedMaterial.SetTexture("_ReflTex", mEyeCamera.targetTexture);
}
}
Shader "Unlit/TransparentEffectUnlitShader"
{
Properties
{
_ReflTex("ReflTexture",2D) = "white" {}
_Speed("Speed",Range(0.1,100)) = 0.5
_Range("Range",Range(0.1,10)) = 0.1
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Cull back
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
};
struct v2f
{
float4 vertex : SV_POSITION;
float4 screenPos : TEXCOORD1;
};
sampler2D _ReflTex; //eyeCamera渲染纹理
float _Speed; //波动速度
float _Range; //波动幅度
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
//使用ComputeScreenPos获取mvpPos在屏幕空间中的齐次坐标
o.screenPos = ComputeScreenPos(o.vertex);
//这里注意,我们先计算完screenPos后再进行顶点乱序三角函数波动
float angle = _Speed*_Time*(o.vertex.x + o.vertex.y + o.vertex.z);
o.vertex += _Range*sin(angle);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
//采样eyeCamera渲染纹理
fixed4 col = tex2D(_ReflTex, i.screenPos.xy / i.screenPos.w);
return col;
}
ENDCG
}
}
}
效果图如下:
下半部的矩形,跟一张扭动的投影布片一样,这效果咋一看也没什么用,还不如上一篇镜面反射的作用大,这里无非就是告诉大家,裁剪空间到屏幕空间这一步很容易让人忽略的变换,也可以拿来做一些效果,后面来点比较实际的。
so,我们接下来继续。