vlambda博客
学习文章列表

更新啦!高薪webGL学习之绘制三角形




前言

三角形是一个最简单、最稳定的面,webgl 中的三维模型都是由三角面组成的。咱们这一篇就说一下三角形的绘制方法。

课堂目标

  1. 理解多点绘图原理。

  2. 可以绘制三角形,并将其组合成多边形。

知识点

  1. 缓冲区对象

  2. 点、线、面图形

第一章 webgl 的绘图方式

我们先看一下webgl是怎么画图的。

  1. 绘制多点

  2. 如果是线,就连点成线

    更新啦!高薪webGL学习之绘制三角形

  3. 如果是面,那就在图形内部,逐片元填色

    更新啦!高薪webGL学习之绘制三角形

webgl 的绘图方式就这么简单,接下咱们就说一下这个绘图方式在程序中是如何实现的。

第二章 绘制多点

在webgl 里所有的图形都是由顶点连接而成的,咱们就先画三个可以构成三角形的点。

这里大家还要注意一下,我现在要画的多点是可以被webgl 加工成线、或者面的,这和我们上一篇单纯的想要绘制多个点是不一样的。

1-绘制多点的整体步骤

  1. 建立着色器源文件

    <script id="vertexShader" type="x-shader/x-vertex">
    attribute vec4 a_Position;
    void main(){
    gl_Position = a_Position;
    gl_PointSize = 20.0;
    }
    </script>
    <script id="fragmentShader" type="x-shader/x-fragment">
    void main(){
    gl_FragColor=vec4(1.0,1.0,0.0,1.0);
    }
    </script>
  2. 获取webgl 上下文

    const canvas = document.getElementById('canvas');
    canvas.width=window.innerWidth;
    canvas.height=window.innerHeight;
    const gl = canvas.getContext('webgl');
  3. 初始化着色器

    const vsSource = document.getElementById('vertexShader').innerText;
    const fsSource = document.getElementById('fragmentShader').innerText;
    initShaders(gl, vsSource, fsSource);
  4. 设置顶点点位

    const vertices=new Float32Array([
    0.0, 0.1,
    -0.1,-0.1,
    0.1, -0.1
    ])
    const vertexBuffer=gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER,vertexBuffer);
    gl.bufferData(gl.ARRAY_BUFFER,vertices,gl.STATIC_DRAW);
    const a_Position=gl.getAttribLocation(gl.program,'a_Position');
    gl.vertexAttribPointer(a_Position,2,gl.FLOAT,false,0,0);
    gl.enableVertexAttribArray(a_Position);
  5. 清理画布

    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);
  6. 绘图

    gl.drawArrays(gl.POINTS, 0, 3);

实际效果:

更新啦!高薪webGL学习之绘制三角形

上面的步骤,主要是先给大家一睹为快,其具体原理,咱们后面细说。

2-绘制多点详解

首先咱们先从概念上疏通一下。

我们在用js定点位的时候,肯定是要建立一份顶点数据的,这份顶点数据是给谁的呢?肯定是给着色器的,因为着色器需要这份顶点数据绘图。

然而,我们在js中建立顶点数据,着色器肯定是拿不到的,这是语言不通导致的。

为了解决这个问题,webgl 系统就建立了一个能翻译双方语言的缓冲区。js 可以用特定的方法把数据存在这个缓冲区中,着色器可以从缓冲区中拿到相应的数据。

接下来咱们就看一下这个缓冲区是如何建的,着色器又是如何从其中拿数据的。

  1. 建立顶点数据,两个浮点数构成一个顶点,分别代表x、y 值。

const vertices=new Float32Array([
//x y
0.0, 0.1, //顶点
-0.1,-0.1, //顶点
0.1, -0.1 //顶点
])

现在上面的这些顶点数据是存储在js 缓存里的,着色器拿不到,所以咱们需要建立一个着色器和js 都能进入的公共区。

  1. 建立缓冲对象。

const vertexBuffer=gl.createBuffer();

现在上面的这个缓冲区是独立存在的,它只是一个空着的仓库,和谁都没有关系。接下来咱们就让其和着色器建立连接。

  1. 绑定缓冲对象。

gl.bindBuffer(gl.ARRAY_BUFFER,vertexBuffer);

gl.bindBuffer(target,buffer) 绑定缓冲区

  • target 要把缓冲区放在webgl 系统中的什么位置

  • buffer 缓冲区

着色器对象在执行initShaders() 初始化方法的时候,已经被写入webgl 上下文对象gl 中了。

当缓冲区和着色器建立了绑定关系,我们就可以往这块空间写入数据了

  1. 往缓冲区对象中写入数据

