音视频开发之旅(40)-贝塞尔曲线和曲面

目录

  1. 贝塞尔曲线基本知识
  2. 画贝塞尔曲线
  3. 让曲线动起来
  4. 画贝塞尔曲面
  5. 资料
  6. 收获

本篇最终实现效果如下:


篇外说明:由于有必要学习使用下kotlin,后续的java层代码实现尽量采用kotlin

一、贝塞尔曲线基本知识

贝塞尔曲线法国汽车工程师Pierre Bézier在1962年在对汽车主体进行设计时的发明,通过贝塞尔曲线可以设计出优美的车身。

在PS、Sketch等图形软件上我们也经常会看到通过钢笔icon进行贝塞尔曲线的绘画。

贝塞尔曲线至少有一个开始点和结束点,以及n个中间控制点。跟进中间控制点的多少,可以分为(n+1)阶贝塞尔曲线。比如二阶贝塞尔曲线有1个控制点,三阶贝塞尔曲线有两个中间控制点。
我们先来看下一阶贝塞尔曲线

变量t就是一个插值,随着时间t的变化,P(t)的值随之变化。

对于二阶贝塞尔曲线也一样。先计算P0和P1的一阶贝塞尔q1,在计算P1和P2的一阶贝塞尔q2,然后在计算q1和q2的一阶贝塞尔就可以得到P(t)
(img)


正如《技术的本质》中讲到技术的创新来源于技术的组合。对于三阶贝塞尔曲线,也是见拆解,拆解成P0 P1 P2以及P1 P2 P3这两个二阶贝塞尔,然后对上面两个结果在做一阶贝塞尔,就得到了真正的应用中用的比较多的三阶贝塞尔曲线。


二、画贝塞尔曲线

我们使用三阶贝塞尔曲线来进行绘制。分别来看下android上通过Path的实现和OpenGL的实现方案。

android上通过Path的实现

class BeizerView : View {

    var path = Path()
    val paint = Paint()

    constructor(context: Context?) : super(context)
    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        updatePath()

        paint.isAntiAlias = true
        paint.strokeWidth = 5f
        paint.color = Color.RED
        paint.style = Paint.Style.STROKE
    }

    private fun updatePath() {
        path.reset()
        path.moveTo(10f, 1500f)
        path.cubicTo(300f, 650f, 800f, 100f, 1050f, 1500f)
        path.moveTo(10f, 100f)
        path.close()
    }

    override fun dispatchDraw(canvas: Canvas?) {
        canvas?.save()
        canvas?.drawPath(path, paint)
        super.dispatchDraw(canvas)
        canvas?.restore()
    }

效果如下:


下面,我们来看下通过OpenGL实现贝塞尔曲线
首先定义下shader,关键的顶点着色器

//顶点着色器

attribute float a_tData;
uniform vec4 u_startEndData;
uniform vec4 u_ControlData;

vec2 bezierMix(vec2 p0, vec2 p1, vec2 p2, vec2 p3, float t)
{
    //使用内置函数mix
    vec2 q0 = mix(p0, p1, t);
    vec2 q1 = mix(p1, p2, t);
    vec2 q2 = mix(p2, p3, t);

    vec2 r1 = mix(q0, q1, t);
    vec2 r2 = mix(q1, q2, t);

    return mix(r1, r2, t);
}

void main() {
    vec4 pos;
    pos.w=1.0;

    vec2 p0 = u_startEndData.xy;
    vec2 p3 = u_startEndData.zw;

    vec2 p1= u_ControlData.xy;
    vec2 p2= u_ControlData.zw;

    float t= a_tData;


    vec2 point = bezierMix(p0, p1, p2, p3, t);


    if (t<0.0)
    {
        pos.xy = vec2(0.0, 0.0);
    } else {
        pos.xy = point;
    }

    gl_PointSize = 4.0f;
    gl_Position = pos;

}

//片源着色器

precision mediump float;

uniform vec4 u_Color;

void main() {
    gl_FragColor = u_Color;
}

对应的Render如下:

class BezierCurveLineRender(private val context: Context) : IGLRender {

    val POINTS_NUM = 256
    val TRIANGLES_PER_POINT = 3

    var mProgram: Int = -1
    var tDataLocation = -1;
    var uOffsetLocation = -1;
    var uStartEndDataLocation = -1;
    var uControlDataLocation = -1;
    var uColorLocation = -1;

    lateinit var vaoBuffers: IntBuffer;


