Skip to content
On this page

码少,趣多

动机

在进行下一步的学习之前,我们需要整理一下我们之前写的代码。我们回顾一下我们之前编写的 WebGL 代码,我们发现,大部分的代码都是极其重复的。比如:

ts
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(pointPos), gl.STATIC_DRAW);

const colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);

const normalBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(normals), gl.STATIC_DRAW);

// ......

const uWorldLoc = gl.getUniformLocation(program, 'u_world');
const uViewInvLoc = gl.getUniformLocation(program, 'u_viewInv');
const uLightPos = gl.getUniformLocation(program, 'u_lightPos');
const uViewPosLoc = gl.getUniformLocation(program, 'u_viewWorldPos');
const uGlossLoc = gl.getUniformLocation(program, 'u_gloss');
const uCoefficientLoc = gl.getUniformLocation(program, 'u_coefficient');
const uSpotDirLoc = gl.getUniformLocation(program, 'u_spotDir');
const uCutoffLoc = gl.getUniformLocation(program, 'u_cutoff');

// ......

我们可以看出这些代码大量的重复,这显得十分的啰嗦!

辅助函数

那么有没有一种办法可以简化这些代码呢?答案是肯定的!我们可以自己编写一些辅助函数来帮助我们来处理这些向 WebGL 传递值的工作,而我们只是需要提供一些必要的信息即可。

当然,我们现在编写的这一套”框架“需要建立在一些假设之上:

  1. 一个attribute变量就对应了一个 WebGLBuffer,我们不采用一个WebGLBuffer对应多个attribute变量的做法
  2. 似乎暂时没有别的约束了。

那我们要产出的辅助函数最终是一个什么东西呢?

我们希望可以通过某种 API,假设我们创建一个setAttribute 的 API,我们可以通过调用它来设置好所有的 attribute变量的数据。类似的,我们也希望创建类似于setUniforms这类的 API 来帮助我们设置好所有的 uniform 变量。

那么这两个 API 的参数又该如何设计?设计方法有很多。这里介绍一下作者的思路:

  1. 首先,参数中必须要包含 attribute/uniform 变量真正的值!
  2. 要往 WebGL 中传递数据的话,我们必须知道 attribute/uniform 变量在 Shader 中的位置(Location)。
  3. 如何设置值同样也是需要我们考虑的部分,比如对于attribute变量来说,它是 3 维还是 4 维向量都需要显示的说明;对于uniform变量来说,设置vecmatrix的值所对应的 API 都是不同的。
  4. 值与 location 还需要一一对应起来,这里我们采用相同的 key 来使值与 location 之间发生联系。

我们先来看 uniform 变量。

给 Uniform 变量设置值

按上面的思考方式,我们设计setUniforms这个 API,首先,我们需要接受真正的 uniform值,所以其中一个参数必然是包含了所有的uniform变量的值。

另外,还需要知道对于不同的 uniform值,我们怎样去设置它。

我们将入参设计为:

ts
setUniform(setters: Record<string, (v: any) => void>, uniformValues: Record<string, any>)

setters表示对于每一个 uniform 变量,如何设置其值。settersuniformValues中的 key 应该是一一对应的。具体的函数实现如下:

ts
function setUniform(
    setters: Record<string, (v: any) => void>,
    uniformValues: 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);
    }
}

在上面的函数中,uniformValues应该是由开发者确定的,而setters可能是通过调用另一个 API 生成的中间产物。比如这个 API 叫做 createUniformSetters

回顾一下如何给 uniform变量传递值,首先我们需要知道它在 shader 中的 location,也就是通过 gl.getUniformLocation 这个 API。然后再通过 gl.uniform1f, gl.uniform2fv等等 API 往其中传递值。所以在创建 setter时,我们需要知道 shader 中有哪些uniform变量,以及如何往其中传递值。所以我们需要所有 uniform变量的名字和类型。

我们大概率会写下这样的代码:

ts
function createUniformSetters(
    program: WebGLProgram,
    uniforms: {
        name: string;
        type: string;
    }[]
): Record<string, (v: any) => void> {
    for (let i = 0; i < uniforms.length; i++) {
        const uniform = uniforms[i];
        const location = gl.getUniformLocation(program, uniform.name);
        if (uniform.type === 'FLOAT') {
            return function (v: number) {
                gl.uniform1f(location, v);
            };
        } else if (uniform.type === 'FLOAT_2f') {
            // ......
        }
        // ......
    }
}

上面的代码似乎是没有什么问题,但是我们需要显示往函数中传递 shader 中用到的所有uniform变量。有没有一种办法可以不用这样做呢?

幸运的是,WebGL 为我们提供了一个 API 可以获取到当前 shader 程序中使用的 uniform变量和 attribute 变量,这个 API 就是:

  • 获取所有的 uniform变量:gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS)

  • 获取所有的 attribute变量:gl.getProgramParameter(program, gl.ACTIVE_ATTRIBUTES);

