OPENGLPG第九版学习 - 细分着色器

发布于:2025-09-14 ⋅ 阅读:(21) ⋅ 点赞:(0)

glad生成器

9.1 细分着色器

  • 顶点着色器的局限性:顶点着色器只是在处理当前顶点的过程中对其关联的数据进行更新而已,它甚至无法做到对图元中其他的顶点数据进行访问。
  • 细分着色器弥补:可以实现生成三角形模型网格等类型,其中用到了一种新的几何体图元类型作为输入,称作面片(patch)
  • 在细分控制着色器执行之前,需要先指定面片(顶点的有序列表,即由一组输入控制点组成)。
  • 在图形渲染管线中,细分曲面着色器分为三个阶段:
    • 细分控制着色器(Tessellation Control Shader),可编程,操作对象输出到TES中的控制点
      • 处理面片顶点,并设置面片中要生成多少几何数据。
      • 可选,有不使用细分控制着色器设置细分所需数据的方法。
    • 细分图元生成器(Tessellation Primitive Generator),不可编程。
      • 在控制着色器的输出传递给图元生成阶段后,将在其中生成几何图元的网格以及细分坐标,供计算着色器使用。
    • 细分计算着色器(Tessellation Evaluation Shader),可编程,操作对象细分后的细分点,功能类似于顶点着色器。
      • 将每个顶点放置在最终网格的对应位置上。

9.2 细分面片

  • 面片 :是几何体图元类型,是一个传递给OpenGL的顶点列表(就是控制着色器的操作对象——控制点),处理过程中需要保证顺序正确。
  • 细分过程并不会对OpenGL的经典几何图元类型进行处理,直接略过。
  • 所有管线中启用的着色阶段都可以处理面片数据。
  • 如果已经启用细分着色器,那么将其他类型的几何体传递给它会生成一个GL_INVALID_OPERATION错误。
  • 但是如果没有绑定任何细分着色器,渲染面片数据也会得到一个GL_INVALID_OPERATION错误。
  • 渲染细分面片:我们可以使用glDrawArrays等OpenGL的绘制命令+图元类型GL_PATCHES,然后设置绘制命令中的顶点总数(当前所有面片的总顶点数),从顶点缓存对象中读取数据即可。
    • 在调用绘制命令前,必须使用glPatchParameteri告知OpenGL顶点数组中构成一个面片的顶点总数,即显示指定当前面片在细分控制着色器(TCS)中的控制点个数。
      • pname必须为GL_PATCH_VERTICES。
      • 如果value小于0或者大于GL_MAX_PATCH_VERTICES,产生GL_INVALIDE_ENUM。
      • 一个面片的默认顶点数量为3。如果面片的顶点数量小于这个值,那么将忽略这个面片,并且不会产生几何体。
    • 同一个绘制命令中处理的面片大小总是相同的。
    • 不同硬件能够指定的最大控制点的数量可能不同,可以使用glGetIntegerv(GL_MAX_PATCH_VERTICES, &maxPatchVertices);查询。
void glPatchParameteri(	GLenum pname,
 	GLint value);
 
 // pname还可以为GL_PATCH_DEFAULT_OUTER_LEVEL 和 GL_PATCH_DEFAULT_INNER_LEVEL 
 // 设置默认细分级别的参数
void glPatchParameterfv(GLenum pname,
 	const GLfloat *values);

设置细分面片

  • 每个面片的顶点都会传入当前绑定的顶点着色器处理,然后初始化细分控制着色器中内置声明的数组 gl_in
  • gl_in中的元素个数与 glPatchParameteri所设置的每一个面片的构成大小是相同的
  • 在细分控制着色器内部,我们可以通过变量gl_PatchVerticesIn来获取gl_in的元素个数(相当于查询gl_in.length())。
GLfloat vertices[8][2] = 
{
{-0.75,-0.25},......
}
glBindVertexArray(vao);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices),vertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, (void*)0);
glEnableVertexAttribArray(0);
//每个面片有4个顶点。渲染两个面片
glPatchParameteri(GL_PATCH_VERTICES,4);
glDrawArrays(GL_PATCHES, 0, 8);//总共8个顶点

