WebGL 利用FBO完成立方体贴图。

时间:2021-12-06 07:19:09

这篇主要记录WebGL的一些基本要点,顺便也学习下如何使用FBO与环境贴图。先看下效果图(需要支持WebGL,Chrome,火狐,IE11)。

  主要实现过程如下,先用FBO输出当前环境在立方体纹理中,再画出当前立方体,最后画球,并且把FBO关联的纹理贴在这个球面上。

  开始WebGL时,最好有些OpenGL基础,在前面讲Obj完善与MD2时,大家可能已经发现了,因为着色器的添加使用,原来一些Opengl大部分API已经没有使用。WebGL就和这差不多,大部分功能是着色器完成主要功能,记录下主要过程,大家可以比较下前面的,看看是不是很像,为了熟悉WebGL基本功能,本文没有利用比较完善的框架,只是用到一个帮助计算矩阵的框架(gl-matrix.js).

  和使用OpenGL一样,我们要初始化使用环境,提取一些全局使用量。相关代码如下:

WebGL 利用FBO完成立方体贴图。
 1 var gl;//WebGLRenderingContext
2 var cubeVBO;//Cube buffer ID
3 var sphereVBO;//球体VBO
4 var sphereEBO;//球体EBO
5 var cubeTexID;//立方体纹理ID
6 var fboBuffer;//桢缓存对象
7 var glCubeProgram;//立方体着色器应用
8 var glSphereProgram;//球体着色器应用
9
10 var fboWidth = 512;//桢缓存绑定纹理宽度
11 var fboHeight = 512;//桢缓存绑定纹理高度
12 var targets;//立方体贴图六个方向
13
14 var pMatrix = mat4.create();//透视矩阵
15 var vMatrix = mat4.create();//视图矩阵
16 var eyePos = vec3.fromValues(0.0, 1.0, 0.0);//眼睛位置
17 var eyeLookat = vec3.fromValues(0.0, -0.0, 0.0);//眼睛方向
18 var spherePos = vec3.fromValues(0.0, -0.0, 0.0);//球体位置
19 var canvanName;
20
21 function webGLStart(cName) {
22 canvanName = cName;
23 InitWebGL();
24 InitCubeShader();
25 InitSphereShader();
26 InitCubeBuffer();
27 InitSphereBuffer();
28 InitFBOCube();
29 //RenderFBO();
30 gl.clearColor(0.0, 0.0, 0.0, 1.0);
31 gl.enable(gl.DEPTH_TEST);
32 tick();
33 }
34
35 function InitWebGL() {
36 //var canvas = document.getElementById(canvanName);
37 InitGL(canvanName);
38 }
39
40 function InitGL(canvas) {
41 try {
42 //WebGLRenderingContext
43 gl = canvas.getContext("experimental-webgl");
44 gl.viewportWidth = canvas.width;
45 gl.viewportHeight = canvas.height;
46
47 targets = [gl.TEXTURE_CUBE_MAP_POSITIVE_X,
48 gl.TEXTURE_CUBE_MAP_NEGATIVE_X,
49 gl.TEXTURE_CUBE_MAP_POSITIVE_Y,
50 gl.TEXTURE_CUBE_MAP_NEGATIVE_Y,
51 gl.TEXTURE_CUBE_MAP_POSITIVE_Z,
52 gl.TEXTURE_CUBE_MAP_NEGATIVE_Z];
53 } catch (e) { }
54 if (!gl) { alert("你的浏览器不支持WebGL"); }
55 }

初始化

  在这里,我们初始化在网页中WebGL的上下方环境,并给出一系列初始化过程。下面先给出房间,也就是其中立方体的相关代码。

