matcap原理
材质捕捉(material capture)简称 MatCap ,材质通过渲染一个球到纹理而被捕捉。同类的词有动态捕捉(motion capture,简称 mocap)。matcap 流行于 2007年时的三维雕刻软件 ZBrush ,在雕刻构建物体模型时,没有网格、材质信息,通过 matcap ,艺术家们可以快速有效地得到反馈,直观地查看模型形状起伏的变化。

渲染需要几何体、光源、材质、shader 的共同参与。matcap 将光源、材质信息直接烘焙到一张纹理上,渲染时直接拿来用。在不同的 matcap 纹理之间切换,它能很真实地表现出各类材质信息。它不考虑光源信息,也就没有实际的光照计算。增加一盏灯或都去掉,照还是不照,效果就在那里,不增不减。你可以把材质的各种属性(粗糙度、各向异性),以及各种效果(硬阴影、反射、Fresnel)信息预先写入纹理,渲染时不会增加开销。甚至可以在 matcap 上作文章,在两个 matcap 纹理之间混合(alpha blending)。

其原理也好理解,圆内任意一点可以映射成半球面上的单位法向量,然后每个像素有一个颜色,所以可以建立从法向量到颜色的映射。圆内存储了半球面上所有朝向的颜色,通过查表向量来给物体表面上色。注意是正交投影,不是透视投影,透视做不到看到半球的边缘。纹理坐标 texcoord = vec2(u, v) 在[0, 1]x[0, 1]范围,法向量 normal = vec3 (x, y, z) 的 Z 轴分量 z ≥ 0。中央的纹理坐标为 (0.5, 0.5),法向量为 (0, 0, 1)。texcoord 和 normal 是一一对应关系,可以在烘焙时从 normal 推导出 texcoord,可以在渲染时从 texcoord 推导出 normal。

渲染时的 GLSL shader 代码如下:
在图形学里会遇到多种坐标系,涉及到物体空间(object space)、世界空间(world space)、视图空间(view space)、切向空间(tangent space)的转换。转换可视作乘以一个矩阵,反向变换则视作乘以其逆矩阵。在软件中雕刻物体,我们希望这个 normal 是视图空间的,即光源相对相机固定,如同入矿井作业工人佩戴在头上的照明灯一样,很方便查看正对位置的几何形状。在游戏中,normal 一般是世界空间的,光源相对世界固定。
从物体空间到世界空间,normal 的变换与 position 的变换有所不同。position 乘以model 矩阵变换到世界空间。normal 则要乘以model 矩阵转置的逆(或逆的转置,效果一样)变换到世界空间。我在这里解答过。
大多时候你发现,normal 按 position 的做法来,好像效果也没错。的确,如果变换的物体是刚体(rigid body),或者矩阵的缩放系数是 vec3(1, 1, 1),仍然正确。此时矩阵是单位正交矩阵,存在性质矩阵的逆等于矩阵的转置 M−1=MT。于是,逆的转置等于两次转置,回到自身M。当然,如果有先验条件(precondition)——变换是统一的缩放(uniform scaling),对乘后得到的向量Mv再 normalize() 一下,也能保证正确。
view 矩阵通过 right/forward/up 三个标准正交基(orthonormal basis)构成,是正交矩阵(orthogonal matrix)无疑。提醒一下括号里的单词,orthogonal 是正交的,orthonormal 比 orthogonal 多出了归一化,是单位正交的。model 矩阵却没有保证,可能混合着缩放系数,甚至非均匀缩放,即各个轴上的缩放系数不一致。如果不确定物体的变换是否满足上面提到的性质,就老老实实来。
matcap 渲染出的效果竟这么好使,那么,哪里可以获取呢?根据难易程度,获取 matcap 有以下几种途径:
网上搜索关键词,下载图片使用。软件社区送温暖,如ZBrush 分享了一些 MatCap 库。
自行打光,安置环境,对材质球取景拍摄。
用 Blender、Modo、ZBrush 这样的3D建模软件渲染。
在游戏引擎中烘焙渲染,写程序生成。
手工绘制纹理,后期可借助 Photoshop 调色。
如果无法从3D建模软件安装包里直接获取 matcap 图片资源,可以在工程中生成。打开建模软件,新建一个球,细分(subdivide)多次到表面足够光滑,应用上软件里内置的 matcap 效果,调整好相机位置,烘焙时用正交投影,导出这张预览图片,尺寸以 512x512、1024x1024 为宜,太小则边缘存在瑕疵。
制作图片是有一定要求的,要求图片是正方形,并保证图片中的圆跟图像边缘相切,不能多也不能少!因为圆中的任意一点,跟球面的法向量对应,渲染计算涉及到半球面上的各个朝向,所以要铺满。然后制作matcap的时候,尽量不要参杂过多的高频细节。

