サイトトップ

Director Flash 書籍 業務内容 プロフィール

HTML5テクニカルノート

Away3D: テクスチャの凹凸と反射 ー 法線マップとスペキュラマップ

ID: FN1407002 Technique: HTML5 and JavaScript Library: Away3D TypeScript

オープンソースのリアルタイム3Dエンジン「Away3D」のJavaScriptへの移植が、「Away3D TypeScript」ライブラリとして進められています。すでにノート「Away3D: パーティクルのアニメーション」では、このAway3D TypeScriptを使った3次元空間のアニメーションで、燃える炎を表現してみました(サンプル001)。このサンプルの床にテクスチャを与え、さらに凹凸や反射の表現も加えてみましょう。

サンプル001■Away3D 14/08/26: Animating particle fires shot by the interactive camera


01 床の平面にテクスチャを与える

床の平面に与えるテクスチャは、Away3D TypeScriptのサイトからダウンロードできるサンプル「GitHub: StageGL Examples」のディレクトリbin/assets/に納められているfloor_diffuse.jpgを用います。マテリアルへのテクスチャの与え方は、前出「Away3D: パーティクルのアニメーション」09「パーティクルの素材にテクスチャを与える」でご説明しましたので、詳しくはこのノートをご参照ください。

図001■床に与えるテクスチャ
図001

前出「Away3D: パーティクルのアニメーション」のコード005「3次元空間のカメラをマウス操作に合わせてパンおよびチルトさせる」をもとに、JavaScriptコードを書き加えていきます。でき上がったscript要素は、後にコード001としてまとめました。その中の床にテクスチャを与える処理は、以下に抜書きしたとおりです。

読込む画像ファイルはこの後も増えるので、URLは変数(imageDiffuseとimageParticle)にとりました(第36〜37行目)。そして、ロードの関数(loadAsset())にそのURLを渡して読込みます(第45〜46行目)。また、床の平面をつくる関数(createPlane())は、そのマテリアルには後でテクスチャを与えるので、オブジェクトをつくるTriangleMethodMaterial()コンストラクタは引数なしに呼出します(第62行目)。

素材が読込まれたとき(AssetLibraryBundle.resourceCompleteイベントで)呼出されるリスナー関数(onResourceComplete())は、扱う画像ファイルが複数になったのでその判定をしなければなりません。引数のイベントオブジェクト(eventObject)から素材の配列(assetsプロパティ)とURL(urlプロパティ)を得て、case文で配列エレメントの素材(asset)をURLに対応したマテリアルのテクスチャに与えます(第187〜195行目)。なお、else文の処理については前出「Away3D: パーティクルのアニメーション」の注[*3]をご参照ください。

  1. var imageDiffuse = "assets/floor_diffuse.jpg";
  2. var imageParticle = "assets/blue.png";
  3. function initialize() {
  1.   loadAsset(imageDiffuse);
      // loadAsset("assets/blue.png");
  2.   loadAsset(imageParticle);
  1. }
  1. function createPlane(width, height, light, specular, y, scene) {
      // var defaultTexture = away.materials.DefaultMaterialManager.getDefaultTexture();
      // var material = new away.materials.TriangleMethodMaterial(defaultTexture);
  2.   var material = new away.materials.TriangleMethodMaterial();
  1. }
  1. function onResourceComplete(eventObject) {
  2.   var assets = eventObject.assets;
  3.   var count = assets.length;
  4.   var url = eventObject.url;
  5.   var planeMaterial = mesh.material;
      // if (assets.length > 0) {
  6.   if (count > 0) {
        // particleMaterial.texture = assets[0];
  7.     for (var i = 0; i < count; i++) {
  8.       var asset = assets[i];
  9.       switch (url) {
  10.         case (imageDiffuse):
  11.           planeMaterial.texture = asset;
  12.           break;
  13.         case (imageParticle):
  14.           particleMaterial.texture = asset;
  15.           break;
  16.       }
  17.     }
  18.   } else {
  19.     loadAsset(eventObject.url);
  20.   }
  21. }

これで、床の平面にテクスチャが与えられました(図002)。表面が滑らかに見えるので、敷石のようなテクスチャではあるものの、質感は模様が入ったプラスチックシートのようです。参照しやすいように、ここまでのscript要素をコード001にまとめました。

図002■床の平面にテクスチャが与えられた
図002