gl.bufferData(gl.ARRAY_BUFFER,vertices,gl.STATIC_DRAW);

bufferData(target, data, usage) 将数据写入缓冲区

  • target 要把缓冲区放在webgl 系统中的什么位置

  • data 数据

  • usage 向缓冲区写入数据的方式,咱们在这里先知道 gl.STATIC_DRAW 方式即可,它是向缓冲区中一次性写入数据,着色器会绘制多次。

现在着色器虽然绑定了缓冲区,可以访问里面的数据了,但是我们还得让着色器知道这个仓库是给哪个变量的,比如咱们这里用于控制点位的attribute 变量。这样做是为了提高绘图效率。

  1. 将缓冲区对象分配给attribute 变量

const a_Position=gl.getAttribLocation(gl.program,'a_Position');
gl.vertexAttribPointer(a_Position,2,gl.FLOAT,false,0,0);

gl.vertexAttribPointer(local,size,type,normalized,stride,offset) 将缓冲区对象分配给attribute 变量

  • local attribute变量

  • size 顶点分量的个数,比如我们的vertices 数组中,两个数据表示一个顶点,那咱们就写2

  • type 数据类型,比如 gl.FLOAT 浮点型

  • normalized 是否将顶点数据归一

  • stride 相邻两个顶点间的字节数,我的例子里写的是0,那就是顶点之间是紧挨着的

  • offset 从缓冲区的什么位置开始存储变量,我的例子里写的是0,那就是从头开始存储变量

到了这里,着色就知道缓冲区的数据是给谁的了。因为咱们缓冲区里的顶点数据是数组,里面有多个顶点。所以我们得开启一个让着色器批量处理顶点数据的属性。默认着色器只会一个一个的接收顶点数据,然后一个一个的绘制顶点。

  1. 开启顶点数据的批处理功能。

gl.enableVertexAttribArray(a_Position);
  • location attribute变量

好啦,现在已经是万事俱备,只欠绘图了。

  1. 绘图

gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.POINTS, 0, 3);

drawArrays(mode,first,count)

  • mode 绘图模式,比如 gl.POINTS 画点

  • first 从哪个顶点开始绘制

  • count 要画多少个顶点

关于绘制多点,我就说到这,接下来咱们说一下基于多点绘制图形。

第三章 绘制图形

在数学中,我们知道,三个点可以确定一个唯一的三角面。接下来咱们画一下。

1-绘制三角面

我们在之前绘制多点的基础上做一下修改。

  1. 顶点着色器中的gl_PointSize = 20.0 不要,因为这个属性是控制顶点大小的,咱们已经不需要显示顶点了。

<script id="vertexShader" type="x-shader/x-vertex">
attribute vec4 a_Position;
void main(){
gl_Position = a_Position;
//gl_PointSize = 20.0;
}
</script>
  1. 在js 中修改绘图方式

// gl.drawArrays(gl.POINTS, 0, 3);
gl.drawArrays(gl.TRIANGLES, 0, 3);

上面的gl.TRIANGLES 就是绘制三角面的意思。

看一下效果:

更新啦!高薪webGL学习之绘制三角形

webgl 既然可以画面了,那它是否可以画线呢,这个是必须可以,我们可以在gl.drawArrays() 方法的第一个参数里进行设置。

2-基本图形

gl.drawArrays(mode,first,count) 方法可以绘制一下图形:

  • POINTS 可视的点

  • LINES 单独线段

  • LINE_STRIP 线条

  • LINE_LOOP 闭合线条

  • TRIANGLES 单独三角形

  • TRIANGLE_STRIP 三角带

  • TRIANGLE_FAN 三角扇

上面的POINTS 比较好理解,就是一个个可视的点。

线和面的绘制方式各有三种,咱们接下来就详细说一下。

2-1-点的绘制

POINTS 可视的点

更新啦!高薪webGL学习之绘制三角形

上面六个点的绘制顺序是:v0, v1, v2, v3, v4, v5

2-2-线的绘制

  1. LINES 单独线段

更新啦!高薪webGL学习之绘制三角形

上面三条有向线段的绘制顺序是:

v0>v1

v2>v3

v4>v5

  1. LINE_STRIP 线条

更新啦!高薪webGL学习之绘制三角形

上面线条的绘制顺序是:v0>v1>v2>v3>v4>v5

  1. LINE_LOOP 闭合线条

更新啦!高薪webGL学习之绘制三角形

上面线条的绘制顺序是:v0>v1>v2>v3>v4>v5>v0

2-3-面的绘制

