From 63f1b1ec924bbf9ea7e450155b9099750fbc457e Mon Sep 17 00:00:00 2001 From: ICEYSELF Date: Thu, 27 Jul 2023 22:09:26 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8D=81=E5=88=86=E9=92=9F=E8=AE=A1=E7=AE=97?= =?UTF-8?q?=E6=9C=BA=E5=9B=BE=E5=BD=A2=E5=AD=A6=EF=BC=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- extra/blog-images/basic-rasterization.png | Bin 0 -> 24419 bytes .../10mins-computer-graphics.jl | 301 ++++++-------- markdown/10mins-computer-graphics.md | 373 +++++++++++++++++- 3 files changed, 490 insertions(+), 184 deletions(-) create mode 100644 extra/blog-images/basic-rasterization.png diff --git a/extra/blog-images/basic-rasterization.png b/extra/blog-images/basic-rasterization.png new file mode 100644 index 0000000000000000000000000000000000000000..57d73d1d2b6a4f053e5fe506fb80cf9a8274bc3c GIT binary patch literal 24419 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 z zXh&q?{vv03GPY~|BNSrN7N_R;VRdKAM?U4F+N$jx@xv_IhBO4nVO$$*`3>jrGSn)!w!Zm$vye)k`Uc7hR-dHWffu~EBVV%cBFtyx~ ztgz4rV|uAwI0ZUqXG)4k70qEV(M&8n?2JvZ=PgxxjAK^V1i1MuxX!)w>8vfADtsV!6Ik&y>|8706Rb`TomScMU#ozqc^Y!D=X|q`p%T?|X7w%@?+LVZ#?ujn7~F@S{Vf_#&Cd zUn&>u*i68e1_mffx2}G{O~st1lA1hGj*ipuH53qB?PIy6#KY%H(nm*Fe>QQ*y*w>4 zX~_(<5Q%v6$WAPh z>%fOkWLwt_mM{_sr3(CtWI>+#YpX zRv$W>0O6EEN{`HHnuYn%kKub;xYK!7q@!Al@BAY>A3J?}Hi;$B7eZHXV!Z~Qm7Eqq zjc_4%j`s<~P8k4!!|pJC4DUz28#jVNRm$PU;6pu0QgQ$km-UZ*v!K7?YJ(i zyNdR{n`vnyTj>^N`9yjYCk%$OHp3`Hcx=D9P)srKnC7xKneYe(JU)3^OG^Fksq|5A z5%#Azg8?^G<4*w?VG;Ot9q^OPzl$gah3hfyxL`I36A$XUwWEDz(n-SC)SvEpZ};ML&C?rn=ca9Gs=E z&f!NfJ^>r+&GE#cDUs!s1<85h+sg{NYf~b0EPm}kjoYyJmOieq1Pzmqh zEkN~G#L*FPYiN5icgpT3a(BuJ!lBK7^bUVkdRUoTe0wG!)|7>o`N%irJmQ<=I^(-K zzMwM|vql#TYx(rIjt7!w;dtW`+ka#zA&Eh2VW9K#n;{~L$7aK-t&3Ay8fco9N@L*~ zh+2Dfxl)(mrtPo^jcXqwD4)x52eY(W>muXo-KsSeg!-RN0D*`ZKpNH;7Z?t1`~3qg zE(5EwtaKG4TrtYhYDzo`gDGNxUdr6WkeG&EU2=o6N62)1K|%Fvz$|tU7h@0pBG1gn zVriN3762J>d1gs`D7Am2;|lK9LZ&x2m%R&oGlR7dNijYIm1O}rogIYWBB=w$?pqVe zTSn()x@2dLGh_mawcP~MAyBpouAprmsqBzwIks20wN935+#^Ys4ZYe8CFb9!8k7Q+%bTS@2ud)}p zFn~2j7fIFX@&U!!)7xJ=iIN&ac(WX`xP3DnAMorthpiJ;vW-yH@f34oI=G1_N@M+=pkVSePCewJ~&(85lX%B^nEGoR?dNVxMy;rUKr zMcj?QoqyCKFMsX{JisX#IL!@S{X2Fqi~;lTGRjR{0PxZN=go+{D{aew^%kPx@QbJA zN8Dz7H|2Le-EE&s{t&qny*&6fV*_u%08jBUhe7KS@AXB*RrriUKr)~N`mD|8G_}CJ zLJBsLL;&?Slk3A<>qTe{^9(&MwY#AjZ|?{96?RRdR=(h$}OB11c@{M7H|eizJJ74GlQsNAS1$# zM(wQ|2K&FG;k~mPJbw}T<_`DjSLN5>dawqgJ>_zlJ#pb@laRl>NCc<0|Nq1PvQJV3 XGdVmKAa--{9<;xP7!x#A$07P(7oRzb literal 0 HcmV?d00001 diff --git a/extra/code/10mins-computer-graphics/10mins-computer-graphics.jl b/extra/code/10mins-computer-graphics/10mins-computer-graphics.jl index 96878d6..4ad7008 100644 --- a/extra/code/10mins-computer-graphics/10mins-computer-graphics.jl +++ b/extra/code/10mins-computer-graphics/10mins-computer-graphics.jl @@ -1,77 +1,32 @@ module Softpipe -using Base.Threads -using Distributed +# CG 中的线性代数运算经常需要一些定长的向量和矩阵 using StaticArrays +# 由 MWORKS.Syslab 2023b 提供,仅用于最后阶段显示图片,你也可以替换成其他库 using TyImages +# 常用尺寸的向量 +# CG 渲染不需要太高的精度,并且 32 位浮点数的使用非常广泛 +# 而且这样我们可以少担心一点类型稳定性的问题 const Vec2 = SVector{2,Float32} const Vec3 = SVector{3,Float32} const Vec4 = SVector{4,Float32} -const Mat4x4 = SMatrix{4,4,Float32} - -function rotate_x(angle::Number)::Mat4x4 - return Mat4x4( - 1.0, 0.0, 0.0, 0.0, - 0.0, cosd(angle), sind(angle), 0.0, - 0.0, -sind(angle), cosd(angle), 0.0, - 0.0, 0.0, 0.0, 1.0 - ) -end - -function rotate_y(angle::Number)::Mat4x4 - return Mat4x4( - cosd(angle), 0.0, -sind(angle), 0.0, - 0.0, 1.0, 0.0, 0.0, - sind(angle), 0.0, cosd(angle), 0.0, - 0.0, 0.0, 0.0, 1.0 - ) -end - -function rotate_z(angle::Number)::Mat4x4 - return Mat4x4( - cosd(angle), sind(angle), 0.0, 0.0, - -sind(angle), cosd(angle), 0.0, 0.0, - 0.0, 0.0, 1.0, 0.0, - 0.0, 0.0, 0.0, 1.0 - ) -end - -function translate(x::Number, y::Number, z::Number)::Mat4x4 - return Mat4x4( - 1.0, 0.0, 0.0, 0.0, - 0.0, 1.0, 0.0, 0.0, - 0.0, 0.0, 1.0, 0.0, - x, y, z, 1.0 - ) -end - -function scale(x::Number, y::Number, z::Number)::Mat4x4 - return Mat4x4( - x, 0.0, 0.0, 0.0, - 0.0, y, 0.0, 0.0, - 0.0, 0.0, z, 0.0, - 0.0, 0.0, 0.0, 1.0 - ) -end -function perspective(fovy::Number, aspect::Number, near::Number, far::Number)::Mat4x4 - f = cotd(fovy / 2.0) - return Mat4x4( - f / aspect, 0.0, 0.0, 0.0, - 0.0, f, 0.0, 0.0, - 0.0, 0.0, (far + near) / (near - far), -1.0, - 0.0, 0.0, (2.0 * far * near) / (near - far), 0.0 - ) -end +# 常用的 4x4 矩阵 +const Mat4x4 = SMatrix{4,4,Float32} +# 帧缓冲,记录每个像素点上的颜色 const Framebuffer = Matrix{Vec4} - -const DepthBuffer = Matrix{Float32} - -# 此算法取自 https://codeplea.com/triangular-interpolation -# 这是我能找到的对重心坐标插值最简单的解释 -function barycentric(v1::V, v2::V, v3::V, v::V)::Tuple{Float32,Float32,Float32} where {V<:AbstractVector{Float32}} +# 深度缓冲,记录每个像素点上的深度值 +const Depthbuffer = Matrix{Float32} + +# 重心坐标计算 +function barycentric( + v1::V, + v2::V, + v3::V, + v::Vec2 +)::Tuple{Float32,Float32,Float32} where {V<:AbstractVector} denom = (v2[2] - v3[2]) * (v1[1] - v3[1]) + (v3[1] - v2[1]) * (v1[2] - v3[2]) w1 = ((v2[2] - v3[2]) * (v[1] - v3[1]) + (v3[1] - v2[1]) * (v[2] - v3[2])) / denom @@ -81,27 +36,23 @@ function barycentric(v1::V, v2::V, v3::V, v::V)::Tuple{Float32,Float32,Float32} return (w1, w2, w3) end -function interpolate3(p::Vec4, v1::T, v2::T, v3::T)::Union{T,Nothing} where {T} +# 基于重心坐标的插值 +function interpolate3(p::Vec2, v1::T, v2::T, v3::T)::Union{T,Nothing} where {T} + # 这里用了一些 Julia 的反射机制,不用在意 @assert hasfield(T, :position) @assert isa(v1.position, Vec4) - p_xy = p[1:2] v1_xy = v1.position[1:2] v2_xy = v2.position[1:2] v3_xy = v3.position[1:2] - w1, w2, w3 = barycentric(v1_xy, v2_xy, v3_xy, p_xy) + w1, w2, w3 = barycentric(v1_xy, v2_xy, v3_xy, p) if w1 < 0.0 || w2 < 0.0 || w3 < 0.0 return nothing end ret = T() - setfield!(ret, :position, p) for field in fieldnames(T) - if field == :position - continue - end - v1_field = getfield(v1, field) v2_field = getfield(v2, field) v3_field = getfield(v3, field) @@ -110,108 +61,131 @@ function interpolate3(p::Vec4, v1::T, v2::T, v3::T)::Union{T,Nothing} where {T} return ret end -function render( - width, - height, - depth_test::Bool, - clear_color::Vec4, - vertices::V, +function render!( + # 目标帧缓冲,或者可以理解为“画布” + framebuffer::Framebuffer, + # 顶点数据。注意定点数据不止包含位置,还可以包含用户定义的任意属性 + # 所以这里使用一个泛型参数来表示顶点类型 + vertices::Vector{V}, + # 顶点着色器 vertex_shader::VS, - fragment_shader::FS, -)::Framebuffer where {V, VS <: Function, FS <: Function} + # 片段着色器 + fragment_shader::FS +) where {V,VS<:Function,FS<:Function} + width = size(framebuffer, 2) + height = size(framebuffer, 1) + vertex_count = length(vertices) + + # 做一些基本的检查 @assert width > 0 && height > 0 - @assert length(vertices) % 3 == 0 - - # fill framebuffer with clear color - framebuffer = fill(clear_color, (height, width)) - depthbuffer::Union{DepthBuffer,Nothing} = nothing - if depth_test - depthbuffer = fill(floatmax(Float32), (height, width)) - end - - vs_outputs = pmap(vertex_shader, vertices) - @threads for vs_output in vs_outputs - t = vs_output.position[4] - vs_output.position = vs_output.position / t + @assert vertex_count > 0 + # 本文中我们只讨论三角形 + # 多边形可以视为多个三角形的组合,线段则使用另外的算法,本文暂不讨论 + @assert vertex_count % 3 == 0 + + vs_outputs = map(vertex_shader, vertices) + # 在这之后,“正规化”着色器输出的顶点位置。不用太在意数学上发生了什么 + for i in 1:vertex_count + vs_outputs[i].position /= vs_outputs[i].position.w end - for i in 1:3:length(vs_outputs) + for i in 1:3:vertex_count + # 三角形的三个顶点 v1 = vs_outputs[i] v2 = vs_outputs[i+1] v3 = vs_outputs[i+2] + # 三个顶点的位置 v1_pos = v1.position::Vec4 v2_pos = v2.position::Vec4 v3_pos = v3.position::Vec4 - - # calculate bounding box + + # 计算三角形的包围盒 min_x = min(v1_pos[1], v2_pos[1], v3_pos[1]) - max_x = max(v1_pos[1], v2_pos[1], v3_pos[1]) min_y = min(v1_pos[2], v2_pos[2], v3_pos[2]) + max_x = max(v1_pos[1], v2_pos[1], v3_pos[1]) max_y = max(v1_pos[2], v2_pos[2], v3_pos[2]) - - # convert to pixel coordinate - min_x_pixel = round(Int, (min_x + 1.0) * width / 2.0)::Int - max_x_pixel = round(Int, (max_x + 1.0) * width / 2.0)::Int - min_y_pixel = round(Int, (min_y + 1.0) * height / 2.0)::Int - max_y_pixel = round(Int, (max_y + 1.0) * height / 2.0)::Int - - # ensure the bounding box is inside the framebuffer - min_x_pixel = max(min_x_pixel, 1) - max_x_pixel = min(max_x_pixel, width) - min_y_pixel = max(min_y_pixel, 1) - max_y_pixel = min(max_y_pixel, height) - - # rasterization - for y_pixel in min_y_pixel:max_y_pixel - @threads for x_pixel in min_x_pixel:max_x_pixel - x = 2.0 * x_pixel / width - 1.0 - y = 2.0 * y_pixel / height - 1.0 - p = Vec4(x, y, 0.0, 1.0) - - # interpolate - v = interpolate3(p, v1, v2, v3) - if v === nothing + + # 将包围盒的坐标转换为帧缓冲上像素的坐标 + min_x_pix = round(Int, (min_x + 1.0) * width / 2.0) + min_y_pix = round(Int, (min_y + 1.0) * height / 2.0) + max_x_pix = round(Int, (max_x + 1.0) * width / 2.0) + max_y_pix = round(Int, (max_y + 1.0) * height / 2.0) + + # 确保包围盒在帧缓冲内 + min_x_pix = max(min_x_pix, 1) + min_y_pix = max(min_y_pix, 1) + max_x_pix = min(max_x_pix, width) + max_y_pix = min(max_y_pix, height) + + # 对包围盒内的每个像素点进行处理 + for y_pix in min_y_pix:max_y_pix + for x_pix in min_x_pix:max_x_pix + x = 2.0 * x_pix / width - 1.0 + y = 2.0 * y_pix / height - 1.0 + + # 通过重心坐标插值得到这个像素点的属性 + fragment = interpolate3(Vec2(x, y), v1, v2, v3) + + # 如果这个像素点不在三角形内,就跳过 + if isnothing(fragment) continue end - - # depth test - if depth_test - if v.position[3] > depthbuffer[y_pixel, x_pixel] - continue - end - depthbuffer[y_pixel, x_pixel] = v.position[3] - end - - # fragment shader - color = fragment_shader(v) - - # write to framebuffer - framebuffer[y_pixel, x_pixel] = color + + # 运行片元着色器,得到这个像素点的颜色 + color = fragment_shader(fragment) + # 将颜色写入帧缓冲 + framebuffer[y_pix, x_pix] = color end end end +end - return framebuffer +# 三角形顶点的属性 +struct Vertex + # 位置,我们暂时只用到 x 和 y,所以两个分量就够了 + position::Vec2 + # 颜色,我们暂时不用透明度,所以三个分量就够了 + color::Vec3 end -mutable struct Vertex - position::Vec4 - color::Vec4 +# 我们采用和 OpenGL 相同的坐标系,即 y 轴向上,z 轴向屏幕外 +vertices = [ + Vertex(Vec2(-1.0, -1.0), Vec3(1.0, 0.0, 0.0)), + Vertex(Vec2(1.0, -1.0), Vec3(0.0, 1.0, 0.0)), + Vertex(Vec2(0.0, 1.0), Vec3(0.0, 0.0, 1.0)) +] - Vertex() = new() - Vertex(position::Vec4, color::Vec4) = new(position, color) +mutable struct ManipulatedVertex + position::Vec4 # 我们统一输出 Vec4,这样后续步骤会更统一 + color::Vec3 # 顶点颜色 + + ManipulatedVertex() = new() + ManipulatedVertex(position::Vec4, color::Vec3) = new(position, color) end -function vertex_shader(vertex::Vertex)::Vertex - return vertex +function vertex_shader_identity(vertex::Vertex)::ManipulatedVertex + ManipulatedVertex( + # 顶点位置不变,补上 z 和 t 分量 + Vec4(vertex.position[1], vertex.position[2], 0.0, 1.0), + # 顶点颜色不变 + vertex.color + ) end -function fragment_shader(vertex::Vertex)::Vec4 - return vertex.color +function fragment_shader_identity(fragment::ManipulatedVertex)::Vec4 + return Vec4( + fragment.color[1], + fragment.color[2], + fragment.color[3], + 1.0 + ) end -function packed2planar(packed::Matrix{Vec4})::Array{Float32,3} +framebuffer = zeros(Vec4, 512, 512) +render!(framebuffer, vertices, vertex_shader_identity, fragment_shader_identity) + +function packed2planar(packed::Framebuffer)::Array{Float32,3} ret = Array{Float32}(undef, size(packed, 1), size(packed, 2), 3) height = size(packed, 1) @@ -228,43 +202,6 @@ function packed2planar(packed::Matrix{Vec4})::Array{Float32,3} return ret end -function test() - fb = render( - 600, - 600, - true, - Vec4(0.0, 0.0, 0.0, 1.0), - [ - Vertex(Vec4(-1.0, -1.0, 0.0, 1.0), Vec4(1.0, 0.0, 0.0, 1.0)), - Vertex(Vec4(1.0, -1.0, 0.0, 1.0), Vec4(0.0, 1.0, 0.0, 1.0)), - Vertex(Vec4(0.0, 1.0, 0.0, 1.0), Vec4(0.0, 0.0, 1.0, 1.0)), - ], - vertex_shader, - fragment_shader, - ) - - imshow(packed2planar(fb)) -end - -using BenchmarkTools -using InteractiveUtils - -function bench() - @btime render( - 600, - 600, - true, - Vec4(0.0, 0.0, 0.0, 1.0), - [ - Vertex(Vec4(-1.0, -1.0, 0.0, 1.0), Vec4(1.0, 0.0, 0.0, 1.0)), - Vertex(Vec4(1.0, -1.0, 0.0, 1.0), Vec4(0.0, 1.0, 0.0, 1.0)), - Vertex(Vec4(0.0, 1.0, 0.0, 1.0), Vec4(0.0, 0.0, 1.0, 1.0)), - ], - vertex_shader, - fragment_shader, - ) -end +imshow(packed2planar(framebuffer)) end # module Softpipe - -Softpipe.test() diff --git a/markdown/10mins-computer-graphics.md b/markdown/10mins-computer-graphics.md index 1c46848..e437b8a 100644 --- a/markdown/10mins-computer-graphics.md +++ b/markdown/10mins-computer-graphics.md @@ -4,8 +4,6 @@ !!meta-define:time:2023-07-26T22:30:15+08:00 !!meta-define:tags:图形学,软件渲染器,Julia !!meta-define:brief:七通设计算机图形学与 Vk 入门专用教材,你还在等什么,赶紧拨打热线电话订购吧! -!!meta-define:hidden:true - 计算机图形学,从应用的角度上来说,其实是一种非常简单的技术。不幸的是,现有的教程通常会在一开始拘泥于一些数学上的问题,或者执着于一些数学上的细节。本教程尝试通过另一种方式,借由一个基本软件渲染器的实现,概览整个渲染管线,来介绍计算机图形学的基本概念。 @@ -13,6 +11,377 @@ 简单来说,计算机图形学的*意图*就是将一系列的图元(点、线、三角形,以及它们的空间位置、颜色属性)转化为屏幕上的像素点。这个转换的过程被称为*渲染*。 +
+光栅化 +
+ 本教程中,我们会使用 Julia 实现一个基本的软件渲染器,它能*光栅化*基本的三角形图元,并且能够以*可编程*的方式配置一部分渲染管线。 ## 准备工作 + +在开工之前,我们需要先准备一些东西。首先,创建一个 Julia 文件,然后在里面开一个模块: + +```julia +module Softpipe + +# 之后我们的代码都会放到这里面 + +end # module Softpipe +``` + +之所以要开这个模块,是因为在 Julia REPL 中求值脚本的时候,脚本中的类型、函数和变量会被引入到 REPL 的顶层作用域中。之后重新求值这个脚本的时候,原先的定义也不会消失,并且以各种形式干扰重新求值的过程。如果把所有东西包在一个模块里,重新求值脚本就能完全地替换模块中的内容,从而避免上述问题。 + +接着,我们引入需要的包: + +```julia +# CG 中的线性代数运算经常需要一些定长的向量和矩阵 +using StaticArrays +# 由 MWORKS.Syslab 2023b 提供,仅用于最后阶段显示图片,你也可以替换成其他库 +using TyImages + +# 常用尺寸的向量 +# CG 渲染不需要太高的精度,并且 32 位浮点数的使用非常广泛 +# 而且这样我们可以少担心一点类型稳定性的问题 +const Vec2 = SVector{2,Float32} +const Vec3 = SVector{3,Float32} +const Vec4 = SVector{4,Float32} + +# 常用的 4x4 矩阵 +const Mat4x4 = SMatrix{4,4,Float32} + +# 帧缓冲,记录每个像素点上的颜色 +const Framebuffer = Matrix{Vec4} +# 深度缓冲,记录每个像素点上的深度值,之后我们会看到它的作用 +const Depthbuffer = Matrix{Float32} +``` + +## `render!` 函数 + +如果“渲染”是一个函数的话,回顾我们刚讲过*意图*: + +> 简单来说,计算机图形学的*意图*就是将一系列的图元(点、线、三角形,以及它们的空间位置、颜色属性)转化为屏幕上的像素点 + +那么,这个函数的签名也就呼之欲出了: + +```julia +function render!( + # 目标帧缓冲,或者可以理解为“画布” + framebuffer::Framebuffer, + # 顶点数据。注意定点数据不止包含位置,还可以包含用户定义的任意属性 + # 所以这里使用一个泛型参数来表示顶点类型 + vertices::Vector{V} + + # 这些还不是全部,我们会在后面添加更多参数 +) where {V} + width = size(framebuffer, 2) + height = size(framebuffer, 1) + vertex_count = length(vertices) + + # 做一些基本的检查 + @assert width > 0 && height > 0 + @assert vertex_count > 0 + # 本文中我们只讨论三角形 + # 多边形可以视为多个三角形的组合,线段则使用另外的算法,本文暂不讨论 + @assert vertex_count % 3 == 0 +end +``` + +## 准备数据 + +接下来,我们准备一点供我们渲染的数据,就从一个彩色三角形开始: + +```julia +# 三角形顶点的属性 +struct Vertex + # 位置,我们暂时只用到 x 和 y,所以两个分量就够了 + position::Vec2 + # 颜色,我们暂时不用透明度,所以三个分量就够了 + color::Vec3 +end + +# 我们采用和 OpenGL 相同的坐标系,即 y 轴向上,z 轴向屏幕外 +vertices = [ + Vertex(Vec2(-1.0, -1.0), Vec3(1.0, 0.0, 0.0)), + Vertex(Vec2(1.0, -1.0), Vec3(0.0, 1.0, 0.0)), + Vertex(Vec2(0.0, 1.0), Vec3(0.0, 0.0, 1.0)) +] +``` + +## 顶点变换与顶点着色器 + +在渲染过程中,我们要做的第一件事就是将所有顶点的坐标,根据我们“看”这个顶点的角度以及摄像头的配置,将其转换到一个 `(-1, 1) × (-1, 1)` 的区域内的坐标。数学上来说,这会涉及到几个变换矩阵和一系列线性代数运算。本文中我们不会讨论数学上的问题,只讨论工程问题 —— 即这些东西如何体现在代码中,更确切地说,如何体现在图形管线的 API 里。 + +在一些旧的图形 API,例如 OpenGL 1.x 中,顶点的属性是由类似这样的一系列函数来“设置”的: + +```c +glBegin(GL_TRIANGLES); + glColor3f(1.0f, 0.0f, 0.0f); + glVertex2f(-1.0f, -1.0f); + + glColor3f(0.0f, 1.0f, 0.0f); + glVertex2f(1.0f, -1.0f); + + glColor3f(0.0f, 0.0f, 1.0f); + glVertex2f(0.0f, 1.0f); +glEnd(); +``` + +而各种变换矩阵也是通过类似的 API 来设置的: + +```c +glMatrixMode(GL_PROJECTION); // 告诉 OpenGL 接下来我们要操作投影矩阵 +glLoadIdentity(); // 重置为单位矩阵 +glPerspective(45.0f, 1.0f, 0.1f, 100.0f); // 设置投影矩阵 + +glMatrixMode(GL_MODELVIEW); // 告诉 OpenGL 接下来我们要操作模型-视图矩阵 +glLoadIdentity(); // 重置为单位矩阵 +glTranslatef(0.5f, 0.0f, 5.0f); // 平移变换 +// 添加 +``` + +显然,这样做是有很大局限性的 +- 我们所能设置的属性种类(位置、颜色、纹理坐标等)是有限的,并且完全地受限于 OpenGL API 本身的功能 +- 我们能进行的变换也是有限的,在顶点变换阶段只有“给顶点乘一个矩阵来进行变换”这一种操作 + +因此,各大图形 API 都陆续引入了*可编程*的部分。简单来说,就是允许用户向图形 API 提交一段自己写的程序(你可以理解为“回调函数”,接下来我们也会这么实现),当图形 API 需要执行某个操作的时候就调用这段程序,这样用户就能为所欲为做对数据做自己想做的处理了。这段程序就是*着色器*。而用于变换顶点数据的着色器就是*顶点着色器*。 + +顶点着色器要接受一个顶点的所有属性,返回顶点在 `(-1, 1) × (-1, 1)` 区域内的坐标,以及其他属性。例如,我们可以编写这样一个顶点着色器: + +```julia +mutable struct ManipulatedVertex + position::Vec4 # 我们统一输出 Vec4,这样后续步骤会更统一 + color::Vec3 # 顶点颜色 + + ManipulatedVertex() = new() + ManipulatedVertex(position::Vec4, color::Vec3) = new(position, color) +end + +function vertex_shader_identity(vertex::Vertex)::ManipulatedVertex + ManipulatedVertex( + # 顶点位置不变,补上 z 和 t 分量 + Vec4(vertex.position[0], vertex.position[1], 0.0, 1.0), + # 顶点颜色不变 + vertex.color + ) +end +``` + +那么,我们修改 `render!` 函数的签名,让它接受一个代表“顶点着色器”的函数: + +```julia +function render!( + framebuffer::Framebuffer, + vertices::Vector{V}, + # 顶点着色器 + vertex_shader::VS, +) where {V, VS <: Function} + # ... +end +``` + +然后,我们就可以在 `render!` 函数中调用这个着色器了: + +```julia + # ... + + vs_outputs = map(vertex_shader, vertices) + # 在这之后,“正规化”着色器输出的顶点位置。不用太在意数学上发生了什么 + for i in 1:vertex_count + vs_outputs[i].position /= vs_outputs[i].position.w + end +``` + +## 光栅化 + +接下来,我们把变换后的顶点转换为屏幕上实际的像素了。我们每次渲染一个三角形: + +```julia + for i in 1:3:vertex_count + # 三角形的三个顶点 + v1 = vs_outputs[i] + v2 = vs_outputs[i + 1] + v3 = vs_outputs[i + 2] + + # 三个顶点的位置 + v1_pos = v1.position::Vec4 + v2_pos = v2.position::Vec4 + v3_pos = v3.position::Vec4 + + # 计算三角形的包围盒 + min_x = min(v1_pos[1], v2_pos[1], v3_pos[1]) + min_y = min(v1_pos[2], v2_pos[2], v3_pos[2]) + max_x = max(v1_pos[1], v2_pos[1], v3_pos[1]) + max_y = max(v1_pos[2], v2_pos[2], v3_pos[2]) + + # 将包围盒的坐标转换为帧缓冲上像素的坐标 + min_x_pix = round(Int, (min_x + 1.0) * width / 2.0) + min_y_pix = round(Int, (min_y + 1.0) * height / 2.0) + max_x_pix = round(Int, (max_x + 1.0) * width / 2.0) + max_y_pix = round(Int, (max_y + 1.0) * height / 2.0) + + # 确保包围盒在帧缓冲内 + min_x_pix = max(min_x_pix, 1) + min_y_pix = max(min_y_pix, 1) + max_x_pix = min(max_x_pix, width) + max_y_pix = min(max_y_pix, height) + + # 对包围盒内的每个像素点进行处理 + for y_pix in min_y_pix:max_y_pix + for x_pix in min_x_pix:max_x_pix + # Question: 这个像素是否在三角形内?如果在,我们应该把它填充成什么颜色? + end + end + end +``` + +现在还差一个问题:如何判断一个像素是否在三角形内?如果这个像素在三角形内,我们应该把它填充成什么颜色? + +## 重心坐标与插值 + +简单来说,如果我们有三个点 `(x1, y1)`,`(x2, y2)`,`(x3, y3)`,那么平面内的任意一个点 `(x, y)` 都可以表示为: + +``` +(x, y) = w1 * (x1, y1) + w2 * (x2, y2) + w3 * (x3, y3) +``` + +我们把 `w` 抽出来,那么 `(w1, w2, w3)` 就是 `(x, y)` 在三个点上的*重心坐标*。如果 `(w1, w2, w3)` 的每个分量都为正,那么 `(x, y)` 就在三角形内。详细的数学推导过程可以参见[这篇博客](https://codeplea.com/triangular-interpolation),本文依旧无视这些细节。以下是 Julia 代码实现: + +```julia +# 重心坐标计算 +function barycentric( + v1::V, + v2::V, + v3::V, + v::V +)::Tuple{Float32,Float32,Float32} where {V <: AbstractVector} + denom = (v2[2] - v3[2]) * (v1[1] - v3[1]) + (v3[1] - v2[1]) * (v1[2] - v3[2]) + + w1 = ((v2[2] - v3[2]) * (v[1] - v3[1]) + (v3[1] - v2[1]) * (v[2] - v3[2])) / denom + w2 = ((v3[2] - v1[2]) * (v[1] - v3[1]) + (v1[1] - v3[1]) * (v[2] - v3[2])) / denom + w3 = 1.0 - w1 - w2 + + return (w1, w2, w3) +end +``` + +重心坐标还有一个意义,那就是三个点的数据对于这个点的*影响程度*,或者说*权重*。有了重心坐标,实现插值就像呼吸一样简单: + +```julia +# 基于重心坐标的插值 +function interpolate3(p::Vec2, v1::T, v2::T, v3::T)::Union{T,Nothing} where {T} + # 这里用了一些 Julia 的反射机制,不用在意 + @assert hasfield(T, :position) + @assert isa(v1.position, Vec4) + + v1_xy = v1.position[1:2] + v2_xy = v2.position[1:2] + v3_xy = v3.position[1:2] + + w1, w2, w3 = barycentric(v1_xy, v2_xy, v3_xy, p) + if w1 < 0.0 || w2 < 0.0 || w3 < 0.0 + return nothing + end + + ret = T() + for field in fieldnames(T) + v1_field = getfield(v1, field) + v2_field = getfield(v2, field) + v3_field = getfield(v3, field) + setfield!(ret, field, w1 * v1_field + w2 * v2_field + w3 * v3_field) + end + return ret +end +``` + +现在,我们已经知道了三角形内某个像素点的所有属性。我们暂时跳过深度测试,接下来就剩一件事要做了:通过这个像素点的属性,确定这个像素点的颜色。 + +## 片元着色器 + +和顶点着色器一样,片元着色器旨在为图形管线增加可编程特性,从而让用户能够自定义渲染管线的行为。片元着色器接受一个像素点的所有属性作为输入,输出这个像素点的颜色。我们可以这样实现一个简单的片元着色器: + +```julia +function fragment_shader_identity(fragment::ManipulatedVertex)::Vec4 + return Vec4( + fragment.color[1], + fragment.color[2], + fragment.color[3], + 1.0 + ) +end +``` + +接着我们修改 `render!` 函数的签名,让它再接受一个代表“片元着色器”的函数: + +```julia +function render!( + framebuffer::Framebuffer, + vertices::Vector{V}, + vertex_shader::VS, + # 片元着色器 + fragment_shader::FS, +) where {V, VS <: Function, FS <: Function} + # ... +end +``` + +终于,我们可以着手完成 `render!` 函数中间那个循环里最后一步了: + +```julia + for y_pix in min_y_pix:max_y_pix + for x_pix in min_x_pix:max_x_pix + x = 2.0 * x_pix / width - 1.0 + y = 2.0 * y_pix / height - 1.0 + + # 通过重心坐标插值得到这个像素点的属性 + fragment = interpolate3(Vec2(x, y), v1, v2, v3) + + # 如果这个像素点不在三角形内,就跳过 + if isnothing(fragment) + continue + end + + # 运行片元着色器,得到这个像素点的颜色 + color = fragment_shader(fragment) + # 将颜色写入帧缓冲 + framebuffer[y_pix, x_pix] = color + end + end +``` + +在这之后,我们就可以着手进行组装: + +```julia +framebuffer = zeros(Vec4, 512, 512) +render!(framebuffer, vertices, vertex_shader_identity, fragment_shader_identity) +``` + +## 显示图像 (Syslab 限定) + +以下函数用于将帧缓冲转换为 `TyImage.imshow` 能显示的格式,你可以不用太在意具体细节。如果你选择用其他库来显示图像,你可能也需要自己实现一个类似的函数。 + +```julia +function packed2planar(packed::Framebuffer)::Array{Float32,3} + ret = Array{Float32}(undef, size(packed, 1), size(packed, 2), 3) + + height = size(packed, 1) + width = size(packed, 2) + + for i in 1:height + for j in 1:width + ret[i, j, 1] = packed[height-i+1, j][1] + ret[i, j, 2] = packed[height-i+1, j][2] + ret[i, j, 3] = packed[height-i+1, j][3] + end + end + + return ret +end + +imshow(packed2planar(framebuffer)) +``` + +## 总结 + +大功告成!你现在应该已经看到了一个彩色三角形了。如果你在哪一步遇到了问题,可以参考[完整代码](/extra/code/10mins-computer-graphics/10mins-computer-graphics.jl)。 + +到目前为止,我们已经实现了一个基本的软件渲染器,但还有很多事情没有做。例如,我们还没有实现深度测试,也没有利用我们的可编程功能做一些更有趣的事情。但是,我们已经完成了本文的目标:概览整个渲染管线,介绍计算机图形学的基本概念。*敬请期待之后的更多作品,咕咕咕,咕咕咕咕咕咕。*