01UV坐标操控
01UV坐标操控
教程初期使用VisualShader,但后期统一使用GDShader来进行操作。课程的初期其实是介绍图形学管线中的基础流程和概念,从2D入手开始讲解各种效果的实现细节。其中最重要的一部分便是UV的使用和讲解。
通常从图形学程序的角度来说,UV主要用来做面片采样的映射,每个顶点给定一个UV坐标值,然后根据面片双线性插值采样对应贴图像素来进行渲染。但延展开来,至少在2D层面可以做出很多效果。
基础图形
例如最简单的在给定的2D贴图上绘制指定图形,在Godot中就是Sprite2D这种。
圆形
思路就是计算到某一个点的距离,小于该距离的进行对应设置即可。这在gdshader中可以通过如下方式简单计算获得
// 圆形半径
uniform float circle_radius : hint_range(0.0,1.0);
void fragment(){
float circle_inner = step(distance(UV,vec2(0.5,0.5)),circle_radius);
COLOR.rgb =vec3(1.0);
COLOR.a = circle_inner;
}即可获得一个白色的圆形,圆大小由circle_radius来控制。

这其中的关键就是,2D贴图UV数值实际上给出了一个平铺的坐标系,范围为$[0,1]\times[0,1]$每个点直接对应一个坐标。直接计算距离函数并且截取即可。
step(a,b)阶跃函数,本质等价于if (b < a) { return 0.0; } else { return 1.0; }。这里相当于距离比circle_radius小的都返回1。所以选取出圆形区域。
所以可以看到这里对绘制图形的设计,实际上就是与几何计算直接相关。
例如对矩形的绘制,就是筛选出矩形范围内的点,并设置。
矩形
矩形思路也一致,两种方式皆可描述矩形区域
- 给定一个点,加长度宽度
- 给定两个点,在两个点之间
其实两个方式都是一样的,需要四个参数来描述,同时也需要用if语句来控制。给定两个点的方式可简单如下描述
uniform vec2 rect_left_up = vec2(0.25,0.25);
uniform vec2 rect_right_down = vec2(0.75,0.75);
void fragment() {
float color_value = 0.0;
if(UV.x>rect_left_up.x&&UV.x<rect_right_down.x&&UV.y>rect_left_up.y&&UV.y<rect_right_down.y){
color_value = 1.0;
}
COLOR = vec4(color_value);
}
线的切分
线的判断就比较细节一些了。从前面可以看到,这些部分都是几何代数的判断计算过程。所以熟练学习知晓集合代数结构至关重要。
2维平面的线有几种刻画方式。
- 一种是通用的描述是,两点即可确定一条直线,假如给定两个点$P,M$,该直线相当于如下的参数方程:
这个方式可以拓展到任意维度。对任意维度的直线来说,都可以这么描述。
- 一种是点斜式,使用点和斜率来表示直线,2维情况下可以转换表示为如下公式:
其中k表示斜率,b表示y轴截距。点斜式的本质是一般方程的特例。
- 还有一种就是一般方程描述,2维情况下标准形式为如下:
这个公式实际上表示的是$(x,y)\cdot(A,B)+C=0$。也就是说$\vec{d}=(A,B)$相当于给定一个方向向量,点乘表示在这个方向上的投影距离。所以这个一般式的含义就是在说,在这个方向向量上投影距离为给定值所组成的线。但这个方式扩展到更高维度就成了超平面方式,而不再是直线。
这三个描述方式中,点斜式是有缺陷的,其无法描述平行于y轴的直线。同时,通常情况下对于曲线来说,一般就有两种描述结构,参数方程和一般方程。两种都可以描述平面上所有曲线,但出发角度略有不同。
所以根据传入参数的含义不一样,操作运算方式也有所不一样。对于直线用的最多的就是扫光效果。这里在后面扫光部分在仔细描述,(教程里面用的是点斜式描述,实际上是一般方程的结构)。
两种描述给出的判断可以用下图描述

