使用曲面细分绘制甜甜圈

使用曲面细分绘制甜甜圈

构造圆环的思路

一开始想着使用贝塞尔曲面,通过调整控制点直接成环,或是先卷成圆柱再成环,后面想着使用贝塞尔曲面过于麻烦,且通过操纵控制点来变换不如直接变换平面直观,因此决定先构造一个平面,再卷成圆柱后成环

构造平面

由于输入控制点外壳着色器的控制点数量与输出的数量可以不相同,因此可以在控制点外壳着色器中将输入的两个float4拆包,还原出原点方向向量内外圈半径等信息。然后根据这些信息构造三个新的点,第四个点可以在域着色器中通过前三个点确定,减少传递的顶点。由于构造的是平面,因此使用的是四边形的常量外壳着色器。

外壳着色器代码

#include "Torus.hlsli"

[domain("quad")]
[partitioning("fractional_even")]
[outputtopology("triangle_cw")]
[outputcontrolpoints(5)]    // 输出5个点
[patchconstantfunc("QuadConstantHS")]	// 四边形常量着色器
[maxtessfactor(64.0f)]
float3 HS( 
	InputPatch<VertexPosF4, 2> patch, 
	uint i : SV_OutputControlPointID,
	uint PatchID : SV_PrimitiveID ) : POSITION
{
    /*  length
   0---------------1
    |             |
    |             |weight
   3---------------2
    */
    float3 newPoint[5];

	float3 origin = patch[0].posL.xyz;
    float3 normal = patch[1].posL.xyz;  // 圆环平面的法向量
    float r = patch[0].posL.w;
    float R = patch[1].posL.w;
 
    float torusR = (R - r) / 2; //圆环半径
    float length = 2 * M_PI * (r + torusR);
    float weight = 2 * M_PI * torusR; 

    newPoint[0] = float3(0, weight, 0);  // 加上原点后为绝对坐标
    newPoint[1] = float3(length, weight, 0);
    newPoint[2] = float3(length, 0, 0);
    newPoint[3] = normalize(normal);   // 传递法向量
    newPoint[4] = origin;

    return newPoint[i];
}

注意这里的原点不能直接加在新控制点上,否则会产生预期外的变化,如更改x会是圆环旋转,更改y会使圆环上的点绕着圆环旋转,更改z则无变化,正确的做法是在域着色器中加在输出的点上。

成柱后成环

首先是根据接收到的五个点还原出先前数据:

// 获取先前变量
float3 normal = quad[3].posL;
float3 origin = quad[4].posL;
float3 v10 = quad[0].posL - quad[1].posL;
float3 v12 = quad[2].posL - quad[1].posL;
float length = sqrt(pow(v10.x, 2) + pow(v10.y, 2) + pow(v10.z, 2));
float weight = sqrt(pow(v12.x, 2) + pow(v12.y, 2) + pow(v12.z, 2));
float torusR = weight / (2 * M_PI);
float r = length / (2 * M_PI) - torusR;
float R = 2 * torusR + r;

再是求出面片的第四个点:

// 求第四个点 
float3 v3 = quad[1].posL + v10 + v12;

再是进行插值确定新的点在面片的位置:

// 双线性插值
float3 v1 = lerp(quad[0].posL, quad[1].posL, uv.x);
float3 v2 = lerp(v3, quad[2].posL, uv.x);
float3 p = lerp(v1, v2, uv.y);

再是先进行圆柱变换:

float theta = p.y / torusR; // 弧度制
p = float3(p.x, sin(theta) * torusR, cos(theta) * torusR);  // 面成柱

然后是成环变换:

float newR = r + torusR * 2 * (p.y + weight / 2) / weight;
theta = p.x / (r + torusR);
p = float3(cos(M_PI / 2 - theta) * newR, sin(M_PI / 2 - theta) * newR, p.z);    // 柱成环

这里圆柱和圆环变换都是相同的公式,将要变换的边的长度当成圆的周长,那么对应边的坐标就是弧长,根据弧长和角度的关系就可求出角度,再根据极坐标就可求出对应的xyz