9.3 细分控制着色器TCS

  • 使用glCreateShader(GL_TESS_CONTROL_SHADER)
  • 可以调用细分控制着色器(如果已经绑定)并完成下面的操作:
    • 生成细分输出面片的顶点(tessellation output patch vertices)并传递到细分计算着色器,同时更新所有逐顶点或者逐面片的属性值。
    • 设置细分层次因数,以控制生成图元的操作。这些因数属于特殊的细分控制着色器变量,分别为gl_TessLevelInnergl_TessLevelOuter,它们在细分控制着色器中是隐式声明的。

9.3.1 生成输出面片的顶点

  • 细分控制着色器使用的是应用程序设置的顶点,也就是输入面片的顶点(input-patch vertices),是控制点
  • 生成新的顶点列表就是输出面片的顶点(output-patchvertice),它们保存在细分控制着色器的gl_out数组中。
    • 输出控制点,可以只是传递。
  • 可以通过在着色器中使用布局限定符(layout )来设置输出面片顶点的数量:例如设置输出面片顶点的总数为16。
    • 设置了gl_out的数量。
    • 设置细分控制着色器执行的次数:即每个输出面片顶点执行一次。
      layout(vertices = 16) out;
  • 判断当前处理的时那个输出顶点 gl_out[gl_InvocationID]
    • 可以使用**barrier()**函数,同步同一个Patch内所有控制点。
      • 注意不能在子函数中调用;所有控制点必须执行相同次数的barrier()调用。
  • 直接传递面片顶点的细分控制着色器示例:
#version 460 core
layout(vertices = 4) out; 
void main()
{
	// 每次调用处理自己的控制点
    gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
    //然后设置细分的层级...
}

9.3.2 细分控制着色器的变量

gl_in 和gl_out

in gl_PerVertex {
    vec4  gl_Position;
    float gl_PointSize;
    float gl_ClipDistance[];
    float gl_CullDistance[];
} gl_in[gl_PatchVerticesIn];

out gl_PerVertex {
    vec4  gl_Position;
    float gl_PointSize;
    float gl_ClipDistance[];
    float gl_CullDistance[];
} gl_out[gl_PatchVerticesOut];

细分控制着色器的输入变量

变量 描述
gl_InvocationID 当前细分控制着色器请求的输出顶点索引
gl_primitiveID 当前输人面片的图元索引
gl_PatchVerticesIn 输人面片的顶点数量,也就是 gl_in的大小
gl_PatchVerticesOut 输出面片的顶点数量,也就是gl_out的大小

9.3.3 细分的控制

  • 细分控制着色器负责控制要渲染的输出面片类型,也就是细分发生的区域。
    • OpenGL支持三种不同的细分域(tessellation domain):四边形、三角形,或者等值线集合。
  • 细分的总量是通过两组数据来控制的:内侧和外侧细分层级,都是浮点数。
    • 外侧细分层级负责控制细分区域的周长,即控制面片边界(边缘)的细分程度,它保存在一个声明为4个元素的数组gl_TessLevelOuter中。
      • 例如gl_TessLevelOuter[0] = 2.0代表了左边缘分成2段。
    • 内侧细分层级设置的是细分区域的内部划分方式,即控制面片内部的细分程度,它保存在一个声明为2个元素的数组gl_TessLevelInner中。
      • 例如 四边形面片中,gl_TessLevelInner[0] = 3.0(内部水平分成3段),gl_TessLevelInner[1] = 4.0(内部垂直分成4段),这个四边形面片会被细分成一个3x4的内部网格,而每条边按照各自的细分级别进行划分。
    • 每个细分层级因数都设置了细分区域的“段数”,以及要产生的细分坐标和几何图元。
    • 这些隐式声明的细分层级因数数组都是固定大小的,并且数组中可用数据的数量依赖于细分域的类型。

四边形细分

  • 四边形面片被映射到一个2D 参数化坐标系中。
  • 实体的原点表示细分的坐标,每个细分坐标都是细分计算着色器的一个输人值。
  • 对于四边形域来说,细分坐标有两个坐标值(u,v),它们的范围均为[0,1],并且每个细分坐标都会被传人细分计算着色器的一个请求当中。
