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.pswp.cn/web/13567.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/13567.shtml
英文地址,請注明出處:http://en.pswp.cn/web/13567.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Python高克勒-曼寧-斯特里克勒公式計算一維流量

&#x1f4dc;曼寧公式-用例 &#x1f4dc;Python流體數據統計模型和淺水滲流平流模型模擬 | &#x1f4dc;Python蒸發散物理問題(微積分-線性代數-拉普拉斯和傅立葉變換) ??Python計算一維流量 高克勒-曼寧-斯特里克勒公式公式基于一維&#xff08;橫截面平均&#xff09…

iPhone實況照片從Windows資源管理器復制的JPG+MOV無法正常還原到iPhone

背景&#xff1a; 之前使用的iPhone 15 Pro&#xff0c;使用的Windows資源管理器當中復制導出的實況照片&#xff0c;復制出來的格式例如IMG_0001.JPG, IMG_0001.MOV。之后手機就賣掉了。現在使用的iPhone 14 Pro Max&#xff0c;想要導回之前備份的實況照片。嘗試使用愛思助手…

Java18新特性

Java 18引入了若干新特性&#xff0c;以增強語言的功能性和性能。具體如下&#xff1a; 服務提供者接口&#xff08;Service Provider Interfaces, SPI&#xff09;&#xff1a;允許開發者為Java模塊系統定義服務加載機制&#xff0c;從而能夠更靈活地發現和加載服務實現。簡單…

supOS NEO科技普惠!永久免費!億元補貼

數字化轉型正在全球蓬勃發展&#xff0c;工業操作系統進入大規模推廣期&#xff01; 如果您正在被預算不足、技術團隊不強、數字化投入產出比等問題困擾&#xff0c;supOS NEO是您最好的選擇。 “讓supOS走進萬千工廠、千行百業&#xff01;讓全世界每個工廠都能用得上supOS&am…

MM模塊學習三 (創建采購申請)

采購信息記錄比較特殊既是主數據又是貨源 注&#xff1a;發票校驗是指把供應商提供的發票做到系統里面產生一張應付憑證。 1.決定采購需求 采購需求可以手工創建&#xff08;ME51N&#xff09;&#xff0c;也可以自動產生&#xff08;比如&#xff1a;MRP&#xff0c;以及比如…

Java——內部類

1.什么是內部類 在一個類的里面再定義一個類&#xff0c;新定義的這個類就是內部類 舉例&#xff1a;在Outer類的里面定義一個Inter類 class Outer{class Inter{} } 在這里Outer叫外部類&#xff0c;Inter叫內部類 內部類的應用場景 定義一個汽車類&#xff1a; 屬性&#xf…

CTFshow之文件上傳web入門151關-161關解密。包教包會!!!!

這段時間一直在搞文件上傳相關的知識&#xff0c;正好把ctf的題目做做寫寫給自字做個總結&#xff01; 不過有一個確定就是所有的測試全部是黑盒測試&#xff0c;無法從代碼層面和大家解釋&#xff0c;我找個時間把upload-labs靶場做一做給大家講講白盒的代碼審計 一、實驗準…

2024-5-23 石群電路-14

2024-5-23&#xff0c;星期四&#xff0c;22:20&#xff0c;天氣&#xff1a;晴&#xff0c;心情&#xff1a;晴。今天沒有什么重要的事情發生&#xff0c;心情一如既往的平靜&#xff0c;距離返校假期還有兩天~~~。 今天觀看了石群老師電路基礎課程的第23/24個視頻&#xff0…

Flutter 中的 AnimatedThere 小部件:全面指南

Flutter 中的 AnimatedThere 小部件&#xff1a;全面指南 在Flutter中&#xff0c;動畫是增強用戶體驗的強大工具。雖然Flutter沒有一個名為AnimatedThere的官方小部件&#xff0c;但我們可以根據常見的動畫模式來構建一個類似的自定義動畫效果。本文將指導您如何使用Flutter的…

Python 安裝 ONNX Runtime (ORT)

1. Python 安裝 ONNX Runtime 安裝cpu版的 onnx runtime&#xff1a; pip install onnxruntime 安裝gpu版的 onnx runtime&#xff08;cuda 11.x&#xff09;&#xff1a;(默認安裝適用于cuda 11.8 的onnx runtime) pip install onnxruntime-gpu 安裝gpu版的 onnx runtime…

