unity 2d摄像机类型_实时渲染中的坐标系变换(1):2D变换基础

4de9a572c71632ffb135a1287ffd043f.png

做实时渲染时,最常用到的数学运算就是坐标系变换。比如图形学一道几乎必问的面试题就是:

请问在常见的3D渲染管线里,从制作好mesh,到这个mesh渲染到显示屏上,中间经历了哪几个坐标系变换?

一说到坐标系变换,玩图形的人第一时间都能想到3D空间中的旋转/平移/缩放。但是我还是准备从2D变换部分写起。原因有两点:

  1. 3D变换的很多内容,可以从2D部分衍生得来;
  2. 2D坐标系变换,实际上在实时渲染中用得绝对不比3D坐标系少,因为实时渲染中存在两个很重要的2维空间:
    1. 贴图纹理空间(texture space
    2. 屏幕空间(screen space)

本文主要介绍实时渲染中常见的2D坐标系(以Unity和UE4来进行分析)、常见的4种2D坐标系变换的推导,以及2D坐标系变换的应用的大致描述。最后附带一个只使用4种基础变换得到的demo效果:

7eccd6142e606d769e6cd207636f5cfe.gif

常用2D坐标系:Unity / Unreal

常见的2维空间如下图所示:

ce2177b1e4feb0fea827e11a66801d51.png
2D坐标系:x右y上

但是渲染里面常用的还有另一个2维空间,跟上图的区别是y轴反向:

31dc203c65d6a64130fa660599d83f79.png
2D坐标系:x右y下

举例来说,先看Unity。测试Unity的 texture space:往场景中丢一个 quad模型,给quad指定用下面这段 shader 生成的 material:

Shader "Custom/CheckUVSpace"
{
    Properties
    {
    }
    SubShader
    {
        Tags { 
        "RenderType"="Opaque" 
        }

        Pass
        {
            Lighting Off
            Blend Off
            Cull Off

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            struct VertexInput {
                float4 vertex : POSITION;
                float2 texcoord0 : TEXCOORD0;
            };
            struct VertexOutput {
                float4 pos : SV_POSITION;
                float2 uv0 : TEXCOORD0;
            };
            VertexOutput vert (VertexInput v) {
                VertexOutput o = (VertexOutput)0;
                o.uv0 = v.texcoord0;
                o.pos = UnityObjectToClipPos(v.vertex);
                return o;
            }
            float4 frag(VertexOutput i) : COLOR {
                //return float4(i.uv0.xxx, 1.0);
                return float4(i.uv0.yyy, 1.0);
            }
            ENDCG
        }
    }
    FallBack "Diffuse"
}

得到的两张效果图为:

5458500c90a7e6f098c31f39eea0d649.png
Unity, texture coordinate x

3968e8281e0ec9e98af3183e863e17f6.png
Unity, texture coordinate y

上图可以理解为,Unity的 texture space 是“x右y上”坐标系。(当然,如果把相机颠倒180度,会得到另一个坐标系。此处的分析,是在保证x轴向右的情况下,对应地确定y轴朝向)

Unity的 screen space:将如下C#脚本挂到camera上,然后将脚本的material属性设置为上面这个shader对应的material。可以得到如下画面:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;

[ExecuteInEditMode]
public class PP_Blog : MonoBehaviour
{

	public Material material;

        private void OnRenderImage(RenderTexture source, RenderTexture destination)
        {
        Graphics.Blit(source, destination, material);
        }

}

0b6880c0ef9641f50f3b5e35b5720dd1.png
Unity, screen space x

b71f07de3df07ba2beea61a81d75eae5.png
unity, screen space y

到了Unreal里,情况有些不同。

Unreal的texture space:使用默认的quad,按与Unity相同的方式进行测试:(与上同理,在确保x轴朝右的情况下,确定y轴朝向)

002c662220d1399c138254ad4e565ce3.png
Unreal, texture space x & y

Unreal的screen space:

4a250dab4e44c1891a360e9080060ff2.png
Unreal, screen space x

810f84a0e1abcc6a8fcde94f87ba68dc.png
Unreal, screen space y

结论:

  • Unity的 texture space / screen space,都是“x轴向右,y轴向上”的2D坐标系
  • Unreal的 texture space / screen space,都是“x轴向右,y轴向下”的2D坐标系
  • “x轴向左”的情况不用考虑,因为跟“x轴向右”没有区别,只是换个角度观察而已。比如下图中的“x轴向左,y轴向下”,把屏幕颠倒过来看(旋转180度),其实就是“x轴向右,y轴向上”

5be80d08e41fe84a92d004593d811332.png
“x轴向左”不用单独分析,因为跟”x轴向右“其实是一样的(旋转180度)

"x轴向右,y轴向上"的2D坐标系变换

然后可以开始推导坐标系变换的公式了。先分析"x右y上"类型的坐标系,因为这符合以前数学课本的常用习惯。之后再推导"x右y下"。

2D坐标系中,常见的变换主要有以下几种:平移/旋转/缩放/错切。(图像处理领域的基础图像坐标系变换也是这几种)

为了将这些变换用统一的矩阵乘法方式表示,会使用到齐次坐标(齐次坐标是啥暂时不展开了,可以理解为在向量

后添加一个1,把2维向量变成3维向量
)。而
之所以要将所有变换都用矩阵乘法方式来表示,是为了便于用矩阵乘法的方式来”级联“多个变换效果。下面分别来说明各个变换的矩阵计算情况(为便于理解,把row-major/行向量表示法和column-major/列向量表示法的公式都列了出来。这个细微区别在具体分析各个引擎的时候还会用到):
  • 平移

列向量表示法

行向量表示法

  • 旋转

推导:假设在unit circle(半径为1的圆)上有两个点

与x轴正方向的夹角为
相对于
绕逆时针有一个角度为
的旋转。如下图:

e065001c8e3e0df218fedc348cadffe5.png
“x右y上”坐标系中的逆时针旋转

用极坐标来表示:

用极坐标来表示:

(如果有较真的宝宝忘了三角函数求和公式,可以用几何的方法来证明,见下图)

ca9c328d46d3dff5f807354b23df7c6a.png
三角函数求和公式可用几何的方法证明,图引自https://en.wikipedia.org/wiki/Proofs_of_trigonometric_identities

用齐次坐标系来表示在”x右y上“的2D坐标系中逆时针旋转

角度:

列向量表示法

行向量表示法

  • 缩放

列向量表示法

行向量表示法

这里的缩放系数,除了可以拿来做”缩放“效果外,还可以拿来做”镜像“效果(当

时)
  • 错切

列向量表示法

行向量表示法

错切这个操作乍一看,似乎除了在理论上学过以外,并无多大实际用途。但是这种”使x的变换结果依赖于y取值“的思路,是所有非刚性变形的基础。而且,3D图形学中的“透视投影矩阵”/perspective camera projection matrix,其实用到的也是”使x/y的变换结果依赖于z“,以此来实现”近大远小“的效果的。


"x轴向右,y轴向下"的2D坐标系变换

这个坐标系与”x右y上“的区别就是 y轴反向了。

缩放和错切这两种变换,“x右y下”与“x右y上”的变换矩阵相同。

平移:如果考虑的是”x方向移动

,y方向移动
“,那么变换矩阵与”x右y上“是完全一样的。但是,实际上一般人在设计效果时,思考的都不是”沿着y轴正方向平移多少“,而是”向显示器屏幕上方平移多少”。因此在“x右y下”坐标系中的“向上平移”,前进方向就是y轴的负方向了。

旋转:跟平移类似,会有一个“反向”,这个反向指的是:顺时针和逆时针的区别。同样一套公式,在“x右y上”中是逆时针旋转,到了“x右y下”中就成了顺时针旋转了。


Unity/UE4 shader里的2D变换是 row-major 还是 column-major

Unity:使用这张图片来进行测试

e60c4f3c2e7cc8c1e6b072afffcae264.png

Unity的测试材质用这个:

Shader "Custom/CheckUVSpace"
{
    Properties
    {
        _Tex("Tex", 2D) = "white" {}
        _RotateSpeed("RotateSpeed", Float) = 0.1
    }
    SubShader
    {
        Tags { 
        "RenderType"="Opaque" 
        }

        Pass
        {
            Lighting Off
            Blend Off
            Cull Off

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            uniform sampler2D _Tex;
            uniform float4 _Tex_ST;
            uniform float _RotateSpeed;

            struct VertexInput {
                float4 vertex : POSITION;
                float2 texcoord0 : TEXCOORD0;
            };
            struct VertexOutput {
                float4 pos : SV_POSITION;
                float2 uv0 : TEXCOORD0;
            };
            VertexOutput vert (VertexInput v) {
                VertexOutput o = (VertexOutput)0;
                o.uv0 = v.texcoord0;
                o.pos = UnityObjectToClipPos(v.vertex);
                return o;
            }
            float4 frag(VertexOutput i) : COLOR {
                float rotateAngle = _Time.x * _RotateSpeed;
                float cosVal = cos(rotateAngle);
                float sinVal = sin(rotateAngle);
                float2x2 rotateMatrix = float2x2(cosVal, -sinVal, sinVal, cosVal);
  
                //float2 uv = mul(rotateMatrix,i.uv0); // 顺时针旋转
                float2 uv = mul(i.uv0, rotateMatrix); // 逆时针旋转
  
                float4 _Tex_Var = tex2D(_Tex, uv);
                return _Tex_Var;
            }
            ENDCG
        }
    }
    FallBack "Diffuse"
}

测试结果如代码注释所示:当向量乘到矩阵右边(“mul(rotateMatrix,i.uv0)” )时,为顺时针旋转;当向量乘到矩阵左边(“mul(i.uv0, rotateMatrix);”)时,为逆时针旋转。联系上面的推导,可知:

  • Unity shader中的这个“float2x2(cosVal, -sinVal, sinVal, cosVal)”,始终是按照 如下方式来解读
  • 但是“float2 uv”,用来左乘矩阵时,会被当成行向量;用来右乘矩阵时,会被当成列向量。

Unreal:采用类似的方法测试,材质如下

535636a9b3025de11c75ac721574cf92.png

其中Custom结点的hlsl代码段为:

float c = cos(Time);
float s = sin(Time);
float2x2 rotm = float2x2(c,-s,s,c);
//return mul(UV,rotm); // 顺时针
return mul(rotm,UV); // 逆时针

测试结果是“左乘/右乘”带来的“顺时针/逆时针”效果刚好跟Unity相反。不过考虑到UE4的 texture space/ screen space 都是“x右y下”,所以是合理的,没问题。


2D变换的效果设计示例

写了半天2D空间的坐标系变换,到底有什么用?图形渲染不是3D空间吗?

嗯,我的理解是,在实时渲染里,2D坐标系变换的重要性,丝毫不少于3D坐标系变换。包括但不限于:

  1. 3D mesh的材质计算中,会使用到大量的贴图,贴图内容包括光照模型计算常见的 basecolor/normal/metallic/roughness/lightmap等,还包括各种特效类贴图(各种noise和pattern)。这些贴图通常不只是用模型uv读取一下就万事大吉的,而是需要对uv做各种变换、来配合贴图制作各种效果
  2. 整个后处理PostProcessing,都可以理解为是图像处理(处理的图像可能只是color,也能包含depth/normal等)。既然是图像处理,那简直是可以把2D坐标系变换玩出花来。
  3. 曾经,实时渲染中的各种贴图都是美术同学一笔一笔画出来的。自从有了Subtance之类的东西以后,很多细节丰富的贴图都可以用程序化生成的方式来制作了。程序化贴图设计,本质上也是一个2D坐标系空间的创作。

当然上面讲的这些2D坐标系的东西已经超出“平移/旋转/缩放/错切”的范畴了,不过这几个基础变换确实是基础。在UE的材质设计里大量用到的“走uv”技巧,对应的Panner和Rotator结点,其实就是2D坐标系的“平移”和“旋转”。

何况,使用好“平移/旋转/缩放/错切”,就已经能够组合出一些“古怪”效果了。比如,使用如下这张简单贴图:

897527b9e011e095b5a6e667b3308d77.png

用Unity制作一个材质,只使用 旋转/平移/缩放,和一个用"sin*cos"构造的周期函数,就可以制作下面这个效果:

7eccd6142e606d769e6cd207636f5cfe.gif

Unity shader代码如下:

Shader "Custom/CheckUVDemo"
{
    Properties
    {
        _Tex("Tex", 2D) = "white" {}

        _RotateSpeed("RotateSpeed", Float) = 0.1

        _ScaleX("ScaleX", Float) = 1
        _ScaleY("ScaleY", Float) = 1

        _AnimSpeed("AnimSpeed", Float) = 1
        _MinAnimControl("MinAnimControl", Float) = -0.2
        _MaxAnimControl("MaxAnimControl", Float) = 0.2

        _AnimBlend("AnimBlend", Range(0,1)) = 1
    }
    SubShader
    {
        Tags {
        "RenderType"="Opaque"
        }

        Pass
        {
            Lighting Off
            Blend Off
            Cull Off

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            uniform sampler2D _Tex;
            uniform float4 _Tex_ST;
            uniform float _RotateSpeed;
            uniform float _ScaleX;
            uniform float _ScaleY;
            uniform float _AnimSpeed;
            uniform float _MinAnimControl;
            uniform float _MaxAnimControl;
            uniform float _AnimBlend;

            struct VertexInput {
                float4 vertex : POSITION;
                float2 texcoord0 : TEXCOORD0;
            };
            struct VertexOutput {
                float4 pos : SV_POSITION;
                float2 uv0 : TEXCOORD0;
            };

            // rotator
            float2 rotator2D(float2 inUV, float2 rotCenter, float rotAngle)
            {
                float cosVal = cos(rotAngle);
                float sinVal = sin(rotAngle);
                float2x2 rotateMatrix = float2x2(cosVal, -sinVal, sinVal, cosVal);

                return mul(inUV-rotCenter, rotateMatrix) + rotCenter;
            }

            // panner
            float2 panner2D(float2 inUV, float2 pan)
            {
                return inUV + pan;
            }

            // scaler
            float2 scaler2D(float2 inUV, float2 scaleCenter, float2 scaleValue)
            {
                return (inUV - scaleCenter) * scaleValue + scaleCenter;
            }

            VertexOutput vert (VertexInput v) {
                VertexOutput o = (VertexOutput)0;
                o.uv0 = v.texcoord0;
                o.pos = UnityObjectToClipPos(v.vertex);
                return o;
            }
            float4 frag(VertexOutput i) : COLOR {
                // 构造一个取值范围 [_MinAnimControl, _MaxAnimControl] 的周期性曲线
                float timeVal = _AnimSpeed * _Time.y + i.uv0.r;
                timeVal = sin(timeVal*15.0)*cos(timeVal*1.0)*0.5 + 0.5;
                timeVal = timeVal * (_MaxAnimControl - _MinAnimControl) + _MinAnimControl;

                // panner
                float2 uv = panner2D(i.uv0, float2(0, timeVal));

                // scaler
                uv = scaler2D(uv, float2(0.5, 0.5), float2(_ScaleX, _ScaleY));

                // rotator
                float rotateAngle = _Time.y * _RotateSpeed;
                uv = rotator2D(uv, float2(0.5,0.5), rotateAngle);
                uv = lerp( uv, i.uv0, _AnimBlend );

                float4 _Tex_Var = tex2D(_Tex, uv);
                return _Tex_Var;
            }
            ENDCG
        }
    }
    FallBack "Diffuse"
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值