自定义渲染3D 物体

142 阅读9分钟

简介

这是我在2023年参加公司的hackson项目,实现了一个3d鞋子的捏脸功能。很多相关知识都是第一次学习,写的比较糙,在这里简单记录一下,方便后续回顾。实现3d功能的方法有很多种,这里我使用了比较原始的方法,直接用openGL渲染,直接使用最本质的技术。

鞋子的各部位、左右脚、材料、颜色可自由组合,搭配出你喜欢的模型。本次实现主要用的OpenGl2.0实现,下面重点介绍下opengl相关技术。

image.png image.png

OpenGL的渲染原理简介

这里以一个简单的彩色三角形为例。

整体上可以简化为以下5个步骤

image.png
  1. GPU 传入数据。

    1.   通俗简单的讲,就是传入3个顶点的数据,以及三个点的颜色(纹理贴图等先忽略),以及画图的最小单元是三角形。
  2. 顶点着色(Vertex Shading)

    1.   这一步是可以进行编程的。
    2.   二次加工传入的三个点,在这里可以改变三个点的位置,变换出更多的形状,有几个顶点就会调用几次。下面是官方给的架构图(这个是gles2.0的架构,3.0有一些语法上的调整,但整体上变化不大)。
    3.  attribute vec4 vPosition; 
       uniform mat4 vMatrix;
       varying  vec4 vColor;
       attribute vec4 aColor;
      
       void main() {
           gl_Position = vMatrix*vPosition;
           vColor=aColor;
       };
      
  3. 图元装配(Primitive Assembly)

根据上面的三个定点以及绘画方式,决定最终呈现的形状,决定要用屏幕中的哪些点,我们这个例子就是框出来一个三角形。这个阶段是显卡本身自己处理了,我们不用关心细节。大概知道它是干什么的就行。

  1. 光栅化 (Rasterization)

根据上一步获取到的所有点,自动插值计算它的颜色。比如我们上面的栗子,我们给点的只有三个定点的颜色,但是最终画出来的是彩色的,这个就是光栅化自动处理了中间的点,一般都是用重心插值算法,感兴趣的可以自己去搜,这里不做过多的算术推导。

当然,这个阶段也是显卡自身处理了。我们也是知道它大概是干什么的就行

  1. 片段着色(Fragment Shading)

    1.   这一步也是可以编程的,每一个光栅化后的点都会调用一次这个方法,并将光栅化后的颜色值传给片段着色器,在这里我们可以给它赋予更多的颜色。所以这个方法的性能非常的重要。

    2.  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度,如果直接使用旋转矩阵就会发生如下现象。

image.png

纹理映射

简单粗暴的解释:在着色的时候,不去使用光栅化传过来的值,直接去取某张图片的颜色值。

纹理映射+片段着色器很多时候可以玩一些花活,对一张图片进行各种操作会得到酷炫的效果。

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