这样,我们就不用手动的枚举所有的 uniformattribute变量了。

完善一下上面的代码,可以写作:

ts
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;
}

这样我们要想往 shader 中传入 uniform 值就非常的方便了。我们在初始化程序时,就可以通过 createUniformSetters来创建 setter,最后再使用 setUniform(setter, values)API 真正的传入我们需要的值即可。

给 Attribute 变量设置值。

上面我们完成了给uniform变量设置值的辅助函数的编写,对于 attribute变量的设置也是类似的。只是不同的是他们彼此的传值方式不同,因为attribute变量的值是从 WebGLBuffer中读取的,不能够通过 gl.uniform1f这类 API 直接往其中传入。

我们再次回顾一下如何给attribute变量传值。

  1. 创建 WebGLBuffer: const buffer = gl.createBuffer();
  2. 绑定 Buffer: gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
  3. 往 Buffer 中传入数据: gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(pointPos), gl.STATIC_DRAW);
  4. 获取 attribute 变量在 shader 中的位置:const a_position = gl.getAttribLocation(program, 'a_position');
  5. 启用这个attribute变量:gl.enableVertexAttribArray(a_position);
  6. 告诉 WebGL 如何从WebGL读取数据来给attribute变量设置值:gl.vertexAttribPointer( a_position, 3, gl.FLOAT, false, Float32Array.BYTES_PER_ELEMENT * 3, 0 );

其中 1~3 步是在创建WebGLBuffer并填充数据,4~6 步则是在告诉 WebGL 如何读取数据。所以我们提供的值不是简单的 JS 对象了。而是需要真正的 WebGLBuffer

但是除此之外,其他的部分与 uniformSetter 并无太大的区别。这里为了防止啰嗦,直接给出源代码,请读者自行体会。

ts
type BufferInfo = {
    name: string;
    buffer: WebGLBuffer;
    numComponents: number;
    isIndices?: boolean;
};
export function createBufferInfoFromArrays(
    gl: RenderContext,
    arrays: {
        name: string;
        numComponents: number;
        data: 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,
        });
        gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
        gl.bufferData(
            gl.ARRAY_BUFFER,
            new Float32Array(arrays[i].data),
            gl.STATIC_DRAW
        );
    }
    return result;
}

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;
}

优化代码

我们现在就可以用刚刚编写的辅助函数来优化上一节中的代码了。具体完整的代码请见文末。后续我们将继续学习关于 WebGL 和图形学的内容。敬请期待!

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

 

TranslateX
0.63
TranslateY
1.50
TranslateZ
1.60
Gloss
64
ts
const canvas = document.getElementById('canvas4') 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 = lightVert;
// 片元着色器
const fragmentShader = lightFrag;

// 初始化shader程序
const program = initWebGL(gl, vertexShader, fragmentShader);
if (!program) {
    return null;
}
// 告诉WebGL使用我们刚刚初始化的这个程序
gl.useProgram(program);
gl.enable(gl.DEPTH_TEST);
const width = 1;
const height = 1;
const depth = 1;
//prettier-ignore
const pointPos = [
    // front-face
    0, 0, 0, width, 0, 0, width, height, 0, width, height, 0, 0, height, 0, 0, 0, 0,
    // back-face
    0, 0, depth, width, 0, depth, width, height, depth, width, height, depth, 0, height, depth, 0, 0, depth,
    // left-face
    0, 0, 0, 0, height, 0, 0, height, depth, 0, height, depth, 0, 0, depth, 0, 0, 0,
    // right-face
    width, 0, 0, width, height, 0, width, height, depth, width, height, depth, width, 0, depth, width, 0, 0,
    // top-face
    0, height, 0, width, height, 0, width, height, depth, width, height, depth, 0, height, depth, 0, height, 0,
    // bottom-face
    0, 0, 0, width, 0, 0, width, 0, depth, width, 0, depth, 0, 0, depth, 0, 0, 0,
];
//prettier-ignore
const normals = [
    // front-face
    0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1,
    // back-face
    0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1,
    // left-face
    -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, 
    // right-face
    1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 
    // top-face
    0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 
    // bottom-face
    0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 
];

for (let i = 0; i < pointPos.length; i += 3) {
    pointPos[i] += -width / 2;
    pointPos[i + 1] += -height / 2;
    pointPos[i + 2] += -depth / 2;
}

//prettier-ignore
const colors = [
    1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0,
    1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0,
    1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1,
    0, 0.5, 1, 0, 0.5, 1, 0, 0.5, 1, 0, 0.5, 1, 0, 0.5, 1, 0, 0.5, 1,
    0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1,
    0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1,
    0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0,
];

