做一个独一无二的重力版个人博客(二)
在上一篇基础上将画面改成WebGL 3D效果。WebGL是OpenGLES的封装,使用方法跟OpenGL基本相同。在WebGL基础上还有Three.js等更高级的框架,能实现更好的效果。这里为了简便易于学习,我直接使用WebGL。
1 WebGL
Github上找了一下,有mdn的官方demo,一步步实现WebGL的基本功能,很不错。
https://github.com/mdn/webgl-examples
demo的sample7实现了一个方块的展示和光照,我们在它的基础上稍微修改一下。
1.1 修改模型大小
博客图片的大小不一样,因此创建VBO时需要修改模型数据大小。将模型里的数据改成x,y,z,这样就能根据图片大小动态配置了。
如果不修改模型大小,也可以通过后面变换矩阵实现,但会导致图片非常模糊。
function createPos(x,y,z) {
return positions = [
-x, -y, z,
x, -y, z,
x, y, z,
-x, y, z,
-x, -y, -z,
-x, y, -z,
x, y, -z,
x, -y, -z,
-x, y, -z,
-x, y, z,
x, y, z,
x, y, -z,
-x, -y, -z,
x, -y, -z,
x, -y, z,
-x, -y, z,
x, -y, -z,
x, y, -z,
x, y, z,
x, -y, z,
-x, -y, -z,
-x, -y, z,
-x, y, z,
-x, y, -z,
];
}
1.2 绘制模型
这是最重要的一步。
跟上一篇一样,遍历world,将body找出来,根据id找到图片,根据body的位置和旋转方向对模型的矩阵进行变换。
模型的位置跟3D位置和视角有关,几何运算会很复杂。这里我给矩阵的位移和缩放乘以一个简单的系数mul,修改这个系数观察实际位置,就能得到跟实际很接近的结果。
// body
var id = body.GetUserData();
if (id > 0) {
var c = body.GetWorldCenter();
var w = myBlogImages.get(id).width;
var h = myBlogImages.get(id).height;
var a = body.GetAngle();
var modelViewMatrix = mat4.create();
var mul = 0.214
mat4.translate(modelViewMatrix, // destination matrix
modelViewMatrix, // matrix to translate
[c.x * mul, c.y * mul, -6.0]); // amount to translate
mat4.rotate(modelViewMatrix, // destination matrix
modelViewMatrix, // matrix to rotate
a, // amount to rotate in radians
[0, 0, 1]); // axis to rotate around (Z)
mul = myBlogScale * 0.106
var myCache = myCacheMap.get(id);
if (myCache == null) {
myCache = new MyCache()
myCache.tex = loadTexture(gl, myBlogImages.get(id), w, h)
myCache.buffers = initBuffers(gl, w * mul, h * mul)
myCacheMap.set(id, myCache)
}
drawOne(gl, programInfo, myCache.buffers, myCache.tex, deltaTime, projectionMatrix, modelViewMatrix)
}
body = body.GetNext();
1.3 性能优化
如果每次绘制时都重新创建GPU数组会非常卡顿,将它们缓存到一个Map里面,根据id来取,只在第一次读取时创建,这样就比较流畅了。
var myCacheMap = new Map();
var myCache = myCacheMap.get(id);
if (myCache == null) {
myCache = new MyCache()
myCache.tex = loadTexture(gl, myBlogImages.get(id), w, h)
myCache.buffers = initBuffers(gl, w * mul, h * mul)
myCacheMap.set(id, myCache)
}
1.4 调试技巧
前面说过,要让WebGL绘制的位置和大小准确需要多次调整缩放比较参数mul,那么如何判断结果是否准确呢?
我在index.html里原来的canvas下面增加了一个canvas,它们一个用来绘制原来的2d图形,另一个绘制WebGL 3d图形。默认它们是上下放置的,在webgl.css里给下面的加上位移后,就可以让它们完全重叠,再给上层的加上透明度,就可以实时对照两个canvas里面的物体位置了。
<div class='parent' style="margin:auto;width:1280px;padding:2px;border:0px solid #888;text-align:left">
<canvas id="glcanvas" width="1280" height="720" tabindex='1'></canvas>
</div>
<div class='child' style="margin:auto;width:1280px;padding:2px;border:0px solid #888;text-align:left">
<canvas id="canvas" width="1280" height="720" tabindex='1'></canvas>
</div>
.child {
position: relative;
top: -724px;
opacity: 0.2;
}
(位置大小没重叠时如图所示)
webgl-demo.js用来绘制3d图形,embox2d-html5canvas-testbed.js用来绘制2d图形并接收鼠标事件。调试完成后将2d的canvas remove掉,对象换成2d的canvas。这样3d图形就可以接收原来的鼠标事件了。
function init() {
document.getElementById("debugDiv").hidden = hideDebugDiv
canvas = document.getElementById("canvas");
context = canvas.getContext( '2d' );
if (hideCanvas) {
canvas.remove()
canvas = document.getElementById("glcanvas");
}
...
2 其他优化
2.1 添加镜面光
mdn的demo里只有环境光和漫反射光,没有镜面光,我原来在OpenGLES上写过,在着色器里添加一下。
这里有一点语法差异,OpenGLES3.0里的in和out要改成varying highp。
const vsSource = `
attribute vec4 aVertexPosition;
attribute vec3 aVertexNormal;
attribute vec2 aTextureCoord;
uniform mat4 uNormalMatrix;
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
varying highp vec3 aFragPos;
varying highp vec3 aNormal;
varying highp vec2 aTexCoord;
void main(void) {
gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
aFragPos = vec3(uNormalMatrix * aVertexPosition);
aNormal = vec3(uNormalMatrix * vec4(aVertexNormal, 1.0));
aTexCoord = aTextureCoord;
}
`;
// Fragment shader program
const fsSource = `
precision mediump float;
varying highp vec3 aFragPos;
varying highp vec3 aNormal;
varying highp vec2 aTexCoord;
// out vec4 fragColor;
uniform sampler2D uSampler;
vec3 lightColor = vec3(1.0, 1.0, 1.0);
vec3 lightPos= vec3(0.0, 0.5, 0.8);
vec3 viewPos= vec3(0.0, 0.0, 1.0);
void main() {
// 纹理颜色
vec4 texColor = texture2D(uSampler, aTexCoord);
if (texColor.a == 0.0) {
texColor = vec4(1,1,1,1);
}
// 环境光
float ambientStrength = 0.2;
vec3 ambient = ambientStrength * lightColor;
// 漫反射光
vec3 norm = normalize(aNormal);
vec3 lightDir = normalize(lightPos - aFragPos);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor;
// 镜面光
float specularStrength = 0.2;
vec3 viewDir = normalize(viewPos - aFragPos);
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 2.0);
vec3 specular = specularStrength * spec * lightColor;
gl_FragColor = vec4(ambient + diffuse + specular, 1.0) * texColor;
}
`;
2.2 添加招牌
博客标题“Rome753's Blog”做成招牌的样式,放在页面上方,它可以跟别的物体碰撞移动,又有一定的弹性能恢复原位。可以用Box2D里面的DistanceJoint实现。DistanceJoint可以让两个Body之间有固定距离,这个距离可以是0,也可以有弹性。
在tests/fallingImages.js里创建body时加上DistanceJoint用来固定招牌,创建3个防止它旋转。
if (id == 1) {
var def = new b2DistanceJointDef();
def.collideConnected = false;
def.frequencyHz = 2;
def.dampingRatio = 1;
def.length = 0;
def.bodyA = groundBody;
def.bodyB = body;
def.localAnchorA.x = -1;
def.localAnchorA.y = 10;
def.localAnchorB.x = -1;
def.localAnchorB.y = 0;
world.CreateJoint(def);
def.localAnchorA.x = 1;
def.localAnchorB.x = 1;
world.CreateJoint(def);
def.localAnchorA.x = 0;
def.localAnchorB.y = 1;
world.CreateJoint(def);
}
2.3 添加重力传感器
获取设备的加速度数据,将x,y方向的加速度设置成world的重力即可。
注意方向要取反,并且devicemotion只有在https网站下才会生效,并且不同设备还有兼容性问题。
if (window.DeviceMotionEvent) {
alert('Support DeviceMotionEvent')
window.addEventListener('devicemotion', function(event) {
if (world == null) {
return;
}
var ax = event.accelerationIncludingGravity.x;
var ay = event.accelerationIncludingGravity.y;
var g = world.GetGravity();
g.x = -ax * 2;
g.y = -ay * 2;
world.SetGravity(g);
});
} else {
alert('Not Support DeviceMotionEvent')
}