真實案例分享,終端pc直接telnet不到出口路由器。

1、背景信息 我終端pc的網卡地址獲取的網關是在核心交換機上&#xff0c;在核心交換機上telnet出口路由器可以實現。 所有終端網段都不能telnet出口路由器&#xff0c;客戶希望能用最小的影響方式進行解決。 2、現有配置信息 終端的無線和有線分別在兩個網段中&#xff0c;…

【課后練習分享】Java用戶注冊界面設計和求三角形面積的圖形界面程序

目錄 java編程題&#xff08;每日一練&#xff09;&#xff1a; 問題一的答案代碼如下&#xff1a; 問題一的運行截圖如下&#xff1a; 問題二的答案代碼如下&#xff1a; 問題二的運行截圖如下&#xff1a; java編程題&#xff08;每日一練&#xff09;&#xff1a; 1.…

大數據量MySQL的分頁查詢優化

目錄 造數據查看耗時優化方案總結 造數據 我用MySQL存儲過程生成了100多萬條數據&#xff0c;存儲過程如下。 DELIMITER $$ USE test$$ DROP PROCEDURE IF EXISTS proc_user$$CREATE PROCEDURE proc_user() BEGINDECLARE i INT DEFAULT 1;WHILE i < 1000000 DOINSERT INT…

【安裝筆記(目錄)】

安裝筆記-系列文章目錄 第一章 Windows 工具 [1001] Optimizer &#xff1a;一款高級配置實用工具&#xff0c;旨在增強你在Windows上的隱私和安全性。 [1002] WSL2&#xff1a;適用于 Linux 的 Windows 子系統 (WSL) 可讓開發人員直接在 Windows 上按原樣運行 GNU/Linux 環境…

力扣226.翻轉二叉樹101.對稱二叉樹

解決二叉樹的問題&#xff0c;經常要習慣從遞歸角度思考 左子樹/右子樹是否具備某屬性、是否屬于什么類型&#xff08;和題目要求的判斷當前樹是否xxx一樣&#xff09;&#xff1b; 對左/右子樹進行什么操作&#xff08;和題目要求的對當前樹的操作一樣&#xff09;。 226.翻轉…

提權方式及原理匯總

一、Linux提權 1、SUID提權 SUID&#xff08;設置用戶ID&#xff09;是賦予文件的一種權限&#xff0c;它會出現在文件擁有者權限的執行位上&#xff0c;具有這種權限的文件會在其執行時&#xff0c;使調用者暫時獲得該文件擁有者的權限。 為可執行文件添加suid權限的目的是簡…

解決SpringBoot使用@Transactional進行RestTemplate遠程調用導致查詢數據記錄為null的bug

開啟事務過程中&#xff0c;如果遠程調用查詢當前已經開啟但沒有提交的事務&#xff0c;就會查不到數據。 示例代碼 import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.transaction.annotation.Transactional; import o…

二叉樹求解大小操作詳解

目錄 一、求所有結點個數 1.1 遞歸思路 1.2 遞歸分支圖 1.3 遞歸棧幀圖 1.4 C語言實現 二、求葉子結點個數 2.1 遞歸思路 2.2 遞歸分支圖 2.3 遞歸棧幀圖 2.4 C語言實現 三、求第K層的結點個數 3.1 遞歸思路 3.2 遞歸分支圖 3.3 遞歸棧幀圖 3.4 C語言實現 四、求…

【python】使用函數名而不加括號是什么情況?

使用函數名而不加括號通常是為了表示對函數本身的引用&#xff0c;而不是調用函數。這種用法通常出現在下面這幾種情況&#xff1a; 作為回調函數傳遞&#xff1a;將函數名作為參數傳遞給其他函數&#xff0c;以便在需要時調用該函數。例如&#xff0c;在事件處理程序或高階函數…

馮喜運:5.24現貨黃金趨勢解讀,黃金原油行情分析及操作建議

【黃金消息面分析】&#xff1a;美國勞工部公布的最新數據顯示&#xff0c;截至5月18日的一周內&#xff0c;首次申請失業救濟人數下降至21.5萬人&#xff0c;創下自去年9月以來的最大降幅。數據公布后&#xff0c;現貨黃金短線下挫6美元&#xff0c;報2362.71美元/盎司。這表明…