webgl入门-绘制三角形

绘制三角形

前言

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

课堂目标

  1. 理解多点绘图原理。
  2. 可以绘制三角形,并将其组合成多边形。

知识点

  1. 缓冲区对象
  2. 点、线、面图形

第一章 webgl 的绘图方式

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

  1. 绘制多点

    image-20200922151301533

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

    image-20200922153449307

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

    image-20200922153643189

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);
    

实际效果:

image-20200922115356975

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

2-绘制多点详解

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

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

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

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

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

  1. 建立顶点数据,两个浮点数构成一个顶点,分别代表x、y 值。
const vertices=new Float32Array([//x    y0.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 就是绘制三角面的意思。

看一下效果:

image-20200924113247043

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

2-基本图形

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

  • POINTS 可视的点
  • LINES 单独线段
  • LINE_STRIP 线条
  • LINE_LOOP 闭合线条
  • TRIANGLES 单独三角形
  • TRIANGLE_STRIP 三角带
  • TRIANGLE_FAN 三角扇

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

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

2-1-点的绘制

POINTS 可视的点

image-20200927115202967

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

2-2-线的绘制
  1. LINES 单独线段

image-20200927112630534

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

v0>v1

v2>v3

v4>v5

  1. LINE_STRIP 线条

image-20200927113956696

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

  1. LINE_LOOP 闭合线条

image-20200927114345495

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

2-3-面的绘制

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

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

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

  1. TRIANGLES 单独三角形

image-20200927161356266

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

v0>v1>v2

v3>v4>v5

  1. TRIANGLE_STRIP 三角带

image-20200930230050526

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

v0>v1>v2

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

v2>v1>v3

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

v2>v3>v4

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

v4>v3>v5

规律:

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

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

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

  1. TRIANGLE_FAN 三角扇

image-20200927160758122

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

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

image-20200930220329539

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个顶点开始画、画四个。

效果如下:

image-20200930223815832

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

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

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

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

image-20200930223654798

其绘图顺序是:

v0>v1>v2

v0>v2>v3

image-20200930230452532

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

第四章 异步绘制多点

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

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

1-异步绘制线段

1.先画一个点

image-20210306161928219

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

image-20210306162145896

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

image-20210306162351559

接下来看一下代码实现:

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}=thisif(!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}=thisconst len=vertices.lengthvertices.splice(len-size,len)this.updateCount()}setVertice(ind,...params){const {vertices,size}=thisconst i=ind*sizeparams.forEach((param,paramInd)=>{vertices[i+paramInd]=param})}updateBuffer(){const {gl,vertices}=thisthis.updateCount()gl.bufferData(gl.ARRAY_BUFFER,new Float32Array(vertices),gl.STATIC_DRAW)}updateCount(){this.count=this.vertices.length/this.size}updateVertices(params){const {geoData}=thisconst vertices=[]geoData.forEach(data=>{params.forEach(key=>{vertices.push(data[key])})})this.vertices=vertices}draw(types=this.types){const {gl,count}=thisfor(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()
});

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

image-20210309094648689

3-绘制多线

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

3-1-建立容器对象

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

export default class Sky{constructor(gl){this.gl=glthis.children=[]}add(obj){obj.gl=this.glthis.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()
}

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

案例-狮子座

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

image-20210309094611798

效果如下

image-20210309094648689

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}=objconst track = new Track(obj)track.start = new Date()track.timeLen = 2000track.loop = truetrack.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}=polyif(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=xobj.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 单独三角形的的模型:

image-20210312161835601

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

适合TRIANGLE_STRIP三角带的模型:

image-20210312161129484

适合TRIANGLE_FAN三角扇的模型:

image-20210312161335598

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

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

2-图形转面的基本步骤

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

image-20210311153236165

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

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

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

举个例子:

16109600591457223186999880544

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

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

16109600591456188731865428303

解:

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();

image-20210314082945506

先画了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();

image-20210314084138562

效果没有问题。

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();

效果如下:

image-20210314095034861

填坑

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

线条在mac电脑中是断的:

image-20210314162456486

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

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}=thisfor (let type of types) {circleDot&&gl.uniform1f(u_IsPOINTS,type==='POINTS');gl.drawArrays(gl[type],0,count);}
}

