一、思路
- 将法线x,y,z三个分量存储在法线贴图存储在r,g,b三个通道中
- 利用模型的切线T和法向量N(模型的真正法线)计算副切线构建TBN矩阵
- 从法线贴图中rgb值转化到-1到1,即为切线空间normal
- TBN矩阵乘normal转化为世界空间切线
- 后续步骤通过光照模型计算
二、代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Title</title>
<style>
body {
margin: 0;
overflow: hidden;
}
#canvas {
margin: 0;
display: block;
}
div {
position: absolute;
left: 100px;
top: 100px;
z-index: 999;
}
</style>
</head>
<body onload="main()">
<canvas id="canvas" width="1024" height="1024"></canvas>
</body>
<script src="/lib/webgl-utils.js"></script>
<script src="/lib/webgl-debug.js"></script>
<script src="/lib/cuon-utils.js"></script>
<script src="/lib/cuon-matrix.js"></script>
<script>
var canvas = document.getElementById("canvas");
var vertexShaderSource = `
attribute vec4 a_Position;
attribute vec2 a_TexCoord;
attribute vec3 a_Color;
uniform mat4 u_MvpMatrix;
varying vec2 v_TexCoord;
varying vec3 v_Color;
varying mat3 v_TBN;
void main(){
gl_Position = u_MvpMatrix * a_Position;
v_TexCoord = a_TexCoord;
v_Color = a_Color;
vec3 normal=normalize(vec3(0.0,0.0,1.0));
vec3 tanVec=vec3(1.0,0.0,0.0);
vec3 bitTan=normalize(cross(tanVec,normal));
v_TBN=mat3(tanVec,bitTan,normal);
}`;
var fragmentShaderSource = `
#ifdef GL_ES
precision mediump float;
#endif
uniform sampler2D u_Sampler;
uniform float u_Number;
varying vec2 v_TexCoord;
varying vec3 v_Color;
varying mat3 v_TBN;
void main(){
vec3 lightdir=vec3(1.0,-1.0,0.0);
vec3 lightColor=vec3(5.0/255.0,140.0/255.0,255.0/255.0);
vec4 textureColor=texture2D(u_Sampler,vec2(fract(u_Number)+v_TexCoord.x,fract(u_Number)+v_TexCoord.y));
//转化为-1->1
vec3 normal=normalize(textureColor.rgb*2.0-1.0);
vec3 worldNormal=normalize(v_TBN*normal);
//计算漫反射
float dotL=max(0.0,dot(normalize(-lightdir),worldNormal));
vec3 diffcuseColor=lightColor*dotL;
gl_FragColor =vec4(diffcuseColor+vec3(0.0,0.1,0.2),0.9);
}`;
var offset_width = 1024;
var offset_height = 1024;
var z_planeAngle = 0;
var plane_modelMatrix = new Matrix4();
plane_modelMatrix.setTranslate(0, 1, 0);
const viewMatrix = new Matrix4();
viewMatrix.setLookAt(0.0, 3.5, 5.0, 0.0, 0, 0.0, 0.0, 1, 0);
function main() {
const moveSpeed = 0.1;
document.addEventListener("keydown", (e) => {
if (e.key === "ArrowUp") {
cubeModelMatrix.translate(0, moveSpeed, 0);
} else if (e.key === "ArrowDown") {
cubeModelMatrix.translate(0, -moveSpeed, 0);
} else if (e.key === "ArrowLeft") {
cubeModelMatrix.translate(-moveSpeed, 0, 0);
} else if (e.key === "ArrowRight") {
cubeModelMatrix.translate(moveSpeed, 0, 0);
}
});
var gl = getWebGLContext(canvas);
if (!gl) {
console.log("无法获取WebGL的上下文");
return;
}
const program = createProgram(
gl,
vertexShaderSource,
fragmentShaderSource
);
if (!program) {
console.error("创建程序失败!");
}
program.a_Position = gl.getAttribLocation(program, "a_Position");
program.a_TexCoord = gl.getAttribLocation(program, "a_TexCoord");
program.a_Color = gl.getAttribLocation(program, "a_Color");
program.u_MvpMatrix = gl.getUniformLocation(program, "u_MvpMatrix");
program.u_Sampler = gl.getUniformLocation(program, "u_Sampler");
program.u_Number = gl.getUniformLocation(program, "u_Number");
if (
program.a_Position < 0 ||
program.a_TexCoord < 0 ||
!program.u_MvpMatrix ||
!program.a_Color ||
!program.u_Sampler ||
!program.u_Number
) {
console.log("无法获取到变量的存储位置");
return;
}
var plane = initVertexBuffersForPlane(gl);
if (!plane) {
console.log("存入缓冲区数据失败");
return;
}
var texture = createTexture(gl);
if (!texture) {
console.log("无法创建纹理缓冲区");
return;
}
gl.enable(gl.DEPTH_TEST);
var viewProjectMatrix = new Matrix4();
viewProjectMatrix.setPerspective(
80.0,
canvas.width / canvas.height,
0.1,
1000.0
);
viewProjectMatrix.multiply(viewMatrix);
var currentAngle = 0.0;
var number = 0;
function tick() {
currentAngle = animate(currentAngle);
number += 0.001;
draw(
gl,
plane,
currentAngle,
texture,
viewProjectMatrix,
program,
number
);
requestAnimationFrame(tick);
}
tick();
}
const rotateMatrix = new Matrix4();
function draw(
gl,
plane,
angle,
texture,
viewProjectMatrix,
program,
number
) {
gl.viewport(0, 0, canvas.width, canvas.height);
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.useProgram(program);
gl.uniform1f(program.u_Number, number);
drawTexturedPlane(gl, program, plane, 0.0, texture, viewProjectMatrix);
}
var g_mvpMatrix = new Matrix4();
function drawTexturedPlane(
gl,
program,
obj,
angle,
texture,
viewProjectMatrix
) {
const modelMatrix = new Matrix4();
modelMatrix.set(plane_modelMatrix);
modelMatrix.scale(4, 1, 4);
g_mvpMatrix.set(viewProjectMatrix);
g_mvpMatrix.multiply(modelMatrix);
gl.uniformMatrix4fv(program.u_MvpMatrix, false, g_mvpMatrix.elements);
gl.uniform1i(program.u_Sampler, 0);
drawTexturedObject(gl, program, obj, texture);
}
function drawTexturedObject(gl, program, obj, texture) {
initAttributeVariable(gl, program.a_Position, obj.vertexBuffer);
initAttributeVariable(gl, program.a_Color, obj.colorBuffer);
initAttributeVariable(gl, program.a_TexCoord, obj.texCoordBuffer);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, obj.indexBuffer);
gl.drawElements(gl.TRIANGLES, obj.numIndices, obj.indexBuffer.type, 0);
}
function initAttributeVariable(gl, a_attribute, buffer) {
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.vertexAttribPointer(a_attribute, buffer.num, buffer.type, false, 0, 0);
gl.enableVertexAttribArray(a_attribute);
}
var angle_step = 30;
var last = +new Date();
function animate(angle) {
var now = +new Date();
var elapsed = now - last;
last = now;
var newAngle = angle + (angle_step * elapsed) / 1000.0;
return newAngle % 360;
}
function createTexture(gl) {
var img = new Image();
img.src = "/image/waterNormals.jpg";
var texture = gl.createTexture();
if (!texture) {
console.log("无法创建纹理缓冲区");
return null;
}
img.onload = function () {
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
gl.RGBA,
gl.UNSIGNED_BYTE,
img
);
gl.bindTexture(gl.TEXTURE_2D, null);
};
return texture;
}
function initVertexBuffersForPlane(gl) {
var vertices = new Float32Array([
1.0,
0.0,
1.0,
-1.0,
0.0,
1.0,
-1.0,
0.0,
-1.0,
1.0,
0.0,
-1.0,
]);
var colors = new Float32Array([
0.0,
0.0,
1.0,
0.0,
0.0,
1.0,
0.0,
0.0,
1.0,
0.0,
0.0,
1.0,
]);
var texCoords = new Float32Array([
1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0,
]);
var indices = new Uint8Array([0, 3, 2, 0, 2, 1]);
var obj = {};
obj.vertexBuffer = initArrayBufferForLaterUse(gl, vertices, 3, gl.FLOAT);
obj.texCoordBuffer = initArrayBufferForLaterUse(
gl,
texCoords,
2,
gl.FLOAT
);
obj.colorBuffer = initArrayBufferForLaterUse(gl, colors, 3, gl.FLOAT);
obj.indexBuffer = initElementArrayBufferForLaterUse(
gl,
indices,
gl.UNSIGNED_BYTE
);
if (!obj.vertexBuffer || !obj.texCoordBuffer || !obj.indexBuffer)
return null;
obj.numIndices = indices.length;
gl.bindBuffer(gl.ARRAY_BUFFER, null);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
return obj;
}
function initArrayBufferForLaterUse(gl, data, num, type) {
var buffer = gl.createBuffer();
if (!buffer) {
console.log("无法创建缓冲区对象");
return null;
}
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
buffer.num = num;
buffer.type = type;
return buffer;
}
function initElementArrayBufferForLaterUse(gl, data, type) {
var buffer = gl.createBuffer();
if (!buffer) {
console.log("无法创建着色器");
return null;
}
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, data, gl.STATIC_DRAW);
buffer.type = type;
return buffer;
}
</script>
</html>
三、结果