コード001■画像ファイルを読込んで床のテクスチャに定める
  1. <script>
  2. function FireObject(mesh, animator) {
  3.   this.strength = 0;
  4.   this.mesh = mesh;
  5.   this.animator = animator;
  6. }
  7. FireObject.prototype.startAnimation = function() {
  8.   this.animator.start();
  9. }
  10. FireObject.prototype.animateLight = function(fallOff, radius, addition) {
  11.   var light = this.light;
  12.   if (light) {
  13.     if (this.strength < 1) {
  14.       this.strength += 0.1;
  15.     }
  16.     light.fallOff = fallOff;
  17.     light.radius = radius;
  18.     light.diffuse = light.specular = this.strength + addition;
  19.   }
  20. };
  21. </script>
  22. <script>
  23. var view;
  24. var directionalLight;
  25. var mesh;
  26. var cameraController;
  27. var timer;
  28. var fireObjects;
  29. var fireTimer;
  30. var lightPicker;
  31. var particleMaterial;
  32. var lastMouseX;
  33. var lastMouseY;
  34. var lastPanAngle;
  35. var lastTiltAngle;
  36. var imageDiffuse = "assets/floor_diffuse.jpg";
  37. var imageParticle = "assets/blue.png";
  38. function initialize() {
  39.   view = createView(400, 300);
  40.   var scene = view.scene;
  41.   directionalLight = createDirectionalLight(0.5, 0xEEDDDD, 0.5, 0, 0x808090);
  42.   mesh = createPlane(800, 800, directionalLight, 10, -20, scene);
  43.   fireObjects = createParticles(3, 500, 300, 5, scene);
  44.   cameraController = setupCameraController(view.camera, 1000, 0, 90, 45, 20);
  45.   loadAsset(imageDiffuse);
  46.   loadAsset(imageParticle);
  47.   document.onmousedown = startDrag;
  48.   timer = new away.utils.RequestAnimationFrame(render);
  49.   timer.start();
  50.   fireTimer = new away.utils.Timer(1000, fireObjects.length);
  51.   fireTimer.addEventListener(away.events.TimerEvent.TIMER, startFire);
  52.   fireTimer.start();
  53. }
  54. function createView(width, height) {
  55.   var defaultRenderer = new away.render.DefaultRenderer();
  56.   var view = new away.containers.View(defaultRenderer);
  57.   view.width = width;
  58.   view.height = height;
  59.   return view;
  60. }
  61. function createPlane(width, height, light, specular, y, scene) {
  62.   var material = new away.materials.TriangleMethodMaterial();
  63.   var mesh = new away.prefabs.PrimitivePlanePrefab(width, height).getNewObject();
  64.   mesh.material = material;
  65.   lightPicker = new away.materials.StaticLightPicker([light]);
  66.   material.lightPicker = lightPicker;
  67.   material.repeat = true;
  68.   material.specular = specular;
  69.   mesh.y = y;
  70.   scene.addChild(mesh);
  71.   return mesh;
  72. }
  73. function createParticles(numFires, numParticles, radius, y, scene) {
  74.   var ParticlePropertiesMode = away.animators.ParticlePropertiesMode;
  75.   var ParticleVelocityNode = away.animators.ParticleVelocityNode;
  76.   var ParticleColorNode = away.animators.ParticleColorNode;
  77.   var ColorTransform = away.geom.ColorTransform;
  78.   var GLOBAL = ParticlePropertiesMode.GLOBAL;
  79.   var startColor = new ColorTransform(0, 0, 0, 1, 0xFF, 0x33, 0x01);
  80.   var endColor = new ColorTransform(0, 0, 0, 1, 0x99);
  81.   var animations = [
  82.     new away.animators.ParticleBillboardNode(),
  83.     new away.animators.ParticleScaleNode(GLOBAL, false, false, 2.5, 0.5),
  84.     new ParticleVelocityNode(GLOBAL, new away.geom.Vector3D(0, 80, 0)),
  85.     new away.animators.ParticleColorNode(GLOBAL, true, true, false, false, startColor, endColor),
  86.     new ParticleVelocityNode(ParticlePropertiesMode.LOCAL_STATIC)
  87.   ];
  88.   var animationSet = getParticleAnimationSet(animations, initParticle);
  89.   var primitive = new away.prefabs.PrimitivePlanePrefab(10, 10, 1, 1, false);
  90.   var geometry = primitive.geometry;
  91.   var material = particleMaterial = new away.materials.TriangleMethodMaterial();
  92.   var geometrySet = [];
  93.   material.blendMode = away.base.BlendMode.ADD;
  94.   for (var i = 0; i < numParticles; i++) {
  95.     geometrySet[i] = geometry;
  96.   }
  97.   var fireObjects = getFireObjects(geometrySet, numFires, material, animationSet, radius, y, scene);
  98.   return fireObjects;
  99. }
  100. function getFireObjects(geometrySet, numFires, material, animationSet, radius, y, scene) {
  101.   var fireObjects = [];
  102.   var particleGeometry = away.tools.ParticleGeometryHelper.generateGeometry(geometrySet);
  103.   var anglePerFire = Math.PI * 2 / numFires;
  104.   for (var j = 0; j < numFires; j++) {
  105.     var mesh = createAnimationParticle(particleGeometry, material, animationSet, fireObjects);
  106.     var angle = j * anglePerFire;
  107.     mesh.x = radius * Math.sin(angle);
  108.     mesh.z = radius * Math.cos(angle);
  109.     mesh.y = y;
  110.     scene.addChild(mesh);
  111.   }
  112.   return fireObjects;
  113. }
  114. function getParticleAnimationSet(animations, initParticleFunc) {
  115.   var animationSet = new away.animators.ParticleAnimationSet(true, true);
  116.   var count = animations.length;
  117.   for (var i = 0; i < count; i++) {
  118.     animationSet.addAnimation(animations[i]);
  119.   }
  120.   animationSet.initParticleFunc = initParticleFunc;
  121.   return animationSet;
  122. }
  123. function createAnimationParticle(particleGeometry, material, animationSet, fireObjects) {
  124.   var mesh = new away.entities.Mesh(particleGeometry, material);
  125.   var animator = new away.animators.ParticleAnimator(animationSet);
  126.   mesh.animator = animator;
  127.   fireObjects.push(new FireObject(mesh, animator));
  128.   return mesh;
  129. }
  130. function initParticle(prop) {
  131.   var PIx2 = Math.PI * 2;
  132.   var degree1 = Math.random() * PIx2;
  133.   var degree2 = Math.random() * PIx2;
  134.   var radius = 15;
  135.   var sin_1 = Math.sin(degree1);
  136.   var cos_1 = Math.cos(degree1);
  137.   var sin_2 = Math.sin(degree2);
  138.   var cos_2 = Math.cos(degree2);
  139.   var velocityVector = new away.geom.Vector3D(radius * cos_1 * cos_2, radius * sin_1 * cos_2, radius * sin_2);
  140.   prop.startTime = Math.random() * 5;
  141.   prop.duration = Math.random() * 4 + 0.1;
  142.   prop[away.animators.ParticleVelocityNode.VELOCITY_VECTOR3D] = velocityVector;
  143. }
  144. function startFire(eventObject) {
  145.   var fireObject = fireObjects[fireTimer.currentCount - 1];
  146.   var lights = lightPicker.lights;
  147.   var light = new away.entities.PointLight();
  148.   fireObject.startAnimation();
  149.   light.color = 0xFF3301;
  150.   light.diffuse = 0;
  151.   light.specular = 0;
  152.   light.transform.position = fireObject.mesh.transform.position;
  153.   fireObject.light = light;
  154.   lights.push(light);
  155.   lightPicker.lights = lights;
  156. }
  157. function createDirectionalLight(ambient, color, diffuse, specular, ambientColor) {
  158.   var light = new away.entities.DirectionalLight();
  159.   light.direction = new away.geom.Vector3D(0, -1, 0);
  160.   light.ambient = ambient;
  161.   light.color = color;
  162.   light.diffuse = diffuse;
  163.   light.specular = specular;
  164.   light.ambientColor = ambientColor;
  165.   return light;
  166. }
  167. function setupCameraController(camera, distance, minTiltAngle, maxTiltAngle, panAngle, tiltAngle) {
  168.   var cameraController = new away.controllers.HoverController(camera);
  169.   cameraController.distance = distance;
  170.   cameraController.minTiltAngle = minTiltAngle;
  171.   cameraController.maxTiltAngle = maxTiltAngle;
  172.   cameraController.panAngle = panAngle;
  173.   cameraController.tiltAngle = tiltAngle;
  174.   return cameraController;
  175. }
  176. function loadAsset(url) {
  177.   var AssetLibrary = away.library.AssetLibrary;
  178.   AssetLibrary.addEventListener(away.events.LoaderEvent.RESOURCE_COMPLETE, onResourceComplete);
  179.   AssetLibrary.load(new away.net.URLRequest(url));
  180. }
  181. function onResourceComplete(eventObject) {
  182.   var assets = eventObject.assets;
  183.   var count = assets.length;
  184.   var url = eventObject.url;
  185.   var planeMaterial = mesh.material;
  186.   if (count > 0) {
  187.     for (var i = 0; i < count; i++) {
  188.       var asset = assets[i];
  189.       switch (url) {
  190.         case (imageDiffuse):
  191.           planeMaterial.texture = asset;
  192.           break;
  193.         case (imageParticle):
  194.           particleMaterial.texture = asset;
  195.           break;
  196.       }
  197.     }
  198.   } else {
  199.     loadAsset(eventObject.url);
  200.   }
  201. }
  202. function render(timeStamp) {
  203.   var count = fireObjects.length;
  204.   for (var i = 0; i < count; i++) {
  205.     var fireObject = fireObjects[i];
  206.     fireObject.animateLight(380 + Math.random() * 20, 200 + Math.random() * 30, Math.random() * 0.2);
  207.   }
  208.   view.render();
  209. }
  210. function startDrag(eventObject) {
  211.   lastMouseX = eventObject.clientX;
  212.   lastMouseY = eventObject.clientY;
  213.   lastPanAngle = cameraController.panAngle;
  214.   lastTiltAngle = cameraController.tiltAngle;
  215.   document.onmousemove = drag;
  216.   document.onmouseup = stopDrag;
  217. }
  218. function drag(eventObject) {
  219.   cameraController.panAngle = 0.5 * (eventObject.clientX - lastMouseX) + lastPanAngle;
  220.   cameraController.tiltAngle = 0.3 * (eventObject.clientY - lastMouseY) + lastTiltAngle;
  221. }
  222. function stopDrag(eventObject) {
  223.   document.onmousemove = null;
  224.   document.onmouseup = null;
  225. }
  226. </script>