上面是3D渲染方案,你也可以尝试布置一两盏灯,用 Phong 光照明方程 I=Ia+Id+Is=kaIa+kdId(l→⋅n→)+ksIs(v→⋅r→)shineness 生成 matcap 图片。观察方向是正上方,平行投影取 v→ = (0, 0, 1), n→ 在半球面上变化着,反射方向 r→ 用 shader 里的内置函数 reflect 计算,式子为 I - 2 * dot(I, N) * N,这里有其推导。因为不涉及顶点的坐标变换,只有向量,不用走图形学的可编程的功能管线(programmable function pipeline),遍历图片上圆内每个像素计算,每个像素对应一个法向量 n→ ,思路清晰,这里不多讲。
下面给出我用代码生成的打侧光的图片。径向对称。中心最黑,越靠近边缘越亮。

不计算光照,直接将法向量编码成颜色的效果如下。对应blender里面的check_normal+y.exr。需要留意 X、Y 分量的取值范围[-1, +1]到[0, 1]的转换。

附上代码。因为图片的Y轴朝下,纹理的V轴朝上,计算需要上下翻转一下
讲解到这,就可以结束了。饶有兴趣的,可继续观摩一下 3D 建模软件 Blender 里 matcap 的实现,学一学别人的代码和思路。
图中的 matcap 资源在 <DIR>/datafiles/studiolights/matcap/ 目录下。通过命令行 sudo apt-get install blender 安装的,对应的 DIR 为 /usr/share/blender/,如果自己从官方下载解压的包, DIR 则为解压里的版本号目录,如2.91。目录下都是 .exr 格式的图片。比起现有的8位和16位的图像文件格式,OpenEXR 文件有更高的动态范围(HDR)和色彩精确度。图片是 half 浮点精度,可以用 GIMP 软件打开查看或修改。上面我们用代码生成的 normal 图,对应文件夹里的 check_normal+y.exr。
Blender 是开源软件,源码从 git://http://git.blender.org/blender.git 下载,GitHub 上也有官方镜像[1]。matcap 资源在 release/datafiles/studiolights/matcap/,可以用 locate jade.exr 命令马上定位到。Blender 自 2.8 版本开始,要求硬件支持 OpenGL 3.3 以上版本。
进入到 workbench 里的 shaders 目录(source/blender/draw/engines/workbench/shaders/),与 matcap 相关的两个 shader 程序为 workbench_composite_frag.glsl 和 workbench_matcap_lib.glsl。
其中,宏 V3D_LIGHTING_STUDIO、V3D_LIGHTING_MATCAP、V3D_LIGHTING_FLAT 分别对应上面 viewport shading 界面下的 studio | matcap | flat 三个选项。关键代码 get_matcap_lighting() 函数来自 workbench_matcap_lib.glsl 文件。
GLSL 代码是基于字符串编译的,而不是文件,所以看不到 #include 的语法。 OpenGL 的函数 glShaderSource 是支持 shader 字符串数组编译的,少有人用。
Blender、Unreal 引擎采用 #include 语法,灵活性更高,需要在编译 shader 前预处理替换文本[2]。代码中 BLENDER_REQUIRE 的预处理在 draw_manager_shader.c。
materialBuffer 和 normalBuffer 是通过 FBO 渲染出的两张纹理。当选择用 matcap 时,将片元的朝向信息 gl_FrontFacing 编码到 materialBuffer 的 alpha 通道,见workbench_prepass_frag.glsl。在 viewport 成了2D 平面,需要4个顶点数据渲染一个平面,uvcoordsvar 在 vertex shader 阶段是通过 gl_VertexID 计算出的,没有通过 VBO 传递顶点属性数据。
get_view_vector_from_screen_uv 函数见名知意,代码如下。ProjectionMatrix[3][3] == 0.0 用来检测是透视投影,还是平行投影。透视投影的 M[3][3] 为0,平行投影的 M[3][3] 为1。vec4 ViewVecs[2];存储了视锥体 NDC(-1.0, -1.0, -1.0) 和 NDC(1.0, 1.0, 1.0) 的坐标,以及near、far 深度信息。代码可以看成 z = 1 平面上的uv坐标投影到半球面上,得到观看方向上的单位向量。平行投影则始终朝上 (0, 0, 1)。
workbench_normal_decode 来自文件 workbench_common_lib.glsl,有 decode 就有 encode,功能是压缩、解压一个单位法向量。为什么要压缩存储法向量?因为它限定了浮点数的范围在 [-1.0, +1.0] 区间;存在关系 x2+y2+z2=1;延迟渲染(deferred rendering)里的 G-Buffer 要存储很多通道的数据,带宽尤其宝贵,腾出空间给其它通道用。对于压缩出现过多方法[3],最常见的是存储x、y,丢弃z,运行时还原 z = sqrt(1 - x * x - y * y)。其他方法和效率对比分析,见提到的链接。代码中的 workbench_float_pair_decode 则是另一个压缩,将 roughness 和 metallic 合成一个浮点数,再分解使用。
以下为 workbench_matcap_lib.glsl 文件代码。matcap_uv_compute 函数进行了一些“骚”操作,让人不明所以,即使注释中说明了构建正交基。该段代码来自 Frisvad 的文章《从3D单位向量构建正交单位基,省去归一化的方法》[4]。 从单位向量 I = (x, y, z) 找到两个正交基为 b1→=(1−x21+z,−xy1+z,−x) 和 b2→=(−xy1+z,1−y21+z,−y) 。具体的数学推导和改进方案,见我的这篇知乎文章。对于频繁执行的代码片段,优化一点点能得到大的提升。Hughes-Möller 方法用了 normalize() 操作,Frisvad 用一次除法换取移除 normalize() 操作。将向量 N 投影到 b1→,b2→ 张成的平面上,得到该平面上的uv坐标。
代码中的 flipped(或 matcap_orientation)对应上面图中齿轮下的双向箭头<-->,matcap 的效果可以左右镜像,类似图片的左右翻转,这个功能对雕刻还是有用的,方便对比查错。我们将漫反射和高光烘焙到一起了,而 Blender 将 diffuse 和 specular 分开成两张纹理,且用 use_specular 控制是否开启高光,use_specular 对应界面最下方的 specular lighting 单选项。
matcap 选项的左边是 studio,studio light 里预置了几个灯光布局。点击旁边的齿轮图标,可以自定义场景中灯光的布局。参数有漫反射颜色、高光颜色、光滑度、方向,拖拽修改可以看到场景中实时的光照效果变化,默认值初始化在文件 studiolight.c 的BKE_studiolight_default 函数,然后 release/datafiles/studiolights/studio/ 目录下也有几个预制灯光。

对于低模(low poly)的渲染,可以使用法线贴图(normal mapping)技术。对于高细节的模型减面,生成低模,两者建立映射关系后,烘焙出法线贴图。使用 normal map 而不是顶点的 normal 属性,可以展示模型的更多细节。