Drawing Basic Shapes
Geometric Primitives and Transformations
Before starting to draw complex shapes, we need to understand the basic building blocks. In computer graphics, all complex shapes are ultimately built from basic primitives.
Basic Primitive Types
1// 1. Point
2fn draw_point(self) -> vec4 {
3 let point_size = 5.0;
4 // In pixel shader we need to check if we're within point range
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. Line segment
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 // Calculate distance from point to line
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. Triangle
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 // Check if point is inside triangle
27 let inside = point_in_triangle(self.pos, vertices);
28 return vec4(0.0, 0.0, 1.0, inside);
29}
视觉表示:
1Point Line Segment Triangle
2
3● ● ▲
4 ╱ ╱ ╲
5 ● ╱ ╲
6 ▔▔▔▔▔▔▔
Coordinate Transformations
You'll need to perform various operations on these shapes: moving their position, rotating their angle, changing their size. This is where transformation matrices come in - mathematical tools that precisely describe these operations.
Understanding transformation matrices is fundamental to handling graphics. There are three basic transformations:
- Translation Transform
1fn translate(pos: vec2, offset: vec2) -> vec2 {
2 // Simple vector addition
3 return pos + offset;
4}
- Rotation Transform
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}
- Scale Transform
1fn scale(pos: vec2, scale: vec2) -> vec2 {
2 // Component multiplication
3 return pos * scale;
4}
Distance Field Technique
Distance Fields are a core technology in Makepad for achieving high-quality graphics rendering. Let's understand it in depth:
What is a Distance Field?
A distance field is a function that tells us the shortest distance from any point in space to a shape's boundary:
- Points inside the shape have negative distance
- Points on the shape's boundary have zero distance
- Points outside the shape have positive distance
1fn circle_sdf(pos: vec2, center: vec2, radius: float) -> float {
2 // 计算到圆心的距离,减去半径
3 return length(pos - center) - radius;
4}
It's useful to think of distance fields as a stack of semi-transparent circles. In this distance field, pure white areas (value of 1) are on the object. Pure black areas are farthest from the object. Gray areas in between have values from 0 to 1. This is a way to visualize distances between 0 and 1.
Signed Distance Fields (SDF)
are three-dimensional scalar fields where each point's value represents the distance to the nearest surface. These distance values have a "signed" characteristic:
- Positive values: Point is outside the object, indicating distance to the nearest surface
- Negative values: Point is inside the object, negative value indicates distance to surface
- Zero values: Point is exactly on the object's surface
This way, SDFs provide not only the nearest distance from any point to an object's surface but also distinguish whether points are inside or outside the object.
Drawing Shapes Using Signed Distance Fields
Signed Distance Fields (SDF) are based on several key concepts:
- Voxel: A small cubic unit in 3D space, used to partition the entire 3D space. In SDFs, we calculate the shortest distance from each voxel to the object's surface.
- Distance Field: A field where each point's value represents the shortest distance to an object's surface. A distance field can be signed or unsigned.
- Trilinear Interpolation: Method used to estimate SDF values at arbitrary points on a voxel grid in 3D space. Trilinear interpolation calculates by linearly interpolating between neighboring voxel values.
SDF Viewport Introduction (Sdf2d::viewport)
1let sdf = Sdf2d::viewport(self.pos * self.rect_size);
self.pos
: This is the current pixel coordinate in the shader, typically ranging from 0.0 to 1.0. It represents the relative position of the pixel we're processing within the entire drawing area.
self.rect_size
: This is the actual size of the rectangle we want to draw (in pixels).
self.pos * self.rect_size
: This multiplication converts normalized coordinates to actual pixel coordinates.
Imagine we're drawing a button that's 200x100 pixels. The entire conversion process can be divided into three key stages:
1. Initial Coordinate System (self.pos)
1┌─────────────────┐
2│(0,0) │
3│ │
4│ (0.5,0.5) │
5│ │
6│ (1,1) │
7└─────────────────┘
8Normalized coordinates: Range is 0.0 to 1.0
2. Multiply by Size (self.pos * self.rect_size)
1┌─────────────────────┐
2│(0,0) │
3│ │
4│ (100,50) │
5│ │
6│ (200,100) │
7└─────────────────────┘
8Pixel coordinates: Now coordinates represent actual pixel positions
9rect_size = (200,100)
3. SDF Viewport (Sdf2d::viewport)
1┌─────────────────────┐ ↑
2│(-100,-50) │ │
3│ │ 100px
4│ (0,0) │ │
5│ │ │
6│ (100,50) │ ↓
7└─────────────────────┘
8←——— 200px ———→
9Centered coordinates: Origin at center
To better understand this transformation process, let's see how specific coordinate points are transformed:
1Position Normalized Coords Pixel Coords SDF Coords
2Top-left (0.0, 0.0) (0, 0) (-100, -50)
3Center (0.5, 0.5) (100, 50) (0, 0)
4Bottom-right (1.0, 1.0) (200, 100) (100, 50)
The benefits of this coordinate transformation are:
- Precise Positioning: Can precisely control the rendering of each pixel
- Scale Independence: Can easily handle UI elements of different sizes
- Centered Operations: Makes many graphical operations (like rotation) simpler
- Anti-aliasing: Makes edge rendering smoother