2012年05月07日

JavaScriptで3D

突然ですが、ベランダで野菜を育て始めましたクリスマス
少しずつではありますが、元気に育っていく野菜を見るのが楽しみなgood sun晴れこと山口です。

さて少し前からJavaScriptを話題にしているのですが、
今回は前回のドラッグ&ドロップを引き継ぎつつ、今までと違ったJavaScriptのサンプルを用意しました。
お待たせ致しました、ついにWebGL到来です!!
ついにブラウザだけの力で3D表示出来る時代の波に少しだけ乗っかってみました。
その為今回のサンプルが動作するブラウザは限られてしまいます。
推奨ブラウザはGoogle Chromeとなります。

サンプルはこちらから

今回のサンプルは実行されると四角い箱が回っていますプレゼント
ここにブラウザで使用可能な画像ファイル(.png等)をドラッグ&ドロップすることで、
画像をテクスチャとして貼ることが出来ます。
※簡単に試してみましたが、どうもjpegは白い画像になってしまうようです。。。pngでお試し下さい。

ドラッグ&ドロップについては
最後に掲載するソースコードのdropFileの部分を参照して下さい。
dropイベントを受け取って、
中身が画像であればテクスチャを作成しています。
実際に一連のソースコードがあると、
どのような流れで動いているのか分かりやすいのでは無いでしょうか。

今回サンプルを作っていて、昔Java3Dでブラウザに3D表示をしていた時の事を思い出したりしました。
いまではJavaScriptで3D表示が出来ておまけにシェーダが使えてしまうのですから、
技術の進歩は凄まじいものです。

更にJavaScript版COLLADAローダというのもあるようです。
頑張れば3Dモデルデータもブラウザにドラッグ&ドロップで表示出来るようになってしまいます!
これは益々目が離せない感じです目

WebGL入門も果たしブラウザ熱が上がってきたところで今回は終了とさせて頂きたいと思います。
次回もWebGLについて書ければ良いなぁと考えておりますが、予定は未定です。


最後に注意点及び、今回のサンプルのソースコードを掲載します。
まず注意点ですが、
このサンプル、Google Chromeではサーバにソースコードがある場合は問題なく動作しますが、
ソースをローカルにダウンロードして動作させようとした場合に通常はドラッグ&ドロップが働きません。
MacOSXの場合は以下のようなシェルスクリプト経由で起動させることでファイルのドラッグ&ドロップを受け付けるようになります。

#!/bin/sh
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --allow-file-access-from-files

そして以下が今回のサンプルのソースコードになります。
今回のサンプルは行列処理にNewBSDライセンスにて配布されているglMatrixを利用しています。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>javascriptサンプル</title>
<script type="text/javascript" src="http://glmatrix.googlecode.com/files/glMatrix-0.9.5.min.js"></script>
</head>
<body>
<canvas id="canvas" ondragover="scene.dragFile(event);" ondrop="scene.dropFile(event);"></canvas>
<script id="vshader" type="x-shader/x-vertex">
#ifdef GL_ES
precision highp float;
#endif

uniform mat4 mMatrix;
uniform mat4 vpMatrix;

attribute vec3 position;
attribute vec2 uv;

varying vec2 texCoord;

void main() {
gl_Position = vpMatrix * mMatrix * vec4(position, 1.0);
texCoord = uv;
}
</script>
<script id="fshader" type="x-shader/x-fragment">
#ifdef GL_ES
precision highp float;
#endif

uniform sampler2D texture;

varying vec2 texCoord;