对于面的绘制,我们首先要知道一个原理:

  • 面有正反两面。

  • 面向我们的面,如果是正面,那它必然是逆时针绘制的;

  • 面向我们的面,如果是反面,那它必然是顺时针绘制的;

接下来,咱们看一下面的三种绘制方式:

  1. TRIANGLES 单独三角形

更新啦!高薪webGL学习之绘制三角形

上面两个面的绘制顺序是:

v0>v1>v2

v3>v4>v5

  1. TRIANGLE_STRIP 三角带

更新啦!高薪webGL学习之绘制三角形

上面四个面的绘制顺序是:

v0>v1>v2

以上一个三角形的第二条边+下一个点为基础,以和第二条边相反的方向绘制三角形

v2>v1>v3

以上一个三角形的第三条边+下一个点为基础,以和第二条边相反的方向绘制三角形

v2>v3>v4

以上一个三角形的第二条边+下一个点为基础,以和第二条边相反的方向绘制三角形

v4>v3>v5

规律:

第一个三角形:v0>v1>v2

第偶数个三角形:以上一个三角形的第二条边+下一个点为基础,以和第二条边相反的方向绘制三角形

第奇数个三角形:以上一个三角形的第三条边+下一个点为基础,以和第二条边相反的方向绘制三角形

  1. TRIANGLE_FAN 三角扇

更新啦!高薪webGL学习之绘制三角形

上面四个面的绘制顺序是:

v0>v1>v2

以上一个三角形的第三条边+下一个点为基础,按照和第三条边相反的顺序,绘制三角形

v0>v2>v3

以上一个三角形的第三条边+下一个点为基础,按照和第三条边相反的顺序,绘制三角形

v0>v3>v4

以上一个三角形的第三条边+下一个点为基础,按照和第三条边相反的顺序,绘制三角形

v0>v4>v5

关于webgl 可以绘制的基本图像就说到这,接下来咱们画个矩形面,练一下手。

3-实例:绘制矩形面

首先,我们要知道,webgl 可以绘制的面只有三角面,所以咱们要绘制矩形面的话,只能用两个三角形去拼。

接下咱们就说一下如何用三角形拼矩形。

4-1-三角形拼矩形的方法

我们可以用TRIANGLE_STRIP 三角带拼矩形。

下面的两个三角形分别是:

v0>v1>v2

v2>v1>v3

更新啦!高薪webGL学习之绘制三角形

4-2-代码实现

  1. 建立顶点数据

const vertices=new Float32Array([
-0.2, 0.2,
-0.2,-0.2,
0.2, 0.2,
0.2,-0.2,
])

上面两个浮点代表一个顶点,依次是v0、v1、v2、v3,如上图所示。

  1. 绘图

gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

上面参数的意思分别是:三角带、从第0个顶点开始画、画四个。

效果如下

更新啦!高薪webGL学习之绘制三角形

关于矩形的绘制就这么简单,接下来咱们可以去尝试其它的图形。

比如:把TRIANGLE_STRIP 三角带变成TRIANGLE_FAN 扇形

gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);

画出了一个三角带的样子:

更新啦!高薪webGL学习之绘制三角形

其绘图顺序是:

v0>v1>v2

v0>v2>v3

更新啦!高薪webGL学习之绘制三角形

关于基本图形的绘制,咱们就说到这。

第四章 异步绘制多点

在项目实战的时候,用户交互事件是必不可少的。因为事件是异步的,所以我们在绘图的时候,必须要考虑异步绘图。

接下来我通过一个例子来说一下异步绘制多点的方法。

1-异步绘制线段

1.先画一个点

更新啦!高薪webGL学习之绘制三角形

2.一秒钟后,在左下角画一个点

更新啦!高薪webGL学习之绘制三角形

3.两秒钟后,我再画一条线段

更新啦!高薪webGL学习之绘制三角形

接下来看一下代码实现:

1.顶点着色器和片元着色器

<!-- 顶点着色器 -->
<script id="vertexShader" type="x-shader/x-vertex">
attribute vec4 a_Position;
void main(){
gl_Position=a_Position;
gl_PointSize=20.0;
}
</script>
<!-- 片元着色器 -->
<script id="fragmentShader" type="x-shader/x-fragment">
void main(){
gl_FragColor=vec4(1,1,0,1);
}
</script>

2.初始化着色器

import { initShaders } from "../jsm/Utils.js";

const canvas = document.querySelector("#canvas");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

// 获取着色器文本
const vsSource = document.querySelector("#vertexShader").innerText;
const fsSource = document.querySelector("#fragmentShader").innerText;

//三维画笔
const gl = canvas.getContext("webgl");

