Appearance
WebGL 实战(一)—— 绘制三角形
传送门:
前言
之前我们讲过了 WebGL 的相关原理,我们了解到 WebGL 绘图的方式是“连接式”的,WebGL 就像是一个巨大的电路图,我们修改电路中的电路的连接方式或者是增加其中的电气元件,当我们按下开关时,这个电路就会自动运行。今天,我们就进入实战阶段,一切从 0 开始,手把手的带你使用原生的 WebGL 做一些有趣的事情。现在,我们需要从最基础的部分开始,除了 WebGL 的相关知识,我们也会穿插一些必要的图形学知识。从原生 WebGL 和图形学知识学起,就一个字:稳!
实战
WebGL 是一种 JavaScript API,用于在不使用插件的情况下在任何兼容的网页浏览器中呈现交互式 2D 和 3D 图形。WebGL 完全集成到浏览器的所有网页标准中,可将影像处理和效果的 GPU 加速使用方式当做网页 Canvas 的一部分。WebGL 元素可以加入其他 HTML 元素之中并与网页或网页背景的其他部分混合。WebGL 程序由 JavaScript 编写的句柄和 OpenGL Shading Language(GLSL)编写的着色器代码组成,该语言类似于 C 或 C++,并在电脑的图形处理器(GPU)上运行。
WebGL 基于 HTML5 Canvas,所以我们需要使用 Canvas 作为载体。在通过getContext
方法来获取 WebGL 上下文。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Drawing Triangle</title>
</head>
<body>
<canvas id="canvas" width="500" height="500"></canvas>
</body>
<script src="1.js"></script>
</html>
js
/**
* @type {HTMLCanvasElement}
*/
const canvas = document.getElementById('canvas');
const gl = canvas.getContext('webgl');
最简单的 WebGL 绘制程序
现在让我们开始编写一个最简单的 WebGL 程序:
js
/**
* @type {HTMLCanvasElement}
*/
const canvas = document.getElementById('canvas');
const gl = canvas.getContext('webgl');
// 设置清空颜色缓冲区时的颜色
gl.clearColor(1.0, 1.0, 0.0, 1.0);
// 清空颜色缓冲区
gl.clear(gl.COLOR_BUFFER_BIT);
效果如下,我们得到的就是一张黄色的图。gl.clearColor
中接受 RGBA 四个值,这里需要注意,RGBA 的值不是 0 ~ 255,而是 0 ~ 1,这一点需要注意下。
以上就是一个最简单的 WebGL 程序了。
渐入佳境
我们已经完成了一个最简单的 WebGL 程序,可能你还没有什么感觉,现在我们开始编写稍微复杂一点的程序,我们要开始使用着色器了。
初始化 Shader
着色器(shader)分为顶点着色器(vertex shader)和片元着色器(fragment shader),它们总是两两成对出现,顶点着色器和片元着色器组成了一个WebGLProgram
,我们使用哪一个 shader,就是使用哪一个 WebGLProgram
。使用 shader 的步骤如下:
- 创建 Shader
- 创建
WebGLShader
对象 - 往
WebGLShader
对象中传入 shader 源代码 - 编译 shader
- 创建
- 创建 Program
- 创建
WebGLProgram
对象 - 往
WebGLProgram
对象中传入之前创建的WebGLShader
对象 - 链接 program
- 创建
代码如下:
js
/**
*
* @param {WebGLRenderingContext} gl
* @param {string} type
* @param {string} source
*/
function createShader(gl, type, source) {
// 创建 shader 对象
let shader = gl.createShader(type);
// 往 shader 中传入源代码
gl.shaderSource(shader, source);
// 编译 shader
gl.compileShader(shader);
// 判断 shader 是否编译成功
let success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (success) {
return shader;
}
// 如果编译失败,则打印错误信息
console.log(gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
}
/**
*
* @param {WebGLRenderingContext} gl
* @param {WebGLShader} vertexShader
* @param {WebGLShader} fragmentShader
*/
function createProgram(gl, vertexShader, fragmentShader) {
// 创建 program 对象
let program = gl.createProgram();
// 往 program 对象中传入 WebGLShader 对象
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
// 链接 program
gl.linkProgram(program);
// 判断 program 是否链接成功
let success = gl.getProgramParameter(program, gl.LINK_STATUS);
if (success) {
return program;
}
// 如果 program 链接失败,则打印错误信息
console.log(gl.getProgramInfoLog(program));
gl.deleteProgram(program);
}
/**
*
* @param {WebGLRenderingContext} gl
* @param {string} vertexSource
* @param {string} fragmentSource
*/
function initWebGL(gl, vertexSource, fragmentSource) {
// 根据源代码创建顶点着色器
let vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexSource);
// 根据源代码创建片元着色器
let fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource);
// 创建 WebGLProgram 程序
let program = createProgram(gl, vertexShader, fragmentShader);
return program;
}
以上的三个函数就是我们的工具函数了,我们初始化 shader 都会用到上面的三个函数。
编写 Shader 程序
现在让我们开始编写 shader 程序吧。我们的目标是要在屏幕上显示一个点。
顶点着色器程序如下:
js
// 顶点着色器
const vertexShader = `
attribute vec4 a_position;
void main () {
// gl_Position为内置变量,表示当前点的位置
gl_Position = a_position;
// gl_Position为内置变量,表示当前点的大小,为浮点类型,如果赋值是整数类型会报错
gl_PointSize = 10.0;
}
`;
// 片元着色器
const fragmentShader = `
// 设置浮点数精度
precision mediump float;
void main () {
// vec4是表示四维向量,这里用来表示RGBA的值[0~1],均为浮点数,如为整数则会报错
gl_FragColor = vec4(1.0, 0.5, 1.0, 1.0);
}
`;
在WebGL 概述——原理篇中我们也讲到了顶点着色器中的 attribute
是一个存储限定符,它表示这个变量是顶点信息,可以通过 js 传递进入 shader 中。
shader 编写完毕,我们接下来要使用这个着色器了。
js
// 初始化shader程序
const program = initWebGL(gl, vertexShader, fragmentShader);
// 告诉WebGL使用我们刚刚初始化的这个程序
gl.useProgram(program);
// 获取shader中a_position的地址
const a_position = gl.getAttribLocation(program, 'a_position');
// 往a_position这个地址中传值
gl.vertexAttrib3f(a_position, 0.0, 0.0, 0.0);
// 开始绘制,绘制类型是gl.POINTS绘制点,0表示第一个点的索引,1表示共绘制几个点
gl.drawArrays(gl.POINTS, 0, 1);
效果如下:
好了,你现在已经学会了如何绘制一个点,你学会了在 js 和 shader 之间如何传递数据。我们先通过gl.getAttribLocation
获取了 attribute 类型变量的地址,然后使用vertexAttrib3f
往这个地址中填充数据。但是这样的传递数据的方式有缺陷,这只能传递一个点的数据,如果我们有许多的点怎么办呢?现在我们稍微加大一点难度,将绘制一个点变为绘制 3 个点。
我们先准备 3 个点的数据:
js
const pointPos = [-0.5, 0.0, 0.5, 0.0, 0.0, 0.5];
我们采用WebGLBuffer
对象来往 WebGL 中传递数据,还记得下面这个图么?gl.ARRAY_BUFFER
是中间的桥梁,通过这个属性,我们可以往WebGLBuffer
中传入数据。
代码如下:
js
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
// -----------------------------------------------------
// 注意:这里必须采用类型化数组往WebGL传入attribute类型的数据
// -----------------------------------------------------
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(pointPos), gl.STATIC_DRAW);
接下来,我们需要改造之前的这一部分代码
js
const a_position = gl.getAttribLocation(program, 'a_position');
// 我们不再采用这种方式进行传值
// gl.vertexAttrib3f(a_position, 0.0, 0.0, 0.0);
// 采用vertexAttribPointer进行传值
gl.vertexAttribPointer(
a_position,
2,
gl.FLOAT,
false,
Float32Array.BYTES_PER_ELEMENT * 2,
0
);
这里gl.vertexAttribPointer
这个函数是告诉 WebGL 如何从WebGLBuffer
中读取数据。
参数名 | 含义 |
---|---|
index | 指定要修改的顶点属性 |
size | 每个顶点属性的组成数量,换言之就是几个数据组成了一个顶点属性 |
type | 数据的类型 |
normalized | 是否进行归一化处理 |
stride | 顶点之间的偏移量 |
offset | 顶点属性数组中一部分字节的偏移量 |
参考资料:WebGLRenderingContext.vertexAttribPointer()——MDN
这里你可能会对 stride 和 offset 这两个参数感到迷惑,别着急,这里我们先不解释这两个参数,后面我们会发现它们的奥秘。
最后,我们还需要调用
js
gl.enableVertexAttribArray(a_position);
这一句话是告诉 WebGL,在 shader 中的 a_position 这个变量读取的是当前 WebGLBuffer 的内容。(因为一段程序中可能不止一个 WebGLBuffer)
最后,修改绘制的命令:
js
// 之前只绘制一个点,现在绘制3个点
gl.drawArrays(gl.POINTS, 0, 3);
效果如下:
如果我们需要绘制三角形,则把上面的绘制命令修改为:
js
gl.drawArrays(gl.TRIANGLES, 0, 3);
效果如下:
练习
通过鼠标点击屏幕,鼠标每点击一次,在点击的位置绘制一个点。
总结
好了,今天你已经学会了如何在 WebGL 中绘制点和三角形了。今天讲解了初始化 Shader 的方法并且编写了我们的工具函数。js 往 shader 中如何传递数据?我们可以通过vertexAttrib3f
这种方式进行传递,但是只能传递一个点的数据,所以我们改为了使用WebGLBuffer
来传递数据。希望你能回想一下WebGL 核心原理概述中关于WebGLBuffer
传递数据的内容。
今天留下了两个问题:
gl.vertexAttribPointer
的参数中stride
和offset
这两个参数到底是干什么的?- 一段程序中有多个
WebGLBuffer
该如何处理呢?
下一章中我们揭晓这两个问题的答案。敬请期待。
下方是完整代码及 Demo。
ts
import { initWebGL } from './util';
function main() {
/**
* @type {HTMLCanvasElement}
*/
const canvas = document.getElementById('canvas') as HTMLCanvasElement;
const gl = canvas.getContext('webgl');
if (!gl) {
return null;
}
// 设置清空颜色缓冲区时的颜色
gl.clearColor(1.0, 1.0, 1.0, 1.0);
// 清空颜色缓冲区
gl.clear(gl.COLOR_BUFFER_BIT);
// 顶点着色器
const vertexShader = `
attribute vec4 a_position;
void main () {
// gl_Position为内置变量,表示当前点的位置
gl_Position = a_position;
// gl_Position为内置变量,表示当前点的大小,为浮点类型,如果赋值是整数类型会报错
gl_PointSize = 10.0;
}
`;
// 片元着色器
const fragmentShader = `
// 设置浮点数精度
precision mediump float;
void main () {
// vec4是表示四维向量,这里用来表示RGBA的值[0~1],均为浮点数,如为整数则会报错
gl_FragColor = vec4(1.0, 0.5, 1.0, 1.0);
}
`;
// 初始化shader程序
const program = initWebGL(gl, vertexShader, fragmentShader);
if (!program) {
return null;
}
// 告诉WebGL使用我们刚刚初始化的这个程序
gl.useProgram(program);
const pointPos = [-0.5, 0.0, 0.5, 0.0, 0.0, 0.5];
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
// -----------------------------------------------------
// 注意:这里必须采用类型化数组往WebGL传入attribute类型的数据
// -----------------------------------------------------
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(pointPos), gl.STATIC_DRAW);
// 获取shader中a_position的地址
const a_position = gl.getAttribLocation(program, 'a_position');
// 我们不再采用这种方式进行传值
// gl.vertexAttrib3f(a_position, 0.0, 0.0, 0.0);
// 采用vertexAttribPointer进行传值
gl.vertexAttribPointer(
a_position,
2,
gl.FLOAT,
false,
Float32Array.BYTES_PER_ELEMENT * 2,
0
);
gl.enableVertexAttribArray(a_position);
gl.drawArrays(gl.TRIANGLES, 0, 3);
}
export default main;
ts
import { mat4, vec3 } from 'gl-matrix';
import { Camera, Matrix4, Object3D, PerspectiveCamera, Vector3 } from 'three';
function createShader(gl: WebGLRenderingContext, type: number, source: string) {
// 创建 shader 对象
const shader = gl.createShader(type);
// 往 shader 中传入源代码
gl.shaderSource(shader!, source);
// 编译 shader
gl.compileShader(shader!);
// 判断 shader 是否编译成功
const success = gl.getShaderParameter(shader!, gl.COMPILE_STATUS);
if (success) {
return shader;
}
// 如果编译失败,则打印错误信息
console.log(gl.getShaderInfoLog(shader!));
gl.deleteShader(shader);
}
function createProgram(
gl: WebGLRenderingContext,
vertexShader: WebGLShader,
fragmentShader: WebGLShader
): WebGLProgram | null {
// 创建 program 对象
const program = gl.createProgram();
// 往 program 对象中传入 WebGLShader 对象
gl.attachShader(program!, vertexShader);
gl.attachShader(program!, fragmentShader);
// 链接 program
gl.linkProgram(program!);
// 判断 program 是否链接成功
const success = gl.getProgramParameter(program!, gl.LINK_STATUS);
if (success) {
return program;
}
// 如果 program 链接失败,则打印错误信息
console.log(gl.getProgramInfoLog(program!));
gl.deleteProgram(program);
return null;
}
export function initWebGL(
gl: RenderContext,
vertexSource: string,
fragmentSource: string
) {
// 根据源代码创建顶点着色器
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexSource);
// 根据源代码创建片元着色器
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource);
// 创建 WebGLProgram 程序
const program = createProgram(gl, vertexShader!, fragmentShader!);
return program;
}
export enum REPEAT_MODE {
NONE,
REPEAT,
MIRRORED_REPEAT,
}
export function createTexture(gl: WebGLRenderingContext, repeat?: REPEAT_MODE) {
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
let mod: number = gl.CLAMP_TO_EDGE;
switch (repeat) {
case REPEAT_MODE.REPEAT:
mod = gl.REPEAT;
break;
case REPEAT_MODE.MIRRORED_REPEAT:
mod = gl.MIRRORED_REPEAT;
break;
}
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, mod);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, mod);
return texture;
}
export function isMobile(): boolean {
if (typeof window !== 'undefined' && window.navigator) {
const userAgent = window.navigator.userAgent;
return /(mobile)/i.test(userAgent);
}
return false;
}
export function clamp(x: number, min: number, max: number) {
if (x < min) {
x = min;
} else if (x > max) {
x = max;
}
return x;
}
export function readLUTCube(file: string): {
size: number;
data: number[];
} {
let lineString = '';
let isStart = true;
let size = 0;
let i = 0;
let result: number[] = [];
const processToken = (token: string) => {
if (token === 'LUT size') {
i++;
let sizeStart = false;
let sizeStr = '';
while (file[i] !== '\n') {
if (file[i - 1] === ' ' && /\d/.test(file[i])) {
sizeStart = true;
sizeStr += file[i];
} else if (sizeStart) {
sizeStr += file[i];
}
i++;
}
size = +sizeStr;
result = new Array(size * size * size);
} else if (token === 'LUT data points') {
// 读取数据
i++;
let numStr = '';
let count = 0;
while (i < file.length) {
if (/\s|\n/.test(file[i])) {
result[count++] = +numStr;
numStr = '';
} else if (/\d|\./.test(file[i])) {
numStr += file[i];
}
i++;
}
}
};
for (; i < file.length; i++) {
if (file[i] === '#') {
isStart = true;
} else if (isStart && file[i] === '\n') {
processToken(lineString);
lineString = '';
isStart = false;
} else if (isStart) {
lineString += file[i];
}
}
return {
size,
data: result,
};
}
export async function loadImages(srcs: string[]): Promise<HTMLImageElement[]> {
const all: Promise<HTMLImageElement>[] = srcs.map(item => loadImage(item));
return Promise.all(all);
}
export async function loadImage(src: string) {
return new Promise<HTMLImageElement>(resolve => {
const img = new Image();
img.src = src;
img.onload = () => {
resolve(img);
};
});
}
export function compute8ssedt(image: ImageData): number[][] {
// Initialize distance transform image
const distImage: number[][] = [];
for (let i = 0; i < image.height; i++) {
distImage[i] = [];
for (let j = 0; j < image.width; j++) {
distImage[i][j] = 0;
}
}
// Initialize queue for distance transform
const queue: number[][] = [];
const data = image.data;
for (let i = 0; i < image.height; i++) {
for (let j = 0; j < image.width; j++) {
const index = (i * image.width + j) * 4;
if (data[index] == 255) {
queue.push([i, j]);
}
}
}
// Compute distance transform
while (queue.length > 0) {
const p = queue.shift()!;
const x = p[0];
const y = p[1];
let minDist = Number.MAX_SAFE_INTEGER;
let minDir = [-1, -1];
// Compute distance to nearest foreground pixel in 8 directions
for (let i = -1; i <= 1; i++) {
for (let j = -1; j <= 1; j++) {
if (i == 0 && j == 0) continue;
const nx = x + i;
const ny = y + j;
if (
nx >= 0 &&
nx < image.height &&
ny >= 0 &&
ny < image.width
) {
const d = distImage[nx][ny] + Math.sqrt(i * i + j * j);
if (d < minDist) {
minDist = d;
minDir = [i, j];
}
}
}
}
// Update distance transform image and queue
distImage[x][y] = minDist;
if (minDir[0] != -1 && minDir[1] != -1) {
const nx = x + minDir[0];
const ny = y + minDir[1];
if (distImage[nx][ny] == 0) {
queue.push([nx, ny]);
}
}
}
return distImage;
}
// #region createFramebuffer
export function createFramebufferAndTexture(
gl: WebGLRenderingContext,
width: number,
height: number
): [WebGLFramebuffer | null, WebGLTexture | null] {
const framebuffer = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
const texture = createTexture(gl, REPEAT_MODE.NONE);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
width,
height,
0,
gl.RGBA,
gl.UNSIGNED_BYTE,
null
);
gl.framebufferTexture2D(
gl.FRAMEBUFFER,
gl.COLOR_ATTACHMENT0,
gl.TEXTURE_2D,
texture,
0
);
const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
if (status === gl.FRAMEBUFFER_COMPLETE) {
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.bindTexture(gl.TEXTURE_2D, null);
return [framebuffer, texture];
}
return [null, null];
}
// #endregion createFramebuffer
// #region lookat
export function lookAt(cameraPos: vec3, targetPos: vec3): mat4 {
const z = vec3.create();
const y = vec3.fromValues(0, 1, 0);
const x = vec3.create();
vec3.sub(z, cameraPos, targetPos);
vec3.normalize(z, z);
vec3.cross(x, y, z);
vec3.normalize(x, x);
vec3.cross(y, z, x);
vec3.normalize(y, y);
// prettier-ignore
return mat4.fromValues(
x[0], x[1], x[2], 0,
y[0], y[1], y[2], 0,
z[0], z[1], z[2], 0,
cameraPos[0], cameraPos[1], cameraPos[2], 1
);
}
// #endregion lookat
export function ASSERT(v: any) {
if (v === void 0 || v === null || isNaN(v)) {
throw new Error(v + 'is illegal value');
}
}
const lightAttenuationTable: Record<string, number[]> = {
'7': [1, 0.7, 1.8],
'13': [1, 0.35, 0.44],
'20': [1, 0.22, 0.2],
'32': [1, 0.14, 0.07],
'50': [1, 0.09, 0.032],
'65': [1, 0.07, 0.017],
'100': [1, 0.045, 0.0075],
'160': [1, 0.027, 0.0028],
'200': [1, 0.022, 0.0019],
'325': [1, 0.014, 0.0007],
'600': [1, 0.007, 0.0002],
'3250': [1, 0.0014, 0.000007],
};
// #region attenuation
export function lightAttenuationLookUp(dist: number): number[] {
const distKeys = Object.keys(lightAttenuationTable);
const first = +distKeys[0];
if (dist <= first) {
return lightAttenuationTable['7'];
}
for (let i = 0; i < distKeys.length - 1; i++) {
const key = distKeys[i];
const nextKey = distKeys[i + 1];
if (+key <= dist && dist < +nextKey) {
const value = lightAttenuationTable[key];
const nextValue = lightAttenuationTable[nextKey];
const k = (dist - +key) / (+nextKey - +key);
const kl = value[1] + (nextValue[1] - value[1]) * k;
const kq = value[2] + (nextValue[2] - value[2]) * k;
return [1, kl, kq];
}
}
return lightAttenuationTable['3250'];
}
// #endregion attenuation
// #region lesscode
export type BufferInfo = {
name: string;
buffer: WebGLBuffer;
numComponents: number;
isIndices?: boolean;
};
export function createBufferInfoFromArrays(
gl: RenderContext,
arrays: {
name: string;
numComponents: number;
data: Iterable<number>;
isIndices?: boolean;
}[]
): BufferInfo[] {
const result: BufferInfo[] = [];
for (let i = 0; i < arrays.length; i++) {
const buffer = gl.createBuffer();
if (!buffer) {
continue;
}
result.push({
name: arrays[i].name,
buffer: buffer,
numComponents: arrays[i].numComponents,
isIndices: arrays[i].isIndices,
});
if (arrays[i].isIndices) {
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer);
gl.bufferData(
gl.ELEMENT_ARRAY_BUFFER,
new Uint32Array(arrays[i].data),
gl.STATIC_DRAW
);
} else {
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array(arrays[i].data),
gl.STATIC_DRAW
);
}
}
return result;
}
export type AttributeSetters = Record<string, (bufferInfo: BufferInfo) => void>;
export function createAttributeSetter(
gl: RenderContext,
program: WebGLProgram
): AttributeSetters {
const createAttribSetter = (index: number) => {
return function (b: BufferInfo) {
if (!b.isIndices) {
gl.bindBuffer(gl.ARRAY_BUFFER, b.buffer);
gl.enableVertexAttribArray(index);
gl.vertexAttribPointer(
index,
b.numComponents,
gl.FLOAT,
false,
0,
0
);
}
};
};
const attribSetter: AttributeSetters = {};
const numAttribs = gl.getProgramParameter(program, gl.ACTIVE_ATTRIBUTES);
for (let i = 0; i < numAttribs; i++) {
const attribInfo = gl.getActiveAttrib(program, i);
if (!attribInfo) {
break;
}
const index = gl.getAttribLocation(program, attribInfo.name);
attribSetter[attribInfo.name] = createAttribSetter(index);
}
return attribSetter;
}
export type UniformSetters = Record<string, (v: any) => void>;
export function createUniformSetters(
gl: RenderContext,
program: WebGLProgram
): UniformSetters {
let textUnit = 0;
const createUniformSetter = (
program: WebGLProgram,
uniformInfo: {
name: string;
type: number;
}
): ((v: any) => void) => {
const location = gl.getUniformLocation(program, uniformInfo.name);
const type = uniformInfo.type;
if (type === gl.FLOAT) {
return function (v: number) {
gl.uniform1f(location, v);
};
} else if (type === gl.FLOAT_VEC2) {
return function (v: number[]) {
gl.uniform2fv(location, v);
};
} else if (type === gl.FLOAT_VEC3) {
return function (v: number[]) {
gl.uniform3fv(location, v);
};
} else if (type === gl.FLOAT_VEC4) {
return function (v: number[]) {
gl.uniform4fv(location, v);
};
} else if (type === gl.FLOAT_MAT2) {
return function (v: number[]) {
gl.uniformMatrix2fv(location, false, v);
};
} else if (type === gl.FLOAT_MAT3) {
return function (v: number[]) {
gl.uniformMatrix3fv(location, false, v);
};
} else if (type === gl.FLOAT_MAT4) {
return function (v: number[]) {
gl.uniformMatrix4fv(location, false, v);
};
} else if (type === gl.SAMPLER_2D) {
const currentTexUnit = textUnit;
++textUnit;
return function (v: WebGLTexture) {
gl.uniform1i(location, currentTexUnit);
gl.activeTexture(gl.TEXTURE0 + currentTexUnit);
gl.bindTexture(gl.TEXTURE_2D, v);
};
}
return function () {
throw new Error('cannot find corresponding type of value.');
};
};
const uniformsSetters: UniformSetters = {};
const numUniforms = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);
for (let i = 0; i < numUniforms; i++) {
const uniformInfo = gl.getActiveUniform(program, i);
if (!uniformInfo) {
break;
}
let name = uniformInfo.name;
if (name.substr(-3) === '[0]') {
name = name.substr(0, name.length - 3);
}
uniformsSetters[uniformInfo.name] = createUniformSetter(
program,
uniformInfo
);
}
return uniformsSetters;
}
export function setAttribute(
attribSetters: AttributeSetters,
bufferInfos: BufferInfo[]
) {
for (let i = 0; i < bufferInfos.length; i++) {
const info = bufferInfos[i];
const setter = attribSetters[info.name];
setter && setter(info);
}
}
export function setUniform(
uniformSetters: UniformSetters,
uniforms: Record<string, any>
): void {
const keys = Object.keys(uniforms);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const v = uniforms[key];
const setter = uniformSetters[key];
setter && setter(v);
}
}
// #endregion lesscode
export function fromViewUp(view: Vector3, up?: Vector3): Matrix4 {
up = up || new Vector3(0, 1, 0);
const xAxis = new Vector3().crossVectors(up, view);
xAxis.normalize();
const yAxis = new Vector3().crossVectors(view, xAxis);
yAxis.normalize();
// prettier-ignore
return new Matrix4(
xAxis.x, yAxis.x, view.x, 0,
xAxis.y, yAxis.y, view.y, 0,
xAxis.z, yAxis.z, view.z, 0,
0, 0, 0, 1
)
}
export function getMirrorPoint(
p: Vector3,
n: Vector3,
origin: Vector3
): Vector3 {
const op = p.clone().sub(origin);
const normalizedN = n.clone().normalize();
const d = op.dot(normalizedN);
const newP = op.sub(normalizedN.multiplyScalar(2 * d));
return newP;
}
export function getMirrorVector(p: Vector3, n: Vector3): Vector3 {
const normalizedN = n.clone().normalize();
const d = p.dot(normalizedN);
return normalizedN.multiplyScalar(2 * d).sub(p);
}
export function setReflection2(
mainCamera: Camera,
virtualCamera: Camera,
reflector: Object3D
): void {
const reflectorWorldPosition = new Vector3();
const cameraWorldPosition = new Vector3();
reflectorWorldPosition.setFromMatrixPosition(reflector.matrixWorld);
cameraWorldPosition.setFromMatrixPosition(mainCamera.matrixWorld);
const rotationMatrix = new Matrix4();
rotationMatrix.extractRotation(reflector.matrixWorld);
const normal = new Vector3();
normal.set(0, 0, 1);
normal.applyMatrix4(rotationMatrix);
const view = new Vector3();
view.subVectors(reflectorWorldPosition, cameraWorldPosition);
view.reflect(normal).negate();
view.add(reflectorWorldPosition);
rotationMatrix.extractRotation(mainCamera.matrixWorld);
const lookAtPosition = new Vector3();
lookAtPosition.set(0, 0, -1);
lookAtPosition.applyMatrix4(rotationMatrix);
lookAtPosition.add(cameraWorldPosition);
const target = new Vector3();
target.subVectors(reflectorWorldPosition, lookAtPosition);
target.reflect(normal).negate();
target.add(reflectorWorldPosition);
virtualCamera.position.copy(view);
virtualCamera.position.copy(view);
virtualCamera.up.set(0, 1, 0);
virtualCamera.up.applyMatrix4(rotationMatrix);
virtualCamera.up.reflect(normal);
virtualCamera.isCamera = true;
virtualCamera.lookAt(target);
if (
virtualCamera instanceof PerspectiveCamera &&
mainCamera instanceof PerspectiveCamera
) {
virtualCamera.far = mainCamera.far; // Used in WebGLBackground
virtualCamera.updateMatrixWorld();
virtualCamera.projectionMatrix.copy(mainCamera.projectionMatrix);
} else {
// reflectCamera.updateMatrixWorld();
}
}