gl_TessLevelOuter[0] = 2.0;//(左边缘分成2段)
gl_TessLevelOuter[1] = 3.0;//(下边缘分成3段)
gl_TessLevelOuter[2] = 2.0;//(右边缘分成2段)
gl_TessLevelOuter[3] = 4.0;//(上边缘分成4段)
gl_TessLevelInner[0] = 3.0;//(内部垂直分成3段)
gl_TessLevelInner[1] = 4.0;//(内部水平分成4段)

在这里插入图片描述

等值线细分

  • 等值线域也会生成(u,v)形式的细分坐标,并传递给细分计算着色器。
  • 只会用到gl_TessLevelOuter[0]和gl_TessLevelOuter[1],不会用到内侧细分层级。
  • v=1的边上有一条虚线。这是因为等值线的结果并不包含边长上的等值线数据。
gl_TessLevelOuter[0] = 6;
gl_TessLevelOuter[1] = 8;

在这里插入图片描述

三角形细分

  • 三角形区域使用重心坐标系(barycentric coordinates)来设置自己的细分坐标。通过三个数**(u,v,w)**来表示,每个数范围都是[0,1]可以将u、v和w看做是三角形的各个独立顶点的权重值
    • 对于一个三角形ABC(顶点A、B、C),其内部任意点P的重心坐标系可表示为:P = u * A + v * B + w * C
      • 其中u, v, w ≥ 0;u + v + w = 1
      • 这三个系数实际是点P到三角形各边的相对面积比例,例如:
        u = ΔPBC的面积 / ΔABC的面积
      • 在仿射变换(线性+平移)后,三角形内部点的重心坐标保持不变。
      • 线性插值一致性:细分后所有属性(位置、法线、UV)在三角面上连续平滑过渡。
      • GPU硬件可并行化处理重心坐标插值。
  • 只用了外侧细分层级的前三个值和内侧细分层级的第一个值。
  • 与四边形细分(内部被划分为一组四边形网格)相比,三角形域的内部被划分为一组同心的三角形。
  • 外侧细分层级还是用来划分各个边。
  • 内侧细分层级决定了三角形内部区域被细分成多少个小三角形的密度,值越大,内部生成的小三角形越多
    • 如果为奇数,那么到周长为止将生成(内侧细分层级-1)/2个同心三角形,但是中心点(重心坐标)不再是一个细分坐标。
    • 如果为偶数,那么三角形域的中心(重心坐标)将定位于(1/3,1/3,1/3),在中心点和周长之间生成(内侧细分层级/2)-1个同心三角形。
      在这里插入图片描述
gl_TessLevelOuter[0] = 6.0;//(左边缘分成6段)
gl_TessLevelOuter[1] = 5.0;//(下边缘分成5段)
gl_TessLevelOuter[2] = 8.0;//(右边缘分成8段)
gl_TessLevelInner[0] = 5.0;//(内部分成5分)

在这里插入图片描述

忽略细分控制器

  • 很多时候细分控制着色器就是一个传递性质的着色器,只是把输入数据(输入面片)拷贝到输出变量(输出面片)中而已。
  • 可以通过OpenGLAPI而不是着色器来设置细分层级参数:
    • 如果没有绑定细分控制着色器,则用这个函数来设置内侧和外侧细分层级因数。
    • pname必须是GL_PATCH_DEFAULT_OUTER_LEVEL (values必须是4个浮点数值组成的一个数组)或GL_PATCH_DEFAULT_INNER_LEVEL (values必须是2个浮点数值组成的一个数组)。
 // 设置默认细分级别的参数
void glPatchParameterfv(GLenum pname,
 	const GLfloat *values);

