Tesselation(曲面细分)
要点
- 将一个几何块细分成更小的三角形,提供更多细节
- 可以协作动态LOD
- 提供更高效的动画
- 节省空间
- 管线中的位置:VS -> HS(Hull Shader) -> Tessellator Stage -> DS(Domain Shader) -> GS(Geometry Shader)
- 基本概念:
- 不是输出片元,而是输出一个带有若干个控制点(control point)的包(patch)。根据细分拓扑结构,这些控制点会填入拓补结构中的顶点,然后得到诸多三角形。
- 输入的控制点的个数可以用 D3D11_PRIMITIVE_N_CONTROL_POINT_PATCH(n=1~32)来表示。使用 context->IASetPrimitiveTopology() 进行设置。
- 如何决定添加细节?
- 因素
- 根据与摄像机的距离
- 估计所占据的屏幕像素
- 旋转角度与视线的距离
- 粗糙程度
- 注意
- 开关tesselation是比较费的,尽量批量处理draw call
- 8像素以下的面就不要再细分了
- 因素
- Tesselation包括以下几个部分
- hull shader
- contant hull shader
- 输入:InputPatch < VertexOut, 4 >,这里的4代表一个四边形
- 输出: struct PatchTess{ // 这里的4和2是因为上边是四边形,对于三角形则是 3和1
EdgeTess[4] : SV_TessFactor; // 代表4条边上分别细分成几段
InsideTess[2] : SV_InsideTessFactor; // 代表内部的quad的长宽分别细分几段
… // 也可以有其他信息
} - 参数值代表了细分时的拓扑结构(图略)
- 值为0代表这个primitive被丢弃了,从而省略之后的流程。例如对于frustum裁剪、背面消隐引起的不可见,都可以在这时候被直接丢弃。
- 值都为1代表这个primitive实际上没有做什么工作——不要这样浪费GPU。
- control point hull shader
- 输入:
InputPatch < VertexOut, 4 >(输入的控制点们)
SV_OutputControlPointID(本次需要计算的输出的控制点的ID)
注意两者的数量是没什么关系的 - 输出:struct HullOut{
float3 pos : POSITION;
…} - 语法:
- [domain(“quad”)]:patch的类型。包括 tri,quad,isoline。
- [partitioning(“ingeger”)]:决定细分的模式。例如:integer——只factor的整数部分进行增删控制点(会导致突变);fractional_even/odd——使用分数部分来从两个整数factor之间光滑切换
- [outputtopology(“triangle_cw”)]:绕序。还有triangle_ccw,line。
- [outputcontrolpoints(4)]:hull shader输出控制点的数量。SV_OutputControlPointID为其索引。
- [patchconstantfunc(“ConstantHS”)]:指示constant hull shader的名字。
- [maxtessfactor(64.0f)]:告诉shader你会用到的最大的tessellation factor的hint。可以带来优化。最大为64。
- 输入:
- tessellation阶段不受用户控制
- domain shader
- 根据tessellation 阶段输出的顶点,对其进行VS类似的功能,变换到齐次裁剪空间等
- 输入:
SV_DomainLocation(float2):控制点的uv坐标(或者对三角形而言uvw,这种重心坐标(barycentric)对Bezier曲线非常友好) - 输出:struct DomainOut{float3 pos:SV_POSITION;…}
- 语法:
[DOMAIN(“quad”)]
- contant hull shader
- hull shader
遗留问题
- [partitioning(“ingeger”)] 不明白什么意思
- 没太看懂 tessellation 是什么意思
Bezier Curve
定义
- 线性贝塞尔曲线
- B(t) = (1-t) * P0 + t * P1 (0<=t<=1)
- 二次贝塞尔曲线
- B(t) = (1-t) * ((1-t)P0 + tP1) + t * ((1-t)P1 + tP2)
= (1-t)^2 * P0 + 2(1-t)tP1 + t^2 * P2
- B(t) = (1-t) * ((1-t)P0 + tP1) + t * ((1-t)P1 + tP2)
- 三次贝塞尔曲线
- (以此类推)
- 用几何画板的一个简单绘图展示
(线段比例关系可以根据图片大致猜测)
上图中中间BC线段是线性贝塞尔曲线;
上下两条红线是二次贝塞尔曲线,它有1个控制点;
中间红线是三次贝塞尔曲线,它有2个控制点;
可以看出,贝塞尔曲线并不一定经过控制点;两端处的线段指示切线方向。
联系到PhotoShop中常用的制作path的钢笔工具,可见那个钢笔工具其实就是一个三次贝塞尔曲线而已~
远古的画图板,貌似是用的三次贝塞尔曲线~那时那个曲线工具,先拖一条直线,然后隔空拖两次得到一条曲线。。。【我至今还记得那时候无论怎么拖曲线都拖不理想的痛苦~ - 贝塞尔曲面(bezier surface)
例如三次贝塞尔曲线,需要4x4个点。首先进行4行贝塞尔曲线计算,再将t从0遍历到1,计算每列的贝塞尔曲线。从而得到一个曲面。
建立第一人称摄像机
杂记
- 这本书提供的是每次移动时,对摄像机矩阵进行旋转的策略。以前试过这种,误差还是挺大。比不上DX Sample里边记录一个四元数然后每次移动时修改四元数,再生成矩阵的算法。
- 一些函数
- XMMatrixPerspectiveFovLH
- XMVectorMultiplyAdd(step,look,originpos) = step*look+originpos,特别适合摄像机平移
- XMMatrixRotationAxis(axis, angle)
- XMVector3TransformNormal(originVec, transformMat)
- XMVectorSplatZ 将vector的所有分量都设为其z值
- XMVectorReciprocal 计算逐分量的倒数
- viewMat(i, 0) 是x方向,viewMat(i, 1)是y方向,viewMat(i, 2) 是z方向,viewMat(i, 3) 是平移位置
硬件实例化
要点
- 意思就是如何在一个场景中多次画同一个物体
- 如果每次绘制都重设颜色、贴图、世界矩阵,那依然还是有很多API overhead。在DX9中常见CPU是瓶颈,从而限制drawcall的数量。
- 可以使用 batch 技术,来减少drawcall
- 可以配合纹理列表数组(texture array),使用硬件实例化(hardware instancing),来减少drawcall。
- 硬件实例化是对vs做的操作,通过对input layout的input slot class进行修改,来输入多个instance的数据
步骤
- VS
- 增加 SV_InstanceID 类型,指示instance的索引
- Input Layout
- D3D11_INPUT_ELEMENT_DESC
- UINT InputSlotClass: 指示输入element是作为vertex element还是instanced element
- D3D11_INPUT_PER_VERTEX_DATA
- D3D11_INPUT_PER_INSTANCE_DATA
- UINT InstanceDataStepRate: 指示每个instanced data element中有多少个instance。例如要用用3个颜色画6个instance,那么step就是2。对于vertex element,这个应该是0。
- UINT InputSlot: 可以用多个Slot了
- UINT SemanticIndex: 同一类型的数据,这个index要递增了
- UINT InputSlotClass: 指示输入element是作为vertex element还是instanced element
- context->IASetInputLayout(startSlot, numBuffers, pVB, pStride, pOffset)
- numBuffers: slot的个数
- pVB: 两个buffer分开
- pStride: 数组内容为两个buffer各自的stride
- pOffset: 数组内容为两个buffer各自的stride
- context->IASetInputLayout
- 输入desc
- 输入desc
- context->DrawIndextedInstanced(indexCountPerInstance, instanceCount, StartIndexLocation, BaseVertexLocation, StartInstanceLocation)
- indexCountPerInstance: 每个instance的顶点数
- instanceCount: instance数量
- 后三个指示从哪里开始读取vertex
- D3D11_INPUT_ELEMENT_DESC
- 建立Instanced buffer
- 使用dynamic buffer,因为instanced buffer通常是经常变化的
- D3D11_BUFFER_DESC
- Usage: D3D11_USAGE_DYNAMIC
- CPUAccessFlags: D3D11_CPU_ACCESS_WRITE
视锥剔除(frustum culling)
杂记
- 去看看xnacollision.h/cpp里边对于各种几何碰撞的检测~
- AABB: axis-aligned bounding box
OBB:oriented bounding box
bounding sphere(球形包围盒) - 为了碰撞检测和拾取,常常保留一份vertices的备份在CPU
- Frustum体的碰撞检测(xna也提供了)
- Sphere:
- 检测球和六个面的位置关系
- AABB:
- AABB的每个面,检测它和frustum面法线最平行的那个对角线——它的两个端点分别在frustum面两侧的时候会相交
- 最平行:因为是AABB所以只需要按照法线方向来取自己顶点里的最大最小值就行了
- Sphere:
- 用CPU来做视锥剔除,可以减少三角形被送入GPU的流中。在输入数据的时候,不输入不可见的数据,然后减小 context->DrawIndexedInstanced 中的 instancedCount 参数即可。
遗留问题
- 去看看xnacollision.h/cpp里边对于各种几何碰撞的检测~