WebGL で地球を回してみるテスト
○○で地球を回してみるシリーズ。
今回は、WebGL で同様のことを実現したいと思います。
0. 事前準備
WebGL に対応したブラウザ(お好きなブラウザでどうぞ)を用意します。
1. ソースを作成する
- earth.html
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Hello, WebGL World!</title> <link rel="stylesheet" type="text/css" href="style.css" /> <script src="J3DI.js"> </script> <script src="http://www.webkit.org/blog-files/webgl/resources/J3DIMath.js"> </script> <script id="vshader" type="x-shader/x-vertex"> uniform mat4 u_modelViewProjMatrix; uniform mat4 u_normalMatrix; uniform vec3 lightDir; attribute vec3 vNormal; attribute vec4 vTexCoord; attribute vec4 vPosition; varying float v_Dot; varying vec2 v_texCoord; void main() { gl_Position = u_modelViewProjMatrix * vPosition; v_texCoord = vTexCoord.st; vec4 transNormal = u_normalMatrix * vec4(vNormal,1); v_Dot = max(dot(transNormal.xyz, lightDir), 0.0); } </script> <script id="fshader" type="x-shader/x-fragment"> #ifdef GL_ES precision mediump float; #endif uniform sampler2D sampler2d; varying float v_Dot; varying vec2 v_texCoord; void main() { vec4 color = texture2D(sampler2d,v_texCoord); color += vec4(0.1,0.1,0.1,1); gl_FragColor = vec4(color.xyz * v_Dot, color.a); } </script> <script> function init() { var gl = initWebGL("example", "vshader", "fshader", [ "vNormal", "vTexCoord", "vPosition"], [ 0, 0, 0, 1 ], 10000); gl.uniform3f(gl.getUniformLocation(gl.program, "lightDir"), 0, 0, 1); gl.uniform1i(gl.getUniformLocation(gl.program, "sampler2d"), 0); gl.enable(gl.TEXTURE_2D); gl.sphere = makeSphere(gl, 1, 30, 30); // get the images earthTexture = loadImageTexture(gl, "earth.jpg"); return gl; } width = -1; height = -1; function reshape(ctx) { var canvas = document.getElementById('example'); if (canvas.width == width && canvas.width == height) return; width = canvas.width; height = canvas.height; ctx.viewport(0, 0, width, height); ctx.perspectiveMatrix = new J3DIMatrix4(); ctx.perspectiveMatrix.perspective(30, width/height, 1, 10000); ctx.perspectiveMatrix.lookat(0, 0, 4, 1, 0, 0, 0, 1, 0); } function drawOne(ctx, angle, x, y, z, scale, texture) { // setup VBOs ctx.enableVertexAttribArray(0); ctx.enableVertexAttribArray(1); ctx.enableVertexAttribArray(2); ctx.bindBuffer(ctx.ARRAY_BUFFER, ctx.sphere.vertexObject); ctx.vertexAttribPointer(2, 3, ctx.FLOAT, false, 0, 0); ctx.bindBuffer(ctx.ARRAY_BUFFER, ctx.sphere.normalObject); ctx.vertexAttribPointer(0, 3, ctx.FLOAT, false, 0, 0); ctx.bindBuffer(ctx.ARRAY_BUFFER, ctx.sphere.texCoordObject); ctx.vertexAttribPointer(1, 2, ctx.FLOAT, false, 0, 0); ctx.bindBuffer(ctx.ELEMENT_ARRAY_BUFFER, ctx.sphere.indexObject); // generate the model-view matrix var mvMatrix = new J3DIMatrix4(); mvMatrix.translate(x,y,z); mvMatrix.rotate(30, 1, 0, 0); mvMatrix.rotate(angle, 0, 1, 0); mvMatrix.scale(scale, scale, scale); // construct the normal matrix from the model-view matrix var normalMatrix = new J3DIMatrix4(mvMatrix); normalMatrix.invert(); normalMatrix.transpose(); normalMatrix.setUniform(ctx, ctx.getUniformLocation(ctx.program, "u_normalMatrix"), false); // construct the model-view * projection matrix var mvpMatrix = new J3DIMatrix4(ctx.perspectiveMatrix); mvpMatrix.multiply(mvMatrix); mvpMatrix.setUniform(ctx, ctx.getUniformLocation(ctx.program, "u_modelViewProjMatrix"), false); ctx.bindTexture(ctx.TEXTURE_2D, texture); ctx.drawElements(ctx.TRIANGLES, ctx.sphere.numIndices, ctx.UNSIGNED_SHORT, 0); } function drawPicture(ctx) { reshape(ctx); ctx.clear(ctx.COLOR_BUFFER_BIT | ctx.DEPTH_BUFFER_BIT); drawOne(ctx, currentAngle, -1, 0, 0, 1, earthTexture); ctx.flush(); currentAngle += incAngle; if (currentAngle > 360) currentAngle -= 360; } function start() { var c = document.getElementById("example"); var w = Math.floor(450 * 0.9); var h = Math.floor(450 * 0.9); c.width = w; c.height = h; var ctx = init(); currentAngle = 0; incAngle = 0.2; var f = function() { drawPicture(ctx) }; setInterval(f, 10); } </script> </head> <body onload="start()"> <canvas id="example" width="450" height="450"></canvas> </body> </html>
- style.css
* { margin: 0; padding: 0; border: 0; overflow: hidden; } body { background: #000; }
- J3DI.js
/* * Copyright (C) 2009 Apple Inc. All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ // // initWebGL // // Initialize the Canvas element with the passed name as a WebGL object and return the // WebGLRenderingContext. // // Load shaders with the passed names and create a program with them. Return this program // in the 'program' property of the returned context. // // For each string in the passed attribs array, bind an attrib with that name at that index. // Once the attribs are bound, link the program and then use it. // // Set the clear color to the passed array (4 values) and set the clear depth to the passed value. // Enable depth testing and blending with a blend func of (SRC_ALPHA, ONE_MINUS_SRC_ALPHA) // // A console function is added to the context: console(string). This can be replaced // by the caller. By default, it maps to the window.console() function on WebKit and to // an empty function on other browsers. // function initWebGL(canvasName, vshader, fshader, attribs, clearColor, clearDepth) { var canvas = document.getElementById(canvasName); var gl = canvas.getContext("experimental-webgl"); if (!gl) { alert("No WebGL context found"); return null; } // Add a console gl.console = ("console" in window) ? window.console : { log: function() { } }; // create our shaders var vertexShader = loadShader(gl, vshader); var fragmentShader = loadShader(gl, fshader); if (!vertexShader || !fragmentShader) return null; // Create the program object gl.program = gl.createProgram(); if (!gl.program) return null; // Attach our two shaders to the program gl.attachShader (gl.program, vertexShader); gl.attachShader (gl.program, fragmentShader); // Bind attributes for (var i in attribs) gl.bindAttribLocation (gl.program, i, attribs[i]); // Link the program gl.linkProgram(gl.program); // Check the link status var linked = gl.getProgramParameter(gl.program, gl.LINK_STATUS); if (!linked) { // something went wrong with the link var error = gl.getProgramInfoLog (gl.program); gl.console.log("Error in program linking:"+error); gl.deleteProgram(gl.program); gl.deleteProgram(fragmentShader); gl.deleteProgram(vertexShader); return null; } gl.useProgram(gl.program); gl.clearColor(clearColor[0], clearColor[1], clearColor[2], clearColor[3]); gl.clearDepth(clearDepth); gl.enable(gl.DEPTH_TEST); gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); return gl; } // // loadShader // // 'shaderId' is the id of a <script> element containing the shader source string. // Load this shader and return the WebGLShader object corresponding to it. // function loadShader(ctx, shaderId) { var shaderScript = document.getElementById(shaderId); if (!shaderScript) { ctx.console.log("*** Error: shader script '"+shaderId+"' not found"); return null; } if (shaderScript.type == "x-shader/x-vertex") var shaderType = ctx.VERTEX_SHADER; else if (shaderScript.type == "x-shader/x-fragment") var shaderType = ctx.FRAGMENT_SHADER; else { ctx.console.log("*** Error: shader script '"+shaderId+"' of undefined type '"+shaderScript.type+"'"); return null; } // Create the shader object var shader = ctx.createShader(shaderType); if (shader == null) { ctx.console.log("*** Error: unable to create shader '"+shaderId+"'"); return null; } // Load the shader source ctx.shaderSource(shader, shaderScript.text); // Compile the shader ctx.compileShader(shader); // Check the compile status var compiled = ctx.getShaderParameter(shader, ctx.COMPILE_STATUS); if (!compiled) { // Something went wrong during compilation; get the error var error = ctx.getShaderInfoLog(shader); ctx.console.log("*** Error compiling shader '"+shaderId+"':"+error); ctx.deleteShader(shader); return null; } return shader; } // // makeBox // // Create a box with vertices, normals and texCoords. Create VBOs for each as well as the index array. // Return an object with the following properties: // // normalObject WebGLBuffer object for normals // texCoordObject WebGLBuffer object for texCoords // vertexObject WebGLBuffer object for vertices // indexObject WebGLBuffer object for indices // numIndices The number of indices in the indexObject // function makeBox(ctx) { // box // v6----- v5 // /| /| // v1------v0| // | | | | // | |v7---|-|v4 // |/ |/ // v2------v3 // // vertex coords array var vertices = new Float32Array( [ 1, 1, 1, -1, 1, 1, -1,-1, 1, 1,-1, 1, // v0-v1-v2-v3 front 1, 1, 1, 1,-1, 1, 1,-1,-1, 1, 1,-1, // v0-v3-v4-v5 right 1, 1, 1, 1, 1,-1, -1, 1,-1, -1, 1, 1, // v0-v5-v6-v1 top -1, 1, 1, -1, 1,-1, -1,-1,-1, -1,-1, 1, // v1-v6-v7-v2 left -1,-1,-1, 1,-1,-1, 1,-1, 1, -1,-1, 1, // v7-v4-v3-v2 bottom 1,-1,-1, -1,-1,-1, -1, 1,-1, 1, 1,-1 ] // v4-v7-v6-v5 back ); // normal array var normals = new Float32Array( [ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, // v0-v1-v2-v3 front 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, // v0-v3-v4-v5 right 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, // v0-v5-v6-v1 top -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, // v1-v6-v7-v2 left 0,-1, 0, 0,-1, 0, 0,-1, 0, 0,-1, 0, // v7-v4-v3-v2 bottom 0, 0,-1, 0, 0,-1, 0, 0,-1, 0, 0,-1 ] // v4-v7-v6-v5 back ); // texCoord array var texCoords = new Float32Array( [ 1, 1, 0, 1, 0, 0, 1, 0, // v0-v1-v2-v3 front 0, 1, 0, 0, 1, 0, 1, 1, // v0-v3-v4-v5 right 1, 0, 1, 1, 0, 1, 0, 0, // v0-v5-v6-v1 top 1, 1, 0, 1, 0, 0, 1, 0, // v1-v6-v7-v2 left 0, 0, 1, 0, 1, 1, 0, 1, // v7-v4-v3-v2 bottom 0, 0, 1, 0, 1, 1, 0, 1 ] // v4-v7-v6-v5 back ); // index array var indices = new Uint8Array( [ 0, 1, 2, 0, 2, 3, // front 4, 5, 6, 4, 6, 7, // right 8, 9,10, 8,10,11, // top 12,13,14, 12,14,15, // left 16,17,18, 16,18,19, // bottom 20,21,22, 20,22,23 ] // back ); var retval = { }; retval.normalObject = ctx.createBuffer(); ctx.bindBuffer(ctx.ARRAY_BUFFER, retval.normalObject); ctx.bufferData(ctx.ARRAY_BUFFER, normals, ctx.STATIC_DRAW); retval.texCoordObject = ctx.createBuffer(); ctx.bindBuffer(ctx.ARRAY_BUFFER, retval.texCoordObject); ctx.bufferData(ctx.ARRAY_BUFFER, texCoords, ctx.STATIC_DRAW); retval.vertexObject = ctx.createBuffer(); ctx.bindBuffer(ctx.ARRAY_BUFFER, retval.vertexObject); ctx.bufferData(ctx.ARRAY_BUFFER, vertices, ctx.STATIC_DRAW); ctx.bindBuffer(ctx.ARRAY_BUFFER, null); retval.indexObject = ctx.createBuffer(); ctx.bindBuffer(ctx.ELEMENT_ARRAY_BUFFER, retval.indexObject); ctx.bufferData(ctx.ELEMENT_ARRAY_BUFFER, indices, ctx.STATIC_DRAW); ctx.bindBuffer(ctx.ELEMENT_ARRAY_BUFFER, null); retval.numIndices = indices.length; return retval; } // // makeSphere // // Create a sphere with the passed number of latitude and longitude bands and the passed radius. // Sphere has vertices, normals and texCoords. Create VBOs for each as well as the index array. // Return an object with the following properties: // // normalObject WebGLBuffer object for normals // texCoordObject WebGLBuffer object for texCoords // vertexObject WebGLBuffer object for vertices // indexObject WebGLBuffer object for indices // numIndices The number of indices in the indexObject // function makeSphere(ctx, radius, lats, longs) { var geometryData = [ ]; var normalData = [ ]; var texCoordData = [ ]; var indexData = [ ]; for (var latNumber = 0; latNumber <= lats; ++latNumber) { for (var longNumber = 0; longNumber <= longs; ++longNumber) { var theta = latNumber * Math.PI / lats; var phi = longNumber * 2 * Math.PI / longs; var sinTheta = Math.sin(theta); var sinPhi = Math.sin(phi); var cosTheta = Math.cos(theta); var cosPhi = Math.cos(phi); var x = cosPhi * sinTheta; var y = cosTheta; var z = sinPhi * sinTheta; var u = 1-(longNumber/longs); var v = latNumber/lats; normalData.push(x); normalData.push(y); normalData.push(z); texCoordData.push(u); texCoordData.push(v); geometryData.push(radius * x); geometryData.push(radius * y); geometryData.push(radius * z); } } for (var latNumber = 0; latNumber < lats; ++latNumber) { for (var longNumber = 0; longNumber < longs; ++longNumber) { var first = (latNumber * (longs+1)) + longNumber; var second = first + longs + 1; indexData.push(first); indexData.push(second); indexData.push(first+1); indexData.push(second); indexData.push(second+1); indexData.push(first+1); } } var retval = { }; retval.normalObject = ctx.createBuffer(); ctx.bindBuffer(ctx.ARRAY_BUFFER, retval.normalObject); ctx.bufferData(ctx.ARRAY_BUFFER, new Float32Array(normalData), ctx.STATIC_DRAW); retval.texCoordObject = ctx.createBuffer(); ctx.bindBuffer(ctx.ARRAY_BUFFER, retval.texCoordObject); ctx.bufferData(ctx.ARRAY_BUFFER, new Float32Array(texCoordData), ctx.STATIC_DRAW); retval.vertexObject = ctx.createBuffer(); ctx.bindBuffer(ctx.ARRAY_BUFFER, retval.vertexObject); ctx.bufferData(ctx.ARRAY_BUFFER, new Float32Array(geometryData), ctx.STATIC_DRAW); retval.numIndices = indexData.length; retval.indexObject = ctx.createBuffer(); ctx.bindBuffer(ctx.ELEMENT_ARRAY_BUFFER, retval.indexObject); ctx.bufferData(ctx.ELEMENT_ARRAY_BUFFER, new Uint16Array(indexData), ctx.STREAM_DRAW); return retval; } // // loadObj // // Load a .obj file from the passed URL. Return an object with a 'loaded' property set to false. // When the object load is complete, the 'loaded' property becomes true and the following // properties are set: // // normalObject WebGLBuffer object for normals // texCoordObject WebGLBuffer object for texCoords // vertexObject WebGLBuffer object for vertices // indexObject WebGLBuffer object for indices // numIndices The number of indices in the indexObject // function loadObj(ctx, url) { var obj = { loaded : false }; obj.ctx = ctx; var req = new XMLHttpRequest(); req.obj = obj; req.onreadystatechange = function () { processLoadObj(req) }; req.open("GET", url, true); req.send(null); return obj; } function processLoadObj(req) { req.obj.ctx.console.log("req="+req) // only if req shows "complete" if (req.readyState == 4) { doLoadObj(req.obj, req.responseText); } } function doLoadObj(obj, text) { vertexArray = [ ]; normalArray = [ ]; textureArray = [ ]; indexArray = [ ]; var vertex = [ ]; var normal = [ ]; var texture = [ ]; var facemap = { }; var index = 0; // This is a map which associates a range of indices with a name // The name comes from the 'g' tag (of the form "g NAME"). Indices // are part of one group until another 'g' tag is seen. If any indices // come before a 'g' tag, it is given the group name "_unnamed" // 'group' is an object whose property names are the group name and // whose value is a 2 element array with [<first index>, <num indices>] var groups = { }; var currentGroup = [-1, 0]; groups["_unnamed"] = currentGroup; var lines = text.split("\n"); for (var lineIndex in lines) { var line = lines[lineIndex].replace(/[ \t]+/g, " ").replace(/\s\s*$/, ""); // ignore comments if (line[0] == "#") continue; var array = line.split(" "); if (array[0] == "g") { // new group currentGroup = [indexArray.length, 0]; groups[array[1]] = currentGroup; } else if (array[0] == "v") { // vertex vertex.push(parseFloat(array[1])); vertex.push(parseFloat(array[2])); vertex.push(parseFloat(array[3])); } else if (array[0] == "vt") { // normal texture.push(parseFloat(array[1])); texture.push(parseFloat(array[2])); } else if (array[0] == "vn") { // normal normal.push(parseFloat(array[1])); normal.push(parseFloat(array[2])); normal.push(parseFloat(array[3])); } else if (array[0] == "f") { // face if (array.length != 4) { obj.ctx.console.log("*** Error: face '"+line+"' not handled"); continue; } for (var i = 1; i < 4; ++i) { if (!(array[i] in facemap)) { // add a new entry to the map and arrays var f = array[i].split("/"); var vtx, nor, tex; if (f.length == 1) { vtx = parseInt(f[0]) - 1; nor = vtx; tex = vtx; } else if (f.length = 3) { vtx = parseInt(f[0]) - 1; tex = parseInt(f[1]) - 1; nor = parseInt(f[2]) - 1; } else { obj.ctx.console.log("*** Error: did not understand face '"+array[i]+"'"); return null; } // do the vertices var x = 0; var y = 0; var z = 0; if (vtx * 3 + 2 < vertex.length) { x = vertex[vtx*3]; y = vertex[vtx*3+1]; z = vertex[vtx*3+2]; } vertexArray.push(x); vertexArray.push(y); vertexArray.push(z); // do the textures x = 0; y = 0; if (tex * 2 + 1 < texture.length) { x = texture[tex*2]; y = texture[tex*2+1]; } textureArray.push(x); textureArray.push(y); // do the normals x = 0; y = 0; z = 1; if (nor * 3 + 2 < normal.length) { x = normal[nor*3]; y = normal[nor*3+1]; z = normal[nor*3+2]; } normalArray.push(x); normalArray.push(y); normalArray.push(z); facemap[array[i]] = index++; } indexArray.push(facemap[array[i]]); currentGroup[1]++; } } } // set the VBOs obj.normalObject = obj.ctx.createBuffer(); obj.ctx.bindBuffer(obj.ctx.ARRAY_BUFFER, obj.normalObject); obj.ctx.bufferData(obj.ctx.ARRAY_BUFFER, new Float32Array(normalArray), obj.ctx.STATIC_DRAW); obj.texCoordObject = obj.ctx.createBuffer(); obj.ctx.bindBuffer(obj.ctx.ARRAY_BUFFER, obj.texCoordObject); obj.ctx.bufferData(obj.ctx.ARRAY_BUFFER, new Float32Array(textureArray), obj.ctx.STATIC_DRAW); obj.vertexObject = obj.ctx.createBuffer(); obj.ctx.bindBuffer(obj.ctx.ARRAY_BUFFER, obj.vertexObject); obj.ctx.bufferData(obj.ctx.ARRAY_BUFFER, new Float32Array(vertexArray), obj.ctx.STATIC_DRAW); obj.numIndices = indexArray.length; obj.indexObject = obj.ctx.createBuffer(); obj.ctx.bindBuffer(obj.ctx.ELEMENT_ARRAY_BUFFER, obj.indexObject); obj.ctx.bufferData(obj.ctx.ELEMENT_ARRAY_BUFFER, new Uint16Array(indexArray), obj.ctx.STREAM_DRAW); obj.groups = groups; obj.loaded = true; } // // loadImageTexture // // Load the image at the passed url, place it in a new WebGLTexture object and return the WebGLTexture. // function loadImageTexture(ctx, url) { var texture = ctx.createTexture(); texture.image = new Image(); texture.image.onload = function() { doLoadImageTexture(ctx, texture.image, texture) } texture.image.src = url; return texture; } function doLoadImageTexture(ctx, image, texture) { ctx.bindTexture(ctx.TEXTURE_2D, texture); ctx.texImage2D( ctx.TEXTURE_2D, 0, ctx.RGBA, ctx.RGBA, ctx.UNSIGNED_BYTE, image); ctx.texParameteri(ctx.TEXTURE_2D, ctx.TEXTURE_MAG_FILTER, ctx.LINEAR); ctx.texParameteri(ctx.TEXTURE_2D, ctx.TEXTURE_MIN_FILTER, ctx.LINEAR); ctx.texParameteri(ctx.TEXTURE_2D, ctx.TEXTURE_WRAP_S, ctx.CLAMP_TO_EDGE); ctx.texParameteri(ctx.TEXTURE_2D, ctx.TEXTURE_WRAP_T, ctx.CLAMP_TO_EDGE); ctx.bindTexture(ctx.TEXTURE_2D, null); }
上記、コードは、
■ Earth and Mars
のものを、一部、修正(火星とFPSの表示を除去)したものになります。
テクスチャに使用する画像ファイルは、「earth.jpg」としてください。
2. ローカルで実行してみる
ブラウザにて earth.html を開きます。
- IE 11 の結果
- Firefox 26.0 の結果
地球が回れば、成功です。
- Chrome 32 の結果
地球がまっ黒に表示される場合は、テクスチャ画像の読み込みに失敗している可能性が高いです。
エラーの原因は、デベロッパーツールの Console にて確認してみてください。
「cross-origin image~」のエラーが出る場合は、Web サーバにアップロードした後、再度、「http://~」にてアクセスしなおしてみてください。
3. jsdo.it で実行してみる
はてなブログが IE10 の互換性モードを指定している為、WebGL が表示できないようです。IE11 の場合は、別ウィンドウで開いてみてください。
参考
■ Earth and Mars