简介
这是我在2023年参加公司的hackson项目,实现了一个3d鞋子的捏脸功能。很多相关知识都是第一次学习,写的比较糙,在这里简单记录一下,方便后续回顾。实现3d功能的方法有很多种,这里我使用了比较原始的方法,直接用openGL渲染,直接使用最本质的技术。
鞋子的各部位、左右脚、材料、颜色可自由组合,搭配出你喜欢的模型。本次实现主要用的OpenGl2.0实现,下面重点介绍下opengl相关技术。
OpenGL的渲染原理简介
这里以一个简单的彩色三角形为例。
整体上可以简化为以下5个步骤
-
向 GPU 传入数据。
- 通俗简单的讲,就是传入3个顶点的数据,以及三个点的颜色(纹理贴图等先忽略),以及画图的最小单元是三角形。
-
顶点着色(Vertex Shading)
- 这一步是可以进行编程的。
- 二次加工传入的三个点,在这里可以改变三个点的位置,变换出更多的形状,有几个顶点就会调用几次。下面是官方给的架构图(这个是gles2.0的架构,3.0有一些语法上的调整,但整体上变化不大)。
-
attribute vec4 vPosition; uniform mat4 vMatrix; varying vec4 vColor; attribute vec4 aColor; void main() { gl_Position = vMatrix*vPosition; vColor=aColor; };
-
图元装配(Primitive Assembly)
根据上面的三个定点以及绘画方式,决定最终呈现的形状,决定要用屏幕中的哪些点,我们这个例子就是框出来一个三角形。这个阶段是显卡本身自己处理了,我们不用关心细节。大概知道它是干什么的就行。
- 光栅化 (Rasterization)
根据上一步获取到的所有点,自动插值计算它的颜色。比如我们上面的栗子,我们给点的只有三个定点的颜色,但是最终画出来的是彩色的,这个就是光栅化自动处理了中间的点,一般都是用重心插值算法,感兴趣的可以自己去搜,这里不做过多的算术推导。
当然,这个阶段也是显卡自身处理了。我们也是知道它大概是干什么的就行
-
片段着色(Fragment Shading)
-
这一步也是可以编程的,每一个光栅化后的点都会调用一次这个方法,并将光栅化后的颜色值传给片段着色器,在这里我们可以给它赋予更多的颜色。所以这个方法的性能非常的重要。
-
precision highp float; varying vec4 vColor; void main(){ gl_FragColor=vColor; }
-
三维物体如何呈现在二维上
三维物体展示在二维上有两种投影方式,一种是透视投影,近大远小,更符合现实,一般游戏为了更加的逼真都是使用透视投影,物体的x,y值会发生改变。
另外一种是正交投影, 一般用于模型展示,正常情况下它的x/y值都不会改变。
下面就是一个通过透视投影去实现近大远小的模型。这里不做过多的赘述,使用的时候直接套公式。想了解更细的可以看这个文章,tech.bytedance.net/articles/68…
透视虽然复杂,但是呢我们的demo使用的是正交投影。
旋转、放大、平移等动画的实现
在讲各个动画之前,先回顾一下矩阵的计算公式:
平移:
假设一个点P的坐标是(a,b,c),那么平移后(x,y,z)后他的坐标是(a+x,b+y,c+z)。将平移的量转换为矩阵如下,用M表示。那么用矩阵表示平移变换就是 M+P。
{
x
y
z
}
缩放
假设一个点P的坐标是(a,b,c),那么各个方向缩放(sx,sy,sz)后他的坐标是(asx,bsy,csz)。将缩放的量转换为矩阵如下,用M表示。那么用矩阵表示平移变换就是 MP。
{
sx,0,0
0,sy,0
0,0,sz
}
旋转
由于三维的实在是太复杂了,我们用二维的举例子,三维的就直接套公式吧,好奇怎么推导的可以戳这里。
假设一个点P的坐标是(a,b,c),那么它绕z轴旋转后的坐标是(acosθ - bsinθ, asinθ + bcosθ,c)。将缩放的量转换为矩阵如下,用M表示。那么用矩阵表示平移变换就是 M*P。
{
cosθ, -sinθ, 0
sinθ, cosθ, 0
0 , 0, 1
}
这里补充个小知识。
图形学里面的三维向量一般都是用4维表示,这是为什么呢?
从上面的平移、缩放、旋转的公式可以看到,平移跟其他两个是不一样的,为了让所有的变化都可以用M*P的方式表示,所以又引入了一位,并把这种思维的坐标称为齐次坐标。
对于以上矩阵就可以变为
平移矩阵
{
1,0,0,x,
0,1,0,y,
0,0,1,z,
0,0,0,1
}
缩放矩阵
{ sx,0,0,0 0,s,0,0 0,0,sz,0
0,0,0,1 }
旋转
{
cosθ,-sinθ,0,0,
sinθ,cosθ,0,0,
0,0,1,0,
0,0,0,1
}
对于原点不在中心位置的平移、缩放,如果要进行变换,需先移动回原点,再进行操作。否则你的变换会千奇百怪。
这里我举一个最简单的栗子,如果我们要对一个物体自身旋转90度,如果直接使用旋转矩阵就会发生如下现象。
纹理映射
简单粗暴的解释:在着色的时候,不去使用光栅化传过来的值,直接去取某张图片的颜色值。
纹理映射+片段着色器很多时候可以玩一些花活,对一张图片进行各种操作会得到酷炫的效果。
www.shadertoy.com/view/4tcGDr 酷炫的着色器网站
3D模型的简介与渲染(这里用obj格式的3d模型作为例子)
顶点数据 (Vertex data) : ** v 几何体顶点 (Geometric vertices) vt 贴图坐标点 (Texture vertices) vn 顶点法线 (Vertex normals)
面数据
f 面 (Face)
一个最简单的模型
v 0 100 0
v 100 0 0
v 100 0 100
v 100 100 100
vt 0.5 0.5
vt 0.5 0.5
vt 0.5 0.5
vn 0 0 1
vn 0 0 1
vn 0 0 1
f 1/1/1 2/2/2 3/3/3
f 2/2/2 3/3/3 4/4/4
展示效果(为了方便观看,方向旋转了一下)
暂时无法在飞书文档外展示此内容
鞋子不渲染贴图的样子
光照的处理
认真学过初中物理的都知道,物体的颜色是反射出去的颜色,所以决定颜色的因素就是光照。计算机图形学有时候为了提升效率,把光分为以下三种,镜面光、漫反射、环境光。
(该图来自GAMES101-现代计算机图形学入门)
暂时无法在飞书文档外展示此内容
漫反射光:主要由两个因素影响,一个是物体自身的反光系数,另外一个是物体表面的点法向量与光照的夹角。简化后的计算方式为: 漫反射系数 * 法向量与光照方向的夹角。
镜面光:生活中我们经常会被一些玻璃的反射光辣眼睛,出现辣眼睛的原因就是反射光的角度跟我们人眼观察的角度接近。稍微调节一点点角度,将不会再被辣眼睛。目前符合突变的数学模型是cos函数,如何使cos变的前堵后缓?当然是自己乘自己了,如果还不够那么继续乘,所以这里我们是用pow函数,一般cos的50次幂即可。
环境光:附近物体反射的光有可能也会打到物体上,计算起来巨复杂,一般都是给一个固定值。很多游戏调节明暗都是调的这个固定值。
漫反射光跟镜面反射光的计算方式如下:
// vKd 漫反射系数
// vks 镜面反射系数
// vMatrix 旋转平移放大等变换的矩阵
// 漫反射 ,有的人可能会纳闷为什么不直接用 法向量和漫反射系数。而是把法向量一顿操作,那是因为旋转是会改变法向量滴。
vec3 newNormal=normalize((vMatrix*vec4(vNormal+vPosition,1)).xyz-(vMatrix*vec4(vPosition,1)).xyz);
vec3 vp=normalize(lightLocation-(vMatrix*vec4(vPosition,1)).xyz);
vDiffuse=vec4(vKd,1.0)*max(0.0,dot(newNormal,vp));
// 镜面反射光
vec3 eye= normalize(camera-(vMatrix*vec4(vPosition,1)).xyz);
// 求视线与光线的半向量
vec3 halfVector=normalize(vp+eye);
// 法线与半向量的点积就是余弦值
float nDotViewHalfVector=dot(newNormal,halfVector);
// 计算反射光的因子,判断是否要高亮展示
float powerFactor=max(0.0,pow(nDotViewHalfVector,shininess));
vSpecular=vec4(vKs,1.0)*powerFactor; //计算镜面光的最终强度
材质的实现
材质的实现依赖光照系数以及纹理映射。比如棉布是网格状的纹理,橡胶是光滑的表面。
由于我们的需求是实现不同颜色的纹理,直接使用纹理贴图是没法满足的,每一种颜色都要使用一张图,不太现实。
要实现这个效果,只能在颜色着色器中自己绘制纹理。
对于棉布材质的物体,我们可以看出它是有一格一格的效果。这里我们可以通过在片段着色器上根据纹理坐标去计算x或者y处于某个坐标下即可。
皮质的特点是会有凸起的部分点,通过计算x跟y同时处于某个范围即可判断出来。
// fragColor
bool isInRange(float num,float ratio){
return num < 6.0*ratio && num > 3.0*ratio;
}
// 计算出通过漫反射、镜面反射后最终的颜色
vec4 fragColor=finalColor*vAmbient+finalColor*vSpecular+finalColor*vDiffuse;
// 棉质的方格绘制方法。
if (isInRange(modx,0.0001) || isInRange(mody,0.0001) ) {
fragColor = vec4(inFragColor.r*0.5,fragColor.g*0.5,fragColor.b*0.5,0.7);
}
// 皮质的绘制方法
if (isInRange(mod(fx,0.01),0.001) && isInRange(mod(fy,0.01),0.001)) ) {
fragColor = vec4(fragColor.r*0.8,fragColor.g*0.8,fragColor.b*0.8,0.7);
}
捏脸的实现
理解完上面的内容之后,到捏脸这块就非常简单了。只需要让 UE 出图的时候把每个部位模块化,这里就需要介绍obj格式3d资源的另外一个文件mtl,每个部位使用不同的mtl,动态替换mtl的值即可。
我们替换不同部位的颜色值、材料、反光系数等。
// mtl
newmtl shows_body
Ns 32
d 1
Tr 0
Tf 1 1 1
illum 2
Ka 0.8000 0.8000 0.8000
Kd 0.8000 0.8000 0.8000
Ks 0.3500 0.3500 0.3500
map_Kd brown.jpeg
newmtl shoes_tongue_left
Ns 26.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 1.0000 1.0000 1.0000
Kd 1.0000 1.0000 1.0000
Ks 0.3240 0.3240 0.3240
Ke 0.0000 0.0000 0.0000
map_Ka red.webp
map_Kd red.webp
// obj 文件
v 0 100 0
v 100 0 0
v 100 0 100
v 100 100 100
vt 0.5 0.5
vt 0.5 0.5
vt 0.5 0.5
vn 0 0 1
vn 0 0 1
vn 0 0 1
usemtl shoes_tongue_left
f 1/1/1 2/2/2 3/3/3
f 2/2/2 3/3/3 4/4/4