总结

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/329490.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C# run Node.js

C# run nodejs Inter-Process Communication&#xff0c;IPC Process类 启动Node.js进程&#xff0c;通过标准输入输出与其进行通信。 // n.js// 监听来自标准输入的消息 process.stdin.on(data, function (data) {// 收到消息后&#xff0c;在控制台输出并回复消息console.l…

C++设计模式---面向对象原则

面向对象设计原则 原则的目的&#xff1a;高内聚&#xff0c;低耦合 1. 单一职责原则 类的职责单一&#xff0c;对外只提供一种功能&#xff0c;而引起类变化的原因都应该只有一个。 2. 开闭原则 对扩展开放&#xff0c;对修改关闭&#xff1b;增加功能是通过增加代码来实现的&…

探索 Rust 语言的精髓:深入 Rust 标准库

探索 Rust 语言的精髓&#xff1a;深入 Rust 标准库 Rust&#xff0c;这门现代编程语言以其内存安全、并发性和性能优势而闻名。它不仅在系统编程领域展现出强大的能力&#xff0c;也越来越多地被应用于WebAssembly、嵌入式系统、分布式服务等众多领域。Rust 的成功&#xff0…

计算机网络数据链路层知识点总结

3.1 数据链路层功能概述 &#xff08;1&#xff09;知识总览 &#xff08;2&#xff09;数据链路层的研究思想 &#xff08;3&#xff09;数据链路层基本概念 &#xff08;4&#xff09;数据链路层基本功能 3.1 封装成帧和透明传输 &#xff08;1&#xff09;数据链路层功能…

Redis常见数据类型(3)-String, Hash

目录 String 命令小结 内部编码 典型的使用场景 缓存功能 计数功能 共享会话 手机验证码 Hash 哈希 命令 hset hget hexists hdel hkeys hvals hgetall hmget hlen hsetnx hincrby hincrbyfloat String 上一篇中介绍了了String里的基本命令, 接下来总结一…

《Python编程从入门到实践》day37

# 昨日知识点回顾 制定规范、创建虚拟环境并激活&#xff0c;正在虚拟环境创建项目、数据库和应用程序 # 今日知识点学习 18.2.4 定义模型Entry # models.py from django.db import models# Create your models here. class Topic(models.Model):"""用户学习的…

Vue2基础及其进阶面试(一)

简单版项目初始化 新建一个vue2 官网文档&#xff1a;介绍 — Vue.js 先确保下载了vue的脚手架 npm install -g vue-cli npm install -g vue/cli --force vue -V 创建项目 vue create 自己起个名字 选择自己选择特性 选择&#xff1a; Babel&#xff1a;他可以将我们写…

微软开发者大会,Copilot Agents发布,掀起新一轮生产力革命!

把AI融入生产力工具的未来会是什么样&#xff1f;微软今天给出了蓝图。 今天凌晨&#xff0c;微软召开了Microsoft Build 2024 开发者大会&#xff0c;同前两天的Google I/O开发者大会一样&#xff0c;本次大会的核心词还是“AI”&#xff0c;其中最主要的内容是最新的Copilot…

拼多多暂时超越阿里成为电商第一

关注卢松松&#xff0c;会经常给你分享一些我的经验和观点。 拼多多的财报又炸裂了&#xff1a; 拼多多发布了第一季度财报&#xff0c;营收868亿&#xff0c;增长了131%&#xff0c;净利润279亿&#xff0c;增长了246%&#xff0c;营销服务收入424亿&#xff0c;也就是商家的…

BCD编码Java实现

最常用的BCD编码&#xff0c;就是使用"0"至"9"这十个数值的二进码来表示。这种编码方式&#xff0c;在称之为“8421码”&#xff08;日常所说的BCD码大都是指8421BCD码形式&#xff09;。除此以外&#xff0c;对应不同需求&#xff0c;各人亦开发了不同的编…

Linux多线程系列三: 生产者消费者模型,信号量使用,基于阻塞队列和环形队列的这两种生产者消费者代码的实现