9.4 细分计算着色器TES

  • 使用glCreateShader(GL_TESS_EVALUATION_SHADER)
  • 细分控制着色器->细分图元生成器:生成几何图元的网格以及细分坐标。
  • 当前的细分计算着色器可以从gl_TessCoord获得细分坐标
  • 每个通过图元生成得到的细分坐标都需要执行一次细分计算着色器,用以判断从细分坐标而来的顶点位置
    • 一般通过 gl_TessCoord中的细分网格点gl_in中的控制点(细分控制着色器的输出顶点(即gl_out数组中的gl_Position值)),使用双线性插值计算顶点位置计算出实际的齐次坐标系(类似顶点坐标系,给gl_Position 赋值)。
  • 细分计算着色器要将顶点变换到屏幕坐标(除非细分计算着色器的数据还要继续传递到几何着色器处理)去使用(传入片元着色器)
  • 使用TES第一步就是配置图元生成器TPG:通过layout(......) in,可以设置细分域和生成的图元类型实体图元的面朝向(用于背面裁减),以及细分层级在图元生成过程中的使用方式
    • layout限定符中选项的设置顺序并不重要。 一般是layout(primitive_type, spacing_mode, vertex_order, point_mode) in;
    • TPG是固定功能阶段,不可编程,但是TES通过layout可以间接影响TPG。

9.4.1 设置图元生成域

  • 细分计算着色器的图元类型(细分坐标生成的域的类型):
    • 决定细分网格拓扑,必须指定。
图元类型 描述 域坐标
quads 单位块上的一个四边形域 (u,v)对的形式,u和v的范围从0~1
triangles 使用重心坐标的三角形 (a、b,c)坐标形式,a、b和c的范围均为0~1,且有a+b+e=1
isolines 一系列穿过单位块的线段集合 (u,v)对的形式,u的范围从0~1,v的范围从0到接近于1的数值

9.4.2 设置生成图元的面朝向

  • 对于OpenGL中的任何填充类型的图元来说,顶点的顺序都会决定图元的面朝向问题。
  • 使用layout 布局限定符的时候,设置cw表示顶点按照顺时针排列,或者ccw表示顶点按照逆时针顺序排列。
  • 默认为cw。

9.4.3 设置细分坐标的间隔

  • 控制外侧细分层级的小数值,用它来控制周长边上的细分坐标生成方法(内侧细分层级会受到下述选项的影响),从而控制细分点的分布。
    • max表示 OpenGL具体实现中能够支持的最大细分层级值。
选项 描述
equal_spacing 等距划分,细分层级将被截断在[1,max]范围内,然后取整到下一个整数值
fractional_even_spacing 偶分数划分,数值将被截断在[2,max]范围内,然后取整到下一个偶数整数值n。然后将边界划分为n-2个等长的部分,以及2个位于两端的部分(可能比其他部分的长度更短)
fractional_odd_spacing 奇分数划分, 数值将被截断在[1,max-1]范围内,然后取整到下一个奇数整数值n。然后将边界划分为n-2个等长的部分,以及2个位于两端的部分(可能比其他部分的长度更短)

9.4.4 更多的细分计算着色器 layout选项和示例

  • 如果需要输出点集,而不是等值线或者填充区域,那么我们可以使用points 选项,它会为细分计算着色器处理的每个顶点渲染一个单独的点。默认禁用。
  • 示例:
layout(quads, equal_spacing,cw , points) in;

9.4.5 设置顶点的位置(插值计算公式)

  • 着色器中的细分坐标是通过gl_TessCoord变量给出的。
  • 双线性插值计算顶点位置公式:四边形 / quads
    • P₀, P₁, P₂, P₃ 是四边形四个控制点(即TCS中的gl_out或TES中的gl_in)
    • (u,v) 是标准化参数坐标(范围 [0,1]×[0,1]),就是细分域坐标范围。
      P(u,v) = (1-u)(1-v)P_0 + u(1-v)P_1 + uvP_2 + (1-u)vP_3
    • glsl中双线性插值
      • mix / lerp应用场景:
        1. 在两种颜色之间平滑过渡
        2. 在两个纹理采样值之间混合
        3. 在两个向量或位置之间进行插值
        4. 根据一个因子来混合两个材质属性