注意

  1. 由于成环时内外圈半径不同,因此极坐标映射到笛卡尔坐标时乘上的半径要加上一个偏移量形成新半径,该偏移量为该点离内圈平面的距离占圆柱直径的比例
  2. 不能使用新半径来求极坐标角度theta,因为新半径是变化的,会造成如下效果:

    导致圆环不均匀且出现缺口。
    theta应该是均匀变化的,且除的半径需为r + torusR,也就是内圈半径加上圆柱半径,否则会出现缺口或是重叠:

最后时进行旋转变换:

// 绕y轴旋转
theta = acos(normal.y); // 与y轴的夹角
float lengthxz = sin(theta);    //xz平面投影的长度
theta = acos(normal.z / lengthxz);
float3x3 rotate1 = float3x3(    // 绕y轴旋转
    float3(cos(theta), 0, -sin(theta)),
    float3(0, 1, 0),
    float3(sin(theta), 0, cos(theta))
);
// 绕z轴旋转 
theta = acos(normal.z);
float lengthxy = sin(theta);
theta = acos(normal.x / lengthxy);
float3x3 rotate2 = float3x3(
    float3(cos(theta), sin(theta), 0),
    float3(-sin(theta), cos(theta), 0),
    float3(0, 0, 1)
);

float3x3 rotate = mul(rotate2, rotate1);    // 两次旋转即可
p = mul(rotate, p) + origin;    // 移动到绝对位置需要在域着色器中进行

默认下方向向量为从屏幕指向人。绕y轴旋转时将方向向量投影到xz平面,求出投影向量与z轴的夹角,该夹角就是绕y轴旋转的角度,绕z轴旋转同理。

域着色器完整代码

#include "Torus.hlsli"

[domain("quad")]
float4 DS(QuadPatchTess patchTess,
    float2 uv : SV_DomainLocation,
    const OutputPatch<VertexPos, 5> quad) : SV_POSITION
{
    /*
   0-----1
    |   |
    |   |
   3-----2
    */
    // 获取先前变量
    float3 normal = quad[3].posL;
    float3 origin = quad[4].posL;
    float3 v10 = quad[0].posL - quad[1].posL;
    float3 v12 = quad[2].posL - quad[1].posL;
    float length = sqrt(pow(v10.x, 2) + pow(v10.y, 2) + pow(v10.z, 2));
    float weight = sqrt(pow(v12.x, 2) + pow(v12.y, 2) + pow(v12.z, 2));
    float torusR = weight / (2 * M_PI);
    float r = length / (2 * M_PI) - torusR;
    float R = 2 * torusR + r;
    // 求第四个点 
    float3 v3 = quad[1].posL + v10 + v12;

    // 双线性插值
    float3 v1 = lerp(quad[0].posL, quad[1].posL, uv.x);
    float3 v2 = lerp(v3, quad[2].posL, uv.x);
    float3 p = lerp(v1, v2, uv.y);

    float theta = p.y / torusR; // 弧度制
    p = float3(p.x, sin(theta) * torusR, cos(theta) * torusR);  // 面成柱

    float newR = r + torusR * 2 * (p.y + weight / 2) / weight;
    theta = p.x / (r + torusR);
    p = float3(cos(M_PI / 2 - theta) * newR, sin(M_PI / 2 - theta) * newR, p.z);    // 柱成环

    // 绕y轴旋转
    theta = acos(normal.y); // 与y轴的夹角
    float lengthxz = sin(theta);    //xz平面投影的长度
    theta = acos(normal.z / lengthxz);
    float3x3 rotate1 = float3x3(    // 绕y轴旋转
        float3(cos(theta), 0, -sin(theta)),
        float3(0, 1, 0),
        float3(sin(theta), 0, cos(theta))
    );
    // 绕z轴旋转 
    theta = acos(normal.z);
    float lengthxy = sin(theta);
    theta = acos(normal.x / lengthxy);
    float3x3 rotate2 = float3x3(
        float3(cos(theta), sin(theta), 0),
        float3(-sin(theta), cos(theta), 0),
        float3(0, 0, 1)
    );

    float3x3 rotate = mul(rotate2, rotate1);    // 两次旋转即可
    p = mul(rotate, p) + origin;    // 移动到绝对位置需要在域着色器中进行

    float4 posH = mul(float4(p, 1.0f), g_WorldViewProj);
    //float4 posH = float4(p,1.0f);

    return posH;
}

结果图:

posted @ 2025-10-13 23:50  单身喵  阅读(4)  评论(0)    收藏  举报