從Element3入門WebGL Shader(一)

總目錄

  • 入門
    • 環境配置
    • 第一個可運行的Shader
    • 繪製圖形——長方形和圓形
    • 認識SmoothStep
  • 初探GLSL
    • 向量與矩陣
    • 浮點數精度
    • uniform
  • 顏色與形狀
    • 顏色基本知識
    • 生成漸變色
    • 圓角矩形渲染
    • 多變形的渲染
  • 數學與圖形
    • 極座標系
    • 向量幾何
    • 三角函數
    • 分型
  • 生成藝術
    • 噪聲
    • 噪聲場
    • 疊加
    • 模糊

環境配置

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代表環境已經配置好。

第一個可運行的 Shader

接下來咱們建立一個 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.xgl_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中重要的函數,用於解決此問題。

認識smoothstep

咱們試着分析一下圓形看起來鋸齒感明顯的緣由,咱們在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 , 則 d = m n n d = \frac{|m·n|}{|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代碼你們一塊兒討論。

相關文章
相關標籤/搜索