目录
- 贝塞尔曲线基本知识
- 画贝塞尔曲线
- 让曲线动起来
- 画贝塞尔曲面
- 资料
- 收获
本篇最终实现效果如下:
篇外说明:由于有必要学习使用下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)
}
效果如下:
改变顶点的颜色,多画几个贝塞尔曲面就是文章开头的那个效果了。
六、收获
- 了解贝塞尔曲线的由来和实现原理
- 通过androidPath和OpenGL两种方式画贝塞尔曲线,以及进行性能对比
- 让画面动起来
- 实现贝塞尔曲面