绘制基础图形
几何图元与变换
在开始绘制复杂图形之前,我们需要先理解最基本的构建块。在计算机图形学中,所有复杂的形状最终都是由基本图元构建而成的。
让我们从最基础的图元开始:
基本图元类型
1// 1. 点
2fn draw_point(self) -> vec4 {
3 let point_size = 5.0;
4 // 在 pixel shader 中我们需要判断是否在点的范围内
5 let distance = length(self.pos - vec2(0.5));
6 return vec4(1.0, 0.0, 0.0, step(distance, point_size));
7}
8
9// 2. 线段
10fn draw_line(self) -> vec4 {
11 let line_width = 2.0;
12 let p1 = vec2(0.0, 0.0);
13 let p2 = vec2(1.0, 1.0);
14 // 计算点到线的距离
15 let dist = distance_to_line(self.pos, p1, p2);
16 return vec4(0.0, 1.0, 0.0, step(dist, line_width));
17}
18
19// 3. 三角形
20fn draw_triangle(self) -> vec4 {
21 let vertices = [
22 vec2(0.0, 0.0),
23 vec2(1.0, 0.0),
24 vec2(0.5, 1.0)
25 ];
26 // 判断点是否在三角形内
27 let inside = point_in_triangle(self.pos, vertices);
28 return vec4(0.0, 0.0, 1.0, inside);
29}
视觉表示:
1点 线段 三角形
2
3● ● ▲
4 ╱ / ╲
5 ● / ╲
6 ▔▔▔▔▔▔▔
坐标变换
你会需要对这些图形进行各种操作:移动物体的位置、旋转物体的角度、改变物体的大小。这个时候就需要使用变换矩阵,一种用数学方式来精确描述这些操作的工具。
理解变换矩阵是处理图形的基础。主要有三种基本变换:
- 平移变换
1fn translate(pos: vec2, offset: vec2) -> vec2 {
2 // 简单的向量加法
3 return pos + offset;
4}
- 旋转变换
1fn rotate(pos: vec2, angle: float) -> vec2 {
2 let s = sin(angle);
3 let c = cos(angle);
4 return vec2(
5 pos.x * c - pos.y * s,
6 pos.x * s + pos.y * c
7 );
8}
- 缩放变换
1fn scale(pos: vec2, scale: vec2) -> vec2 {
2 // 分量乘法
3 return pos * scale;
4}
距离场技术
距离场(Distance Field)是 Makepad 中实现高质量图形渲染的核心技术。让我们深入理解它:
什么是距离场?
距离场是一个函数,它告诉我们空间中任意一点到形状边界的最短距离:
- 形状内部的点距离为负
- 形状边界上的点距离为零
- 形状外部的点距离为正
1fn circle_sdf(pos: vec2, center: vec2, radius: float) -> float {
2 // 计算到圆心的距离,减去半径
3 return length(pos - center) - radius;
4}
把距离场想象成一堆不太透明的圆是很有用的。这个距离场中,表示纯白色的区域(也就是数值为1的部分)就在对象上。纯黑色部分,就是距离物体最远的点。中间的灰色部分,数值在0-1之间。这是一种可视化0到1之间距离的方法。
有号距离场 (Signed Distance Field, SDF) 是一个三维标量场,其中每个点的值表示从该点到最近表面的距离。这些距离值具有“有号”的特性:
- 正值:点位于物体的外部,距离物体表面最近的距离。
- 负值:点位于物体的内部,负数值表示点到物体表面的距离。
- 零值:点恰好位于物体表面。
通过这种方式,SDF不仅能够提供从任意点到物体表面的最近距离,还能够区分点在物体内部还是外部。
使用有符号距离场绘制形状
有符号距离场 (SDF) 基于以下几个关键概念:
- 体素 (Voxel):3D空间中的一个小立方体单元,用于分割整个3D空间。在SDF中,我们计算每个体素到物体表面的最短距离。
- 距离场 (Distance Field):一个场,其中每个点的值表示到物体表面的最短距离。一个距离场可以是有号(Signed)或无号(Unsigned)的。
- 三线性插值 (Trilinear Interpolation):用于在3D空间中的一个体素网格上估计任意点的SDF值的方法。三线性插值通过对体素点的邻近值进行线性插值来计算。
SDF 视口介绍 (Sdf2d::viewport)
1let sdf = Sdf2d::viewport(self.pos * self.rect_size);
self.pos
:这是着色器中的当前像素坐标,范围通常在 0.0 到 1.0 之间。它代表了我们正在处理的像素在整个绘制区域中的相对位置。
self.rect_size
:这是我们要绘制的矩形区域的实际尺寸(以像素为单位)。
self.pos * self.rect_size
:这个乘法操作将归一化的坐标转换为实际的像素坐标。
想象我们正在绘制一个按钮,尺寸是 200x100
像素。整个转换过程可以分为三个关键阶段:
1. 初始坐标系 (self.pos)
1┌─────────────────┐
2│(0,0) │
3│ │
4│ (0.5,0.5) │
5│ │
6│ (1,1) │
7└─────────────────┘
8归一化坐标:范围是 0.0 到 1.0
2. 乘以尺寸 (self.pos * self.rect_size)
1┌─────────────────────┐
2│(0,0) │
3│ │
4│ (100,50) │
5│ │
6│ (200,100) │
7└─────────────────────┘
8像素坐标:现在坐标表示实际像素位置
9rect_size = (200,100)
3. SDF视口 (Sdf2d::viewport)
1┌─────────────────────┐ ↑
2│(-100,-50) │ │
3│ │ 100px
4│ (0,0) │ │
5│ │ │
6│ (100,50) │ ↓
7└─────────────────────┘
8←——— 200px ———→
9中心化坐标:原点在中心
为了更好地理解这个转换过程,我们可以看一些具体的坐标点是如何转换的:
1位置 归一化坐标 像素坐标 SDF坐标
2左上角 (0.0, 0.0) (0, 0) (-100, -50)
3中心 (0.5, 0.5) (100, 50) (0, 0)
4右下角 (1.0, 1.0) (200, 100) (100, 50)
这种坐标转换的好处是:
- 精确定位:可以精确控制每个像素的渲染
- 缩放无关:可以轻松处理不同尺寸的UI元素
- 中心化操作:让很多图形操作(如旋转)变得更简单
- 抗锯齿:让边缘渲染更平滑