//初始化着色器
initShaders(gl, vsSource, fsSource);

3.建立缓冲对象,并将其绑定到webgl 上下文对象上,然后向其中写入顶点数据。将缓冲对象交给attribute变量,并开启attribute 变量的批处理功能。

//顶点数据
let points=[0, 0.2]
//缓冲对象
const vertexBuffer = gl.createBuffer();
//绑定缓冲对象
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
//写入数据
gl.bufferData(gl.ARRAY_BUFFER,new Float32Array(points),gl.STATIC_DRAW)
//获取attribute 变量
const a_Position=gl.getAttribLocation(gl.program, 'a_Position')
//修改attribute 变量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0)
//赋能-批处理
gl.enableVertexAttribArray(a_Position)

4.刷底色并绘制顶点

//声明颜色 rgba
gl.clearColor(0, 0, 0, 1);
//刷底色
gl.clear(gl.COLOR_BUFFER_BIT);
//绘制顶点
gl.drawArrays(gl.POINTS, 0, 1);

5.一秒钟后,向顶点数据中再添加的一个顶点,修改缓冲区数据,然后清理画布,绘制顶点

setTimeout(()=>{
points.push(-0.2,-0.1)
gl.bufferData(gl.ARRAY_BUFFER,new Float32Array(points),gl.STATIC_DRAW)
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.POINTS, 0, 2);
},1000)

6.两秒钟后,清理画布,绘制顶点,绘制线条

setTimeout(()=>{
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.POINTS, 0, 2);
gl.drawArrays(gl.LINE_STRIP, 0, 2);
},2000)

总结一下上面的原理,当缓冲区被绑定在了webgl 上下文对象上后,我们在异步方法里直接对其进行修改即可,顶点着色器在绘图的时候会自动从其中调用数据。

WebGLBuffer缓冲区中的数据在异步方法里不会被重新置空。

理解了异步绘图原理后,我们还可以对这种图形的绘制进行一个简单的封装。

2-封装多边形对象

建立一个Poly 对象,这个对象是辅助我们理解这一篇的知识的,没做太深层次的考量,因为有的知识点我们还没有讲到。

const defAttr=()=>({
gl:null,
vertices:[],
geoData:[],
size:2,
attrName:'a_Position',
count:0,
types:['POINTS'],
})
export default class Poly{
constructor(attr){
Object.assign(this,defAttr(),attr)
this.init()
}
init(){
const {attrName,size,gl}=this
if(!gl){return}
const vertexBuffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
this.updateBuffer()
const a_Position=gl.getAttribLocation(gl.program,attrName)
gl.vertexAttribPointer(a_Position, size, gl.FLOAT, false, 0, 0)
gl.enableVertexAttribArray(a_Position)
}
addVertice(...params){
this.vertices.push(...params)
this.updateBuffer()
}
popVertice(){
const {vertices,size}=this
const len=vertices.length
vertices.splice(len-size,len)
this.updateCount()
}
setVertice(ind,...params){
const {vertices,size}=this
const i=ind*size
params.forEach((param,paramInd)=>{
vertices[i+paramInd]=param
})
}
updateBuffer(){
const {gl,vertices}=this
this.updateCount()
gl.bufferData(gl.ARRAY_BUFFER,new Float32Array(vertices),gl.STATIC_DRAW)
}
updateCount(){
this.count=this.vertices.length/this.size
}
updateVertices(params){
const {geoData}=this
const vertices=[]
geoData.forEach(data=>{
params.forEach(key=>{
vertices.push(data[key])
})
})
this.vertices=vertices
}
draw(types=this.types){
const {gl,count}=this
for(let type of types){
gl.drawArrays(gl[type],0,count);
}
}
}

属性:

  • gl webgl上下文对象

  • vertices 顶点数据集合,在被赋值的时候会做两件事

    • 更新count 顶点数量,数据运算尽量不放渲染方法里

    • 向缓冲区内写入顶点数据

  • geoData 模型数据,对象数组,可解析出vertices 顶点数据

  • size 顶点分量的数目

  • positionName 代表顶点位置的attribute 变量名

  • count 顶点数量

  • types 绘图方式,可以用多种方式绘图

方法

  • init() 初始化方法,建立缓冲对象,并将其绑定到webgl 上下文对象上,然后向其中写入顶点数据。将缓冲对象交给attribute变量,并开启attribute 变量的批处理功能。

  • addVertice() 添加顶点

  • popVertice() 删除最后一个顶点

  • setVertice() 根据索引位置设置顶点

  • updateBuffer() 更新缓冲区数据,同时更新顶点数量

  • updateCount() 更新顶点数量

  • updateVertices() 基于geoData 解析出vetices 数据

  • draw() 绘图方法