    override fun onSurfaceCreated() {
        val vertexStr = ShaderHelper.loadAsset(context.resources, "vertex_beziercurve.glsl")
        val fragStr = ShaderHelper.loadAsset(context.resources, "frag_beziercurve.glsl")
        mProgram = ShaderHelper.loadProgram(vertexStr, fragStr)

        //通过VAO批量传数据
        tDataLocation = GLES20.glGetAttribLocation(mProgram, "a_tData")

        uOffsetLocation = GLES20.glGetUniformLocation(mProgram, "u_offset")
        uStartEndDataLocation = GLES20.glGetUniformLocation(mProgram, "u_startEndData")
        uControlDataLocation = GLES20.glGetUniformLocation(mProgram, "u_ControlData")
        uColorLocation = GLES20.glGetUniformLocation(mProgram, "u_Color")

        setVaoData();
    }

    fun setVaoData() {
        val tDataSize = POINTS_NUM * TRIANGLES_PER_POINT;
        val floatBuffer: FloatBuffer = FloatBuffer.allocate(tDataSize)

        for (i in 0..tDataSize step TRIANGLES_PER_POINT) {
            //设置数据 0,1/3*256,2/3*256,3/3*356.... 1
            if (i < tDataSize) {
                floatBuffer.put(i, i * 1.0f / tDataSize)
            }
            if (i + 1 < tDataSize) {
                floatBuffer.put(i + 1, (i + 1) * 1.0f / tDataSize)
            }
            if (i + 2 < tDataSize) {
                floatBuffer.put(i + 2, (i + 2) * 1.0f / tDataSize)
            }
        }

        //VBO
        val buffers: IntBuffer = IntBuffer.allocate(1)
        GLES20.glGenBuffers(1, buffers)
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, buffers[0])

        GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, 4 * tDataSize, floatBuffer, GLES20.GL_STATIC_DRAW)

        //VAO
        vaoBuffers = IntBuffer.allocate(1)
        GLES30.glGenVertexArrays(1, vaoBuffers)
        GLES30.glBindVertexArray(vaoBuffers[0])

        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, buffers[0])
        GLES20.glEnableVertexAttribArray(tDataLocation)

        GLES30.glVertexAttribPointer(tDataLocation, 1, GLES20.GL_FLOAT, false, 4, 0)

        //delete
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0)
        GLES30.glBindVertexArray(GLES30.GL_NONE)
    }

    override fun onSurfaceChanged(width: Int, height: Int) {
        GLES20.glViewport(0, 0, width, height)
    }

    override fun draw() {
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f)

        GLES20.glUseProgram(mProgram)

        GLES30.glBindVertexArray(vaoBuffers[0])

        GLES20.glEnableVertexAttribArray(uStartEndDataLocation)
        GLES20.glUniform4f(uStartEndDataLocation, -1f, 0f, 1f, 0f)

        GLES20.glEnableVertexAttribArray(uControlDataLocation)
        GLES20.glUniform4f(uControlDataLocation, -0.04f, 0.99f, 0f, 0.99f)

        GLES20.glEnableVertexAttribArray(uColorLocation)
        GLES20.glUniform4f(uColorLocation, 1f, 0f, 0f, 1f)

        GLES20.glUniform1f(uOffsetLocation, 1f)

        GLES20.glDrawArrays(GLES20.GL_POINTS, 0, POINTS_NUM * TRIANGLES_PER_POINT)


    }
}

效果如下:


三、让曲线动起来

动画的本质是不同的时间渲染不同的画面,由于人的视觉有残留,当画面达到1秒24帧时看起来就像一个放电影,我们手机上的流程度要求更高,一般要1秒60帧,有些项VR类要求会更高些 1秒90帧。
那么我们就可以再Render中onDrawFrame通过时间等变量的因素来改变顶点坐标的值或者上述贝塞尔曲线的几个点的值,进行不同时间渲染不同的曲线,从而让曲线动起来。
为此我们修改下顶点着色器,添加offset变量,用于改变贝塞尔曲线几个点的坐标。

uniform float u_offset;
...
void main() {
    ...


    p0.y *= u_offset;
    p1.y *= u_offset;
    p2.y *= u_offset;
    p3.y *= u_offset;
    ...
}

然后在onDrawFrame时,通过当前的时间或者当前的帧数,来进行offset值的计算和传递

override fun draw() {   
     ...
     mFrameIndex++;
        var newIndex = mFrameIndex
        //通过 frameIndex 归一化得到offset
        var offset = (newIndex % 100) * 1.0f / 100;
        //然后到达一定量之后取反 实现来回的循环的动画
        offset = if ((newIndex / 100) % 2 == 1) (1 - offset) else offset

        GLES20.glEnableVertexAttribArray(uOffsetLocation)
        GLES20.glUniform1f(uOffsetLocation, offset)
    ...
}

动画效果如下:


可以加上矩阵变换,对模型视图进行x轴旋转180,画两次即可得到一个上下对称的贝塞尔曲线
着色器修改

