一、前言
首先对于web系前端来说,我们在使用WebGL开发的首选语言肯定是javascript,在接触着色器语言后,我们知道可以用:
<script type="x-shader/x-vertex"></script>
<script type="x-shader/x-fragment"></script>
引入的方式,将glsl es引入到我们的JavaScript中来使用。
不过在实际开发中,这个引入方式
- 一来是不方便跟踪
- 二来我们在使用ide开发,代码提示方面没有任何优势怎么行?
好在我们在学习TypeScript之后,认识就有了一定的改变,我们使用TypeScript的视点来观察下面这一段代码。
使用类型审查
<script id="vShader" type="x-shader/x-vertex"></script>
<script id="fShader" type="x-shader/x-fragment"></script>
const vertexShader = document.querySelector('#vShader').textContent
const fragmentShader = document.querySelector('#fShader').textContent
console.log(vertexShader, fragmentShader)
其实通过打印你也能知道,这里的textContent无非只是将shader的代码通过字符串方式读取出来了。
TypeScript在这里也会标记vertexShader、fragmentShader两个变量为string类型。
为了验证这一点,我们可以通过TypeScript的类型声明提示来一窥WebGL的gl.shaderSource方法:
<body>
<canvas id="canvas"></canvas>
</body>
<script id="vShader" type="x-shader/x-vertex"></script>
<script id="fShader" type="x-shader/x-fragment"></script>
const canvas = document.querySelect('#canvas')
const vsSource = document.querySelect('#vShader').textContent
const shaderType = gl.VERTEX_SHADER
const shader = gl.createShader(shaderType) as WebGLShader
gl.shaderSource(shader, vsSource)
gl.compileShader(shader)
通过类型提示我们可以知道:
- shaderType 是一个number类型的索引,用于去查找内存上的地址;
- shader是着色器对象;
- 而source只是一个字符串类型;
那么既然是字符串类型,我们何不直接写在TypeScript文件中呢?
于是我们大胆可以这样写:
const canvas = document.querySelect('canvas')
const vsSource = `
attribute vec4 a_Position;
void main(){
gl_Position = a_Position;
}
`
const shaderType = gl.VERTEX_SHADER
const shader = gl.createShader(shaderType) as WebGLShader
gl.shaderSource(shader, vsSource)
gl.compileShader(shader)
照样可以运行。
那么如果可以想把shader相关的字符串抽取出来,是不是也可以呢?
二、现代前端开发 - ide:vsCode - 工程化 - rollup
开发环境 - es6 - ide:vsCode
已经2021年了,我们自然要使用现代浏览器的自带支持的es module来写前端代码。
通过上文我们已经知道了shader在js引擎中通过传递string类型的变量来加载编译,但是我们不再需要去使用script标签来引入。
这里我们得要科普一下着色器的文件类型后缀:.glsl
它也可以是: .vs/.fs/.vert/.fragment 等任意一种后缀
那么为什么会有这么多种后缀呢?
实际上官方并没有一个很明确的后缀规范,以上只是公认使用较多的后缀罢了。
但是所有引擎都是用字符串来理解shader语言的这一点没错。
下面我们使用.vert来举例:
// shader.vert
attribute vec4 a_Position;
void main(){
gl_Position = a_Position;
}
// index.ts
import vsSource from './shader.vert'
const canvas = document.querySelect('canvas')
const shaderType = gl.VERTEX_SHADER
const shader = gl.createShader(shaderType) as WebGLShader
gl.shaderSource(shader, vsSource)
gl.compileShader(shader)
这个时候TypeScript语法会提示报错:
找不到模块“./shaders/cubic/cubic.vert”或其相应的类型声明
意思是告诉你,不行,这里的.vert文件我不认识,你得先给他一个类型声明让我明白他这个模块是干什么的。
我们在项目的根目录下创建一个index.d.ts的类型声明文件:
declare module '*.vert'
declare module '*.frag'
declare module '*.vs'
declare module '*.fs'
declare module '*.glsl'
将上面这些代码复制进去,意思是告诉ts解释器,这里的这些文件你知道就好(默认当作字符串处理)。
保存之后开发环境就不会报错了。
那么为什么我们要大费周章的用这些后缀名的文件呢?
- 首先一点就是关注点分离,一个逻辑中的着色器对象可能会有很多。我们想在ts中直接用他们没错,但是又不想要ts中充斥着这些非js引擎可以理解的臃肿的代码;
- 其次,glsl es语言在我们的ts文件中完全没有任何的代码提示,我们需要健壮的代码提示功能
那么如何可以免费获得代码提示呢?还记得这一趴的小标题吗:ide:vsCode;
我们在vsCode的扩展应用商店中搜索:ext:vert 或 raczzalan.webgl-glsl-editor。会搜索到一款叫“WebGl GLSL Editor”的插件,安装它。
之后我们在html、vert、glsl、fragment后缀的文件中编辑都会有健壮的glsl es的语法提示了。
至此,我们在开发环境就有了强有力的glsl代码支持,同时也秉承了高聚合低耦合的代码风格,分离了我们开发时的关注点。
生产环境 - 工程化 - rollup
以上讲述了TypeScript如何在开发环境使用着色器,但是我们的代码光在本地跑起来可不行。
接下来我介绍一下我在生产环境的方案。
我选择rollup作为我代码的打包工具,它轻量,tree shaking,丰富的社区插件,正好能够满足我的需求。
安装依赖
由于我们的项目使用TypeScript开发,所以,必要的几件不能缺少:
npm i typescript rollup rollup-plugin-node-resolve rollup-plugin-commonjs rollup-plugin-typescript2 --save-dev
安装glsl文件解析器(有能力的也可以自己写这个解析器哈)
npm i rollup-plugin-glslify --save-dev
rollup.config.ts 配置项
import path from "path"; import resolve from "rollup-plugin-node-resolve"; // 依赖引用插件 import commonjs from "rollup-plugin-commonjs"; // commonjs模块转换插件 import glslify from 'rollup-plugin-glslify'; import ts from "rollup-plugin-typescript2"; const getPath = (_path) => path.resolve(__dirname, _path); import packageJSON from "./package.json"; const extensions = [".js", ".ts", ".tsx"]; // 导入本地ts配置 const tsPlugin = ts({ tsconfig: getPath("./tsconfig.json"), tsconfigOverride: { extensions }, }); // 基础配置 const commonConf = { // 入口文件 input: getPath("./index.ts"), plugins: [ resolve({ extensions, }), glslify(), commonjs(), tsPlugin, ], }; // 需要导出的模块类型 const outputMap = [ { file: path.resolve(__dirname, packageJSON.main), // 通用模块 format: "umd", }, { file: path.resolve(__dirname, packageJSON.module), // es6模块 format: "es", }, ]; const buildConf = (options) => Object.assign({}, commonConf, options); export default outputMap.map((output) => { const conf = buildConf({ output: { ...output, name: packageJSON.name, } }) return conf });
package.json 补充配置
{ ... "name": "webgl", "main": "dist/index.umd.js", "module": "dist/index.js", "typings": "dist/types.index.d.ts", "scripts": { "build": "rollup -c rollup.config.ts" } }
项目打包
在根目录
npm run build
构建完成后我们就可以在dist文件中看到我们编译完成的js文件,
我们打开看一下,我们之前import的vertex变量
var vsSource = "#define GLSLIFY 1\nattribute vec4 a_Position;void main(){gl_Position=a_Position;}"; // eslint-disable-line
vsSource被编译成了一个字符串,这样就证明我们使用的TypeScript和外部的glsl文件已经全部成功的编译完成了,
引入到html中,直接运行,至此,生产环境部分也搞定了。
下一篇可能会讲TypeScript在canvas上下文中类型提示的便携性。