//不限制控制点,获得四个角点:
// gridWidth: 网格的宽度(列数),gl_TessLevelInner[1]
// gridHeight: 网格的高度(行数),gl_TessLevelInner[0]
vec3 generalLinearInterpolation(int gridWidth, int gridHeight, float u, float v)
{
    // 将u和v参数映射到网格索引(从0开始)
    float uCell = u * (gridWidth - 1);
    float vCell = v * (gridHeight - 1);
    
    // 确定当前所在的网格单元,floor:向下取整
    int uIndex = int(floor(uCell));
    int vIndex = int(floor(vCell));
    
    // 确保索引在有效范围内
    uIndex = clamp(uIndex, 0, gridWidth - 2);
    vIndex = clamp(vIndex, 0, gridHeight - 2);
    
    // 计算单元内的局部参数,fract:返回一个数的小数部分
    float uLocal = fract(uCell);
    float vLocal = fract(vCell);
    
    // 获取当前单元的四个角点
    int idx00 = vIndex * gridWidth + uIndex;
    int idx01 = vIndex * gridWidth + uIndex + 1;
    int idx10 = (vIndex + 1) * gridWidth + uIndex;
    int idx11 = (vIndex + 1) * gridWidth + uIndex + 1;
    
    vec3 p00 = gl_in[idx00].gl_Position.xyz;
    vec3 p01 = gl_in[idx01].gl_Position.xyz;
    vec3 p10 = gl_in[idx10].gl_Position.xyz;
    vec3 p11 = gl_in[idx11].gl_Position.xyz;
    
    // 执行双线性插值
    vec3 p0 = mix(p00, p01, uLocal);
    vec3 p1 = mix(p10, p11, uLocal);
    
    return mix(p0, p1, vLocal);
}



// 传入四个角点
vec3 bilinearInterpolation(vec3 p00, vec3 p01, vec3 p10, vec3 p11, float u, float v)
{
    // 在U方向上进行两次线性插值
    vec3 p0 = mix(p00, p01, u);
    vec3 p1 = mix(p10, p11, u);
    
    // 在V方向上进行线性插值
    return mix(p0, p1, v);
}

  • 重心插值法:三角形,参考
    • P₀, P₁, P₂是三边形三个控制点(即TCS中的gl_out或TES中的gl_in)
      vec3 pos = u * P0 + v * P1 + w * P2;

简单的细分计算着色器(四边形)

#version 460
layout (quads, equal_spacing, ccw) in; // 声明等距四边形细分
void main() 
{
    // 获取四个控制点
    vec3 P0 = gl_in[0].gl_Position.xyz;
    vec3 P1 = gl_in[1].gl_Position.xyz;
    vec3 P2 = gl_in[2].gl_Position.xyz;
    vec3 P3 = gl_in[3].gl_Position.xyz;
    
    // 当前细分点参数坐标 (由细分图元生成器生成)
    float u = gl_TessCoord.x;
    float v = gl_TessCoord.y;
    
    // 双线性插值 (与间隔类型无关!)
    vec3 pos = 
        (1-u)*(1-v) * P0 + 
        u*(1-v)    * P1 + 
        u*v        * P2 + 
        (1-u)*v    * P3;
    
    gl_Position = vec4(pos, 1.0);
}


9.4.6 细分计算着色器的变量

  • TES也有一个gl_in:
in gl_PerVertex {
    vec4  gl_Position;
    float gl_PointSize;
    float gl_ClipDistance[];
    float gl_CullDistance[];
} gl_in[gl_PatchVerticesIn];

  • 细分计算着色器的输入变量
变量 描述
gl_PrimitiveID 当前输人面片的图元索引
gl_PatchVerticesIn 输人面片的顶点数量,也就是 gl_in的大小
gl_TessLevelOuter[4] 外侧细分层级的值
gl_TessLevelInner[2] 内侧细分层级的值
gl_TessCoord 还未进入细分计算着色器中面片域空间的顶点坐标值
  • 输出顶点数据被存储在如下的接口块当中:
out gl_PerVertex {
    vec4  gl_Position;
    float gl_PointSize;
    float gl_ClipDistance[];
    float gl_CullDistance[];
}

9.5 细分实例

9.5.1 贝塞尔曲线

  • 贝塞尔曲线: 每个控制点都会影响整条曲线的形状。移动一个控制点,整条曲线都会发生变化(除了端点)。
  • 贝塞尔曲线详解
  • de Casteljau 算法:
    • control_points:贝塞尔曲线的控制点集(大小为 n+1)
    • t:曲线参数(0 ≤ t ≤ 1)
