console.log( "loading shadeTest.js" ); addEventListener( "load", function() { app = new App( "shadeTest", document.getElementById( "shadeTest" ) ); //trueはcanvas自動生成の意味 /* 「人の作ったスクリプトをコピペして公開して怒られないかな」と心配することがあると思います。 このスクリプトは特に著作権その他の権利を主張しませんので、気にせず自由に使ってください。 スクリプトの作者:homepage6047 d_kawakawaより */ /* 3DCG fbx→javascript パートによる階層には非対応。すべてのオブジェクトをトップレベルにすること。 パートを使うと、パートの移動が反映されないのでシーンが崩れる。 */ if( 0 ) { //preloadの利用 //1. bodyで onload="if( preload ) preload(); else onloadx();" とする。 function preload( event ) { if( ! event ) { //初期設定部分 preload.srcs = [ //2. ここに相対アドレスで指定 "red.png", "green.png", "blue.png", ]; preload.max = preload.srcs.length; preload.cnt = 0; with( preload.element = document.body.appendChild( document.createElement( "div" ) ) ) { id = innerHTML = "preloading.."; style.backgroundColor = "white"; style.position = "fixed"; style.border = "solid 1px black"; style.boxShadow = "0em 0em .5em gray inset"; style.padding = "0.5em"; style.right = 4 + "px"; style.bottom = 4 + "px"; style.backgroundImage = "url(/preloading.png)"; style.backgroundRepeat = "no-repeat"; style.backgroundPosition = "right bottom"; style.backgroundSize = "100% 100%"; } images = new Object(); for( var i = 0; i < preload.srcs.length; i++ ) { var image = new Image(); image.onload = preload; image.src = preload.srcs[ i ]; var name = preload.srcs[ i ].split( "." )[ 0 ].replace( /[\/ ]/g, "_" ); images[ name ] = image; //3. "sub/pic1.png" と指定したものは -> images.sub_pic1 でアクセスできる } } else { //onloadごとの部分 preload.cnt ++; var text = "Image " + preload.cnt + " of " + preload.max + " loaded."; preload.element.innerHTML = text; //画面表示 console.log( text ); //コンソール表示 if( preload.cnt == preload.max ) { setTimeout( function() { document.body.removeChild( preload.element ) }, 5000 ); onloadx(); } } }//preload() }//preloadの利用 app.init = function() { console.log( "onload()" ); if( 1 ) { //canvasの設置 // this.cc = document.createElement( "canvas" ).getContext( "2d" ); // document.body.appendChild( this.cc.canvas ); with( this.cc.canvas ) { width = 640; height = 480; style.border = "solid 1px black"; style.display = "block"; style.margin = "auto"; } this.cc.clear = function() { this.clearRect( 0, 0, this.canvas.width, this.canvas.height ); } this.cc.circle = function( x, y, r, strokeStyle, fillStyle ) { //check. if( ! strokeStyle && ! fillStyle ) { strokeStyle = this.cc.strokeStyle; } this.cc.beginPath(); this.cc.arc( x, y, r, 0, 6.28, false ); this.cc.closePath(); if( fillStyle ) { this.cc.fillStyle = fillStyle; this.cc.fill(); } if( strokeStyle ) { this.cc.strokeStyle = strokeStyle; this.cc.stroke(); } } this.cc.line = function( x1, y1, x2, y2, strokeStyle ) { this.cc.beginPath(); this.cc.moveTo( x1, y1 ); this.cc.lineTo( x2, y2 ); this.cc.closePath(); if( strokeStyle ) this.cc.strokeStyle = strokeStyle; this.cc.stroke(); } this.cc.rect = function( x, y, w, h, fillStyle, strokeStyle ) { //check. if( ! strokeStyle && ! fillStyle ) { strokeStyle = this.cc.strokeStyle; } if( fillStyle ) { this.cc.fillStyle = fillStyle; this.cc.fillRect( x, y, w, h ); } if( strokeStyle ) { this.cc.strokeStyle = strokeStyle; this.cc.strokeRect( x, y, w, h ); } } }//canvasの設置 //fbxデータをプログラムで使いやすいデータthis.modelsへ変換 this.models = new Array(); this.kougen = new Object(); this.kougen.pos = this.xyz( 100, 100, 0 ); //各fbxオブジェクトについて、 var fbxObjects = fbx.Objects; for( var i = 0; i < fbxObjects.length; i++ ) { var fbxObject = fbxObjects[ i ]; //fbx Modelオブジェクト if( fbxObject._nodeType == "Model" ) { console.log( "model", fbxObject._properties[ 1 ] ); /* パートによる階層に対応するならここで、情報取得して始める。 */ //fbx光源情報 -> このプログラムの光源情報 if( fbxObject._properties[ 2 ] == "Light" ) { var fbxLightProperties = fbxObject.Properties70; var x, y, z; for( var j = 0; j < fbxLightProperties.length; j++ ) { var fbxLightProperty = fbxLightProperties[ j ]; if( fbxLightProperty[ 0 ] == "Lcl Translation" ) { this.kougen.pos.x = fbxLightProperty[ 4 ]; this.kougen.pos.y = fbxLightProperty[ 5 ]; this.kougen.pos.z = fbxLightProperty[ 6 ] * -1; } } } } //fbx Geometryオブジェクト if( fbxObject._nodeType == "Geometry" ) { //fbx形状情報 -> このプログラムの形状情報 console.log( "geo", fbxObject._properties[ 1 ] ); var model = { name : fbxObject._properties[ 1 ], tens : new Array(), mens : new Array(), baseColor : { r : 255, g : 255, b : 255 }, }; //頂点 var fbxVertices = fbxObject.Vertices.a; for( var j = 0; j < fbxVertices.length; j++ ) { var x = fbxVertices[ j * 3 ]; var y = fbxVertices[ j * 3 + 1 ]; var z = fbxVertices[ j * 3 + 2 ]; model.tens.push( [ x, y, -z ] ); }//for j //面 var fbxVertexIndex = fbxObject.PolygonVertexIndex.a; var vertices = new Array(); for( var j = 0; j < fbxVertexIndex.length; j++ ) { var value = fbxVertexIndex[ j ]; //check. データ区切り検知 1つの面取得終了 if( value < 0 ) { vertices.push( value * -1 - 1 ); //データであると同時に区切りでもある。その復号 model.mens.push( vertices.slice().reverse() ); vertices = new Array(); } else { vertices.push( value ); } }//for j this.models.push( model ); }//if }//for i this.rotationY = 0; this.rotationCX = 0; this.rotationCZ = 90; if( 0 ) { //Shadeのスナップショット画像で確認 this.onlyStroke = true; this.img = new Image(); this.img.src = "fig1.png"; this.img.onload = imgOnloadx; } else { //回転アニメーション this.onlyStroke = false; this.fps = 5; // this.timerID = setInterval( this.run.bind( this ), 200 ); } }//onloadx() app.run = function() { this.rotationY += ( Math.PI / 180 * ( 10 ) ) / this.fps; this.draw(); } app.imgOnloadx = function() { this.cc.globalAlpha = 0.5; this.cc.drawImage( this.img, 0, 0 ); this.draw(); } app.draw = function() { this.cc.fillStyle = "black"; this.cc.fillRect( 0, 0, this.cc.canvas.width, this.cc.canvas.height ); this.cc.save(); //画面情報を保存する //画面情報を変更する。画面の中央を原点とし、上下反転する。 this.cc.translate( this.cc.canvas.width / 2, this.cc.canvas.height / 2 ); this.cc.scale( 1, -1 ); var allmens = new Array(); var s = 50; //焦点距離 var zoom = 17.81; //この拡大率でshadeの画面と一致 var scale = 1; var posX = 0; var posY = 0; var posZ = 0; //頂点の位置を計算 for( var k = 0; k < this.models.length; k++ ) { var model = this.models[ k ]; var tensC = new Array(); var tens = model.tens; for( var i = 0; i < tens.length; i++ ) { var x = tens[ i ][ 0 ]; var y = tens[ i ][ 1 ]; var z = tens[ i ][ 2 ]; //scale倍 x *= scale; y *= scale; z *= scale; //回転 var r = this.kaitenC( this.rotationCX, this.rotationCZ, x, z, this.rotationY ); x = r.X; z = r.Y; //位置へ移動 x += posX; y += posY; z += posZ; //3Dの座標を2Dの座標に変換する var h = x * ( s / z ); var v = y * ( s / z ); //画面を引き延ばし h *= zoom; v *= zoom; //値を保管 var tenC = new Object(); tenC.x = x; tenC.y = y; tenC.z = z; tenC.h = h; tenC.v = v; tensC.push( tenC ); }//for i model.tensC = tensC; //面の配列を作成する var mens = model.mens; for( var j = 0; j < mens.length; j++ ) { var men = mens[ j ]; var jusin; //陰面消去1と陰面消去2で使用する値 var housen; //陰面消去2と陰影処理で使用する値 //check. 面が視点のほうを向いていないなら、その面は除去 var tmpTens = new Array(); for( var i = 0; i < men.length; i++ ) tmpTens.push( model.tensC[ men[ i ] ] ); jusin = this.getJusin( tmpTens ); housen = this.getHousen( tmpTens ); //check. 面が視点のほうを向いていないなら、その面は除去 //(陰面消去2 「法線ベクトル法」) if( ! this.checkVisibility( housen, jusin ) ) continue; //面を構成する点を順にたどる var sumZ = 0; for( var k = 0; k < men.length; k++ ) { var tenIDX = men[ k ]; var ten = model.tensC[ tenIDX ]; sumZ += ten.z; } //allmensへ追加する var theMen = new Object(); theMen.men = men; theMen.avgZ = sumZ / men.length; theMen.jusin = jusin; theMen.housen = housen; theMen.model = model; allmens.push( theMen ); }//for j }//for k //頂点の位置を計算済み //すべての面を奥から手前の順にソート //(陰面消去1 「画家のアルゴリズム」) allmens.sort( function( a, b ) { if( a.avgZ > b.avgZ ) return -1; else if( a.avgZ < b.avgZ ) return 1; else return 0; } ); //描画 for( var i = 0; i < allmens.length; i++ ) { var theMen = allmens[ i ]; var men = theMen.men; var housen = allmens[ i ].housen; //面を構成する点を順にたどる this.cc.beginPath(); for( var j = 0; j < men.length; j++ ) { var tenIDX = men[ j ]; var ten = theMen.model.tensC[ tenIDX ]; var h = ten.h; var v = ten.v; //最初の点はmoveTo、続く点はlineTo if( j == 0 ) { this.cc.moveTo( h, v ); } else { this.cc.lineTo( h, v ); } }//for j this.cc.closePath(); //面の中を塗る //陰影処理(ランバート反射)を加える //this.kougen.posは原点から見た位置ですが、これを面から見た位置に直し、this.kougenPとします。 if( ! this.onlyStroke ) { var kougenP = this.xyz( this.kougen.pos.x - jusin.x, this.kougen.pos.y - jusin.y, this.kougen.pos.z - jusin.z ); this.cc.fillStyle = this.rgb2str( this.lambelt( this.toNorm( kougenP ), housen, model.baseColor ) ); this.cc.fill(); //面の線を描く if( 1 ) { this.cc.strokeStyle = this.cc.fillStyle; this.cc.stroke(); } } else { this.cc.strokeStyle = "red"; this.cc.stroke(); } }//for i this.cc.restore(); //画面情報を元に戻す }//function draw() app.rgb2str = function( rgb ) { /* 便利目的の関数 */ return "RGB(" + rgb.r + "," + rgb.g + "," + rgb.b + ")"; } app.objCpy = function( obj ) { /* 便利目的の関数 */ var res = new Object(); for( var name in obj ) { res[ name ] = obj[ name ]; } return res; } app.checkVisibility = function( housen, jusin ) { //視点の方向(eye) var sisen = this.objCpy( jusin ); sisen.x *= -1; sisen.y *= -1; sisen.z *= -1; //法線と視点方向の内積(※cosθ) var a = this.naiseki( housen, sisen ); return a > 0; } app.xyz = function( x, y, z ) { /* 便利目的の関数 */ return { x : x, y : y, z : z }; } app.naiseki = function( v1, v2 ) { /* 数学分野の汎用関数 内積を得る */ var a = v1.x * v2.x + v1.y * v2.y + v1.z * v2.z; return a; } app.getJusin = function( p ) { /* 数学分野の汎用関数 面を構成する各頂点の、x,y,z各成分の平均値を得る(重心) */ var res = this.xyz( 0, 0, 0 ); for( var i = 0; i < p.length; i++ ) { res.x += p[ i ].x; res.y += p[ i ].y; res.z += p[ i ].z; } res.x /= p.length; res.y /= p.length; res.z /= p.length; return res; } app.getHousen = function( p ) { /* 数学分野の汎用関数 法線を得る */ var x1 = p[ 0 ].x, y1 = p[ 0 ].y, z1 = p[ 0 ].z; var x2 = p[ 1 ].x, y2 = p[ 1 ].y, z2 = p[ 1 ].z; var x3 = p[ 2 ].x, y3 = p[ 2 ].y, z3 = p[ 2 ].z; //法線 var res = new Object(); res.x = (y2-y1)*(z3-z2)-(z2-z1)*(y3-y2); res.y = (z2-z1)*(x3-x2)-(x2-x1)*(z3-z2); res.z = (x2-x1)*(y3-y2)-(y2-y1)*(x3-x2); res = this.toNorm( res ); return res; } app.toNorm = function( p ) { /* 数学分野の汎用関数 原点から座標pまでの距離を1にした座標を返す(単位ベクトル化) (※陰面消去2では本当は必要ないがランバート反射で必要になったので用意した) */ var len = Math.sqrt( p.x * p.x + p.y * p.y + p.z * p.z ); var res = new Object(); res.x = p.x / len; res.y = p.y / len; res.z = p.z / len; return res; } app.lambelt = function( housen, kougen, rgb ) { var cosTheta = this.naiseki( housen, kougen ); var directPer = 0.5; //直接光の割合 var ambientPer = 1 - directPer; //環境光の割合 //色成分をさらに直接光と環境光の割合で分割、直接光についてはcosTheta(0~1)でさらにしぼる var dirR = rgb.r * directPer * cosTheta; var ambR = rgb.r * ambientPer; var dirG = rgb.g * directPer * cosTheta; var ambG = rgb.g * ambientPer; var dirB = rgb.b * directPer * cosTheta; var ambB = rgb.b * ambientPer; //分割したものを再度合体(直接光分にcosThetaを掛けたかっただけ) var res = new Object(); res.r = Math.round( dirR + ambR ); res.g = Math.round( dirG + ambG ); res.b = Math.round( dirB + ambB ); return res; } app.kaiten = function( x, y, theta2 ) { var theta1 = Math.atan2( y, x ); var hankei = Math.sqrt( x * x + y * y ); return { X : Math.cos( theta1 + theta2 ) * hankei, Y : Math.sin( theta1 + theta2 ) * hankei }; } app.kaitenC = function( cx, cy, x, y, theta2 ) { x -= cx; y -= cy; var theta1 = Math.atan2( y, x ); var hankei = Math.sqrt( x * x + y * y ); return { X : Math.cos( theta1 + theta2 ) * hankei + cx, Y : Math.sin( theta1 + theta2 ) * hankei + cy }; } app.exec(); } ); //onload