接下来就可以用Poly 对象实现之前的案例了。

const poly=new Poly({
gl,
vertices:[0, 0.2]
})
poly.draw(['POINTS'])

setTimeout(()=>{
poly.addVertice(-0.2,-0.1)
gl.clear(gl.COLOR_BUFFER_BIT);
poly.draw(['POINTS'])
},1000)

setTimeout(()=>{
gl.clear(gl.COLOR_BUFFER_BIT);
poly.draw(['POINTS','LINE_STRIP'])
},2000)

异步绘图原理跑通了,我们也就可以用鼠标绘制线条了。

//实例化多边形
const poly=new Poly({
gl,
types:['POINTS','LINE_STRIP']
})

// 鼠标点击事件
canvas.addEventListener("click", (event) => {
const {x,y}=getMousePosInWebgl(event,canvas)
poly.addVertice(x,y)
gl.clear(gl.COLOR_BUFFER_BIT);
poly.draw()
});

当前我们所能画的是一条线条,如果我们想要绘制多条线呢?就比如我们要画一个狮子座。

更新啦!高薪webGL学习之绘制三角形

3-绘制多线

既然是多线,那就需要有个容器来承载它们,这样方便管理。

3-1-建立容器对象

建立一个Sky 对象,作为承载多边形的容器。

export default class Sky{
constructor(gl){
this.gl=gl
this.children=[]
}
add(obj){
obj.gl=this.gl
this.children.push(obj)
}
updateVertices(params){
this.children.forEach(ele=>{
ele.updateVertices(params)
})
}
draw(){
this.children.forEach(ele=>{
ele.init()
ele.draw()
})
}
}

属性:

  • gl webgl上下文对象

  • children 子级

方法:

  • add() 添加子对象

  • updateVertices() 更新子对象的顶点数据

  • draw() 遍历子对象绘图,每个子对象对应一个buffer 对象,所以在子对象绘图之前要先初始化。

3-2-示例

想象一个场景:鼠标点击画布,绘制多边形路径。鼠标右击,取消绘制。鼠标再次点击,绘制新的多边形。

//夜空
const sky=new Sky(gl)
//当前正在绘制的多边形
let poly=null

//取消右击提示
canvas.oncontextmenu = function(){
return false;
}
// 鼠标点击事件
canvas.addEventListener("mousedown", (event) => {
if(event.button===2){
popVertice()
}else{
const {x,y}=getMousePosInWebgl(event,canvas)
if(poly){
poly.addVertice(x,y)
}else{
crtPoly(x,y)
}
}
render()
});
//鼠标移动
canvas.addEventListener("mousemove", (event) => {
if(poly){
const {x,y}=getMousePosInWebgl(event,canvas)
poly.setVertice(poly.count-1,x,y)
render()
}
});

//删除最后一个顶点
function popVertice(){
poly.popVertice()
poly=null
}
//创建多边形
function crtPoly(x,y){
poly=new Poly({
vertices:[x,y,x,y],
types:['POINTS','LINE_STRIP']
})
sky.add(poly)
}
// 渲染方法
function render(){
gl.clear(gl.COLOR_BUFFER_BIT)
sky.draw()
}

最后,用一个完整的例子结束这一篇的知识。

案例-狮子座

接下来,我要在下图中绘制狮子座。

更新啦!高薪webGL学习之绘制三角形

效果如下

更新啦!高薪webGL学习之绘制三角形

1-产品需求

1-1-基本绘图需求

  1. 鼠标第1次点击画布时:

    • 创建多边形

    • 绘制2个点

  2. 鼠标移动时:

    • 当前多边形最后一个顶点随鼠标移动

  3. 鼠标接下来点击画布时:

    • 新建一个点

  4. 鼠标右击时:

    • 删除最后一个随鼠标移动的点

1-2-优化需求

  1. 顶点要有闪烁动画

  2. 建立顶点的时候,如果鼠标点击了其它顶点,就不要再显示新的顶点

对于上面的基本需求,我们在用鼠标画多线的时候,已经实现了,接下来我们直接实现优化需求。

2-代码实现

1.建立顶点着色器

<script id="vertexShader" type="x-shader/x-vertex">
attribute vec4 a_Attr;
varying float v_Alpha;
void main(){
gl_Position=vec4(a_Attr.x,a_Attr.y,0.0,1.0);
gl_PointSize=a_Attr.z;
v_Alpha=a_Attr.w;
}
</script>
  • a_Attr() 是一个4维向量,其参数结构为(x,y,z,w)

    • x,y代表位置

    • z代表顶点尺寸

    • w代表顶点透明度,w会通过 varying 变量v_Alpha 传递给片元

