繪制三角形
前言
三角形是一個最簡單、最穩定的面,webgl 中的三維模型都是由三角面組成的。咱們這一篇就說一下三角形的繪制方法。
課堂目標
- 理解多點繪圖原理。
- 可以繪制三角形,并將其組合成多邊形。
知識點
- 緩沖區對象
- 點、線、面圖形
第一章 webgl 的繪圖方式
我們先看一下webgl是怎么畫圖的。
-
繪制多點
-
如果是線,就連點成線
-
如果是面,那就在圖形內部,逐片元填色
webgl 的繪圖方式就這么簡單,接下咱們就說一下這個繪圖方式在程序中是如何實現的。
第二章 繪制多點
在webgl 里所有的圖形都是由頂點連接而成的,咱們就先畫三個可以構成三角形的點。
這里大家還要注意一下,我現在要畫的多點是可以被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.0,1.0,0.0,1.0);} </script>
-
獲取webgl 上下文
const canvas = document.getElementById('canvas'); canvas.width=window.innerWidth; canvas.height=window.innerHeight; const gl = canvas.getContext('webgl');
-
初始化著色器
const vsSource = document.getElementById('vertexShader').innerText; const fsSource = document.getElementById('fragmentShader').innerText; initShaders(gl, vsSource, fsSource);
-
設置頂點點位
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);
-
清理畫布
gl.clearColor(0.0, 0.0, 0.0, 1.0); gl.clear(gl.COLOR_BUFFER_BIT);
-
繪圖
gl.drawArrays(gl.POINTS, 0, 3);
實際效果:
上面的步驟,主要是先給大家一睹為快,其具體原理,咱們后面細說。
2-繪制多點詳解
首先咱們先從概念上疏通一下。
我們在用js定點位的時候,肯定是要建立一份頂點數據的,這份頂點數據是給誰的呢?肯定是給著色器的,因為著色器需要這份頂點數據繪圖。
然而,我們在js中建立頂點數據,著色器肯定是拿不到的,這是語言不通導致的。
為了解決這個問題,webgl 系統就建立了一個能翻譯雙方語言的緩沖區。js 可以用特定的方法把數據存在這個緩沖區中,著色器可以從緩沖區中拿到相應的數據。
接下來咱們就看一下這個緩沖區是如何建的,著色器又是如何從其中拿數據的。
- 建立頂點數據,兩個浮點數構成一個頂點,分別代表x、y 值。
const vertices=new Float32Array([//x y0.0, 0.1, //頂點-0.1,-0.1, //頂點0.1, -0.1 //頂點
])
現在上面的這些頂點數據是存儲在js 緩存里的,著色器拿不到,所以咱們需要建立一個著色器和js 都能進入的公共區。
- 建立緩沖對象。
const vertexBuffer=gl.createBuffer();
現在上面的這個緩沖區是獨立存在的,它只是一個空著的倉庫,和誰都沒有關系。接下來咱們就讓其和著色器建立連接。
- 綁定緩沖對象。
gl.bindBuffer(gl.ARRAY_BUFFER,vertexBuffer);
gl.bindBuffer(target,buffer) 綁定緩沖區
- target 要把緩沖區放在webgl 系統中的什么位置
- buffer 緩沖區
著色器對象在執行initShaders() 初始化方法的時候,已經被寫入webgl 上下文對象gl 中了。
當緩沖區和著色器建立了綁定關系,我們就可以往這塊空間寫入數據了
- 往緩沖區對象中寫入數據
gl.bufferData(gl.ARRAY_BUFFER,vertices,gl.STATIC_DRAW);
bufferData(target, data, usage) 將數據寫入緩沖區
- target 要把緩沖區放在webgl 系統中的什么位置
- data 數據
- usage 向緩沖區寫入數據的方式,咱們在這里先知道 gl.STATIC_DRAW 方式即可,它是向緩沖區中一次性寫入數據,著色器會繪制多次。
現在著色器雖然綁定了緩沖區,可以訪問里面的數據了,但是我們還得讓著色器知道這個倉庫是給哪個變量的,比如咱們這里用于控制點位的attribute 變量。這樣做是為了提高繪圖效率。
- 將緩沖區對象分配給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,那就是從頭開始存儲變量
到了這里,著色就知道緩沖區的數據是給誰的了。因為咱們緩沖區里的頂點數據是數組,里面有多個頂點。所以我們得開啟一個讓著色器批量處理頂點數據的屬性。默認著色器只會一個一個的接收頂點數據,然后一個一個的繪制頂點。
- 開啟頂點數據的批處理功能。
gl.enableVertexAttribArray(a_Position);
- location attribute變量
好啦,現在已經是萬事俱備,只欠繪圖了。
- 繪圖
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-繪制三角面
我們在之前繪制多點的基礎上做一下修改。
- 頂點著色器中的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>
- 在js 中修改繪圖方式
// gl.drawArrays(gl.POINTS, 0, 3);
gl.drawArrays(gl.TRIANGLES, 0, 3);
上面的gl.TRIANGLES 就是繪制三角面的意思。
看一下效果:
webgl 既然可以畫面了,那它是否可以畫線呢,這個是必須可以,我們可以在gl.drawArrays() 方法的第一個參數里進行設置。
2-基本圖形
gl.drawArrays(mode,first,count) 方法可以繪制一下圖形:
- POINTS 可視的點
- LINES 單獨線段
- LINE_STRIP 線條
- LINE_LOOP 閉合線條
- TRIANGLES 單獨三角形
- TRIANGLE_STRIP 三角帶
- TRIANGLE_FAN 三角扇
上面的POINTS 比較好理解,就是一個個可視的點。
線和面的繪制方式各有三種,咱們接下來就詳細說一下。
2-1-點的繪制
POINTS 可視的點
上面六個點的繪制順序是:v0, v1, v2, v3, v4, v5
2-2-線的繪制
- LINES 單獨線段
上面三條有向線段的繪制順序是:
v0>v1
v2>v3
v4>v5
- LINE_STRIP 線條
上面線條的繪制順序是:v0>v1>v2>v3>v4>v5
- LINE_LOOP 閉合線條
上面線條的繪制順序是:v0>v1>v2>v3>v4>v5>v0
2-3-面的繪制
對于面的繪制,我們首先要知道一個原理:
- 面有正反兩面。
- 面向我們的面,如果是正面,那它必然是逆時針繪制的;
- 面向我們的面,如果是反面,那它必然是順時針繪制的;
接下來,咱們看一下面的三種繪制方式:
- TRIANGLES 單獨三角形
上面兩個面的繪制順序是:
v0>v1>v2
v3>v4>v5
- TRIANGLE_STRIP 三角帶
上面四個面的繪制順序是:
v0>v1>v2
以上一個三角形的第二條邊+下一個點為基礎,以和第二條邊相反的方向繪制三角形
v2>v1>v3
以上一個三角形的第三條邊+下一個點為基礎,以和第二條邊相反的方向繪制三角形
v2>v3>v4
以上一個三角形的第二條邊+下一個點為基礎,以和第二條邊相反的方向繪制三角形
v4>v3>v5
規律:
第一個三角形:v0>v1>v2
第偶數個三角形:以上一個三角形的第二條邊+下一個點為基礎,以和第二條邊相反的方向繪制三角形
第奇數個三角形:以上一個三角形的第三條邊+下一個點為基礎,以和第二條邊相反的方向繪制三角形
- TRIANGLE_FAN 三角扇
上面四個面的繪制順序是:
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
4-2-代碼實現
- 建立頂點數據
const vertices=new Float32Array([-0.2, 0.2,-0.2,-0.2,0.2, 0.2,0.2,-0.2,
])
上面兩個浮點代表一個頂點,依次是v0、v1、v2、v3,如上圖所示。
- 繪圖
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
上面參數的意思分別是:三角帶、從第0個頂點開始畫、畫四個。
效果如下:
關于矩形的繪制就這么簡單,接下來咱們可以去嘗試其它的圖形。
比如:把TRIANGLE_STRIP 三角帶變成TRIANGLE_FAN 扇形
gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
畫出了一個三角帶的樣子:
其繪圖順序是:
v0>v1>v2
v0>v2>v3
關于基本圖形的繪制,咱們就說到這。
第四章 異步繪制多點
在項目實戰的時候,用戶交互事件是必不可少的。因為事件是異步的,所以我們在繪圖的時候,必須要考慮異步繪圖。
接下來我通過一個例子來說一下異步繪制多點的方法。
1-異步繪制線段
1.先畫一個點
2.一秒鐘后,在左下角畫一個點
3.兩秒鐘后,我再畫一條線段
接下來看一下代碼實現:
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()
});
當前我們所能畫的是一條線條,如果我們想要繪制多條線呢?就比如我們要畫一個獅子座。
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()
}
最后,用一個完整的例子結束這一篇的知識。
案例-獅子座
接下來,我要在下圖中繪制獅子座。
效果如下
1-產品需求
1-1-基本繪圖需求
- 鼠標第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 單獨三角形的的模型:
TRIANGLE_STRIP 和TRIANGLE_FAN 的優點是相鄰的三角形可以共用一條邊,比較省點,然而其缺點也太明顯,因為它們只適合繪制具備相應特點的模型。
適合TRIANGLE_STRIP三角帶的模型:
適合TRIANGLE_FAN三角扇的模型:
three.js 使用的繪制面的方式就是TRIANGLES,我們可以在其WebGLRenderer 對象的源碼的renderBufferImmediate 方法中找到:
_gl.drawArrays( _gl.TRIANGLES, 0, object.count );
2-圖形轉面的基本步驟
在three.js 里有一個圖形幾何體ShapeGeometry,可以把圖形變成面。
我們學到這里,只要有數學支撐,也可以實現這種效果。
接下來我要使用TRIANGLES 獨立三角形的方式,將圖形轉成面。
我使用的方法叫做“砍角”,其原理就是從起點將多邊形中符合特定條件的角逐個砍掉,然后保存到一個集合里,直到把多邊形砍得只剩下一個三角形為止。這時候集合里的所有三角形就是我們想要的獨立三角形。
舉個例子:
已知:逆時針繪圖的路徑G
求:將其變成下方網格的方法
解:
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();
先畫了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();
效果沒有問題。
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();
效果如下:
填坑
我之前在用鼠標繪制線條的時候,還留了一個系統兼容性的坑。
線條在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}=thisfor (let type of types) {circleDot&&gl.uniform1f(u_IsPOINTS,type==='POINTS');gl.drawArrays(gl[type],0,count);}
}
總結
在實際項目中,我們繪制完了圖形,往往還會對其進行修改變換,下一篇咱們會說一下如何變換圖形。