02 テクスチャに凹凸と反射の質感を加える

均一な平面のテクスチャに凹凸と反射の違いを加えると、質感が大きく変わります。もっとも、3次元のオブジェクトのかたちそのものに細かな凹凸を加えたら、負荷が上がるばかりです。それに細かいところはこだわらなくても、それらしく見えれば済みます。そこで、テクスチャに与えるマップで、光の反射の仕方を変えてみます。

マップはふたつ使います。第1は、法線マップです。ノーマルマップ(normal map)とも呼ばれます。"normal"は「普通」ではなく、面に垂直な「法線」を意味します。つまり、テクスチャのピクセルごとの面の向きを示すのです[*1]。なお、テクスチャの画素は「テクセル」ということもあります。法線マップの画像は、各ピクセルの法線のxyz座標値をRGBチャネルの値で表します(図003左)[*2]

第2に用いるのが、スペキュラマップです。ピクセルごとの反射の強さを示します。光を反射しやすいかしにくいかによって、テクスチャの質感はかなり変わります。カラーが白に近いほど光の反射は強く、黒に近づけると光を吸収します(図003右)[*3]。なお、これらふたつのマップ画像として、サンプルのfloor_normal.jpgfloor_specular.jpgを用いました。

図003■法線マップとスペキュラマップ
図003左
法線マップ
図003右
スペキュラマップ
 