vec3f recursive_bezier(const vec3f[4] &control_points, float t) 
{
    // TODO: Implement de Casteljau's algorithm
    if(control_points.size() == 1) return control_points[0];

    vec3f[4] a;
    for(int i = 0;i+1 < control_points.size();i ++) {
        auto p = control_points[i] + t * (control_points[i+1] - control_points[i]);
        a.push_back(p);
    }

    return recursive_bezier(a, t);
};

9.5.2 B样条

  • 详解参考和下文的来源
  • 每个控制点只影响曲线的一小段(局部支撑性)。移动一个控制点,只会改变曲线中与其相关的几个曲线段,其余部分保持不变。
  • 节点数组:m+1个非递减数,在这里插入图片描述
  • 节点可以被看作是将区间[u_0,u_m]细分为更小节点区间的分割点
    • 节点点将B样条曲线分成多个曲线段,每个曲线段都定义在一个节点区间内,并且每个曲线段都是p次贝塞尔曲线。
    • B样条基函数的次数需要人为指定,而贝塞尔基函数的次数取决于控制点的数量
    • 由于一些u_i可能相等,某些节点区间可能不存在,即区间长度为0,那么会导致B样条基函数及其导数的连续性。那么将允许样条曲线在该节点处产生更尖锐的特征(如角点或尖点)。
  • 控制点n+1个,需要n+1个基函数(i=0到i=n),即i代表了在哪个区间。而更多次数p,就需要更多节点,必须满足:节点数m = n + p + 1
    • 例如5个控制点(n=4),次数为3,则需要5+3+1 = 9个节点(m=8),划分了8个区间,如果有重复节点数,那么有些区间长度为0。
  • 第i个节点区间,次数为p的B样条基函数阶数k = p+1
    在这里插入图片描述
    • 计算方法:要计算第i个节点区间,次数为1的B样条基函数,需要知道第i个节点区间,次数为0和第i+1个节点区间,次数为0的B样条基函数,以此类推在这里插入图片描述
      • 例如:第0个节点区间,次数为1的B样条基函数和第0个和第1个节点区间B样条基函数有关。
        • 并且u在不同区间,公式也不同。例如第0个节点区间,次数为0的B样条基函数,只有u在第0个节点区间,才为1,其他均为0。在这里插入图片描述
          在这里插入图片描述
      • 例如N_0,2(u):在这里插入图片描述
    • 基函数支撑区间(非零区间): B样条基函数 Nᵢ,ₚ(u) 的支撑区间是 [uᵢ, uᵢ₊ₚ₊₁]。
      • 即在 [uᵢ, uᵢ₊₁)区间内最多有p+1个p次非零基函数。
        在这里插入图片描述
      • 如果有多重节点,那么在每个内部重复度为k的节点处,非零基函数的数量最多为p-k+1,其中p是基函数的次数。

9.5.2.1 Clamped B

  • 想要强制曲线使得它分别与第一个控制点和最后一个控制点的第一边和最后一边相切,那么第一个节点和最后一个节点都必须具有p+1(p为次数)重复度。
  • 下方例子有n+1 = 10个控制点,次数p为3,那么m = n+p+1 = 9 + 3 + 1 = 13,即必须要有14个节点。
    在这里插入图片描述
    • 为了使得第一个点和最后一个点相切, 那么前p+1 = 4个和最后4个点必须是相同的,其余的节点可以在定义域内任何位置,例如:
      U={0,0,0,0,0.14,0.28,0.42,0.57,0.71,0.85,1,1,1,1}
      在这里插入图片描述

9.5.2.2 Open B

  • 对于开放的B样条,定义域为[U_p,U_m-p].
  • 例如一条由14个控制点(n=13)定义的6次B样条曲线(p=6)。节点的数是21(m=20),开放曲线的定义域为[U_6,U_14]。

9.5.2.3 Closed B(wrapping控制点)

  • 假设我们要构造一条由n+1个控制点P_0,P_1…P_n定义的p次闭合B样条曲线。节点数是m+1,m = n+p+1。
    • 设计一个均匀节点序列
    • wrapping前p个和最后p个控制点,一一对应,即令P_0 = P_n-p+1,P_1 = P_n-p+2…P_p-1 = P_n。
      在这里插入图片描述
    • 例如一条由10个控制点(n=9),均匀节点定义的3次开放B样条曲线,想要闭合,就要让控制点对 0 和 7、1 和 8、2 和 9 彼此靠近直到相同:
      在这里插入图片描述

