Appearance
码少,趣多
动机
在进行下一步的学习之前,我们需要整理一下我们之前写的代码。我们回顾一下我们之前编写的 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 传递值的工作,而我们只是需要提供一些必要的信息即可。
当然,我们现在编写的这一套”框架“需要建立在一些假设之上:
- 一个
attribute
变量就对应了一个WebGLBuffer
,我们不采用一个WebGLBuffer
对应多个attribute
变量的做法 - 似乎暂时没有别的约束了。
那我们要产出的辅助函数最终是一个什么东西呢?
我们希望可以通过某种 API,假设我们创建一个setAttribute
的 API,我们可以通过调用它来设置好所有的 attribute
变量的数据。类似的,我们也希望创建类似于setUniforms
这类的 API 来帮助我们设置好所有的 uniform
变量。
那么这两个 API 的参数又该如何设计?设计方法有很多。这里介绍一下作者的思路:
- 首先,参数中必须要包含
attribute
/uniform
变量真正的值! - 要往 WebGL 中传递数据的话,我们必须知道
attribute
/uniform
变量在 Shader 中的位置(Location)。 - 如何设置值同样也是需要我们考虑的部分,比如对于
attribute
变量来说,它是 3 维还是 4 维向量都需要显示的说明;对于uniform
变量来说,设置vec
和matrix
的值所对应的 API 都是不同的。 - 值与 location 还需要一一对应起来,这里我们采用相同的 key 来使值与 location 之间发生联系。
我们先来看 uniform
变量。
给 Uniform 变量设置值
按上面的思考方式,我们设计setUniforms
这个 API,首先,我们需要接受真正的 uniform
值,所以其中一个参数必然是包含了所有的uniform
变量的值。
另外,还需要知道对于不同的 uniform
值,我们怎样去设置它。
我们将入参设计为:
ts
setUniform(setters: Record<string, (v: any) => void>, uniformValues: Record<string, any>)
setters
表示对于每一个 uniform
变量,如何设置其值。setters
和 uniformValues
中的 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);
这样,我们就不用手动的枚举所有的 uniform
和attribute
变量了。
完善一下上面的代码,可以写作:
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
变量传值。
- 创建
WebGLBuffer
:const buffer = gl.createBuffer();
- 绑定 Buffer:
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
- 往 Buffer 中传入数据:
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(pointPos), gl.STATIC_DRAW);
- 获取
attribute
变量在 shader 中的位置:const a_position = gl.getAttribLocation(program, 'a_position');
- 启用这个
attribute
变量:gl.enableVertexAttribArray(a_position);
- 告诉 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
TranslateY
TranslateZ
Gloss
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);
}