WebGL 利用FBO完成立方体贴图。
 1 function InitCubeShader() {
2 //WebGLShader
3 var shader_vertex = GetShader("cubeshader-vs");
4 var shader_fragment = GetShader("cubeshader-fs");
5 //WebglCubeProgram
6 glCubeProgram = gl.createProgram();
7 gl.attachShader(glCubeProgram, shader_vertex);
8 gl.attachShader(glCubeProgram, shader_fragment);
9 gl.linkProgram(glCubeProgram);
10 if (!gl.getProgramParameter(glCubeProgram, gl.LINK_STATUS)) {
11 alert("Shader hava error.");
12 }
13 gl.useProgram(glCubeProgram);
14 glCubeProgram.positionAttribute = gl.getAttribLocation(glCubeProgram, "a_position");
15 glCubeProgram.normalAttribute = gl.getAttribLocation(glCubeProgram, "a_normal");
16 glCubeProgram.texCoordAttribute = gl.getAttribLocation(glCubeProgram, "a_texCoord");
17
18 glCubeProgram.view = gl.getUniformLocation(glCubeProgram, "view");
19 glCubeProgram.perspective = gl.getUniformLocation(glCubeProgram, "perspective");
20 }
21
22 function InitCubeBuffer() {
23 var cubeData = [
24 -10.0, -10.0, -10.0, 0.0, 0.0, -10.0, 1.0, 0.0,
25 -10.0, 10.0, -10.0, 0.0, 0.0, -10.0, 1.0, 1.0,
26 10.0, 10.0, -10.0, 0.0, 0.0, -10.0, 0.0, 1.0,
27
28 10.0, 10.0, -10.0, 0.0, 0.0, -10.0, 0.0, 1.0,
29 10.0, -10.0, -10.0, 0.0, 0.0, -10.0, 0.0, 0.0,
30 -10.0, -10.0, -10.0, 0.0, 0.0, -10.0, 1.0, 0.0,
31
32 -10.0, -10.0, 10.0, 0.0, 0.0, 10.0, 0.0, 0.0,
33 10.0, -10.0, 10.0, 0.0, 0.0, 10.0, 1.0, 0.0,
34 10.0, 10.0, 10.0, 0.0, 0.0, 10.0, 1.0, 1.0,
35
36 10.0, 10.0, 10.0, 0.0, 0.0, 10.0, 1.0, 1.0,
37 -10.0, 10.0, 10.0, 0.0, 0.0, 10.0, 0.0, 1.0,
38 -10.0, -10.0, 10.0, 0.0, 0.0, 10.0, 0.0, 0.0,
39
40 -10.0, -10.0, -10.0, 0.0, -10.0, 0.0, 0.0, 0.0,
41 10.0, -10.0, -10.0, 0.0, -10.0, 0.0, 1.0, 0.0,
42 10.0, -10.0, 10.0, 0.0, -10.0, 0.0, 1.0, 1.0,
43
44 10.0, -10.0, 10.0, 0.0, -10.0, 0.0, 1.0, 1.0,
45 -10.0, -10.0, 10.0, 0.0, -10.0, 0.0, 0.0, 1.0,
46 -10.0, -10.0, -10.0, 0.0, -10.0, 0.0, 0.0, 0.0,
47
48 10.0, -10.0, -10.0, 10.0, 0.0, 0.0, 0.0, 0.0,
49 10.0, 10.0, -10.0, 10.0, 0.0, 0.0, 1.0, 0.0,
50 10.0, 10.0, 10.0, 10.0, 0.0, 0.0, 1.0, 1.0,
51
52 10.0, 10.0, 10.0, 10.0, 0.0, 0.0, 1.0, 1.0,
53 10.0, -10.0, 10.0, 10.0, 0.0, 0.0, 0.0, 1.0,
54 10.0, -10.0, -10.0, 10.0, 0.0, 0.0, 0.0, 0.0,
55
56 10.0, 10.0, -10.0, 0.0, 10.0, 0.0, 0.0, 0.0,
57 -10.0, 10.0, -10.0, 0.0, 10.0, 0.0, 1.0, 0.0,
58 -10.0, 10.0, 10.0, 0.0, 10.0, 0.0, 1.0, 1.0,
59
60 -10.0, 10.0, 10.0, 0.0, 10.0, 0.0, 1.0, 1.0,
61 10.0, 10.0, 10.0, 0.0, 10.0, 0.0, 0.0, 1.0,
62 10.0, 10.0, -10.0, 0.0, 10.0, 0.0, 0.0, 0.0,
63
64 -10.0, 10.0, -10.0, -10.0, 0.0, 0.0, 0.0, 0.0,
65 -10.0, -10.0, -10.0, -10.0, 0.0, 0.0, 1.0, 0.0,
66 -10.0, -10.0, 10.0, -10.0, 0.0, 0.0, 1.0, 1.0,
67
68 -10.0, -10.0, 10.0, -10.0, 0.0, 0.0, 1.0, 1.0,
69 -10.0, 10.0, 10.0, -10.0, 0.0, 0.0, 0.0, 1.0,
70 -10.0, 10.0, -10.0, -10.0, 0.0, 0.0, 0.0, 0.0,
71 ];
72 cubeVBO = gl.createBuffer();
73 gl.bindBuffer(gl.ARRAY_BUFFER, cubeVBO);
74 gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(cubeData), gl.STATIC_DRAW);
75 }
76
77 function RenderCube() {
78 gl.useProgram(glCubeProgram);
79 gl.bindBuffer(gl.ARRAY_BUFFER, cubeVBO);
80
81 gl.vertexAttribPointer(glCubeProgram.positionAttribute, 3, gl.FLOAT, false, 32, 0);
82 gl.enableVertexAttribArray(glCubeProgram.positionAttribute);
83
84 gl.vertexAttribPointer(glCubeProgram.normalAttribute, 3, gl.FLOAT, false, 32, 12);
85 gl.enableVertexAttribArray(glCubeProgram.normalAttribute);
86
87 gl.vertexAttribPointer(glCubeProgram.texCoordAttribute, 2, gl.FLOAT, false, 32, 24);
88 gl.enableVertexAttribArray(glCubeProgram.texCoordAttribute);
89
90 gl.uniformMatrix4fv(glCubeProgram.view, false, vMatrix);
91 gl.uniformMatrix4fv(glCubeProgram.perspective, false, pMatrix);
92
93 gl.drawArrays(gl.TRIANGLES, 0, 36);
94 }