uniform mat4 u_MVPMatrix;
……
void main() {
    ……
//    gl_Position = pos;
    gl_Position = u_MVPMatrix * pos;
    ……
}

对应的Render的实现如下
 override fun onSurfaceCreated() {
    ……
        //设置视图矩阵
        Matrix.setLookAtM(mViewMatrix, 0, 0f, 0f, 5f, 0f, 0f, 0f, 0f, 1f, 0f)

        //设置正交矩阵
        Matrix.orthoM(mPorjectMatrix, 0, -1f, 1f, -1f, 1f, 0.1f, 100f)
    }

 override fun draw() {
    ……
//设置模型矩阵
        Matrix.setIdentityM(mModelMatrix, 0)
        Matrix.rotateM(mModelMatrix, 0, 0f, 1f*offset1, 0f, 0f)

        //矩阵相乘得到mvp矩阵变换
        Matrix.multiplyMM(mMVPMatrix, 0, mViewMatrix, 0, mModelMatrix, 0)
        Matrix.multiplyMM(mMVPMatrix, 0, mPorjectMatrix, 0, mMVPMatrix, 0)

        GLES20.glUniformMatrix4fv(uMVPMatrixLocation, 1, false, mMVPMatrix, 0)

        GLES20.glDrawArrays(GLES20.GL_POINTS, 0, POINTS_NUM * TRIANGLES_PER_POINT)

        //沿着x轴翻转,绘制另外半边
        //设置模型矩阵
        Matrix.setIdentityM(mModelMatrix, 0)

        Matrix.rotateM(mModelMatrix, 0, 180f, 1f*offset1, 0f, 0f)

        //矩阵相乘得到mvp矩阵变换
        Matrix.multiplyMM(mMVPMatrix, 0, mViewMatrix, 0, mModelMatrix, 0)
        Matrix.multiplyMM(mMVPMatrix, 0, mPorjectMatrix, 0, mMVPMatrix, 0)

        GLES20.glUniformMatrix4fv(uMVPMatrixLocation, 1, false, mMVPMatrix, 0)

        GLES20.glDrawArrays(GLES20.GL_POINTS, 0, POINTS_NUM * TRIANGLES_PER_POINT)
    }

效果如下


四、贝塞尔曲面

上面的绘制时由一个个点构成,大力出奇迹,一个个点组成了按照陪塞尔曲线的PO点渲染出对应的曲线。
那么如何实现一个曲面呐?OpenGL中基本图形是点、线、三角形。对于面体我们可以通过多个三角形的绘制来实现。比如三角形的一个顶点始终固定在起点位置,两位两个点在通过贝塞尔曲线公式进行计算。

改变floatBuffer的数据

 fun setVaoData() {
    ... 
    for (i in 0..tDataSize step TRIANGLES_PER_POINT) {
        if (i < tDataSize) {
                            floatBuffer.put(i, i * 1.0f / tDataSize)
                        }
                        if (i + 1 < tDataSize) {
                            floatBuffer.put(i + 1, (i + 3) * 1.0f / tDataSize)
                        }
                        if (i + 2 < tDataSize) {
                            floatBuffer.put(i + 2, -1f)
                        }
    
            }
    ...
    }

顶点着色器修改如下

void main() {
    ...  
  if (t<0.0)
    {
        pos.xy = vec2(0.0, 0.0);
    } else {
        pos.xy = point;
    }
    ...
}

在绘制的时候把点改为线

   private fun drawArray() {

            //GLES20.glDrawArrays(GLES20.GL_POINTS, 0, POINTS_NUM * TRIANGLES_PER_POINT)
  GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, POINTS_NUM * TRIANGLES_PER_POINT)
    }

效果如下:


改变顶点的颜色,多画几个贝塞尔曲面就是文章开头的那个效果了。


完整代码请查看 github https://github.com/ayyb1988/mediajourney

五、资料

  1. GAMES101-现代计算机图形学入门(Curses)-闫令琪
  2. GAMES101-现代计算机图形学入门(Animation)-闫令琪
  3. 清华大学-计算机图形学基础(国家级精品课)
  4. OpenGL ES 绘制贝塞尔曲线
  5. Sound Visualization on Android: Drawing a Cubic Bezier with OpenGL ES

六、收获

  1. 了解贝塞尔曲线的由来和实现原理
  2. 通过androidPath和OpenGL两种方式画贝塞尔曲线,以及进行性能对比
  3. 让画面动起来
  4. 实现贝塞尔曲面

感谢你的阅读

下一篇我们学习实践天空盒,欢迎关注公众号“音视频开发之旅”,一起学习成长。

欢迎交流

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容