void main() {
gl_FragColor = texture2D(texture, texCoord);
}
</script>
<script type="text/javascript">
var scene;
var frame;
window.onload = function() {
frame = new Frame();
if(frame.enabled){
scene = new Scene(frame.context3d);
frame.drawFunc = function(context3d){scene.draw(context3d);}
frame.run(new Date());
}
}
</script>
</body>
<script type="text/javascript">
Screen = {
width:640,
height:480
};
//! カラー定義
function Color(r,g,b,a){
this.r = r;
this.g = g;
this.b = b;
this.a = a;
}
//! シェーダ
function Shader(context3d, vshaderSource, fshaderSource, attrNames, uniformNames){
this.vshader = null;
this.fshader = null;
this.program = null;
this.attributes = [];
this.uniforms = [];
// 頂点シェーダー
this.vshader = context3d.createShader(context3d.VERTEX_SHADER);
context3d.shaderSource(this.vshader, vshaderSource);
context3d.compileShader(this.vshader);
if(!context3d.getShaderParameter(this.vshader, context3d.COMPILE_STATUS)){
alert(context3d.getShaderInfoLog(this.vshader));
}
// フラグメントシェーダー
this.fshader = context3d.createShader(context3d.FRAGMENT_SHADER);
context3d.shaderSource(this.fshader, fshaderSource);
context3d.compileShader(this.fshader);
if(!context3d.getShaderParameter(this.fshader, context3d.COMPILE_STATUS)){
alert(context3d.getShaderInfoLog(this.fshader));
}

this.program = context3d.createProgram();
context3d.attachShader(this.program, this.vshader);
context3d.attachShader(this.program, this.fshader);

context3d.linkProgram(this.program);
if(!context3d.getProgramParameter(this.program, context3d.LINK_STATUS)){
alert(context3d.getProgramInfoLog(this.program));
}

for(var i = 0; i < attrNames.length; i++){
var attr = context3d.getAttribLocation(this.program, attrNames[i]);
this.attributes = this.attributes.concat(attr);
}

for(var i = 0; i < uniformNames.length; i++){
this.uniforms = this.uniforms.concat(context3d.getUniformLocation(this.program, uniformNames[i]));
}
}
//! ボックスデータを作る
function Box(context3d){
this.vbuffer = [];
this.ibuffer = null;
this.indexCount = 0;
this.initVertexBuffer(context3d);
this.initIndexBuffer(context3d);
}
Box.prototype.initVertexBuffer = function(context3d){
var position = [
[-1,-1,-1],
[ 1,-1,-1],
[-1,-1, 1],
[ 1,-1, 1],

[-1, 1,-1],
[ 1, 1,-1],
[-1, 1, 1],
[ 1, 1, 1],
];
var uv = [
[0,1],
[1,1],
[0,0],
[1,0]
];
// 頂点配列を組み立てる
var index = [[0,1,2,3],[0,1,4,5],[2,3,6,7],[0,2,4,6],[1,3,5,7],[4,5,6,7]];
var vertexPosition = [];
for(var i = 0; i < 6; i++){
for(var j = 0; j < 4; j++){
vertexPosition = vertexPosition.concat(position[index[i][j]]);
}
}
var vertexTexCoord = [];
for(var i = 0; i < 6; i++){
for(var j = 0; j < 4; j++){
vertexTexCoord = vertexTexCoord.concat(uv[j]);
}
}
// Buffer作成
var vbuffer = context3d.createBuffer();
context3d.bindBuffer(context3d.ARRAY_BUFFER, vbuffer);
context3d.bufferData(context3d.ARRAY_BUFFER, new Float32Array(vertexPosition), context3d.STATIC_DRAW);
vbuffer.itemSize = 3;
this.vbuffer = this.vbuffer.concat(vbuffer);

vbuffer = context3d.createBuffer();
context3d.bindBuffer(context3d.ARRAY_BUFFER, vbuffer);
context3d.bufferData(context3d.ARRAY_BUFFER, new Float32Array(vertexTexCoord), context3d.STATIC_DRAW);
vbuffer.itemSize = 2;
this.vbuffer = this.vbuffer.concat(vbuffer);

context3d.bindBuffer(context3d.ARRAY_BUFFER, null);
}
Box.prototype.initIndexBuffer = function(context3d){
var index = [];
var base = 0;
for(var i = 0 ; i < 6 ; i++) {
index = index.concat(base + 0);
index = index.concat(base + 1);
index = index.concat(base + 2);
index = index.concat(base + 2);
index = index.concat(base + 1);
index = index.concat(base + 3);
base += 4;
}
// バッファを作成
this.ibuffer = context3d.createBuffer(context3d);
context3d.bindBuffer(context3d.ELEMENT_ARRAY_BUFFER, this.ibuffer);
context3d.bufferData(context3d.ELEMENT_ARRAY_BUFFER, new Int16Array(index), context3d.STATIC_DRAW);
context3d.bindBuffer(context3d.ELEMENT_ARRAY_BUFFER, null);
this.indexCount = index.length;
}
Box.prototype.draw = function(context3d, shader){
context3d.enableVertexAttribArray(shader.attributes[0]);
context3d.bindBuffer(context3d.ARRAY_BUFFER, this.vbuffer[0]);
context3d.vertexAttribPointer(shader.attributes[0], this.vbuffer[0].itemSize, context3d.FLOAT, false, 0, 0);
context3d.enableVertexAttribArray(shader.attributes[1]);
context3d.bindBuffer(context3d.ARRAY_BUFFER, this.vbuffer[1]);
context3d.vertexAttribPointer(shader.attributes[1], this.vbuffer[1].itemSize, context3d.FLOAT, false, 0, 0);
context3d.bindBuffer(context3d.ELEMENT_ARRAY_BUFFER, this.ibuffer);
context3d.drawElements(context3d.TRIANGLES, this.indexCount, context3d.UNSIGNED_SHORT, 0);
}