const bufferInfo = createBufferInfoFromArrays(gl, [
    { numComponents: 3, data: pointPos, name: 'a_position' },
    { numComponents: 3, data: colors, name: 'a_color' },
    { numComponents: 3, data: normals, name: 'a_normal' },
]);
const attribSetters = createAttributeSetter(gl, program);
const uniformSetters = createUniformSetters(gl, program);

const uniforms = {
    u_world: [],
    u_viewInv: [],
    u_lightPos: [],
    u_viewWorldPos: [],
    u_gloss: [],
    u_coefficient: [],
    u_spotDir: [],
    u_cutoff: [],
    u_proj: [],
};

let translateX = 0; //
let translateY = 0; //
let translateZ = 0; //

const uProj = gl.getUniformLocation(program, 'u_proj');
const projMat = mat4.create();
mat4.perspective(projMat, 45, canvas.width / canvas.height, 1, 2000);
gl.uniformMatrix4fv(uProj, false, projMat);

let cameraMat = mat4.create();

const worldMat = mat4.create();
mat4.translate(worldMat, worldMat, [0, 0, 0]);
const pointLightPos = vec3.fromValues(0, 2, 1.5);
let gloss = 64;
const coEfficient = lightAttenuationLookUp(30);

const render = () => {
    gl.useProgram(program);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); //
    mat4.identity(cameraMat);
    const cameraPos = vec3.fromValues(translateX, translateY, translateZ);

    cameraMat = lookAt(
        new Float32Array([translateX, translateY, translateZ]),
        new Float32Array([0, 0, 0])
    );
    mat4.invert(cameraMat, cameraMat);

    const cameraWorldPos = vec3.create();
    vec3.transformMat4(cameraWorldPos, cameraPos, worldMat);

    const pointLightWorldPos = vec3.create();

    vec3.transformMat4(pointLightWorldPos, pointLightPos, worldMat);
    uniforms.u_world = worldMat as any;
    uniforms.u_viewInv = cameraMat as any;
    uniforms.u_lightPos = pointLightWorldPos as any;
    uniforms.u_viewWorldPos = cameraWorldPos as any;
    uniforms.u_coefficient = coEfficient as any;
    uniforms.u_spotDir = [0, -1, -1] as any;
    uniforms.u_cutoff = [
        Math.cos((10 / 180) * Math.PI),
        Math.cos((9 / 180) * Math.PI),
    ] as any;
    uniforms.u_gloss = gloss as any;
    uniforms.u_proj = projMat as any;

    setAttribute(attribSetters, bufferInfo);
    setUniform(uniformSetters, uniforms);
    gl.drawArrays(gl.TRIANGLES, 0, pointPos.length / 3);
};

render();
ts
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);
    }
}
glsl
attribute vec4 a_position;
attribute vec3 a_color;
attribute vec3 a_normal;
uniform mat4 u_world;
uniform mat4 u_viewInv;
uniform mat4 u_proj;
varying vec3 v_color;
varying vec3 v_worldPos;
varying vec3 v_normal;
void main() {
    vec4 worldPos = u_world * a_position;
    vec4 worldNormal = u_world * vec4(a_normal, 1.0);
    v_worldPos = worldPos.xyz / worldPos.w;
    v_color = a_color;
    v_normal = worldNormal.xyz / worldNormal.w;
    gl_Position = u_proj * u_viewInv * worldPos;
}
glsl
precision mediump float;
varying vec3 v_color;
varying vec3 v_normal;
varying vec3 v_worldPos;
uniform vec3 u_viewWorldPos;
uniform float u_gloss;
uniform vec3 u_lightPos;
uniform vec3 u_coefficient;
uniform vec3 u_spotDir;
uniform vec2 u_cutoff;
void main() {
    float kc = u_coefficient[0];
    float kl = u_coefficient[1];
    float kq = u_coefficient[2];
    vec3 n = normalize(v_normal);
    vec3 lightDir = normalize(u_lightPos - v_worldPos);
    float dis = distance(u_lightPos, v_worldPos);
    vec3 viewDir = normalize(u_viewWorldPos - v_worldPos);
    vec3 r = normalize(2.0 * dot(n, lightDir) * n - lightDir);
    float atten = 1.0 / (kc + kl * dis + kq * dis * dis);
    float LdotN = dot(lightDir, n);
    float RdotV = dot(viewDir, r);
    float LdotS = dot(-lightDir, normalize(u_spotDir));
    float m = smoothstep(u_cutoff[0], u_cutoff[1], LdotS) * 0.8 + 0.2;
    vec3 dColor = vec3(1.0, 0.8, 0.5);
    vec3 sColor = vec3(1.0, 0.8, 0.5);
    vec3 ambient = vec3(0.2);
    vec3 diffuse = dColor * max(0.0, LdotN);
    vec3 specular = sColor * pow(max(0.0, RdotV), u_gloss);

    vec3 color = ambient + (diffuse + specular) * atten * m;

    color = pow(color, vec3(1.0 / 1.5));
    gl_FragColor = vec4(color, 1.0);
}