Skip to content
On this page

图像处理技术进阶

本文紧接着图像处理技术一文,如果你还没有读过,我建议你从那儿开始读起。

在上篇文章中,我们学会了如何使用一个 Shader 来处理一幅图像。今天我们要解决的问题是:

如何在一幅图像中应用多个效果呢?比如:我想对图片进行一次调色,然后再对其进行模糊的操作。当然,这一切你可以都写在同一个 Shader 中。但是这也会带来很多的问题:

  1. 不够灵活,把所有的效果都写在一个 Shader 中会让我们的组合变得非常的不灵活。
  2. 有的效果在一个 shader 中根本就难以实现,比如:模糊效果

一种灵活的方式是:

利用多个 shader,一个 shader 代表了一种图像处理效果,我们可以利用多个 shader 来反复的处理图像。具体的流程可以参考下图:

帧缓冲技术 / Framebuffer

在上图中,我们可以看到我们使用了一种中间纹理来将每一步的处理结果暂存起来。首先,我们介绍一下什么是帧缓冲技术。

什么是帧缓冲技术?

WebGL 中的帧缓冲技术是一种允许你渲染到纹理或渲染缓冲中的技术。

  1. 它可以用来实现一些高级的图形效果,如后处理、阴影、反射等
  2. 不过,“帧缓冲”这个名字事实上是一个糟糕的名字,一个帧缓冲对象(Framebuffer Object)是一个包含了多个附件(attachments)的对象,其中每个附件都可以是一个纹理(Texture)或一个渲染缓冲(RenderBuffer)。

如何创建帧缓冲对象