立方体

  上面的代码主要分为初始化立方体的着色器对象,初始化相关缓存,然后绘制立方体,可以说在Opengl中,如果用着色器来画,过程也是差不多的,在Opengl里,已经没有固定管线的一些功能如InterleavedArrays来指定是顶点还是法线或是纹理了,统一用vertexAttribPointer来传递应用程序与着色器之间的数据。在前面 MD2桢动画实现里面后面的参数传递改进版也有相关应用。

  相应着立方体着色器主要代码如下.

WebGL 利用FBO完成立方体贴图。
 1     <script id="cubeshader-fs" type="x-shader/x-fragment">
2 precision mediump float;
3
4 varying vec3 normal;
5 varying vec3 tex1;
6 varying vec3 tex2;
7 void main( void )
8 {
9 float x = tex1.x * 6.28 * 8.0; //2兀 * 8
10 float y = tex1.y * 6.28 * 8.0; //2兀 * 8
11 //cos(x)= 8个 (1 -1 1)
12 gl_FragColor = vec4(tex2,1.0) * vec4(sign(cos(x)+cos(y))); //
13 //gl_FragColor = vec4(normal*vec3(0.5)+vec3(0.5), 1);
14 }
15 </script>
16
17 <script id="cubeshader-vs" type="x-shader/x-vertex">
18 attribute vec3 a_position;
19 attribute vec3 a_normal;
20 attribute vec2 a_texCoord;
21
22 uniform mat4 view;
23 uniform mat4 perspective;
24 varying vec3 normal;
25 varying vec3 tex1;
26 varying vec3 tex2;
27 void main( void )
28 {
29 gl_Position = perspective * view * vec4(a_position,1.0);
30 normal = a_normal;
31 tex1 = vec3(a_texCoord,0.0);
32 tex2 = normalize(a_position)*0.5+0.5;
33 }
34 </script>