画像としてつくった法線とスペキュラのマップをマテリアルに与えるには、それぞれTriangleMethodMaterial.normalMapTriangleMethodMaterial.specularMapプロパティに定めます(表001)。画像ファイルの読込みと、ロードした素材のプロパティへの割当は、以下の抜書きのようにテクスチャに用いたふたつの関数(loadAsset()とonResourceComplete())で扱います。

表001■TriangleMethodMaterialクラスのマップを定めるプロパティ
TriangleMethodMaterialプロパティ 値とマップの機能
normalMap 法線マップとするテクスチャ。ピクセル(テクセル)ごとの面の向きを示し、陰影を与える。
specularMap スペキュラマップとするテクスチャ。ピクセル(テクセル)ごとの光の反射の強さを示す。
  1. var imageNormal = "assets/floor_normal.jpg";
  2. var imageSpecular = "assets/floor_specular.jpg";
  1. function initialize() {
  1.   loadAsset(imageNormal);
  2.   loadAsset(imageSpecular);
  1. }
  1. function onResourceComplete(eventObject) {
  1.   for (var i = 0; i < count; i++) {
  2.     var asset = assets[i];
  3.     switch (url) {
  1.       case (imageNormal):
  2.         planeMaterial.normalMap = asset;
  3.         break;
  4.       case (imageSpecular):
  5.         planeMaterial.specularMap = asset;
  6.         break;
  1.     }
  2.   }
  1. }

法線マップにより敷石の凹凸が示され、それらの境目は反射がなくなりました(図004)。もとののっぺりとしたテクスチャに、細かな質感が加わっています。マウスドラッグでパン・チルトすると、違いがよくわかるでしょう。ここまで手を加えたJavaScriptは、コード002にまとめました。なお、クラス(FireObject)の定めは変えていないので省いています。また、ご参考までに、jsdo.itにもサンプル002のコードを掲げました。

図004■床のテクスチャに凹凸や反射の違いが加えられた
図004

