前のプログラムからの変更点について:

左の画像

右の画像
「視点を変更できるプログラム(整頓済み)」から「面に陰影をつけるプログラム」へ変更しています。
光源の位置を設定し、その位置と各面との角度の関係で色の暗さを変更しています。
この計算はランバート反射と呼ばれています。
陰影をつけるだけの変更なのに画面の構図が変わっているのは、陰影のようすをわかりやすくするために s (焦点距離)をいじったからです

プログラム中の赤マーカーの部分は、陰影をつけるための変更点を示しています。
Skin:
[NORMAL]
[BLUE] [DOS] [LIGHT]  / コピーするための表示 / 実行
比較対象からの変更点をマーキング
このファイル: canvas - 8(陰影付け).html
比較対象: canvas - 7-2(視点の移動と回転).html

1 <html>
2 <head>
3 <meta content="text/html; charset=UTF-8" http-equiv="content-type">
4 <title>untitled canvas</title>
5 <script>
6 console.log( "=============== script ==============" );
7 /*
8 『原理を使用した3DCG - 基本プログラム』
9
10 実行すると、面に陰影をつけて描かれる。
11
12
13 3DCG仕様:
14 Z軸は奥へ行くほど増加し、手前に来るほど減少する。
15
16
17
18 */
19 function $( id ) { return document.getElementById( id ); }
20 var HereDocument = /\/\*\s*([^]*?)\s*\*\//;
21 var canvasEL, canvas;
22
23 function onloadx() {
24 canvasEL = $( "canvasELID" );
25 canvas = canvasEL.getContext( '2d' );
26 screenW = canvasEL.width;
27 screenH = canvasEL.height;
28
29
30
31 //マスターモデル
32 masters = new Object();
33 masters.cube = new Object();
34
35 //正六面体の頂点(中心を原点としたときの位置)
36 masters.cube.tens = [
37 { x : -1, y : -1, z : -1 }, //0: 左下 手前
38 { x : +1, y : -1, z : -1 }, //1: 右下 手前
39 { x : +1, y : +1, z : -1 }, //2: 右上 手前
40 { x : -1, y : +1, z : -1 }, //3: 左上 手前
41
42 { x : -1, y : -1, z : +1 }, //4: 左下 奥
43 { x : +1, y : -1, z : +1 }, //5: 右下 奥
44 { x : +1, y : +1, z : +1 }, //6: 右上 奥
45 { x : -1, y : +1, z : +1 }, //7: 左上 奥
46 ];
47
48 masters.cube.mens = [
49 [ 3, 2, 1, 0 ], //向かって正面
50 [ 1, 2, 6, 5 ], //向かって右面
51 [ 0, 4, 7, 3 ], //向かって左面
52 [ 7, 6, 2, 3 ], //向かって上面
53 [ 0, 1, 5, 4 ], //向かって下面
54 [ 5, 6, 7, 4 ], //向かって背面
55 ]; //※各面の点の並びは時計回り
56
57 //正六面体の拡大
58 masters.cube.scale = 10;
59
60 //正六面体の位置
61 masters.cube.posX = 0;
62 masters.cube.posY = 0;
63 masters.cube.posZ = 80;
64
65 //正六面体の回転
66 masters.cube.kaitenH = 3.14 / 8;
67 masters.cube.kaitenV = 0;
68
69 //正六面体のベース色
70 masters.cube.baseColor = { r : 196, g : 196, b : 0 };
71
72 //リンクモデル
73 models = new Array();
74 var makeLink = function( master ) {
75 var model = new Object();
76 model.tens = master.tens; //参照
77 model.mens = master.mens; //参照
78 model.scale = master.scale; //コピー
79 model.posX = master.posX; //コピー
80 model.posY = master.posY; //コピー
81 model.posZ = master.posZ; //コピー
82 model.kaitenH = master.kaitenH; //コピー
83 model.kaitenV = master.kaitenV; //コピー
84 model.baseColor = objCpy( master.baseColor ); //コピー
85 return model;
86 };
87
88 //リンク1 赤
89 var model = makeLink( masters.cube );
90 model.posX = -15;
91 model.posZ -= 1;
92 model.baseColor = { r : 196, g : 48, b : 48 };
93 models.push( model );
94
95 //リンク2 緑
96 var model = makeLink( masters.cube );
97 model.scale *= 1.2;
98 model.posX = 9;
99 model.posY = -models[ 0 ].scale + model.scale; //高さ合わせ
100 model.posZ += 5;
101 model.baseColor = { r : 48, g : 196, b : 48 };
102 models.push( model );
103
104 //リンク3 青
105 var model = makeLink( masters.cube );
106 model.scale *= 0.3;
107 model.posX = 1;
108 model.posY = -models[ 0 ].scale + model.scale; //高さ合わせ
109 model.posZ = 62;
110 model.kaitenH = 0.02;
111 model.baseColor = { r : 48, g : 48, b : 196 };
112 models.push( model );
113
114 cam = new Object();
115
116 //望遠レンズなら200、普通のカメラなら50(レンズサイズまたは焦点距離という)
117 cam.s = 30;
118
119 //視点回転
120 cam.kaitenH = 0;
121 cam.kaitenV = 0;
122
123 //視点位置
124 cam.posX = 0;
125 cam.posY = 0;
126 cam.posZ = 0;
127
128 //ある位置を注視しながら回転する
129 var center = new Object();
130 center.x = models[ 2 ].posX;
131 center.y = models[ 2 ].posY;
132 center.z = models[ 2 ].posZ;
133 doTarget( cam, center );
134 var kaitenH = -3.14 / 4;
135 var kaitenV = -3.14 / 16 * 5;
136 camDomeKaiten( cam, center, kaitenH, kaitenV );
137
138 //光源
139 kougen = new Object();
140 kougen.pos = xyz( 0, 60, -60 );
141
142 draw();
143 }
144
145 function draw() {
146
147 //各モデルについて
148 for( var m = 0; m < models.length; m++ ) {
149 var model = models[ m ];
150 model.tensC = new Array();
151
152 //頂点の位置を計算
153 for( var i = 0; i < model.tens.length; i++ ) {
154 var x = model.tens[ i ].x;
155 var y = model.tens[ i ].y;
156 var z = model.tens[ i ].z;
157
158 //scale倍
159 x *= model.scale;
160 y *= model.scale;
161 z *= model.scale;
162
163 //回転
164 var res = kaiten( x, z, model.kaitenH );
165 x = res.X;
166 z = res.Y;
167
168 var res = kaiten( z, y, model.kaitenV );
169 z = res.X;
170 y = res.Y;
171
172 //位置へ移動
173 x += model.posX;
174 y += model.posY;
175 z += model.posZ;
176
177 //カメラ移動
178 x += -cam.posX;
179 y += -cam.posY;
180 z += -cam.posZ;
181
182
183 //カメラ回転
184 var res = kaiten( x, z, -cam.kaitenH );
185 x = res.X;
186 z = res.Y;
187
188 var res = kaiten( z, y, -cam.kaitenV );
189 z = res.X;
190 y = res.Y;
191
192
193 //3Dの座標を2Dの座標に変換する
194 var h = x * ( cam.s / z );
195 var v = -y * ( cam.s / z );
196
197 //画面を引き延ばし
198 h *= 14.25;
199 v *= 14.25;
200
201 //値を保管
202 model.tensC[ i ] = new Object();
203 model.tensC[ i ].x = x;
204 model.tensC[ i ].y = y;
205 model.tensC[ i ].z = z;
206 model.tensC[ i ].h = h;
207 model.tensC[ i ].v = v;
208
209 }//for i
210 }//for m
211 //頂点の位置を計算済み
212
213
214 //面の配列を作成する
215 var allmens = new Array();
216
217 //各モデルについて
218 for( var m = 0; m < models.length; m++ ) {
219 var model = models[ m ];
220
221 for( var j = 0; j < model.mens.length; j++ ) {
222 var men = model.mens[ j ];
223
224 var jusin; //陰面消去1と陰面消去2で使用する値
225 var housen; //陰面消去2と陰影処理で使用する値
226
227 var tmpTens = new Array();
228 for( var i = 0; i < men.length; i++ )
229 tmpTens.push( model.tensC[ men[ i ] ] );
230
231 jusin = getJusin( tmpTens );
232 housen = getHousen( tmpTens );
233
234 //check. 面が視点のほうを向いていないなら、その面は除去
235 //(陰面消去2 「法線ベクトル法」)
236 if( ! checkVisibility( housen, jusin ) ) continue;
237
238 //allmensへ追加する
239 var theMen = new Object();
240 theMen.men = men;
241 theMen.jusin = jusin;
242 theMen.housen = housen;
243 theMen.model = model;
244 allmens.push( theMen );
245
246 }//for j
247 }//for m
248
249
250 //すべての面を奥から手前の順にソート
251 //(陰面消去1 「画家のアルゴリズム」)
252 allmens.sort( function( a, b ) {
253 if( a.jusin.z > b.jusin.z ) return -1;
254 else if( a.jusin.z < b.jusin.z ) return 1;
255 else return 0;
256 } );
257
258
259 //描画
260 for( var i = 0; i < allmens.length; i++ ) {
261 var men = allmens[ i ].men;
262 var model = allmens[ i ].model;
263 var housen = allmens[ i ].housen;
264
265 //面を構成する点を順にたどる
266 canvas.beginPath();
267 for( var j = 0; j < men.length; j++ ) {
268 var tenIDX = men[ j ];
269 var ten = model.tensC[ tenIDX ];
270
271 var h = ten.h;
272 var v = ten.v;
273
274 //最初の点はmoveTo、続く点はlineTo
275 if( j == 0 ) {
276 canvas.moveTo( screenW / 2 + h, screenH / 2 + v );
277 } else {
278 canvas.lineTo( screenW / 2 + h, screenH / 2 + v );
279 }
280 }
281 canvas.closePath();
282
283 //面の中を塗る
284 //陰影処理(ランバート反射)を加える
285 //kougen.posは原点から見た位置ですが、これを面から見た位置に直し、kougenPとします。
286 var kougenP = xyz( kougen.pos.x - jusin.x, kougen.pos.y - jusin.y, kougen.pos.z - jusin.z );
287 canvas.fillStyle = rgb2str( lambelt( toNorm( kougenP ), housen, model.baseColor ) );
288 canvas.fill();
289
290 //面の線を描く
291 canvas.strokeStyle = "black";
292 canvas.stroke();
293 }
294 }
295
296 //---その他関数
297
298 function checkVisibility( housen, jusin ) {
299
300 //視点の方向(eye)
301 var sisen = objCpy( jusin );
302 sisen.x *= -1;
303 sisen.y *= -1;
304 sisen.z *= -1;
305
306 //法線と視点方向の内積(※cosθ)
307 var a = naiseki( housen, sisen );
308
309 return a > 0;
310 }
311
312 function kaiten( x, y, theta2 ) {
313 /*
314 数学分野の汎用関数
315 原点を中心にして、回転する
316 */
317 var theta1 = Math.atan2( y, x );
318 var hankei = Math.sqrt( x * x + y * y );
319 var kaitenX = Math.cos( theta1 + theta2 ) * hankei;
320 var kaitenY = Math.sin( theta1 + theta2 ) * hankei;
321 return { X : kaitenX, Y : kaitenY };
322 }
323
324 function rgb2str( rgb ) {
325 /*
326 便利目的の関数
327 */
328 console.log( "RGB(" + rgb.r + "," + rgb.g + "," + rgb.b + ")" );
329 return "RGB(" + rgb.r + "," + rgb.g + "," + rgb.b + ")";
330 }
331
332 function objCpy( obj ) {
333 /*
334 便利目的の関数
335 */
336 var res = new Object();
337 for( var name in obj ) {
338 res[ name ] = obj[ name ];
339 }
340 return res;
341 }
342
343 function xyz( x, y, z ) {
344 /*
345 便利目的の関数
346 */
347 return { x : x, y : y, z : z };
348 }
349
350 function getJusin( p ) {
351 /*
352 数学分野の汎用関数
353 面を構成する各頂点の、x,y,z各成分の平均値を得る(重心)
354 */
355 var res = xyz( 0, 0, 0 );
356 for( var i = 0; i < p.length; i++ ) {
357 res.x += p[ i ].x;
358 res.y += p[ i ].y;
359 res.z += p[ i ].z;
360 }
361 res.x /= p.length;
362 res.y /= p.length;
363 res.z /= p.length;
364 return res;
365 }
366
367 function doTarget( cam, targetP ) {
368 /*
369 カメラの位置はそのままで、targetの方向を向かせる。
370 */
371
372 //カメラを0,0,0としたターゲットの位置
373 var tx = targetP.x - cam.posX;
374 var ty = targetP.y - cam.posY;
375 var tz = targetP.z - cam.posZ;
376
377 //カメラから見たターゲットの角度
378 var kakudoTH = Math.atan2( tz, tx );
379 var kakudoTV = Math.atan2( ty, tz );
380
381 //カメラに反映
382 cam.kaitenH = kakudoTH - 1.57; //角度はxの方向から数えるのに対し、カメラはZ方向を見て回転0としているから-1.57
383 cam.kaitenV = kakudoTV;
384 }
385
386 function camDomeKaiten( cam, center, theta2H, theta2V ) {
387 /*
388 カメラを、ある位置を注視しながら回転させる
389 */
390
391 //水平回転
392 if( theta2H != 0 ) {
393 //水平面において、カメラの位置をcenterを中心にtheta2だけ加算回転、
394 var res = kaiten2( center.x, center.z, cam.posX, cam.posZ, theta2H );
395 cam.posX = res.X;
396 cam.posZ = res.Y;
397 cam.kaitenH += theta2H;
398 }
399
400 //垂直回転
401 if( theta2V != 0 ) {
402
403 //check. 真上、真下になったら回転しない
404 /*
405 縦角度の加算後が、真上(-1.57)や真下(1.57)を超えるなら、
406 加算後の縦角度が-1.57か1.57になるように加算値を変更する。
407 */
408 var v = Math.abs( Math.floor( ( cam.kaitenV + theta2V ) * 100 ) / 100 );
409 if( v >= 3.14 / 2 ) {
410 theta2V = 3.14 / 2 * ( theta2V > 0 ? 1 : -1 ) - cam.kaitenV;
411 //check.
412 if( theta2V == 0 ) return;
413 }
414
415 //1. 水平回転分を回転なしへ戻す
416 var kaitenHbak = cam.kaitenH;
417
418 var res = kaiten2( center.x, center.z, cam.posX, cam.posZ, -cam.kaitenH );
419 cam.posX = res.X;
420 cam.posZ = res.Y;
421 cam.kaitenH = 0;
422
423 //2. 垂直回転を行う
424 var res = kaiten2( center.z, center.y, cam.posZ, cam.posY, theta2V );
425 cam.posZ = res.X;
426 cam.posY = res.Y;
427 cam.kaitenV += theta2V;
428
429 //3. 水平回転を復元する
430 var res = kaiten2( center.x, center.z, cam.posX, cam.posZ, kaitenHbak );
431 cam.posX = res.X;
432 cam.posZ = res.Y;
433 cam.kaitenH = kaitenHbak;
434
435 }//if
436 }//camDomeKaiten()
437
438 function kaiten2( centerX, centerY, x, y, theta2 ) {
439 /*
440 数学分野の汎用関数
441 ある場所を中心にして、回転する
442 */
443 x -= centerX;
444 y -= centerY;
445 var theta1 = Math.atan2( y, x );
446 var hankei = Math.sqrt( x * x + y * y );
447 var kaitenX = Math.cos( theta1 + theta2 ) * hankei;
448 var kaitenY = Math.sin( theta1 + theta2 ) * hankei;
449 kaitenX += centerX;
450 kaitenY += centerY;
451 return { X : kaitenX, Y : kaitenY };
452 }
453
454 function naiseki( v1, v2 ) {
455 /*
456 数学分野の汎用関数
457 内積を得る
458 */
459 var a = v1.x * v2.x + v1.y * v2.y + v1.z * v2.z;
460 return a;
461 }
462
463 function getHousen( p ) {
464 /*
465 数学分野の汎用関数
466 法線を得る
467 */
468 var x1 = p[ 0 ].x, y1 = p[ 0 ].y, z1 = p[ 0 ].z;
469 var x2 = p[ 1 ].x, y2 = p[ 1 ].y, z2 = p[ 1 ].z;
470 var x3 = p[ 2 ].x, y3 = p[ 2 ].y, z3 = p[ 2 ].z;
471
472 //法線
473 var res = new Object();
474 res.x = (y2-y1)*(z3-z2)-(z2-z1)*(y3-y2);
475 res.y = (z2-z1)*(x3-x2)-(x2-x1)*(z3-z2);
476 res.z = (x2-x1)*(y3-y2)-(y2-y1)*(x3-x2);
477 res = toNorm( res );
478
479 return res;
480 }
481
482 function toNorm( p ) {
483 /*
484 数学分野の汎用関数
485 原点から座標pまでの距離を1にした座標を返す(単位ベクトル化)
486 (※陰面消去2では本当は必要ないがランバート反射で必要になったので用意した)
487 */
488 var len = Math.sqrt( p.x * p.x + p.y * p.y + p.z * p.z );
489 var res = new Object();
490 res.x = p.x / len;
491 res.y = p.y / len;
492 res.z = p.z / len;
493 return res;
494 }
495
496 function lambelt( housen, kougen, rgb ) {
497 var cosTheta = naiseki( housen, kougen );
498 var directPer = 0.6; //直接光の割合
499 var ambientPer = 1 - directPer; //環境光の割合
500
501 //色成分をさらに直接光と環境光の割合で分割、直接光についてはcosTheta(0~1)でさらにしぼる
502 var dirR = rgb.r * directPer * cosTheta;
503 var ambR = rgb.r * ambientPer;
504 var dirG = rgb.g * directPer * cosTheta;
505 var ambG = rgb.g * ambientPer;
506 var dirB = rgb.b * directPer * cosTheta;
507 var ambB = rgb.b * ambientPer;
508
509 //分割したものを再度合体(直接光分にcosThetaを掛けたかっただけ)
510 var res = new Object();
511 res.r = Math.round( dirR + ambR );
512 res.g = Math.round( dirG + ambG );
513 res.b = Math.round( dirB + ambB );
514
515 return res;
516 }
517
518
519 </script>
520 <style>
521 </style>
522 </head>
523 <body onload="onloadx();" style="
524 background-color : lightgray;
525 height : 100%;
526 margin : 0;
527 ">
528 <canvas id="canvasELID" width="512" height="448" style="
529 display : block;
530 margin : auto;
531 background-color : white;
532 border : solid 1px black;
533 ">There is no canvas.</canvas>
534 </body>
535 </html>