目录

  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 = truepaint.strokeWidth = 5fpaint.color = Color.REDpaint.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)
{//使用内置函数mixvec2 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 = 256val TRIANGLES_PER_POINT = 3var mProgram: Int = -1var 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.... 1if (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)}}//VBOval 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)//VAOvaoBuffers = 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)//deleteGLES20.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 归一化得到offsetvar offset = (newIndex % 100) * 1.0f / 100;//然后到达一定量之后取反 实现来回的循环的动画offset = if ((newIndex / 100) % 2 == 1) (1 - offset) else offsetGLES20.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. 实现贝塞尔曲面

感谢你的阅读

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

欢迎交流

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

  1. 安卓 camera 调用流程_音视频开发之旅(四)Camera视频采集

    目录 Camera基础知识 视频采集的流程 遇到的问题和常见的坑(重点) 收获 一. Camera基础知识 Camera 有几个重要的基础概念. facing相机的方向,一般后置摄像头和前置摄像头. ...

  2. 音视频开发之旅(16) OpenGL ES粒子效果-烟花爆炸

    目录 烟花爆竹场景和属性 实践以及遇到的问题 资料 收获 通过该篇的实践实现如下效果 一.烟花爆竹场景和属性 在上一篇 音视频开发之旅(15) OpenGL ES粒子系统 - 喷泉 的基础上 实现烟花 ...

  3. 音视频开发之旅(15) OpenGL ES粒子系统 - 喷泉

    目录 粒子和粒子系统 实践:喷泉效果 遇到的问题 资料 收获 通过该篇的实践实现如下效果 一.什么是粒子和粒子系统 如何定义粒子? 一个粒子有位置信息(x,y,z).运动方向.颜色.生命值(开始和结束 ...

  4. 音视频开发之旅(32)-音视频学习资料

    目录 为什么要学习音视频? 如何学习系统性音视频? 音视频相关的资料 学习实践的输出文章分类聚合 收获 最近有朋友问想学习音视频,应该怎么学,有什么资料吗? 这个问题也困扰我很久,几年前就想开始音视频 ...

  5. 音视频开发之旅(二)AudioRecord录制PCM音频

    目录 音频采集API AudioRecord和MediaRecorder介绍 PCM的介绍 AudioRecord的使用(构造.开始录制.停止录制.其他细节点) ffplay播放pcm pcm转为wa ...

  6. 音视频开发之旅(36) -FFmpeg +OpenSL ES实现音频解码和播放

    目录 OpenSL ES基本介绍 OpenSL ES播放音频流程 代码实现 遇到的问题 资料 收获 上一篇我们通过AudioTrack实现了FFmpeg解码后的PCM音频数据的播放,在Android上 ...

  7. 音视频开发之旅(34) - 基于FFmpeg实现简单的视频解码器

    目录 FFmpeg解码过程流程图和关键的数据结构 mp4通过FFmpeg解码YUV裸视频数据 遇到的问题 资料 收获 一.FFmpeg解码过程流程图和关键的数据结构 FFmpeg解码涉及的知识点比较多 ...

  8. 音视频开发之旅(66) - 音频变速不变调的原理

    目录 声音的基本知识 时域压扩(TSM)的原理 波形相似叠加(WSOLA) 资料 收获 音频的原始pcm数据是由 采样率.采样通道数以及位宽而定.常见的音频采样率是44100HZ,即一秒内采样4410 ...

  9. 音视频开发之旅(一)三种方式绘制图片

    在android开发中我们最常使用的绘制图片的方式就是ImageView,设置src.那么有没有其他方案可以实现图片的绘制呐? 三种方案 通过Imageview设置setImageBitmap fin ...

最新文章

  1. php 序列化储存和转化 json_encode() json_decode($q,true)
  2. 重构战略到执行的绩效管理体系
  3. ZOJ4118 Stones in the Bucket
  4. 作业一 郝树伟 1101210664
  5. Python二维码生成库qrcode示例
  6. 『软件工程1』详解软件是什么
  7. JavaSE--类加载器
  8. Asp.Net MVC学习总结(三)——过滤器你怎么看?
  9. networking常用命令
  10. 游侠原创:VMware ESXi 5安装图文教程
  11. (转)券商IT研发现状:一年最多花5亿 中小公司靠外包
  12. html制作菱锥旋转,几何画板制作正三棱锥的旋转动画
  13. xx闪购—主体选项卡
  14. Skyline软件二次开发初级——11如何在WEB页面中的三维地图上加载和保存工程文件...
  15. java md5,md2,md4 加密算法
  16. JAVA 多线程并发
  17. 【数理逻辑】命题和联结词 ( 命题 | 命题符号化 | 真值联结词 | 否 | 合取 | 析取 | 非真值联结词 | 蕴涵 | 等价 )
  18. 基于微信小程序的药店管理系统毕业设计
  19. c++ stl源码-我理解的空间配置器
  20. ConnectBot连接Linux服务器手机端工具

热门文章

  1. SEO站群外链建设批量管理
  2. tf::Stamped<tf::Pose>
  3. 话费API 洗车API接口源码分享
  4. nginx配置赛门铁克ssl
  5. StarlingX 补丁升级功能
  6. 鸿蒙系统2.0 评测,鸿蒙2.0终于上机实测 多大内存能跑?
  7. 专利代理行业小公司的现状
  8. Vue-Vant—打包apk
  9. vivo手机哪款支持鸿蒙系统,vivo手机可以安装华为鸿蒙系统吗?
  10. 对象数组根据多个属性排序