コード002■床の平面に法線マップとスペキュラマップを与える
  1. var view;
  2. var directionalLight;
  3. var mesh;
  4. var cameraController;
  5. var timer;
  6. var fireObjects;
  7. var fireTimer;
  8. var lightPicker;
  9. var particleMaterial;
  10. var lastMouseX;
  11. var lastMouseY;
  12. var lastPanAngle;
  13. var lastTiltAngle;
  14. var imageDiffuse = "assets/floor_diffuse.jpg";
  15. var imageNormal = "assets/floor_normal.jpg";
  16. var imageSpecular = "assets/floor_specular.jpg";
  17. var imageParticle = "assets/blue.png";
  18. function initialize() {
  19.   view = createView(400, 300);
  20.   var scene = view.scene;
  21.   directionalLight = createDirectionalLight(0.5, 0xEEDDDD, 0.5, 0, 0x808090);
  22.   mesh = createPlane(800, 800, directionalLight, 10, -20, scene);
  23.   fireObjects = createParticles(3, 500, 300, 5, scene);
  24.   cameraController = setupCameraController(view.camera, 1000, 0, 90, 45, 20);
  25.   loadAsset(imageDiffuse);
  26.   loadAsset(imageNormal);
  27.   loadAsset(imageSpecular);
  28.   loadAsset(imageParticle);
  29.   document.onmousedown = startDrag;
  30.   timer = new away.utils.RequestAnimationFrame(render);
  31.   timer.start();
  32.   fireTimer = new away.utils.Timer(1000, fireObjects.length);
  33.   fireTimer.addEventListener(away.events.TimerEvent.TIMER, startFire);
  34.   fireTimer.start();
  35. }
  36. function createView(width, height) {
  37.   var defaultRenderer = new away.render.DefaultRenderer();
  38.   var view = new away.containers.View(defaultRenderer);
  39.   view.width = width;
  40.   view.height = height;
  41.   return view;
  42. }
  43. function createPlane(width, height, light, specular, y, scene) {
  44.   var material = new away.materials.TriangleMethodMaterial();
  45.   var mesh = new away.prefabs.PrimitivePlanePrefab(width, height).getNewObject();
  46.   mesh.material = material;
  47.   lightPicker = new away.materials.StaticLightPicker([light]);
  48.   material.lightPicker = lightPicker;
  49.   material.repeat = true;
  50.   material.specular = specular;
  51.   mesh.y = y;
  52.   scene.addChild(mesh);
  53.   return mesh;
  54. }
  55. function createParticles(numFires, numParticles, radius, y, scene) {
  56.   var ParticlePropertiesMode = away.animators.ParticlePropertiesMode;
  57.   var ParticleVelocityNode = away.animators.ParticleVelocityNode;
  58.   var ParticleColorNode = away.animators.ParticleColorNode;
  59.   var ColorTransform = away.geom.ColorTransform;
  60.   var GLOBAL = ParticlePropertiesMode.GLOBAL;
  61.   var startColor = new ColorTransform(0, 0, 0, 1, 0xFF, 0x33, 0x01);
  62.   var endColor = new ColorTransform(0, 0, 0, 1, 0x99);
  63.   var animations = [
  64.     new away.animators.ParticleBillboardNode(),
  65.     new away.animators.ParticleScaleNode(GLOBAL, false, false, 2.5, 0.5),
  66.     new ParticleVelocityNode(GLOBAL, new away.geom.Vector3D(0, 80, 0)),
  67.     new away.animators.ParticleColorNode(GLOBAL, true, true, false, false, startColor, endColor),
  68.     new ParticleVelocityNode(ParticlePropertiesMode.LOCAL_STATIC)
  69.   ];
  70.   var animationSet = getParticleAnimationSet(animations, initParticle);
  71.   var primitive = new away.prefabs.PrimitivePlanePrefab(10, 10, 1, 1, false);
  72.   var geometry = primitive.geometry;
  73.   var material = particleMaterial = new away.materials.TriangleMethodMaterial();
  74.   var geometrySet = [];
  75.   material.blendMode = away.base.BlendMode.ADD;
  76.   for (var i = 0; i < numParticles; i++) {
  77.     geometrySet[i] = geometry;
  78.   }
  79.   var fireObjects = getFireObjects(geometrySet, numFires, material, animationSet, radius, y, scene);
  80.   return fireObjects;
  81. }
  82. function getFireObjects(geometrySet, numFires, material, animationSet, radius, y, scene) {
  83.   var fireObjects = [];
  84.   var particleGeometry = away.tools.ParticleGeometryHelper.generateGeometry(geometrySet);
  85.   var anglePerFire = Math.PI * 2 / numFires;
  86.   for (var j = 0; j < numFires; j++) {
  87.     var mesh = createAnimationParticle(particleGeometry, material, animationSet, fireObjects);
  88.     var angle = j * anglePerFire;
  89.     mesh.x = radius * Math.sin(angle);
  90.     mesh.z = radius * Math.cos(angle);
  91.     mesh.y = y;
  92.     scene.addChild(mesh);
  93.   }
  94.   return fireObjects;
  95. }
  96. function getParticleAnimationSet(animations, initParticleFunc) {
  97.   var animationSet = new away.animators.ParticleAnimationSet(true, true);
  98.   var count = animations.length;
  99.   for (var i = 0; i < count; i++) {
  100.     animationSet.addAnimation(animations[i]);
  101.   }
  102.   animationSet.initParticleFunc = initParticleFunc;
  103.   return animationSet;
  104. }
  105. function createAnimationParticle(particleGeometry, material, animationSet, fireObjects) {
  106.   var mesh = new away.entities.Mesh(particleGeometry, material);
  107.   var animator = new away.animators.ParticleAnimator(animationSet);
  108.   mesh.animator = animator;
  109.   fireObjects.push(new FireObject(mesh, animator));
  110.   return mesh;
  111. }
  112. function initParticle(prop) {
  113.   var PIx2 = Math.PI * 2;
  114.   var degree1 = Math.random() * PIx2;
  115.   var degree2 = Math.random() * PIx2;
  116.   var radius = 15;
  117.   var sin_1 = Math.sin(degree1);
  118.   var cos_1 = Math.cos(degree1);
  119.   var sin_2 = Math.sin(degree2);
  120.   var cos_2 = Math.cos(degree2);
  121.   var velocityVector = new away.geom.Vector3D(radius * cos_1 * cos_2, radius * sin_1 * cos_2, radius * sin_2);
  122.   prop.startTime = Math.random() * 5;
  123.   prop.duration = Math.random() * 4 + 0.1;
  124.   prop[away.animators.ParticleVelocityNode.VELOCITY_VECTOR3D] = velocityVector;
  125. }
  126. function startFire(eventObject) {
  127.   var fireObject = fireObjects[fireTimer.currentCount - 1];
  128.   var lights = lightPicker.lights;
  129.   var light = new away.entities.PointLight();
  130.   fireObject.startAnimation();
  131.   light.color = 0xFF3301;
  132.   light.diffuse = 0;
  133.   light.specular = 0;
  134.   light.transform.position = fireObject.mesh.transform.position;
  135.   fireObject.light = light;
  136.   lights.push(light);
  137.   lightPicker.lights = lights;
  138. }
  139. function createDirectionalLight(ambient, color, diffuse, specular, ambientColor) {
  140.   var light = new away.entities.DirectionalLight();
  141.   light.direction = new away.geom.Vector3D(0, -1, 0);
  142.   light.ambient = ambient;
  143.   light.color = color;
  144.   light.diffuse = diffuse;
  145.   light.specular = specular;
  146.   light.ambientColor = ambientColor;
  147.   return light;
  148. }
  149. function setupCameraController(camera, distance, minTiltAngle, maxTiltAngle, panAngle, tiltAngle) {
  150.   var cameraController = new away.controllers.HoverController(camera);
  151.   cameraController.distance = distance;
  152.   cameraController.minTiltAngle = minTiltAngle;
  153.   cameraController.maxTiltAngle = maxTiltAngle;
  154.   cameraController.panAngle = panAngle;
  155.   cameraController.tiltAngle = tiltAngle;
  156.   return cameraController;
  157. }
  158. function loadAsset(url) {
  159.   var AssetLibrary = away.library.AssetLibrary;
  160.   AssetLibrary.addEventListener(away.events.LoaderEvent.RESOURCE_COMPLETE, onResourceComplete);
  161.   AssetLibrary.load(new away.net.URLRequest(url));
  162. }
  163. function onResourceComplete(eventObject) {
  164.   var assets = eventObject.assets;
  165.   var count = assets.length;
  166.   var url = eventObject.url;
  167.   var planeMaterial = mesh.material;
  168.   if (count > 0) {
  169.     for (var i = 0; i < count; i++) {
  170.       var asset = assets[i];
  171.       switch (url) {
  172.         case (imageDiffuse):
  173.           planeMaterial.texture = asset;
  174.           break;
  175.         case (imageNormal):
  176.           planeMaterial.normalMap = asset;
  177.           break;
  178.         case (imageSpecular):
  179.           planeMaterial.specularMap = asset;
  180.           break;
  181.         case (imageParticle):
  182.           particleMaterial.texture = asset;
  183.           break;
  184.       }
  185.     }
  186.   } else {
  187.     loadAsset(eventObject.url);
  188.   }
  189. }
  190. function render(timeStamp) {
  191.   var count = fireObjects.length;
  192.   for (var i = 0; i < count; i++) {
  193.     var fireObject = fireObjects[i];
  194.     fireObject.animateLight(380 + Math.random() * 20, 200 + Math.random() * 30, Math.random() * 0.2);
  195.   }
  196.   view.render();
  197. }
  198. function startDrag(eventObject) {
  199.   lastMouseX = eventObject.clientX;
  200.   lastMouseY = eventObject.clientY;
  201.   lastPanAngle = cameraController.panAngle;
  202.   lastTiltAngle = cameraController.tiltAngle;
  203.   document.onmousemove = drag;
  204.   document.onmouseup = stopDrag;
  205. }
  206. function drag(eventObject) {
  207.   cameraController.panAngle = 0.5 * (eventObject.clientX - lastMouseX) + lastPanAngle;
  208.   cameraController.tiltAngle = 0.3 * (eventObject.clientY - lastMouseY) + lastTiltAngle;
  209. }
  210. function stopDrag(eventObject) {
  211.   document.onmousemove = null;
  212.   document.onmouseup = null;
  213. }

