当前位置: 首页 > news >正文

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

总结

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

相关文章:

webgl入门-绘制三角形

绘制三角形 前言 三角形是一个最简单、最稳定的面&#xff0c;webgl 中的三维模型都是由三角面组成的。咱们这一篇就说一下三角形的绘制方法。 课堂目标 理解多点绘图原理。可以绘制三角形&#xff0c;并将其组合成多边形。 知识点 缓冲区对象点、线、面图形 第一章 web…...

深入分析 Android Activity (三)

深入分析 Android Activity (三) 1. Activity 的配置变化处理 当设备配置&#xff08;如屏幕方向、语言、屏幕大小等&#xff09;发生变化时&#xff0c;默认情况下&#xff0c;Android 会销毁并重新创建当前的 Activity。这种行为确保了新配置能够正确应用&#xff0c;但在某…...

电影《朝云暮雨》观后感

上周看了电影《朝云暮雨》&#xff0c;看完之后&#xff0c;感觉自己整个人都不太好了&#xff0c;也不是说电影太差&#xff0c;只是觉得电影没有传达正能量&#xff0c;让人很不舒服。 &#xff08;1&#xff09;演技在线 对于著名的演员“范伟”&#xff0c;或者说&#x…...

Isaac Sim仿真平台学习(1)认识Isaac Sim

0.前言 上一个教程中我们下载好了Isaac Sim&#xff0c;这一章我们将来简单了解一下Isaac Sim平台。 isaac Sim仿真平台安装-CSDN博客 1.Isaac Sim是啥&#xff1f; What Is Isaac Sim? — Omniverse IsaacSim latest documentation Isaac Sim是NVDIA Omniverse平台的机器…...

C++:vector基础讲解

hello&#xff0c;各位小伙伴&#xff0c;本篇文章跟大家一起学习《C&#xff1a;vector基础讲解》&#xff0c;感谢大家对我上一篇的支持&#xff0c;如有什么问题&#xff0c;还请多多指教 &#xff01; 如果本篇文章对你有帮助&#xff0c;还请各位点点赞&#xff01;&#…...

Grafana 路径遍历所有路径 CVE-2021-43798漏洞预警

简介​ ​Grafana是一个跨平台、开源的数据可视化网络应用程序平台。用户配置连接的数据源之后&#xff0c;Grafana可以在网络浏览器里显示数据图表和警告。 漏洞危害等级 高危 CVE 编号​ CVE-2021-43798 FOFA查询 ​app"Grafana" ​zoomeyes查询 ​app:"gr…...

基于Docker部署GitLab环境搭建

文件在D:\E\学习文档子目录压缩\专项进阶&#xff0c;如ngnix,webservice,linux,redis等\docker 建议虚拟机内存2G以上 1.下载镜像文件 docker pull beginor/gitlab-ce:11.0.1-ce.0 注意&#xff1a;一定要配置阿里云的加速镜像 创建GitLab 的配置 (etc) 、 日志 (log) 、数…...

初始化是什么

定义 初始化&#xff08;Initialization&#xff09;是指在计算机科学和软件开发中&#xff0c;将系统、变量、对象或其他可用组件设置为其初始状态或初始值的过程。这通常是在程序开始执行或组件第一次使用之前进行的&#xff0c;以确保其处于可预测和稳定的状态。 初始化的…...

Python图形界面(GUI)Tkinter笔记(九):用【Button()】功能按钮实现人机交互