立方体着色器实现

  着色器中,已经没有ftransform()功能可供调用,要自己传递如模型,视图,透视矩阵,在这里,模型是以原点为中心来绘画,意思模型视图矩阵也就是视图矩阵,所以屏幕位置的计算只需要视图和透视矩阵。在片断着色器中,x,y是从顶点着色器中的纹理坐标传递过来,相应过程6.28*8.0,相当于8个360度,用于控制立方体上的方块显示,而tex2是着色器中的顶点映射[0,1]的值,分别给立方体的六面分别设置不同的意思,然后用二个矢量的乘积来混合这二种颜色显示,gl_FragColor = vec4(tex2,1.0) * vec4(sign(cos(x)+cos(y)))。

  在显示球体之前,应该先生成当前环境的立方体绘图,在这里使用FBO,先生成桢缓存和立方体绘理,并关联,然后以原点为中心,分别向上下左右前右绘图,然后利用桢缓冲分别输出到立方体上的六个面,主要代码如下:

WebGL 利用FBO完成立方体贴图。
 1 function InitFBOCube() {
2 // WebGLFramebuffer
3 fboBuffer = gl.createFramebuffer();
4 gl.bindFramebuffer(gl.FRAMEBUFFER, fboBuffer);
5 fboBuffer.width = 512;
6 fboBuffer.height = 512;
7
8 cubeTexID = gl.createTexture();
9 gl.bindTexture(gl.TEXTURE_CUBE_MAP, cubeTexID);
10 gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
11 gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
12 gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
13 gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
14
15 for (var i = 0; i < targets.length; i++) {
16 gl.texImage2D(targets[i], 0, gl.RGBA, fboBuffer.width, fboBuffer.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
17 }
18 gl.bindFramebuffer(gl.FRAMEBUFFER, null);
19 }
20
21 function RenderFBO() {
22 gl.disable(gl.DEPTH_TEST);
23 gl.viewport(0, 0, fboBuffer.width, fboBuffer.height);
24 gl.clearColor(0.0, 0.0, 0.0, 1.0);
25 gl.bindFramebuffer(gl.FRAMEBUFFER, fboBuffer);
26 for (var i = 0; i < targets.length; i++) {
27 gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, targets[i], cubeTexID, null);
28 gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
29 }
30
31 mat4.perspective(pMatrix, 45, fboBuffer.width / fboBuffer.height, 0.1, 100.0);
32 for (var i = 0; i < targets.length; i++) {
33 gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, targets[i], cubeTexID, null);
34 var lookat = vec3.create();
35 var up = vec3.create();
36 up[1] = 1.0;
37 if (i == 0) {
38 lookat[0] = -1.0;
39 } else if (i == 1) {
40 lookat[0] = 1.0;
41 } else if (i == 2) {
42 lookat[1] = -1.0;
43 up[0] = 1.0;
44 } else if (i == 3) {
45 lookat[1] = 1.0;
46 up[0] = 1.0;
47 } else if (i == 4) {
48 lookat[2] == -1.0;
49 } else if (i == 5) {
50 lookat[2] = 1.0;
51 } else {
52 }
53 //vec3.fromValues(0.0, 0.0, 0.0)
54 vMatrix = mat4.create();
55 mat4.lookAt(vMatrix, vec3.fromValues(0.0, 0.0, 0.0), lookat, up);
56 //mat4.scale(vMatrix, vMatrix, vec3.fromValues(-1.0, -1.0, -1.0));
57 //mat4.translate(vMatrix, vMatrix, spherePos);
58 RenderCube();
59 }
60 gl.bindFramebuffer(gl.FRAMEBUFFER, null);
61 gl.enable(gl.DEPTH_TEST);
62 }

FBO与立方体纹理

  在上面不知是gl-matrix提供的矩阵算法有问题,还是本来应该这样,在上下面的时候生成的纹理图不对,需要偏转摄像机的向上矢量。因为这是摄像机位置与目标的生成的Z轴和设定的UP轴平行了,这样导致不能正确计算X轴,然后对应的UP轴也计算不出来,相应视图矩阵出现错误。

  最后是球体的绘画,代码主要和立方体的差不多,注意球体的顶点算法。

