数字孪生常见特效Shader实现4 直线 PolyLine

97 阅读2分钟

之前有读者留言如何将 Shader迁移到 Three.js构建的 3D 场景中之中, 当然直接放一个 Plane大多数时间肯定是搞不定需求。 但当理解所谓的特效只不是一些数学的计算, 慢慢会触觉底层的技法完全一样,同时由于渲染引擎有 Geometry可以帮助存储很多提前计算的数据放到 Vertex 的Attribute中,整体反而更加简单

本篇将简单了解一些线的特性推导. 后续将基于本篇的基础理解去实现一些线的特效包括(地球飞线,流光线,粒子线),基本上都是 youtube博主 yuri的直播里面有,做了一些理解。

平面到线的最短距离 SDF

  • geogebra: www.geogebra.org/classic/pnz… 假定有线段Segment(A, B), 空间中所有点到线段的最近距离分为三种情况。

  • P1 在AB中间,距离为点到线的垂直线

  • P2 在AB之外,距离B较近 距离为P2到B的距离

  • P3 在AB之外,距离A较近,距离为P3到A的距离

image (9).gif 一种复合直觉的求法,便是把三个值\overrightarrow{PB}$$\overrightarrow{PA}$$\overrightarrow{PC}的长度都求出来。取最小的一个长度便可。\overrightarrow{PB}$$\overrightarrow{PA}长度非常好求。PC\overrightarrow{PC}的长度可以通过\overrightarrow{PA}$$\overrightarrow{PB}的夹角θ\theta,然后求sin(θ)×PAsin(\theta) \times ||\overrightarrow{PA}||θ\theta可以通过向量点积求得。 注意上图的hhhh参数,表示PPABAB上投影长度在ABAB长度的占比,如果这个值(0,1)(0, 1)区间,就说明我们需要求PC\overrightarrow{PC} 转化成为代码

float sd_segment(vec2 pct, vec2 pa, vec2 pb) {
    vec2 v1 = pct - pa;
    vec2 v2 = pb - pa;
    vec2 v3 = pct - pb;
    
    
    float hh = dot(v1, v2) / length(v2) / length(v2);
    
    if (0. < hh && hh < 1.0) {
        float theta = acos(dot(v1 ,v2) /length(v1)/length(v2));
        float d1 = sin(theta) * length(v1);
        return d1;
    } else {
        return min(length(v1), length(v3)) ;
    }
}

这里可以对代码进行优化, PC\overrightarrow{PC} 的长度其实PAAC\overrightarrow{PA} - \overrightarrow{AC}AC\overrightarrow{AC}可以有hhhh决定 ,我们已经知道hhhh的大小。并且当hh<0hh<0,距离就是Length(PA)Length(\overrightarrow{PA}),当hh>0hh>0,距离就是Length(PB)Length(\overrightarrow{PB}). 于是代码可以优化为

float sd_segment2(vec2 pct, vec2 pa, vec2 pb) {
    vec2 v1 = pct - pa;
    vec2 v2 = pb - pa;
    vec2 v3 = pct - pb;
    
    
    float hh = dot(v1, v2) / length(v2) / length(v2);

    if (hh < 0.0) {      
      return length(v1 - 0. * v2);
    } else if (0. < hh && hh < 1.0) {
        return length(v1 - hh * v2);
    } else {
        // v3 = v1 - v2 = pct -pb;
        return length(v1 - v2);
    }
}

通过上面的优化可以发现,三个判断条件的相似性。 通过引入clamp函数去掉if判断优化性能,通过dot(v2, v2)减少一次length的计算。 最后得到IQ大神最终结果

float sd_segment3(vec2 pct, vec2 pa, vec2 pb) {
    vec2 v1 = pct - pa;
    vec2 v2 = pb - pa;
    float h = clamp(dot(v1,v2)/dot(v2,v2), 0.0, 1.0);
    return length(v1-h*v2); 
}

PolyLine生成

three-line-2d这个库给出了如何生成Polyline,其介绍是A utility for 2D line drawing in ThreeJS, by expanding a polyline in a vertex shader for variable thickness, anti-aliasing, gradients, line dashes, and other GPU effects. 这个库被大量的使用,好奇看了下源码,非常简单。 主要有该作者三个库组成

  1. polyline-miter-util: 一些向量运算, 我特地查了下,是高一平面向量的内容
  2. polyline-normals: 从path(一堆点) 生成 polyline, 最关键的 vertex attr是 normal
  3. three-line-2d: 通过 normal的值在 shader中实现了readme里面介绍的gpu effects 上图为作者 readme中的图,可以观察到他对于每一个点需要求的一些值. 例如normal的方向,以及当设置线的宽度 thickness为 1 的时候,在拐角处沿着 normal方向应该做多远的顶点偏移系数 miter. normal的求解如上图所示,对于PointB的其实就是以下的数学公式
Direction=Point(C)Point(B)Normal=(Direction.y,Direction.x)\begin{aligned} Direction &= Point(C) - Point(B) \\ Normal &= (-Direction.y, Direction.x) \end{aligned}

稍微复杂的是 miter的求解, geogbra可以查看 www.geogebra.org/classic/mrm…

当我们需要一个 thinkness宽度,如何求拐角处的 miter. 最关键的就是求出 α\alpha 角度, 在代码中他使用了 dot 获得 cos(α)cos(\alpha) .

Vec1=Point(C)Point(B)Vec2=Point(B)Point(A)Vec3=Vec1+Vec2Vec3Normal=Normalize(Vec3.y,Vec3.x)Vec2Normal=(Vec2.y,Vec2.x)cos(α)=dot(Vec3Normal,Vec2Normal)miter=thickness/cos(α)\begin{aligned} Vec1 &= Point(C) - Point(B) \\ Vec2 &= Point(B) - Point(A) \\ Vec3 &= Vec1 + Vec2 \\ Vec3Normal &= Normalize(-Vec3.y, Vec3.x) \\ Vec2Normal &= (-Vec2.y, Vec2.x) \\ cos(\alpha) &= dot(Vec3Normal, Vec2Normal) \\ miter &= thickness / cos(\alpha) \\ \end{aligned}

当有了 miternoral的vertex attribute之后,便可以做一些作者在 readme里面写到的 gpu effects .

Dash

上看的 Polyline也提供了 Dash的方法,但是其 Dash还是依赖于Path的各点的等距。 Dash本质是一个 step函数,如这个 www.shadertoy.com/view/ldGfWt

对于一个线来说,我们为线上的每个点都设置一个值, 如果这个值为 1.0就显示,如果为 0.0就不显示。

float dashed(float x, float length, float dash) {

    float w = 1.0; // pixel smoothing length hard coded

    x = mod(x, l); /

    float a = length*dash/2.0 - w/2.0;
    float b = length*dash/2.0 + w/2.0;
    float c = length - b;
    float d = length - a;

    // this can probably be made non-branching with some math, but cba right now. 
    if (x < a) {
        x = 1.0;
    } else if (x < b) {
        x = 1.0 - (x - a)/w;
    } else if (x < c) {
        x = 0.0;
    } else if (x < d) {
        x = (x - c)/w;
    } else {
        x = 1.0;
    }
    
    return 1.0 - smoothstep(0.0, 1.0, x);

而有时候我们会见到如下面第二行这样的线段,点的组合, 其实很简单。 就是第一行的值和第三行的值取 min就可以了。 其代码是

    if      (y ==  0) d = f(p.x, 20.0, 0.2);
    else if (y ==  1) d = min(f(p.x, 20.0, 0.2), f(p.x, 40.0, 0.3));
    else if (y ==  2) d = f(p.x, 40.0, 0.3);