step1. 首先固然是安裝nodejs,咱們能夠選擇從nodejs.org下載對應的操做系統和CPU指令集的安裝包,也能夠用homebrew、apt等工具安裝,多數前端工程師都已經有nodejs環境,此處不詳細展開了。html
step2. (可選)全局安裝vite,爲了比較方便地使用vite,建議全局安裝vite。若是不全局安裝vite,咱們必需利用npx執行本項目的vite。使用npm install -g vite
命令便可。前端
step3. 初始化項目,在一個喜歡的路徑建立一個新的目錄,好比這裏我建立了一個element3-demovue
mkdir element3-demo
cd element3-demo
複製代碼
進入目錄後,執行npm init
,並填寫必要信息。以後,咱們獲得了一個基礎的package.json文件。node
step4. 接下來,咱們爲項目添加依賴,並安裝相關包web
首先咱們用本身喜歡的文本編輯工具打開package.json,而且爲它添加dependencies和devDependencies:算法
{
"dependencies": {
"element3-core": "0.0.7",
"vue": "^3.0.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^1.2.2",
"@vue/compiler-sfc": "^3.0.5",
"rollup-plugin-element3-webgl": "0.0.5",
"typescript": "^4.1.3",
"vite": "^2.3.0",
"vue-tsc": "^0.0.24"
}
}
複製代碼
以後咱們回到終端,使用npm install命令。typescript
step5. 建立文件和基本目錄結構。shell
編寫index.html
文件:npm
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
複製代碼
編寫src/main.ts
文件:json
import { createApp } from "vue";
import App from "./App.vue";
createApp(App).mount("#app");
複製代碼
編寫src/app.vue
文件:
<template>
<div>
Hello
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "App",
components: {
},
setup(){
return {
}
}
});
</script>
複製代碼
編寫vite.config.js文件:
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import element3Webgl from "rollup-plugin-element3-webgl";
// https://vitejs.dev/config/
export default defineConfig({
base: "/", // TODO 開發環境是 / 生產環境是 /webgl
plugins: [vue(), element3Webgl()],
});
複製代碼
編寫完成後,咱們在命令行使用npx vite
,打開網頁看到Hello代表環境已經配置好。
接下來咱們建立一個 src/pure.frag
文件。
Fragment Shader 使用的語言並不是 JavaScript,而是一種叫作 GLSL 的專用語言,在後面的教程中,我會逐漸爲你們介紹這門語言特性,這裏咱們先嚐試寫出第一個可運行的Fragment Shader。
咱們首先要理解 Fragment Shader 的概念,一段 Fragment Shader 是繪製屏幕上一個點的過程。它的執行頻率很是高,繪製一個100x100區域的圖像,須要執行10000次 Shader 中的代碼,Shader一般是由GPU承擔的。
接下來咱們編寫一段代碼,把畫布區域塗上純色:
precision mediump float;
void main(){
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
複製代碼
element3的rollup插件可以直接把這個Shader代碼加載成一個vue組件,這可以幫助咱們忽略掉調用 WebGL API的冗繁過程。
接下來咱們更改App.vue的代碼,展現這個繪製的效果:
<template>
<div>
<DrawBlock width=100 height=100></DrawBlock>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import DrawBlock from "./pure.frag";
export default defineComponent({
name: "App",
components: {
DrawBlock
},
setup(){
return {
}
}
});
</script>
複製代碼
咱們能夠看到一個純紅色的方塊區域。
接下來咱們來稍微解釋一下這段 GLSL 代碼。
咱們首先來看第一句:precision mediump float;
。這一句是必要的,他規定了程序的全局浮點數精度,此處使用了中等精度,幾乎每個Fragment Shader代碼都會包含這一句,咱們能夠暫且認爲它是固定的。
一切GLSL代碼都是從main
函數開始執行的。在GLSL中,main
函數能夠不返回值,這種函數咱們用void來代替類型的部分。
接下來咱們來看main
函數的函數體,函數體中只有一個語句。這裏咱們使用了一個 gl_fragColor
變量,這個名字是GLSL語言規定的名字,並非能夠隨意命名的變量,咱們前面講過,Fragment Shader 是繪製一個點的代碼,這個 gl_fragColor
就是咱們最後要輸出的點的顏色。
接下來咱們看等號的另外一端,這裏的vec4
表示一個長度爲4的浮點數向量類型,它裏面能夠存儲4個浮點數。你們還記得線性代數裏學習的向量吧?這裏的vec4
就是來自數學中的向量概念。使用起來它有點像JavaScript中的數組,不一樣的是,它是固定長度的,這樣的數據結構對圖形算法很是的有用,咱們將會在將來與它打不少交道。
最後提醒一下,GLSL語言不容許省略分號,忘記的話會致使整個程序沒法編譯,必定要注意哦。
進行到這一步,咱們已經學會了如何使用element3的rollup插件來加載一段Fragment Shader,得到了一個基本的代碼調試和運行的環境。
接下來,咱們學習一下如何控制Shader繪製一些想要的東西。
首先,咱們嘗試縮小一下繪製的範圍,要想控制範圍,咱們必需要知道當前所繪製的點的座標,這時候,咱們就要介紹GLSL中另外一個重要的變量了: gl_fragCoord
。
若是說gl_fragColor
是Fragment Shader的輸出的話,gl_fragCoord
就是Fragment Shader的輸入了,它表示的是當前繪製點的座標,它是一個vec4
類型,但這裏咱們只須要用到它的前兩項。
咱們能夠分別使用 gl_fragCoord.x
和 gl_fragCoord.y
來訪問它的座標,也可使用 gl_fragCoord.xy
來把它變爲2維向量。
那麼,回到咱們的問題,如何繪製一個長方形呢?咱們只須要判斷一下它的座標範圍就能夠了,請看示例代碼:
precision mediump float;
void main(){
if(gl_FragCoord.x > 25.0 && gl_FragCoord.x < 75.0 &&
gl_FragCoord.y > 25.0 && gl_FragCoord.y < 75.0)
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
else
gl_FragColor = vec4(0.0, 0.0, 1.0, 1.0);
}
複製代碼
這裏咱們要注意,不少同窗從JS帶來的習慣是整數類型和浮點數類型不區分,而在GLSL中,整數和浮點數是徹底兩種類型,不進行強制轉換的話,無法混合運算。當咱們寫直接量時,也要很是明確地帶上小數點,表示這是一個浮點數。
咱們也能夠用相似float(25)
這樣的代碼來強制轉換整數到浮點數類型,可是這裏不管從可讀性的角度,仍是執行效率的角度,我都不推薦這種寫法。
畫完了方形,咱們來嘗試一下更復雜的圓形,根據初中解析幾何知識,咱們能夠知道圓形就是到圓心距離小於半徑的點的集合,因而咱們能夠根據公式x²+y²<r²
來繪製圓形。
咱們當然能夠用乘法來實現平方,不過,根據DRY原則,咱們最好仍是使用系統內置函數來實現平方,在GLSL中,多數數學函數均可以直接使用,不用像JS同樣加Math.
。
最後實現代碼以下:
precision mediump float;
void main(){
if(pow(gl_FragCoord.x - 50.0, 2.0) + pow(gl_FragCoord.y - 50.0, 2.0) < pow(25.0, 2.0))
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
else
gl_FragColor = vec4(0.0, 0.0, 1.0, 1.0);
}
複製代碼
這樣咱們的圓形就畫好了。可是,若是咱們把這個圓形放大一些來看,你會發現,它有嚴重的鋸齒感,接下來咱們將會介紹一個GLSL中重要的函數,用於解決此問題。
咱們試着分析一下圓形看起來鋸齒感明顯的緣由,咱們在Shader代碼中,採起了一種非黑即白的策略,而受限於顯示設備,咱們無法讓像素小到肉眼沒法分辨,所以產生了鋸齒感。
那麼,計算機中通常的圖形顯示方案是怎麼處理的呢?方法很簡單,就是咱們在這個圓形的邊緣,產生一個細微的漸變,這樣,顏色過渡就沒那麼生硬了。
咱們首先整理下Shader的代碼,把點到圓心的距離單獨設爲一個變量。這裏咱們使用了一個新的函數,開平方函數sqrt
:
float l = sqrt(pow(gl_FragCoord.x - 50.0, 2.0) + pow(gl_FragCoord.y - 50.0, 2.0));
複製代碼
接下來,咱們嘗試根據變量l來混合兩種顏色,這裏咱們介紹一個新的函數mix
,它可以根據比例混合兩種顏色(其實還有別的用途,暫且不表)。mix
有三個參數,前兩個是待混合的值,最後一個參數是混合的比例。
咱們嘗試根據點到圓心的距離來l來混合兩種顏色,最終代碼以下:
precision mediump float;
void main(){
float l = sqrt(pow(gl_FragCoord.x - 50.0, 2.0) + pow(gl_FragCoord.y - 50.0, 2.0));
gl_FragColor = mix(vec4(1.0, 0.0, 0.0, 1.0), vec4(0.0, 0.0, 1.0, 1.0), l / 25.0);
}
複製代碼
執行後,咱們能夠看到明顯的漸變,可是這並非咱們最終想要的效果,咱們並不但願整個圓變成漸變的,咱們只但願圓形靠近邊緣有幾個像素寬的漸變,雖然咱們能夠用四則運算和if組合出這個效果,可是GLSL中提供了更優雅的解決方案,那就是smoothstep
函數。
smoothstep接受三個參數min, max和x,它的功能是,當x小於min時,返回0.0,當x大於max時,返回1.0,而x介於min和max之間時,返回一個0.0到1.0之間的值,表示x在這個區間內與min距離的佔比。
接下來,咱們來修改GLSL代碼,利用smoothstep來繪製一個柔邊的圓形。爲了效果明顯,這裏故意設置的smoothstep範圍較大,實際使用中,只作1-2像素模糊是比較合適的。
precision mediump float;
void main(){
float l = sqrt(pow(gl_FragCoord.x - 50.0, 2.0) + pow(gl_FragCoord.y - 50.0, 2.0));
gl_FragColor = mix(vec4(1.0, 0.0, 0.0, 1.0), vec4(0.0, 0.0, 1.0, 1.0), smoothstep(20.5, 25.5, l));
}
複製代碼
到這裏,相信你已經瞭解了smoothstep的基本知識。接下來,咱們就來活學活用一番。咱們的下一個任務是繪製直線。
說是繪製直線,其實直線仍是有寬度的,這就要求咱們可以計算出點到直線的距離。這裏咱們直接使用向量幾何中的結論:
定理:給定直線 l , 其方向向量爲 m . A 爲 l 外一點, 若要求 A 到直線 l 的距離 d , 可任取 l 上一點 B, 點 A 到點 B 的向量記做 n , 則
根據此公式,這裏咱們須要用到向量點乘運算dot
,和向量長度函數length
,最後寫出的GLSL代碼以下:
precision mediump float;
void main(){
vec2 m = vec2(1., -1.);
vec2 n = vec2(25., 0.) - gl_FragCoord.xy;
float d = length(dot(m, n)) / length(m);
gl_FragColor = mix(vec4(1.0, 0.0, 0.0, 1.0), vec4(0.0, 0.0, 1.0, 1.0), smoothstep(0.0, 1.0, d));
}
複製代碼
從這裏咱們能夠看出向量運算的強大,結合解析幾何和線性代數知識,咱們能夠用簡潔的代碼來處理各類圖形圖像問題。
看完了以上內容,你是否躍躍欲試了呢?這裏留一個小練習給你們:
用Fragment Shader繪製一個Vue的Logo。
歡迎貼出Shader代碼你們一塊兒討論。