Linux多线程系列三: 生产者消费者模型,信号量,基于阻塞队列和环形队列的这两种生产者消费者代码的实现 一.生产者消费者模型的理论1.现实生活中的生产者消费者模型2.多线程当中的生产者消费者模型3.理论 二.基于阻塞队列的生产者消费者模型的基础代码1.阻塞队列的介绍2.大致框架…

rust的版本问题,安装问题,下载问题

rust的版本、安装、下载问题 rust版本问题&#xff0c; 在使用rust的时候&#xff0c;应用rust的包&#xff0c;有时候包的使用和rust版本有关系。 error: failed to run custom build command for pear_codegen v0.1.2 Caused by: process didnt exit successfully: D:\rus…

【Mac】Dreamweaver 2021 for mac v21.3 Rid中文版安装教程

软件介绍 Dreamweaver是Adobe公司开发的一款专业网页设计与前端开发软件。它集成了所见即所得&#xff08;WYSIWYG&#xff09;编辑器和代码编辑器&#xff0c;可以帮助开发者快速创建和编辑网页。Dreamweaver提供了丰富的功能和工具&#xff0c;包括代码提示、语法高亮、代码…

es数据备份和迁移Elasticsearch

Elasticsearch数据备份与恢复 前提 # 注意&#xff1a; 1.在进行本地备份时使用--type需要备份索引和数据&#xff08;mapping,data&#xff09; 2.在将数据备份到另外一台ES节点时需要比本地备份多备份一种数据类型&#xff08;analyzer,mapping,data,template&#xff09; …

STM32看门狗

文章目录 WDG&#xff08;Watchdog&#xff09;看门狗独立看门狗独立看门狗框图超时时间计算 窗口看门狗超时时间 独立看门狗与窗口看门狗对比补充 WDG&#xff08;Watchdog&#xff09;看门狗 看门狗可以监控程序的运行状态&#xff0c;当程序因为设计漏洞、硬件故障、电磁干…

SpringCloud Alibaba的相关组件的简介及其使用

Spring Cloud Alibaba是阿里巴巴为开发者提供的一套微服务解决方案&#xff0c;它基于Spring Cloud项目&#xff0c;提供了一系列功能强大的组件&#xff0c;包括服务注册与发现、配置中心、熔断与限流、消息队列等。 本文将对Spring Cloud Alibaba的相关组件进行简介&#xff…

如何搭建一个vue项目(完整步骤)

搭建一个新的vue项目 一、安装node环境二、搭建vue项目环境1、全局安装vue-cli2、进入你的项目目录&#xff0c;创建一个基于 webpack 模板的新项目3、进入项目&#xff1a;cd vue-demo&#xff0c;安装依赖4、npm run dev&#xff0c;启动项目 三、vue项目目录讲解四、开始我们…

安装和使用图像处理软件GraphicsMagick @FreeBSD

GraphicsMagick是一个用于处理图像的读取、写入和操作的工具软件。它被誉为图像处理领域的“瑞士军刀”&#xff0c;短小精悍&#xff0c;支持超过88种图像格式&#xff0c;包括DPX、GIF、JPEG、JPEG-2000、PNG、PDF、PNM和TIFF等。 GraphicsMagick的主要特点包括&#xff1a;…

【openlayers系统学习】3.5colormap详解(颜色映射)

五、colormap详解&#xff08;颜色映射&#xff09; ​colormap​ 包是一个很好的实用程序库&#xff0c;用于创建颜色图。该库已作为项目的依赖项添加&#xff08;1.7美化&#xff08;设置style&#xff09;&#xff09;。要导入它&#xff0c;请编辑 main.js​ 以包含以下行…

蓝桥楼赛第30期-Python-第三天赛题 从参数中提取信息题解

楼赛 第30期 Python 模块大比拼 提取用户输入信息 介绍 正则表达式&#xff08;英文为 Regular Expression&#xff0c;常简写为regex、regexp 或 RE&#xff09;&#xff0c;也叫规则表达式、正规表达式&#xff0c;是计算机科学的一个概念。 所谓“正则”&#xff0c;可以…