WebGL 利用FBO完成立方体贴图。
 1 function InitSphereShader() {
2 //WebGLShader
3 var shader_vertex = GetShader("sphereshader-vs");
4 var shader_fragment = GetShader("sphereshader-fs");
5 //WebglCubeProgram
6 glSphereProgram = gl.createProgram();
7 gl.attachShader(glSphereProgram, shader_vertex);
8 gl.attachShader(glSphereProgram, shader_fragment);
9 gl.linkProgram(glSphereProgram);
10 if (!gl.getProgramParameter(glSphereProgram, gl.LINK_STATUS)) {
11 alert("Shader hava error.");
12 }
13 glSphereProgram.positionAttribute = gl.getAttribLocation(glSphereProgram, "a_position");
14 glSphereProgram.normalAttribute = gl.getAttribLocation(glSphereProgram, "a_normal");
15
16 glSphereProgram.eye = gl.getUniformLocation(glSphereProgram, "eye");
17 glSphereProgram.mapCube = gl.getUniformLocation(glSphereProgram, "mapCube");
18
19 glSphereProgram.model = gl.getUniformLocation(glSphereProgram, "model");
20 glSphereProgram.view = gl.getUniformLocation(glSphereProgram, "view");
21 glSphereProgram.perspective = gl.getUniformLocation(glSphereProgram, "perspective");
22 }
23
24 function InitSphereBuffer() {
25 var radius = 1;
26 var segments = 16;
27 var rings = 16;
28 var length = segments * rings * 6;
29 var sphereData = new Array();
30 var sphereIndex = new Array();
31 for (var y = 0; y < rings; y++) {
32 var phi = (y / (rings - 1)) * Math.PI;
33 for (var x = 0; x < segments; x++) {
34 var theta = (x / (segments - 1)) * 2 * Math.PI;
35 sphereData.push(radius * Math.sin(phi) * Math.cos(theta));
36 sphereData.push(radius * Math.cos(phi));
37 sphereData.push(radius * Math.sin(phi) * Math.sin(theta));
38 sphereData.push(Math.sin(phi) * Math.cos(theta));
39 sphereData.push(radius * Math.cos(phi))
40 sphereData.push(Math.sin(phi) * Math.sin(theta));
41 }
42 }
43 for (var y = 0; y < rings - 1; y++) {
44 for (var x = 0; x < segments - 1; x++) {
45 sphereIndex.push((y + 0) * segments + x);
46 sphereIndex.push((y + 1) * segments + x);
47 sphereIndex.push((y + 1) * segments + x + 1);
48
49 sphereIndex.push((y + 1) * segments + x + 1);
50 sphereIndex.push((y + 0) * segments + x + 1)
51 sphereIndex.push((y + 0) * segments + x);
52 }
53 }
54 sphereVBO = gl.createBuffer();
55 gl.bindBuffer(gl.ARRAY_BUFFER, sphereVBO);
56 gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(sphereData), gl.STATIC_DRAW);
57 sphereVBO.numItems = segments * rings;
58 sphereEBO = gl.createBuffer();
59 gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, sphereEBO);
60 gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(sphereIndex), gl.STATIC_DRAW);
61 sphereEBO.numItems = sphereIndex.length;
62 }
63
64 function RenderSphere() {
65 gl.useProgram(glSphereProgram);
66 gl.bindBuffer(gl.ARRAY_BUFFER, sphereVBO);
67
68 gl.vertexAttribPointer(glSphereProgram.positionAttribute, 3, gl.FLOAT, false, 24, 0);
69 gl.enableVertexAttribArray(glSphereProgram.positionAttribute);
70
71 gl.vertexAttribPointer(glSphereProgram.normalAttribute, 3, gl.FLOAT, false, 24, 12);
72 gl.enableVertexAttribArray(glSphereProgram.normalAttribute);
73
74 var mMatrix = mat4.create();
75 mat4.translate(mMatrix, mMatrix, spherePos);
76 gl.uniform3f(glSphereProgram.eye, eyePos[0],eyePos[1],eyePos[2]);
77 gl.uniformMatrix4fv(glSphereProgram.model, false, mMatrix);
78 gl.uniformMatrix4fv(glSphereProgram.view, false, vMatrix);
79 gl.uniformMatrix4fv(glSphereProgram.perspective, false, pMatrix);
80
81 gl.activeTexture(gl.TEXTURE0);
82 gl.bindTexture(gl.TEXTURE_CUBE_MAP, cubeTexID);
83 //gl.uniformMatrix4fv(glSphereProgram.mapCube, 0);
84
85 gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, sphereEBO);
86 gl.drawElements(gl.TRIANGLES, sphereEBO.numItems, gl.UNSIGNED_SHORT, 0);
87 gl.bindTexture(gl.TEXTURE_2D, null);
88 }