//! 描画シーン
function Scene(context3d){
this.updateTexture = false;
this.image = null;
this.texture = this.createEmptyTexture(context3d);
this.shader = new Shader(
context3d,
window.document.getElementById("vshader").text,
window.document.getElementById("fshader").text,
["position", "uv"],
["mMatrix", "vpMatrix"]);
this.box = new Box(context3d);
this.viewMatrix = mat4.create();
mat4.identity(this.viewMatrix);
mat4.translate(this.viewMatrix, [0, 0, -8]);
this.projectionMatrix = mat4.create();
mat4.identity(this.projectionMatrix);
mat4.perspective(30, Screen.width / Screen.height, 0.1, 1000, this.projectionMatrix);

this.count = 0;
}
Scene.prototype.draw = function(context3d){
this.count++;
if(this.updateTexture){
this.updateTexture = false;
this.texture = this.createTexture(context3d, this.image);
}
context3d.disable(context3d.CULL_FACE);
context3d.useProgram(this.shader.program);
var matrix = mat4.create();
mat4.identity(matrix);
mat4.rotate(matrix, 10 * this.count * Math.PI / 180, [0, 1, 0]);

var camMatrix = mat4.create(this.projectionMatrix);
mat4.multiply(camMatrix, this.viewMatrix);

context3d.uniformMatrix4fv(this.shader.uniforms[0], false, matrix);
context3d.uniformMatrix4fv(this.shader.uniforms[1], false, camMatrix);

context3d.bindTexture(context3d.TEXTURE_2D, this.texture);
context3d.texParameteri(context3d.TEXTURE_2D, context3d.TEXTURE_MAG_FILTER, context3d.LINEAR);
context3d.texParameteri(context3d.TEXTURE_2D, context3d.TEXTURE_MIN_FILTER, context3d.LINEAR_MIPMAP_LINEAR);
context3d.texParameteri(context3d.TEXTURE_2D, context3d.TEXTURE_WRAP_S, context3d.CLAMP_TO_EDGE);
context3d.texParameteri(context3d.TEXTURE_2D, context3d.TEXTURE_WRAP_T, context3d.CLAMP_TO_EDGE);

this.box.draw(context3d, this.shader);
}
Scene.prototype.dragFile = function(event){
if (event.dataTransfer.types[0] == "Files"){
event.preventDefault();
}
}
Scene.prototype.dropFile = function(event){
var file = event.dataTransfer.files[0];
if(file.type.match(/image\/\w+/)){
var reader = new FileReader();
reader.filename = event.dataTransfer.files[0].fileName;
var _this = this;
// エンコード完了後のコールバック
reader.onloadend = function() {
_this.setupImage(reader);
}
// Base64URI変換
reader.readAsDataURL(file);
}
}
Scene.prototype.setupImage = function(reader){
if(reader.error){
return;
}
this.image = new Image();
this.image.src = reader.result;
this.image = this.checkSize(this.image);
this.updateTexture = true;
}
Scene.prototype.createEmptyTexture = function(context3d){
var canvas = document.createElement('canvas');
canvas.height = canvas.width = 8;
var context = canvas.getContext('2d');
context.fillStyle = "#ffffff";
context.fillRect(0, 0, canvas.width, canvas.height);

return this.createTexture(context3d, canvas);
}
Scene.prototype.createTexture = function(context3d, image){
var texture = context3d.createTexture();
context3d.bindTexture(context3d.TEXTURE_2D, texture);
context3d.texImage2D(context3d.TEXTURE_2D, 0, context3d.RGBA, context3d.RGBA, context3d.UNSIGNED_BYTE, image);
context3d.generateMipmap(context3d.TEXTURE_2D);
context3d.bindTexture(context3d.TEXTURE_2D, null);
return texture;
}
Scene.prototype.checkSize = function(image) {
var w = image.naturalWidth;
var h = image.naturalHeight;
var size = Math.pow(2, Math.log(Math.min(w, h)) / Math.LN2 | 0);
if (w !== h || w !== size) {
var canvas = document.createElement('canvas');
canvas.height = canvas.width = size;
canvas.getContext('2d').drawImage(image, 0, 0, w, h, 0, 0, size, size);
image = canvas;
}
return image;
}
//! フレーム管理クラス
function Frame(){
this.enabled = false;
// コンテキストの準備
this.canvas = window.document.getElementById("canvas");
var contextName = ["webgl", "experimental-webgl", "webkit-3d", "moz-webgl"];
for(var i = 0; i < contextName.length; i++){
try{
this.context3d = this.canvas.getContext(contextName[i]);
}catch(e){
}
if(this.context3d){
break;
}
}
if(!this.context3d){
return;
}
this.canvas.width = Screen.width;
this.canvas.height = Screen.height;
this.context3d.viewport(0, 0, Screen.width, Screen.height);
this.context3d.enable(this.context3d.TEXTURE_2D);
this.clearColor = new Color(0.1,0.1,0.1,1);
this.clearDepth = 1000;
this.enabled = true;
this.drawFunc = null;
}
Frame.prototype.clear = function(){
var cc = this.clearColor;
this.context3d.clearColor(cc.r, cc.g, cc.b, cc.a);
this.context3d.clearDepth(this.clearDepth);
this.context3d.clear(this.context3d.COLOR_BUFFER_BIT | this.context3d.DEPTH_BUFFER_BIT);
}
Frame.prototype.run = function(lastTime){
// クリア
this.clear();
this.context3d.enable(this.context3d.DEPTH_TEST);
this.drawFunc(this.context3d);
this.context3d.flush();
//! 更新を行う
var nowTime = new Date();
// 20FPS
var nextTime = 50 - (nowTime.getTime() - lastTime.getTime());
if(nextTime <= 0){
nextTime = 1;
}
var _this = this;
setTimeout(function(){_this.run(nowTime);}, nextTime);
}
</script>
</html>


posted by 管理人 at 16:33 | プログラミング