我们可以通过以下的步骤来创建一个帧缓冲对象,并为其绑定一个纹理。

  1. 创建一个帧缓冲对象(gl.createFramebuffer)
  2. 绑定一个帧缓冲对象(`gl.bindFramebuffer``)
  3. 创建一个纹理(util.createTexture 此 API 在我们之前编写的 util.ts 文件中),
  4. 使用 gl.texImage2D 为这个纹理传入数据,注意观察这个 API 有两种形式,我们现在使用第二种形式,我们可以直接使用ArrayBuffer 直接传入图像数据,这里我们传入null,并制定图像的宽高。
  5. 为帧缓冲对象添加附件(gl.framebufferTexture2D)
  6. 检查帧缓冲对象是否完整 (gl.checkFramebufferStatus)

详细的代码如下: <<< @/scripts/webgl/util.ts#createFramebuffer [util.ts]

如何使用我们的帧缓冲对象?

一旦帧缓冲对象完成了创建,使用它是非常简单的。我们可以使用 gl.bindFramebuffer(gl.FRAMBUFFER, 刚刚创建的framebuffer)。完成帧缓冲对象的绑定后,我们再进行绘制(drawArrays, drawElements)时,我们的绘制结果就会绘制到帧缓冲对象所绑定的纹理上。

但是,一旦这样做了后,我们无法在屏幕上查看到我们的绘制结果。所以,为了保证我们的绘制结果正确,我们可以先不绑定到帧缓冲区,我们直接绘制到屏幕上查看渲染结果是否正确。

那如果我们不想要使用帧缓冲对象又该怎么办呢?我们还是调用这个 API,gl.bindFramebuffer(gl.FRAMEBUFFER, null) 这样,我们的渲染结果就会直接绘制到我们的屏幕上面。

实战——使用帧缓冲实现模糊效果

在这之前,我们需要简单的了解一下什么是模糊效果。模糊效果有很多种,有高斯模糊均值模糊, 中值模糊等。他们具有一个共同点,就是需要进行卷积运算。但是,什么是卷积

什么是卷积

在图像处理中,卷积操作指的就是使用一个卷积核 (kernel) 对一张图像中的每个像素进行操作。卷积核通常是一个四方形网格结构(例如 2x2 3x3 的方形区域),该区域内每个方格都有一个权重值。如下图:

当对图像中的某个像素进行卷积时,我们会把卷积核的中心放置于该像素上,如下图所示,再一次计算核中的每个元素和其覆盖的图像像素值的乘积并求和,得到的结果就是该位置的新像素值。如下图所示,蓝色方块表示进行卷积操作的像素,则该点的新的像素值应该为:

101+2501+551+2001+981+1261+1001+2001+1110 * 1 + 250 * 1 + 55 * 1 + 200 * 1 + 98 * 1 + 126 * 1 + 100 * 1 + 200 * 1 + 1 * 1

这样的计算过程虽然简单,但是可以实现很多图像处理效果,除了模糊之外还可以实现边缘检测等效果。如果我们想要实现均值模糊,我们可以使用一个 3x3 的卷积核,核内的每个元素的值均为 1/9。卷积核越大,均值模糊的效果则越好,但是性能会下降。

均值模糊

那么现在我们就来实现一个简单的均值模糊效果,我们依然基于图像处理技术中的代码进行修改。

我们只需要修改我们的片元着色器:

glsl
#define HALF_KERNEL_SIZE 1
precision highp float;
uniform sampler2D u_tex;
varying vec2 v_uv;
uniform vec4 u_uv_transform;
uniform vec2 u_resolution;

void main () {
    vec2 uv = v_uv * u_uv_transform.xy + u_uv_transform.zw;
    vec4 col = vec4(0.0);
    for (int i = -HALF_KERNEL_SIZE; i <= HALF_KERNEL_SIZE; i++) {
        for (int j = -HALF_KERNEL_SIZE; j <= HALF_KERNEL_SIZE; j++) {
            vec2 offset = vec2(float(i), float(j)) / u_resolution;
            col += texture2D(u_tex, uv + offset);
        }
    }
    col /= (float(HALF_KERNEL_SIZE) * 2. + 1.) * (float(HALF_KERNEL_SIZE) * 2. + 1.);
    gl_FragColor = col;
}

以上就是一个简单的均值模糊的片元着色器的代码。我们可以看一下卷积核的大小分别为 3、7、13 时的效果:

原图 vs 3x3卷积核
原图 vs 7x7卷积核
原图 vs 13x13卷积核

我们可以发现,当卷积核的大小在 7x7 大小时,我们才能看到一些模糊的效果。但是卷积核越大,其 GPU 的处理速度越慢。这是因为纹理采样在 GPU 中是一种很慢的操作!!!我们需要尽可能的减少在 GPU 中进行采样的次数。

一种常见的方案是:始终使用 3x3 的卷积核,但是进行多次迭代。简单的说就是先将图像进行一次均值模糊处理,将图像处理的结果存于一张中间纹理中(还记得上面我们将的帧缓冲区吗?)。然后将这张中间纹理作为输入,又进行一次均值模糊的处理!如此迭代多次后,我们可以得到一个不错的效果。

基于多次迭代实现均值模糊

接下来,我们基于上面的方案进行实现。

首先,由于我们需要进行多次的迭代,所以我们需要几个帧缓冲区?现在思考一下,我们到底需要几个帧缓冲区?我们迭代几次就需要几个帧缓冲区吗?事实上,这的确也没错。不过这有点点浪费我们的资源。

实际上,我们只需要 2 个帧缓冲区就可以完成。流程如下:

TIP

原图像 --> 帧缓冲 1 --> 帧缓冲 2 --> 帧缓冲区 1 --> 帧缓冲 2 --> ...... --> 输出到屏幕

类似于打乒乓球一样,我们不断的利用这两个帧缓冲反复的处理这张图片即可。

首先我们需要创建两组 framebuffertexture

ts
const [framebuffer1, renderTexture1] = createFramebufferAndTexture(
    gl,
    canvas.width * devicePixelRatio,
    canvas.height * devicePixelRatio
);
const [framebuffer2, renderTexture2] = createFramebufferAndTexture(
    gl,
    canvas.width * devicePixelRatio,
    canvas.height * devicePixelRatio
);

接着,利用for循环不断的对这幅图片进行卷积核为 3x3 的均值模糊。

ts
const iterations = 30;
for (let i = 0; i < iterations; i++) {
    if (i % 2 === 0) {
        gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer1);
    } else {
        gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer2);
    }
    render();
    gl.bindTexture(
        gl.TEXTURE_2D,
        i % 2 === 0 ? renderTexture1 : renderTexture2
    );
}
// 最后一次输出到屏幕上
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
render();

最终的渲染结果如下:

迭代1次
迭代5次
迭代30次

更进一步优化

在理解了上述代码后,我们还可以更进一步优化代码。我们将利用缩放对图像进行降采样,从而减少需要处理的像素个数以提高性能。我们在创建 framebuffer 时,就将其附加的纹理 texture的尺寸缩小。

ts
const downSample = 1;
const renderTextureWidth = canvas.width / downSample;
const renderTextureHeight = canvas.height / downSample;
const [framebuffer1, renderTexture1] = createFramebufferAndTexture(
    gl,
    renderTextureWidth,
    renderTextureHeight
);
const [framebuffer2, renderTexture2] = createFramebufferAndTexture(
    gl,
    renderTextureWidth,
    renderTextureHeight
);

其中有一个细节需要注意,由于我们的画布尺寸与 framebuffer 所附加的纹理尺寸不一致了,所以在绘制到 framebuffer上时,我们需要使用 gl.viewport这个 API 重新指定 viewport

ts
const iterations = 30;
for (let i = 0; i < iterations; i++) {
    if (i % 2 === 0) {
        gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer1);
    } else {
        gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer2);
    }
    gl.viewport(
        0,
        0,
        (canvas.width * devicePixelRatio) / downSample,
        (canvas.height * devicePixelRatio) / downSample
    );
    render();
    gl.bindTexture(
        gl.TEXTURE_2D,
        i % 2 === 0 ? renderTexture1 : renderTexture2
    );
}
gl.viewport(0, 0, canvas.width, canvas.height);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
render();

最终的效果如下,我们与上面同样是 3x3 卷积核、迭代 30 次的图像作对比。只不过我们最终修改的模糊版本是对其降采样 2 次的结果。

迭代30次未降采样
迭代30次, 降采样2次

不过值得注意的是,如果降采样的倍率过大的话,图像会出现非常明显的马赛克块的效果。

Demo

本文的最终 Demo 如下,完整代码见文末

迭代次数
1
降采样倍数
1

如果觉得本文有用,可以请作者喝杯咖啡~

 

ts
const canvas = document.getElementById('canvas') as HTMLCanvasElement;
if (!canvas) {
    return;
}

const gl = canvas.getContext('webgl');
if (!gl) {
    console.error('该设备不支持WebGL!');
    return;
}
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
// 设置清空颜色缓冲区时的颜色
gl.clearColor(1.0, 1.0, 1.0, 1.0);

// 清空颜色缓冲区
gl.clear(gl.COLOR_BUFFER_BIT);

// 顶点着色器
const vertexShader = `
    attribute vec4 a_position; 
    attribute vec2 a_uv;
    varying vec2 v_uv;
    void main () {
        v_uv = a_uv;
        gl_Position =  a_position; 
    }  
`;
// 片元着色器
const fragmentShader = `
    #define HALF_KERNEL_SIZE 1
    precision highp float;
    uniform sampler2D u_tex;
    varying vec2 v_uv;
    uniform vec4 u_uv_transform;
    uniform vec2 u_resolution;

    void main () {
        vec2 uv = v_uv * u_uv_transform.xy + u_uv_transform.zw;
        vec4 col = vec4(0.0);
        for (int i = -HALF_KERNEL_SIZE; i <= HALF_KERNEL_SIZE; i++) {
            for (int j = -HALF_KERNEL_SIZE; j <= HALF_KERNEL_SIZE; j++) {
                vec2 offset = vec2(float(i), float(j)) / u_resolution;
                col += texture2D(u_tex, uv + offset);
            }
        }
        col /= (float(HALF_KERNEL_SIZE) * 2. + 1.) * (float(HALF_KERNEL_SIZE) * 2. + 1.);
        gl_FragColor = col;
    }
`;

// 初始化shader程序
const program = initWebGL(gl, vertexShader, fragmentShader);
if (!program) {
    console.error('WebGLProgram初始化失败!');
    return;
}
// 告诉WebGL使用我们刚刚初始化的这个程序
gl.useProgram(program);

const pointPos = [-1, -1, 1, -1, 1, 1, 1, 1, -1, 1, -1, -1];
const uvs = [0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 0];
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(pointPos), gl.STATIC_DRAW);

const uvBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, uvBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(uvs), gl.STATIC_DRAW);

gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
const a_position = gl.getAttribLocation(program, 'a_position');
const a_uv = gl.getAttribLocation(program, 'a_uv');
gl.vertexAttribPointer(
    a_position,
    2,
    gl.FLOAT,
    false,
    Float32Array.BYTES_PER_ELEMENT * 2,
    0
);
gl.enableVertexAttribArray(a_position);

gl.bindBuffer(gl.ARRAY_BUFFER, uvBuffer);
gl.vertexAttribPointer(
    a_uv,
    2,
    gl.FLOAT,
    false,
    Float32Array.BYTES_PER_ELEMENT * 2,
    0
);
gl.enableVertexAttribArray(a_uv);

const texture1 = createTexture(gl, REPEAT_MODE.NONE);

const uvTransformLoc = gl.getUniformLocation(program, 'u_uv_transform');
let uvTransform = [1, 1, 0, 0];
gl.uniform4fv(uvTransformLoc, uvTransform);
const brightContrastHue = [1, 1, 0];

const uResolutionLoc = gl.getUniformLocation(program, 'u_resolution');
gl.uniform2fv(uResolutionLoc, [canvas.width, canvas.height]);
const imgPromise1 = loadImage(withBase('/img/5-imgprocess/lenna.jpeg'));
const devicePixelRatio = window.devicePixelRatio;

let downSample = 4;
let renderTextureWidth = canvas.width / downSample;
let renderTextureHeight = canvas.height / downSample;
let [framebuffer1, renderTexture1] = createFramebufferAndTexture(
    gl,
    renderTextureWidth,
    renderTextureHeight
);
let [framebuffer2, renderTexture2] = createFramebufferAndTexture(
    gl,
    renderTextureWidth,
    renderTextureHeight
);

Promise.all([imgPromise1]).then(imgs => {
    gl.activeTexture(gl.TEXTURE0);
    gl.bindTexture(gl.TEXTURE_2D, texture1);
    gl.texImage2D(
        gl.TEXTURE_2D,
        0,
        gl.RGBA,
        gl.RGBA,
        gl.UNSIGNED_BYTE,
        imgs[0]
    );
    renderAll();
});
const render = () => {
    gl.clear(gl.COLOR_BUFFER_BIT);
    gl.drawArrays(gl.TRIANGLES, 0, 6);
};

let iterations = 30;
const renderAll = () => {
    gl.bindTexture(gl.TEXTURE_2D, texture1);
    for (let i = 0; i < iterations; i++) {
        if (i % 2 === 0) {
            gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer1);
        } else {
            gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer2);
        }
        gl.viewport(
            0,
            0,
            (canvas.width * devicePixelRatio) / downSample,
            (canvas.height * devicePixelRatio) / downSample
        );
        render();
        gl.bindTexture(
            gl.TEXTURE_2D,
            i % 2 === 0 ? renderTexture1 : renderTexture2
        );
    }
    gl.viewport(0, 0, canvas.width, canvas.height);
    gl.bindFramebuffer(gl.FRAMEBUFFER, null);
    render();
};
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();
    }
}