9.5.2.4 de Boor算法

源码

9.5.3 贝塞尔曲面

原链接参考

9.5.4 Utah茶壶

  • 其实就是使用了32个面片 * 16 个控制点构成的贝塞尔曲面来绘制的茶壶模型。
    控制点参考

着色器参考

  • 顶点着色器
const char* vertexShaderSource = 
"#version 450 core\n"
"layout (location = 0) in vec4 position;\n"
"void main()\n"
"{\n"
"   gl_Position = position;\n"
"}\0";
  • 细分控制着色器
#version 430
layout (vertices = 16) out;

void main(void)
{	int TL = 32;  // 曲面细分等级(内外都是32)
	if (gl_InvocationID ==0)
	{	gl_TessLevelOuter[0] = TL;
		gl_TessLevelOuter[2] = TL;
		gl_TessLevelOuter[1] = TL;
		gl_TessLevelOuter[3] = TL;
		gl_TessLevelInner[0] = TL;
		gl_TessLevelInner[1] = TL;
	}
	gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;//传递gl_Position属性
}
  • 细分计算着色器
#version 430

layout (quads, equal_spacing,ccw) in;
uniform mat4 mvp_matrix;
uniform mat4 model_matrix;
out vec2 texcoord_tes_out;// 每个网格顶点纹理坐标以标量形式传出
out vec3 FragPos;// 用于光照计算

// 16个控制点的三次贝塞尔曲面计算函数
//utah就是由32个16个控制点的贝塞尔曲面构成的
vec4 BezierSurface(vec4 controlPoints[16],float u, float v)
{
	vec4 result = vec4(0.0);

	// 贝塞尔基函数
	float BU[4],BV[4];
	float u1=1.0-u;
	float v1=1.0-v;
	BU[0]= u1* u1 * u1;
	BU[1]= 3.0 * u1 * u1 *u;
	BU[2]= 3.0 * u1 * u *u;
	BU[3]= u*u*u;

	BV[0] = v1 * v1 * v1;
	BV[1] = 3.0 * v1 * v1 * v;
	BV[2] = 3.0 * v1 * v * v;
	BV[3] = v * v * v;

	// 计算曲面上的点
	for(int i = 0;i < 4;i++)
	{
		for(int j =0; j <4; j++)
		{
			result  += BU[i]*BV[j] *controlPoints[i*4*j];
		}
	}
	return result;
}

void main()
{
	float u = gl_TessCoord.x;
	float v = gl_TessCoord.y;
	vec4 controlPoints[16];
	for(int i =0; i< 16;i++)
	{
		controlPoints[i] = gl_in[i].gl_Position;
	}
	
	vec4 position = BezierSurface(controlPoints,u,v);
	gl_Position = mvp_matrix * position ;
	FragPos = vec3(model_matrix*position);
	texcoord_tes_out = vec2(u,v);
}
  • 片元着色器
#version 430
out vec4 color;

in vec2 texcoord_tes_out;// 每个网格顶点纹理坐标以标量形式传出
in vec3 FragPos;// 用于光照计算

uniform vec3 viewPos;
uniform vec3 lightColor;
uniform vec3 lightPos;

layout(binding =0)uniform sampler2D tex_color;
void main()
{
	vec3 normal = normalize(cross(dFdx(FragPos),dFdy(FragPos)));
	float ambientStrength =0.1;
	vec3 ambient = ambientStrength * lightColor;
	
	vec3 lightDir =normalize(lightPos-FragPos);
	float diff=max(dot(normal ,lightDir),0.0);
	vec3 diffuse=diff * lightColor;
	
	float specularStrength=0.8;
	vec3 viewDir=normalize(viewPos-FragPos);
	vec3 reflectDir =reflect(-lightDir, normal);
	float spec =pow(max(dot(viewDir,reflectDir)0.0)64);
	vec3 specular=specularStrength *spec *lightColor;
	
	vec3 objectColor =vec3(texture(tex_color,texcoord_tes_out));
	vec3 result =(ambient + diffuse + specular)* objectColor;
	color vec4(result,1.0);
}