在Tkinter库中,功能按钮(Button)是实现人机交互的一个非常重要的组件: 【一】主要可实现功能及意义: (1)响应用户交互: Button组件允许用户通过点击来触发某个事件或动作。当用户点击按钮时,可以执行一个指定的函数或方法。 (2)提供用户输入: Button组件是图形用户界面(G…...

linux 内核安装、切换版本,禁用内核更新

安装内核 版本5.15.0-105 sudo apt-get install linux-image-5.15.0-105-generic sudo apt-get install linux-headers-5.15.0-105-generic切换内核版本 #查看已安装内核版本 grep menuentry /boot/grub/grub.cfg sudo dpkg --get-selections |grep linux-image#修改文件/etc…...

充电桩中PE接地实时监测的一种电路.pdf

pdf下载链接&#xff1a;https://pan.baidu.com/s/18k8tEwa6h3WAOGJs3lAsTQ 提取码&#xff1a;Ronv...

鲲泰新闻丨第七届数字中国建设峰会正式启幕,神州鲲泰携手天翼云共筑智算云生态

2024年5月23日&#xff0c;由国家发展改革委、国家数据局、国家网信办、科技部、国务院国资委、福建省人民政府共同主办的“第七届数字中国建设峰会”在福建省福州市海峡国际会展中心盛大开幕。 数字中国建设峰会是展示数字中国建设成就的盛会&#xff0c;本次峰会以“释放数据…...

零基础学Java第二十二天之IO流之内存流,打印流,随机流

IO流之内存流&#xff0c;打印流&#xff0c;随机流 1、内存流 1、理解 内存流"&#xff08;Memory Stream&#xff09;在计算机编程中通常指的是一种特殊的数据流&#xff0c;它在内存中存储和操作数据&#xff0c;而不是在外部存储&#xff08;如硬盘、网络等&#xf…...

vue-router路由懒加载以及三种实现方式

什么是路由懒加载&#xff1f; 延迟加载或按需加载路由所对应的组件&#xff0c;而不是在应用初始化时就一次性加载所有组件。 路由懒加载做了什么事情&#xff1f; 主要作用是将路由对应的组件打包成一个个的js代码块 只有在这个路由被访问到的时候&#xff0c;才加载对应…...

Java轻松转换Markdown文件到Word和PDF文档

Markdown 凭借其简洁易用的特性&#xff0c;成为创建和编辑纯文本文档的常用选择。但某些时候我们需要更加精致的展示效果&#xff0c;例如在专业分享文档或打印成离线使用的纸质版时&#xff0c;就需要将Markdown文件以其他固定的文档格式呈现。通过将 Markdown 转换为 Word 和…...

【JAVA基础之内部类】匿名内部类

&#x1f525;作者主页&#xff1a;小林同学的学习笔录 &#x1f525;小林同学的专栏&#xff1a;JAVA之基础专栏 目录 1.内部类 1.1 概述 1.1.1 什么是内部类 1.1.2 什么时候使用内部类 1.2 内部类的分类 1.3 成员内部类 1.3.1 获取成员内部类对象的两种方式 1.3.2 经典面试…...

远动通讯屏的原理和应用

远动通讯屏的原理和应用 远动通讯屏&#xff0c;是一种集显示和远程控制于一体的智能化控制设备。它可以通过网络、通信线路等方式实现与远程设备的通讯和交互&#xff0c;从而实现远程监控和控制。 远动通讯屏实现远程控制的核心原理是基于PLC&#xff08;Programmable Logic …...

C++ (week4):Linux基础

文章目录 零、Linux简介1.配置环境2.Linux历史3.Linux模型 一、vim二、Linux命令行 (shell命令)1.常用命令与快捷键(1)常用命令①man命令&#xff1a;查看帮助手册 (2)快捷键 2.用户子系统(1)Linux用户(2)用户命令 3.文件子系统命令(1)目录命令1.创建文件&#xff1a;mkdir2.删…...

如何将手机中的音乐转移到 SD 卡上?轻松传输音乐

概括 如何将音乐从手机转移到 SD 卡&#xff1f;我们的智能手机可以充当个人点唱机&#xff0c;因此有效管理我们的音乐库变得至关重要。无论您是存储空间不足还是只是想整理您的音乐收藏&#xff0c;将音乐从手机传输到 SD 卡都是一个实用的解决方案。 在本指南中&#xff0…...

JKTECH柔性振动盘用途

柔性振动盘的作用与用途 在现代工业自动化领域&#xff0c;柔性振动盘凭借其独特的功能和广泛的应用场景&#xff0c;正逐渐成为生产线上的重要工具。柔性振动盘&#xff0c;又称柔性供料器&#xff0c;它结合了传统振动盘的高效性和现代自动化技术的灵活性&#xff0c;为各种…...

【职场心灵伴侣】文心一言智能体

【文心一言】智能体 写在最前面名称和简介&#xff1a;职场心灵伴侣AI生成头像添加工具智能体调优 &#x1f308;你好呀&#xff01;我是 是Yu欸 &#x1f30c; 2024每日百字篆刻时光&#xff0c;感谢你的陪伴与支持 ~ &#x1f680; 欢迎一起踏上探险之旅&#xff0c;挖掘无限…...

【运维】笔记本电脑风扇清洁

笔记本电脑是我们不可或缺的工具&#xff0c;无论是工作、学习还是娱乐。然而&#xff0c;随着时间的推移&#xff0c;笔记本电脑的性能可能会因为各种原因受到影响&#xff0c;尤其是散热问题。过热不仅会降低性能&#xff0c;还可能缩短硬件的寿命。最近&#xff0c;在使用我…...

3.1 掌握RDD的创建

在Apache Spark中&#xff0c;RDD&#xff08;Resilient Distributed Dataset&#xff09;是一个基本的、不可变的、分布式的和可分区的数据集。它能够自动进行容错处理&#xff0c;并支持在大规模集群上的并行操作。RDD之间存在依赖关系&#xff0c;可以实现管道化&#xff0c…...

深入理解 Java 中的 `volatile` 关键字:可见性与有序性的保障

深入理解 Java 中的 volatile 关键字&#xff1a;可见性与有序性的保障 volatile主要做了两个事情可见性保证 和 有序性 可见性保证就是&#xff1a; 对volatile的写操作会对其他线程可见。 简单来说我们A线程的修改了volatile的值&#xff0c;那么我B线程也可以看见。 有序…...

1077: 平衡二叉树的判定

解法&#xff1a; 平衡二叉树是一种特殊的二叉树&#xff0c;它满足以下两个条件&#xff1a; 左子树和右子树的高度差不超过1&#xff08;即&#xff0c;左右子树高度差的绝对值不超过1&#xff09;。左子树和右子树都是平衡二叉树。 后序遍历过程中每次判断左右子树高度差…...

深度学习-Softmax回归+损失函数+图像分类数据集

目录 Softmax回归回归 VS 分类Kaggle上的分类问题 从回归到多类分类回归分类从回归到多类分类-均方损失从回归到多类分类-无校验比例从回归到多类分类-校验比例 Softmax和交叉熵损失总结损失函数均方损失绝对值损失函数鲁棒损失 图像分类数据集通过框架中内置函数将FashionMNIS…...

【论文解读】Overview of the Scalable Video Coding Extension of the H.264/AVC Standard

介绍 该篇论文是一篇关于H.264/AVC标准可扩展视频编码(SVC)扩展的综述论文,由Heiko Schwarz、Detlev Marpe和Thomas Wiegand撰写,发表在《IEEE Transactions on Circuits and Systems for Video Technology》2007年9月第17卷第9期上。 论文解读 摘要: H.264/AVC视频编…...

【C语言】程序员自我修养之文件操作

【C语言】程序员自我修养之文件操作 &#x1f525;个人主页&#xff1a;大白的编程日记 &#x1f525;专栏&#xff1a;C语言学习之路 文章目录 【C语言】程序员自我修养之文件操作前言一.文件介绍1.1为什么使用文件1.2文件分类1.3二进制文件和文本文件 二.文件的打开和关闭2.…...

一种获取java代码结构的实现思路

一种获取java代码结构的实现思路 有时,我们需要获取java文件里的代码结构,即,只需要里面的class定义、方法声明、属性定义。不需要额外的方法实现 这里提供一下实现思路: 采用语法解析器Tree-sitter对java代码进行解析,获取里面的方法实现遍历第一步获取到的方法列表,在源…...

MySQL---增删改查

MySQL是一个流行的关系型数据库管理系统,它使用结构化查询语言(SQL)来管理数据库中的数据。以下是MySQL中增删改查(CRUD)操作的基本命令: 创建(Create): 创建新表:CREATE TABLE table_name (column1 datatype,column2 datatype,...PRIMARY KEY (column) );插入数据:…...