2.建立片元着色器

<script id="fragmentShader" type="x-shader/x-fragment">
precision mediump float;
varying float v_Alpha;
void main(){
float dist=distance(gl_PointCoord,vec2(0.5,0.5));
if(dist<0.5){
gl_FragColor=vec4(0.87,0.91,1.0,v_Alpha);
}else{
discard;
}
}
</script>

通过v_Alpha接收透明度,然后设置片元的颜色。

3.建立夜空对象,用于承载多边形

const sky=new Sky(gl)

4.建立合成对象,用于对顶点数据做补间运算

const compose = new Compose();

5.声明两个变量,用于表示当前正在绘制的多边形和鼠标划上的点

//当前正在绘制的多边形
let poly=null
//鼠标划上的点
let point=null

6.取消右击提示

//取消右击提示
canvas.oncontextmenu = function(){
  return false;
}

7.鼠标按下事件

// 鼠标按下事件
canvas.addEventListener("mousedown", (event) => {
if(event.button===2){
//右击删除顶点
poly&&popVertice()
}else{
const {x,y}=getMousePosInWebgl(event,canvas)
if(poly){
//连续添加顶点
addVertice(x,y)
}else{
//建立多边形
crtPoly(x,y)
}
}
});
  • getMousePosInWebgl() 方法是用于获取鼠标在webgl 画布中的位置,我们之前说过。

  • crtPoly() 创建多边形

function crtPoly(x,y){
let o1=point?point:{x,y,pointSize:random(),alpha:1}
const o2={x,y,pointSize:random(),alpha:1}
poly=new Poly({
size:4,
attrName:'a_Attr',
geoData:[o1,o2],
types:['POINTS','LINE_STRIP']
})
sky.add(poly)
crtTrack(o1)
crtTrack(o2)
}

建立两个顶点数据o1,o2,如果鼠标点击了其它顶点,o1的数据就是此顶点的数据。

顶点的尺寸是一个随机数random()

function random(){
return Math.random()*8.0+3.0
}

基于两个顶点数据,建立多边形对象和两个时间轨对象。

  • crtTrack() 建立时间轨

function crtTrack(obj){
const {pointSize}=obj
const track = new Track(obj)
track.start = new Date()
track.timeLen = 2000
track.loop = true
track.keyMap = new Map([
[
"pointSize",
[
[500, pointSize],
[1000, 0],
[1500, pointSize],
],
],
[
"alpha",
[
[500, 1],
[1000, 0],
[1500, 1],
],
],
]);
compose.add(track)
}
  • addVertice() 添加顶点

function addVertice(x,y){
const {geoData}=poly
if(point){
geoData[geoData.length-1]=point
}
let obj={x,y,pointSize:random(),alpha:1}
geoData.push(obj)
crtTrack(obj)
}

如果鼠标点击了其它顶点,就让多边形的最后一个顶点数据为此顶点。

建立下一个顶点的顶点数据,添加新的顶点,建立新的时间轨。

  • popVertice() 删除最后一个顶点

function popVertice(){
poly.geoData.pop()
compose.children.pop()
poly=null
}

8.鼠标移动事件

canvas.addEventListener("mousemove", (event) => {
const {x,y}=getMousePosInWebgl(event,canvas)
point=hoverPoint(x,y)
if(point){
canvas.style.cursor='pointer'
}else{
canvas.style.cursor='default'
}
if(poly){
const obj=poly.geoData[poly.geoData.length-1]
obj.x=x
obj.y=y
}
});

基于鼠标是否划上顶点,设置鼠标的视觉状态。

设置正在绘制的多边形的最后一个顶点点位。

  • hoverPoint() 检测所有顶点的鼠标划入,返回顶点数据

function hoverPoint(mx,my){
for(let {geoData} of sky.children){
for(let obj of geoData){
if(poly&&obj===poly.geoData[poly.geoData.length-1]){
continue
}
const delta={
x:mx-obj.x,
y:my-obj.y
}
const {x,y}=glToCssPos(delta,canvas)
const dist=x*x+y*y;
if(dist<100){
return obj
}
}
}
return null
}

遍历sky 中的所有顶点数据

忽略绘图时随鼠标移动的点

获取鼠标和顶点的像素距离

若此距离小于10像素,返回此点;否则,返回null。

  • glToCssPos() webgl坐标系转css坐标系,将之前说过的getMousePosInWebgl() 方法逆向思维即可