9.5.5 更多绘制参考

参考源码

9.6 更多的细分技术

9.6.1 视口相关的细分(结合LOD机制)

  • 细分的一个关键特性就是在细分控制着色器中对细分层级的动态计算。
  • 利用细节层次机制LOD,通过眼睛在场景中的位置与面片的距离来进行计算:
uniform vec3 EyePosition;
void main()
{
	vec4 center = vec4(0.0);
	for(int i = 0; i < gl_in.length();i++)
	{
		center += gl_in[i].gl_Position;
	}
	center /= gl_in.length();
	float d = distance(center,vec4(EyePosition,1.0));
	const float lodscale=2.5;
	float tessLOD =mix(0.0,gl_MaxTessGenLevel ,d * lodScale);
	for(int i = 0;i < 4;i++)
	{
		gl_TessLevelOuter[i]= tessLOD;
	}
	tessLoD=clamp(0.5*tessL0D,0.0,gl_MaxTessGenLevel );
	gl_TessLevelInner[0]= tessLOD;
	gl_TessLevelInner[1]= tessLOD;
	gl_out[gl_InvocationID.gl_Position] = .....
}

  • 如果有多个共享同一条边的面片,避免共享边上出现裂缝的问题:
    • 根据周长边的中心点设置细分层次因数。
    • 需要将各个边在世界空间中的中心位置传递数组patch中。
struct EdgeCenters
{
	vec4 edgeCenter[4];//假设PATCH顶点数为4,使用quads
}
uniform vec3 EyePosition;
uniform EdgeCenters patch[];
void main()
{
	vec4 center = vec4(0.0);
	for(int i = 0; i <4;i++)
	{
		float d = distance(patch[gl_PrimitiveID].edgeCenter[i],vec4(EyePosition,1.0));
		const float lodscale=2.5;
		float tessLOD =mix(0.0,gl_MaxTessGenLevel ,d * lodScale);
		gl_TessLevelOuter[i]= tessLOD;
	}
	
	tessLoD=clamp(0.5*tessL0D,0.0,gl_MaxTessGenLevel );
	gl_TessLevelInner[0]= tessLOD;
	gl_TessLevelInner[1]= tessLOD;
	gl_out[gl_InvocationID.gl_Position] = .....


9.6.2 细分的共享边与裂缝

  • OpenGL中的细分可以确保面片中生成的几何体不会有任何的裂缝存在,但是它无法保证共享同一条边的面片也不存在裂缝(存在共点)。
  • 一种避免裂缝的方法就是在着色器计算的时候,如果存在两个着色器请求的顶点顺序不一致的情况,就使用precise限定符。
    • 可以限定准备用于计算的out或in,和设置任何计算中的变量、内置变量或者函数的返回值。
      在这里插入图片描述
  • 另一种就是对细分计算着色器中的顶点处理过程进行重新排序。一个常见的解决方法是,找到所有对周长边的顶点有贡献的输出面片顶点,按照预设的方式进行排序,也就是沿着边长向量增加大小的方式。

9.6.3 置换贴图映射

  • 置换贴图映射是一种在曲面细分管线中使用的技术,它通过一张纹理(置换贴图)来改变细分后顶点的高度或位置,从而在低多边形模型上创造出真正的高精度几何细节。
  • 理解图参考

可以把它理解为一种 “几何体的3D打印”:

  • 有一个简单的、低精度的基础模型(比如一个平坦的平面)。
  • 细分着色器将它切成数百万个微小的三角形。
  • 置换贴图就像一张“高度蓝图”,告诉每个顶点应该沿着法线方向移动多远。
  • 一个简单的平面就变成了复杂的山脉、砖墙、雕刻图案等。
  • 以utah茶壶为例:
// 需要在使用贝塞尔函数获得正确细分坐标后:
uniform mat4 MVP;
uniform sampler2D DisplacementMap;
void main()
{
	...
	// 假设p为正确细分坐标
	p += texture(DisplacementMap,gl_TessCoord.xy);
	gl_Position = MVP * p;
}