サンプル002■Away3D 14/08/26: Animating particle fires on a floor with texture

[*1] 法線マップについて詳しくは、3Dグラフィックス・マニアックス (14)「微細凹凸表現の基本形『法線マップ』(1)」をご参照ください。

[*2] 画面に対して左右が赤、上下は緑、前後が青で示されます。「GLSLでバンプマッピング」をご参照ください。

[*3] スペキュラマップは厳密には、赤チャネルが反射の強さ、緑チャンネルで光沢を定めます。とくにふたつを分けて決めなくてもよい場合には、グレースケールで与えて構いません。


03 炎の数を増やす

今回つくるサンプルで参考にしたのは、Away3D TypeScriptの「GitHub: StageGL Examples」からダウンロードでき、サイトの「Examples」の欄にも掲げられている「Animating particles simulating fire」です(図001)。仕上げに、炎の数を同じように増やしてみます。

炎をつくる関数(createParticles())は、初期化の関数(initialize())から呼出しました。呼出す関数に渡す第1引数が、つくる炎の数を決めますので、この数値を大きくすればよいでしょう(第23行目)。

  1. function initialize() {
      // fireObjects = createParticles(3, 500, 300, 5, scene);
  1.   fireObjects = createParticles(10, 500, 300, 5, scene);
  1. }

ところが、ブラウザで確かめてみると、炎がすべて表示される前にオブジェクトがみな消え去って、真っ黒な背景しかが残りません。そして、ブラウザの開発ツールには、「Register overflow!」のエラーが示されます(図005)[*4]。これは、Away3Dのレンダラの処理能力が追いつかないために起こるエラーです。

図005■Safari / Mac OS Xのエラーコンソールに示されたメッセージ
図005

この問題を解決するには、TriangleMethodMaterial.materialModeプロパティを定数TriangleMaterialMode.MULTI_PASSに定めます(デフォルト値はTriangleMaterialMode.SINGLE_PASS)。後に掲げたでき上がりのコード003は、つぎの抜書きように床の平面をつくる関数(createPlane())で、マテリアル(material)にこのプロパティ値を定めました(第46行目)。

  1. function createPlane(width, height, light, specular, y, scene) {
  2.   var material = new away.materials.TriangleMethodMaterial();
  1.   material.materialMode = away.materials.TriangleMaterialMode.MULTI_PASS;
  1. }

これで、床のテクスチャに凹凸や反射が加わったうえ、燃え上がる炎のパーティクルの数も増えました(図006)。参考にしたAway3D TypeScriptサイトの例「Animating particles simulating fire」とほぼ同じ表現です。ただし、スクリプトの組立て方は、筆者がよりわかりやすいと考えるスタイルにしてあります(コード003)。でき上がったコードは、サンプル003としてjsdo.itにも掲げました。

図006■床の上に10の炎が燃え上がる
図006