function glToCssPos({x,y},{width,height}){
const [halfWidth, halfHeight] = [width / 2, height / 2]
return {
x:x*halfWidth,
y:-y*halfHeight
}
}

9.连续渲染方法

!(function ani() {
compose.update(new Date())
sky.updateVertices(['x','y','pointSize','alpha'])
render()
requestAnimationFrame(ani)
})();
  • 更新动画数据

  • 更新Vertices 数据

  • render() 渲染

function render(){
gl.clear(gl.COLOR_BUFFER_BIT)
sky.draw()
}

扩展-图形转面

1-webgl三种面的适应场景

之前咱们说过,webgl 可以绘制三种面:

  • TRIANGLES 单独三角形

  • TRIANGLE_STRIP 三角带

  • TRIANGLE_FAN 三角扇

在实际的引擎开发中,TRIANGLES 是用得最多的。

TRIANGLES 的优势是可以绘制任意模型,缺点是比较费点。

适合TRIANGLES 单独三角形的的模型:

更新啦!高薪webGL学习之绘制三角形

TRIANGLE_STRIP 和TRIANGLE_FAN 的优点是相邻的三角形可以共用一条边,比较省点,然而其缺点也太明显,因为它们只适合绘制具备相应特点的模型。

适合TRIANGLE_STRIP三角带的模型:

更新啦!高薪webGL学习之绘制三角形

适合TRIANGLE_FAN三角扇的模型:

更新啦!高薪webGL学习之绘制三角形

three.js 使用的绘制面的方式就是TRIANGLES,我们可以在其WebGLRenderer 对象的源码的renderBufferImmediate 方法中找到:

_gl.drawArrays( _gl.TRIANGLES, 0, object.count );

2-图形转面的基本步骤

在three.js 里有一个图形几何体ShapeGeometry,可以把图形变成面。

更新啦!高薪webGL学习之绘制三角形

我们学到这里,只要有数学支撑,也可以实现这种效果。

接下来我要使用TRIANGLES 独立三角形的方式,将图形转成面。

我使用的方法叫做“砍角”,其原理就是从起点将多边形中符合特定条件的角逐个砍掉,然后保存到一个集合里,直到把多边形砍得只剩下一个三角形为止。这时候集合里的所有三角形就是我们想要的独立三角形。

举个例子:

更新啦!高薪webGL学习之绘制三角形

已知:逆时针绘图的路径G

求:将其变成下方网格的方法

更新啦!高薪webGL学习之绘制三角形

解:

1.寻找满足以下条件的▲ABC:

  • ▲ABC的顶点索引位置连续,如012,123、234

  • 点C在向量AB的正开半平面里,可以理解为你站在A点,面朝B点,点C要在你的左手边

  • ▲ABC中没有包含路径G 中的其它顶点

2.当找到▲ABC 后,就将点B从路径的顶点集合中删掉,然后继续往后找。

3.当路径的定点集合只剩下3个点时,就结束。

4.由所有满足条件的▲ABC构成的集合就是我们要求的独立三角形集合。

3-绘制路径G

1.路径G的顶点数据

const pathData = [0, 0,
0, 600,
600, 600,
600, 200,
200, 200,
200, 400,
300, 400,
300, 300,
500, 300,
500, 500,
100, 500,
100, 100,
600, 100,
600, 0
];

在pathData里两个数字为一组,分别代表顶点的x位和y位。

pathData里的数据是我以像素为单位画出来的,在实际项目协作中,UI给我们的svg文件可能也是以像素为单位画出来的,这个我们要做好心理准备。

因为,webgl画布的宽和高永远都是两个单位。

所以,我们要将上面的点画到webgl 画布中,就需要做一个数据映射。

2.在webgl 中绘制正方形。

从pathData 数据中我们可以看出,路径G的宽高都是600,是一个正方形。

所以,我可以将路径G映射到webgl 画布的一个正方形中。

这个正方形的高度我可以暂且定为1,那么其宽度就应该是高度除以canvas画布的宽高比。

//宽高比
const ratio = canvas.width / canvas.height;
//正方形高度
const rectH = 1.0;
//正方形宽度
const rectW = rectH / ratio;

3.正方形的定位,把正方形放在webgl画布的中心。

获取正方形尺寸的一半,然后求出其x、y方向的两个极值即可。

//正方形宽高的一半
const [halfRectW, halfRectH] = [rectW / 2, rectH / 2];
//两个极点
const minX = -halfRectW;
const minY = -halfRectH;
const maxX = halfRectW;
const maxY = halfRectH;

我想把

4.利用之前的Poly对象绘制正方形,测试一下效果。