球体

  可以看到,也是和立方体一样的三步,初始化着色器,初始化顶点与法线,绘画。下面给出着色器代码:

WebGL 利用FBO完成立方体贴图。
 1     <script id="sphereshader-fs" type="x-shader/x-fragment">
2 precision mediump float;
3
4 varying vec3 normal;
5 varying vec3 eyevec;
6 uniform samplerCube mapCube;
7 void main( void )
8 {
9 gl_FragColor = textureCube(mapCube, reflect(normalize(-eyevec), normalize(normal)));
10 }
11 </script>
12
13 <script id="sphereshader-vs" type="x-shader/x-vertex">
14 attribute vec3 a_position;
15 attribute vec3 a_normal;
16
17 uniform mat4 model;
18 uniform mat4 view;
19 uniform mat4 perspective;
20 uniform vec3 eye;
21
22 varying vec3 normal;
23 varying vec3 eyevec;
24
25 void main( void )
26 {
27 gl_Position = perspective * view * model * vec4(a_position,1.0);
28 eyevec = -eye;// a_position.xyz;
29 normal = a_normal;
30 }
31 </script>

球体着色器

  和前面立方体有点不同的是,球体有自己的模型矩阵,这也是一般正常的用法,然后传递眼睛对应球体顶点矢量与法线传递在片断着色器中,在片断着色器中,就有用到前面所生成的立方体纹理,我们根据眼睛经过顶点通过对应法向量反射到立体体纹理上的点来获取当前球体所对应的环境颜色,在这里,我们可以直接调用textureCube来完成上面所说的过程,不需要我们手动来计算。

其中GetShader函数的使用,参照了http://msdn.microsoft.com/zh-TW/library/ie/dn302360(v=vs.85) 这里的讲解。

  可以说,上面主要的绘制函数已经完成,但是我们这个是能动的,所以需要模拟如客户端环境每隔多久绘制一次,主要代码如下:

WebGL 利用FBO完成立方体贴图。
 1 function tick() {
2 Update();
3 OnDraw();
4 setTimeout(function () { tick() }, 15);
5 }
6 function OnDraw() {
7 //fbo rander CUBE_MAP
8 RenderFBO();
9 //element rander
10 gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
11 gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
12 mat4.perspective(pMatrix, 45, gl.viewportWidth / gl.viewportHeight, 0.1, 200.0);
13 mat4.lookAt(vMatrix, eyePos, eyeLookat, vec3.fromValues(0.0, 1.0, 0.0));
14 RenderCube();
15 RenderSphere();
16 }
17
18 var lastTime = new Date().getTime();
19 function Update() {
20 var timeNow = new Date().getTime();
21 if (lastTime != 0) {
22 var elapsed = timeNow - lastTime;
23 //3000控制人眼的旋转速度。8控制人眼的远近
24 eyePos[0] = Math.cos(elapsed / 3000) * 8;
25 eyePos[2] = Math.sin(elapsed / 2000) * 8;
26
27 spherePos[0] = Math.cos(elapsed / 4000) * 3;
28 spherePos[2] = Math.cos(elapsed / 4000) * 3;
29 }
30
31 }

动画

  在上面,每隔15毫秒调用一次Update与Draw函数,其中Update用于更新眼睛与球体位置,Draw绘画。

  有二年没做网页上的开发,可能其中javascript的用法让大家见笑了,动态语言实在太*了,反而不好写。

  源代码

  在新的一年的第一天,祝大家事事如意。