给定一条直线判断点在直线的哪一侧
如果用参数方程描述
即$L(t)=P+t\vec{PM}$这种形式。该情况下需要给定任意两个点,或者一个点加一个方向。
假定给定一个点$P$和方向$\vec{PM}$,对于任意一个给定的点$N$,可以通过叉乘的方式判断出来其在线的哪一侧。把$\vec{PN},\vec{PM}$视为三维空间的两个向量,通过$\vec{PN}\times\vec{PM}$叉乘可以判断$\vec{PN}$在$\vec{PM}$的哪一个方向,叉乘计算如下
$$ \begin{vmatrix} \vec{i} & \vec{j} & \vec{k} \\ PN_x & PN_y & 0\\ PM_x & PM_y & 0 \end{vmatrix}=(PN_x\cdot PM_y-PN_y\cdot PM_x)\vec{k} $$其计算实际给出的是一个3维向量,由于限定在平面上,所以其实只会有$\vec{k}$值分量。通过该向量方向,实际也就是该分量正负号正好说明了点$N$在直线的哪一侧。这里的点$N$实际就是$UV$坐标值,所以方向判断如下
uniform vec2 line_vec = vec2(1,0);
uniform vec2 point = vec2(0,0);
// 思路是叉乘给出的符号判断方向
void fragment() {
vec2 uv_vector = UV-point;
float dir = uv_vector.x*line_vec.y-uv_vector.y*line_vec.x;
COLOR.a = step(dir,0);
}如果用一般方程描述
即$Ax+By+C=0$表示的直线,考虑$(x,y)\cdot(A,B)+C=0$这种形式。该情况下需要提供一个与线垂直的向量$\vec{d}=(A,B)$,以及一个距离参数$C$。对于任意一个给定的点$N$来说,带入直线一般表达式$A\cdot N_x+B\cdot N_y+C$,根据前面描述,实际上与该点到该直线垂直距离有关。所以可以直接通过这个值的符号来判断所在方向,判断如下:
uniform vec2 perpendicular_dir = vec2(0,1);
uniform float line_dis;
// 思路是一般方程的距离表达判断方向
void fragment() {
float uv_dis = dot(UV,perpendicular_dir);
COLOR.rgb = vec3(step(uv_dis-line_dis,0));
}给定一条直线判断点在直线的距离
现在来看细节,点到直线的距离。如果要计算到直线的距离也对应两种描述有不同计算方式,但是出发点都是几何代数的描述。
如果用参数方程描述
根据叉乘的几何含义,可以知道$\vec{PN}\times\vec{PM}$的模,即向量长度大小,为$|\vec{PN}||\vec{PM}|\sin(\theta)$。根据三角形原理,点$N$到直线的距离实际就是$|\vec{PN}|\sin(\theta)$。也就是说点$N$到直线带符号的距离就是:
$$ \frac{PN_x\cdot PM_y-PN_y\cdot PM_x}{|\vec{PM}|}=\frac{PN_x\cdot PM_y-PN_y\cdot PM_x}{\sqrt{PM_x^2+PM_y^2}} $$如果方向向量$\vec{PM}$使用单位向量,计算则可以进一步简化。注意的是这个计算的符号也同时说明了点在线的哪一侧
如果用一般方程描述
根据点乘的几何含义,可知假设坐标系远点为$O$,此时一个点$N$到该垂直方向上的投影距离为$\vec{ON}\cdot \vec{d}/|\vec{d}|$。
由一般方程可知,线上的点$E$满足$\vec{OE}\cdot\vec{d}+C=0$。换句话说线上点$E$到该垂直方向上的投影距离为$\vec{OE}\cdot\vec{d}/|\vec{d}|=-C/|\vec{d}|$。所以点$N$到该直线的带符号垂直距离即:
$$ \frac{\vec{ON}\cdot \vec{d}}{|\vec{d}|}-\frac{\vec{OE}\cdot\vec{d}}{|\vec{d}|}=\frac{\vec{ON}\cdot \vec{d}+C}{|\vec{d}|}=\frac{AN_x+BN_y+C}{\sqrt{A^2+B^2}} $$同样的如果$\vec{d}=(A,B)$是单位向量则计算可以进一步简化。同时参数$C$也会是明确的距离值表示。