const rect = new Poly({
gl,
vertices: [
minX, maxY,
minX, minY,
maxX, minY,
maxX, maxY,
],
});
rect.draw();

更新啦!高薪webGL学习之绘制三角形

先画了4个点,效果没问题。

5.建立x轴和y轴比例尺。

const scaleX = ScaleLinear(0, minX, 600, maxX);
const scaleY = ScaleLinear(0, minY, 600, maxY);
function ScaleLinear(ax, ay, bx, by) {
const delta = {
x: bx - ax,
y: by - ay,
};
const k = delta.y / delta.x;
const b = ay - ax * k;
return function (x) {
return k * x + b;
};
}

ScaleLinear(ax, ay, bx, by) 方法使用的就是点斜式,用于将x轴和y轴上的数据像素数据映射成 webgl数据

  • ax 像素数据的极小值

  • ay webgl数据的极小值

  • bx 像素数据的极大值

  • by webgl数据的极大值

6.将路径G中的像素数据解析为webgl 数据

const glData = [];
for (let i = 0; i < pathData.length; i += 2) {
glData.push(scaleX(pathData[i]), scaleY(pathData[i + 1]));
}

画一下看看:

const path = new Poly({
gl,
vertices: glData,
types: ["POINTS", "LINE_LOOP"],
});
path.draw();

更新啦!高薪webGL学习之绘制三角形

效果没有问题。

4.将图形网格化

1.我自己建立了一个ShapeGeo 对象,用于将图形网格化。

const shapeGeo = new ShapeGeo(glData)

属性:

  • pathData 平展开的路径数据

  • geoData 由路径数据pathData 转成的对象型数组

  • triangles 三角形集合,对象型数组

  • vertices 平展开的对立三角形顶点集合

方法:

  • update() 更新方法,基于pathData 生成vertices

  • parsePath() 基于路径数据pathData 转成对象型数组

  • findTriangle(i) 寻找符合条件的三角形

    • i 顶点在geoData 中的索引位置,表示从哪里开始寻找三角形

  • includePoint(triangle) 判断三角形中是否有其它顶点

  • inTriangle(p0, triangle) 判断一个顶点是否在三角形中

  • cross([p0, p1, p2]) 以p0为基点,对二维向量p0p1、p0p2做叉乘运算

  • upadateVertices() 基于对象数组geoData 生成平展开的vertices 数据

2.绘制G形面

const face = new Poly({
gl,
vertices: shapeGeo.vertices,
types: ["TRIANGLES"],
});
face.draw();

效果如下:

更新啦!高薪webGL学习之绘制三角形

填坑

我之前在用鼠标绘制线条的时候,还留了一个系统兼容性的坑。

线条在mac电脑中是断的:

这种效果是由片元着色器导致的:

precision mediump float;    
void main(){
float dist=distance(gl_PointCoord,vec2(0.5,0.5));
if(dist<0.5){
gl_FragColor=vec4(1,1,0,1);
}else{
discard;
}
}

我们在用上面的片元着色器绘图的时候,把线给过滤掉了。

因此,我需要告诉着色器当前绘图的方式,如果是POINTS 方式绘图的话,就过滤一下圆圈以外的片元,否则就正常绘图。

接下来咱们就看一下代码实现。

1.给片元着色器添加一个uniform 变量。

precision mediump float;
uniform bool u_IsPOINTS;
void main(){
if(u_IsPOINTS){
float dist=distance(gl_PointCoord,vec2(0.5,0.5));
if(dist<0.5){
gl_FragColor=vec4(1,1,0,1);
}else{
discard;
}
}else{
gl_FragColor=vec4(1,1,0,1);
}
}

2.给Poly 对象添加两个属性。

const defAttr=()=>({
circleDot:false,
u_IsPOINTS:null,
……
})
  • circleDot 是否是圆点

  • u_IsPOINTS uniform变量

3.在初始化方法中,如果是圆点,就获取一下uniform 变量

init(){
……
if (circleDot) {
this.u_IsPOINTS = gl.getUniformLocation(gl.program, "u_IsPOINTS");
}
}

4.在渲染的时候,如果是圆点,就基于绘图方式修改uniform 变量

draw(types=this.types){
const {gl,count,u_IsPOINTS,circleDot}=this
for (let type of types) {
circleDot&&gl.uniform1f(u_IsPOINTS,type==='POINTS');
gl.drawArrays(gl[type],0,count);
}
}

总结

在实际项目中,我们绘制完了图形,往往还会对其进行修改变换,下一篇咱们会说一下如何变换图形。



点个在看你最好看