コード003■TriangleMethodMaterial.materialModeをTriangleMaterialMode.MULTI_PASSに定めて炎のパーティクルを増やした
  1. var view;
  2. var directionalLight;
  3. var mesh;
  4. var cameraController;
  5. var timer;
  6. var fireObjects;
  7. var fireTimer;
  8. var lightPicker;
  9. var particleMaterial;
  10. var lastMouseX;
  11. var lastMouseY;
  12. var lastPanAngle;
  13. var lastTiltAngle;
  14. var imageDiffuse = "assets/floor_diffuse.jpg";
  15. var imageNormal = "assets/floor_normal.jpg";
  16. var imageSpecular = "assets/floor_specular.jpg";
  17. var imageParticle = "assets/blue.png";
  18. function initialize() {
  19.   view = createView(400, 300);
  20.   var scene = view.scene;
  21.   directionalLight = createDirectionalLight(0.5, 0xEEDDDD, 0.5, 0, 0x808090);
  22.   mesh = createPlane(800, 800, directionalLight, 10, -20, scene);
  23.   fireObjects = createParticles(10, 500, 300, 5, scene);
  24.   cameraController = setupCameraController(view.camera, 1000, 0, 90, 45, 20);
  25.   loadAsset(imageDiffuse);
  26.   loadAsset(imageNormal);
  27.   loadAsset(imageSpecular);
  28.   loadAsset(imageParticle);
  29.   document.onmousedown = startDrag;
  30.   timer = new away.utils.RequestAnimationFrame(render);
  31.   timer.start();
  32.   fireTimer = new away.utils.Timer(1000, fireObjects.length);
  33.   fireTimer.addEventListener(away.events.TimerEvent.TIMER, startFire);
  34.   fireTimer.start();
  35. }
  36. function createView(width, height) {
  37.   var defaultRenderer = new away.render.DefaultRenderer();
  38.   var view = new away.containers.View(defaultRenderer);
  39.   view.width = width;
  40.   view.height = height;
  41.   return view;
  42. }
  43. function createPlane(width, height, light, specular, y, scene) {
  44.   var material = new away.materials.TriangleMethodMaterial();
  45.   var mesh = new away.prefabs.PrimitivePlanePrefab(width, height).getNewObject();
  46.   material.materialMode = away.materials.TriangleMaterialMode.MULTI_PASS;
  47.   mesh.material = material;
  48.   lightPicker = new away.materials.StaticLightPicker([light]);
  49.   material.lightPicker = lightPicker;
  50.   material.repeat = true;
  51.   material.specular = specular;
  52.   mesh.y = y;
  53.   scene.addChild(mesh);
  54.   return mesh;
  55. }
  56. function createParticles(numFires, numParticles, radius, y, scene) {
  57.   var ParticlePropertiesMode = away.animators.ParticlePropertiesMode;
  58.   var ParticleVelocityNode = away.animators.ParticleVelocityNode;
  59.   var ParticleColorNode = away.animators.ParticleColorNode;
  60.   var ColorTransform = away.geom.ColorTransform;
  61.   var GLOBAL = ParticlePropertiesMode.GLOBAL;
  62.   var startColor = new ColorTransform(0, 0, 0, 1, 0xFF, 0x33, 0x01);
  63.   var endColor = new ColorTransform(0, 0, 0, 1, 0x99);
  64.   var animations = [
  65.     new away.animators.ParticleBillboardNode(),
  66.     new away.animators.ParticleScaleNode(GLOBAL, false, false, 2.5, 0.5),
  67.     new ParticleVelocityNode(GLOBAL, new away.geom.Vector3D(0, 80, 0)),
  68.     new away.animators.ParticleColorNode(GLOBAL, true, true, false, false, startColor, endColor),
  69.     new ParticleVelocityNode(ParticlePropertiesMode.LOCAL_STATIC)
  70.   ];
  71.   var animationSet = getParticleAnimationSet(animations, initParticle);
  72.   var primitive = new away.prefabs.PrimitivePlanePrefab(10, 10, 1, 1, false);
  73.   var geometry = primitive.geometry;
  74.   var material = particleMaterial = new away.materials.TriangleMethodMaterial();
  75.   var geometrySet = [];
  76.   material.blendMode = away.base.BlendMode.ADD;
  77.   for (var i = 0; i < numParticles; i++) {
  78.     geometrySet[i] = geometry;
  79.   }
  80.   var fireObjects = getFireObjects(geometrySet, numFires, material, animationSet, radius, y, scene);
  81.   return fireObjects;
  82. }
  83. function getFireObjects(geometrySet, numFires, material, animationSet, radius, y, scene) {
  84.   var fireObjects = [];
  85.   var particleGeometry = away.tools.ParticleGeometryHelper.generateGeometry(geometrySet);
  86.   var anglePerFire = Math.PI * 2 / numFires;
  87.   for (var j = 0; j < numFires; j++) {
  88.     var mesh = createAnimationParticle(particleGeometry, material, animationSet, fireObjects);
  89.     var angle = j * anglePerFire;
  90.     mesh.x = radius * Math.sin(angle);
  91.     mesh.z = radius * Math.cos(angle);
  92.     mesh.y = y;
  93.     scene.addChild(mesh);
  94.   }
  95.   return fireObjects;
  96. }
  97. function getParticleAnimationSet(animations, initParticleFunc) {
  98.   var animationSet = new away.animators.ParticleAnimationSet(true, true);
  99.   var count = animations.length;
  100.   for (var i = 0; i < count; i++) {
  101.     animationSet.addAnimation(animations[i]);
  102.   }
  103.   animationSet.initParticleFunc = initParticleFunc;
  104.   return animationSet;
  105. }
  106. function createAnimationParticle(particleGeometry, material, animationSet, fireObjects) {
  107.   var mesh = new away.entities.Mesh(particleGeometry, material);
  108.   var animator = new away.animators.ParticleAnimator(animationSet);
  109.   mesh.animator = animator;
  110.   fireObjects.push(new FireObject(mesh, animator));
  111.   return mesh;
  112. }
  113. function initParticle(prop) {
  114.   var PIx2 = Math.PI * 2;
  115.   var degree1 = Math.random() * PIx2;
  116.   var degree2 = Math.random() * PIx2;
  117.   var radius = 15;
  118.   var sin_1 = Math.sin(degree1);
  119.   var cos_1 = Math.cos(degree1);
  120.   var sin_2 = Math.sin(degree2);
  121.   var cos_2 = Math.cos(degree2);
  122.   var velocityVector = new away.geom.Vector3D(radius * cos_1 * cos_2, radius * sin_1 * cos_2, radius * sin_2);
  123.   prop.startTime = Math.random() * 5;
  124.   prop.duration = Math.random() * 4 + 0.1;
  125.   prop[away.animators.ParticleVelocityNode.VELOCITY_VECTOR3D] = velocityVector;
  126. }
  127. function startFire(eventObject) {
  128.   var fireObject = fireObjects[fireTimer.currentCount - 1];
  129.   var lights = lightPicker.lights;
  130.   var light = new away.entities.PointLight();
  131.   fireObject.startAnimation();
  132.   light.color = 0xFF3301;
  133.   light.diffuse = 0;
  134.   light.specular = 0;
  135.   light.transform.position = fireObject.mesh.transform.position;
  136.   fireObject.light = light;
  137.   lights.push(light);
  138.   lightPicker.lights = lights;
  139. }
  140. function createDirectionalLight(ambient, color, diffuse, specular, ambientColor) {
  141.   var light = new away.entities.DirectionalLight();
  142.   light.direction = new away.geom.Vector3D(0, -1, 0);
  143.   light.ambient = ambient;
  144.   light.color = color;
  145.   light.diffuse = diffuse;
  146.   light.specular = specular;
  147.   light.ambientColor = ambientColor;
  148.   return light;
  149. }
  150. function setupCameraController(camera, distance, minTiltAngle, maxTiltAngle, panAngle, tiltAngle) {
  151.   var cameraController = new away.controllers.HoverController(camera);
  152.   cameraController.distance = distance;
  153.   cameraController.minTiltAngle = minTiltAngle;
  154.   cameraController.maxTiltAngle = maxTiltAngle;
  155.   cameraController.panAngle = panAngle;
  156.   cameraController.tiltAngle = tiltAngle;
  157.   return cameraController;
  158. }
  159. function loadAsset(url) {
  160.   var AssetLibrary = away.library.AssetLibrary;
  161.   AssetLibrary.addEventListener(away.events.LoaderEvent.RESOURCE_COMPLETE, onResourceComplete);
  162.   AssetLibrary.load(new away.net.URLRequest(url));
  163. }
  164. function onResourceComplete(eventObject) {
  165.   var assets = eventObject.assets;
  166.   var count = assets.length;
  167.   var url = eventObject.url;
  168.   var planeMaterial = mesh.material;
  169.   if (count > 0) {
  170.     for (var i = 0; i < count; i++) {
  171.       var asset = assets[i];
  172.       switch (url) {
  173.         case (imageDiffuse):
  174.           planeMaterial.texture = asset;
  175.           break;
  176.         case (imageNormal):
  177.           planeMaterial.normalMap = asset;
  178.           break;
  179.         case (imageSpecular):
  180.           planeMaterial.specularMap = asset;
  181.           break;
  182.         case (imageParticle):
  183.           particleMaterial.texture = asset;
  184.           break;
  185.       }
  186.     }
  187.   } else {
  188.     loadAsset(eventObject.url);
  189.   }
  190. }
  191. function render(timeStamp) {
  192.   var count = fireObjects.length;
  193.   for (var i = 0; i < count; i++) {
  194.     var fireObject = fireObjects[i];
  195.     fireObject.animateLight(380 + Math.random() * 20, 200 + Math.random() * 30, Math.random() * 0.2);
  196.   }
  197.   view.render();
  198. }
  199. function startDrag(eventObject) {
  200.   lastMouseX = eventObject.clientX;
  201.   lastMouseY = eventObject.clientY;
  202.   lastPanAngle = cameraController.panAngle;
  203.   lastTiltAngle = cameraController.tiltAngle;
  204.   document.onmousemove = drag;
  205.   document.onmouseup = stopDrag;
  206. }
  207. function drag(eventObject) {
  208.   cameraController.panAngle = 0.5 * (eventObject.clientX - lastMouseX) + lastPanAngle;
  209.   cameraController.tiltAngle = 0.3 * (eventObject.clientY - lastMouseY) + lastTiltAngle;
  210. }
  211. function stopDrag(eventObject) {
  212.   document.onmousemove = null;
  213.   document.onmouseup = null;
  214. }

サンプル003■Away3D 14/08/26: Animating particle fires on a floor with texture finished

[*4] 2014年8月26日付ビルドでは、このエラーは起こらなくなりました。けれど、修正・改善の中身が明らかにされてないため、本文の記述はそのまま残すことにしました。

なお、"Register overflow!"のエラーは、RegisterPool.requestFreeVectorReg()メソッドから投げられていたようです。また、8月18日付ビルドからライブラリ名が変わりました(「Away3D: 立方体を回してみる」01「Away3D TypeScriptライブラリを使う」参照)。



作成者: 野中文雄
更新日: 2014年9月1日 2014年8月26日付ビルドにもとづいて加筆・補正。
作成日: 2014年7月14日


Copyright © 2001-2014 Fumio Nonaka.  All rights reserved.