このサイトの炎の揺らぎを解読してみようと思います。
実際に見てもらうと分かりますが、立体的でだいぶリアルな感じの炎が表現されています。
ちなみに解説用にThree.jsで実装しなおしたやつをjsdo.itに上げてあります。
そして結論から言うと、メインとなる処理のところはなんとなくの推測しかできませんでした( ;´Д`)
おそらく、視点位置から炎の輪郭となる頂点郡を計算し、3DのCubeTexture的なことをやっているんだと予想。
(なのでメインの処理のメソッド名がslice
なんだと思う)
[2015.06.17 追記]
ここで紹介している実装、サンプルは「ボリュームレンダリング」と呼ばれるものです。
(今回の炎は「Volumetric Flame」)
これは炎などのようにポリゴンで表現しづらい現象を実現するものです。
仕組みとしてはボリューム(今回の例ではキューブ)をスライスして、各断面にテクスチャを貼り、それぞれのピクセルを加算していくことで、炎やガスなどを表現します。
この辺りが参考になりそうです。
- Oculus Rift でリアルタイムボリュームレンダリング
- WEBGL VOLUME RENDERING MADE SIMPLE
- コメントでもらったPDFファイルのリンク - ftp://download.nvidia.com/developer/presentations/2005/GDC/Sponsored_Day/GDC_2005_VolumeRenderingForGames.pdf
ということでコードを見ていきます。
GLSLコード
上記サイトのGLSL部分を抜粋したものが以下です。
見てみたら未使用のコードがあったのでそれらは省いています。
もともとのコメントも残しつつ、自分が付け加えたものは4つスラッシュ(////
)で表現しています。
Vertex shader
まずは頂点シェーダ。
といっても、特殊なことはほぼなにもしていません。
普通に座標変換して、テクスチャ座標をフラグメントシェーダに送っているだけですね。
attribute vec3 position;
attribute vec3 tex;
uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;
varying vec3 texOut;
void main(void) {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1);
texOut = tex;
}
Fragment shader
続いてフラグメントシェーダ。
こっちは結構込みいった感じになっています。
が、イメージとしてはパーリンノイズの亜種、といった感じでしょうか。
////
でコメントしているところが追記したところです。
precision highp float;
// Modified Blum Blum Shub pseudo-random number generator.
//// ランダム数値生成器?
vec2 mBBS(vec2 val, float modulus) {
val = mod(val, modulus); // For numerical consistancy.
return mod(val * val, modulus);
}
// Pregenerated noise texture.
uniform sampler2D nzw;
const float modulus = 61.0; // Value used in pregenerated noise texture.
/**
* Modified noise function.
* @see http://www.csee.umbc.edu/~olano/papers/index.html#mNoise
**/
//// 事前に生成したノイズテクスチャからランダムにテクセルをフェッチ
float mnoise(vec3 pos) {
float intArg = floor(pos.z);
float fracArg = fract(pos.z);
vec2 hash = mBBS(intArg * 3.0 + vec2(0, 3), modulus);
vec4 g = vec4(texture2D(nzw, vec2(pos.x, pos.y + hash.x) / modulus).xy,
texture2D(nzw, vec2(pos.x, pos.y + hash.y) / modulus).xy) * 2.0 - 1.0;
return mix(g.x + g.y * fracArg,
g.z + g.w * (fracArg - 1.0),
smoothstep(0.0, 1.0, fracArg));
}
const int octives = 4;
const float lacunarity = 2.0;
const float gain = 0.5;
/**
* Adds multiple octives of noise together.
**/
//// 雰囲気的にパーリンノイズ風?
float turbulence(vec3 pos) {
float sum = 0.0;
float freq = 1.0;
float amp = 1.0;
for(int i = 0; i < octives; i++) {
sum += abs(mnoise(pos * freq)) * amp;
freq *= lacunarity;
amp *= gain;
}
return sum;
}
const float magnatude = 1.3;
uniform float time;
uniform sampler2D fireProfile;
/**
* Samples the fire.
*
* @param loc the normalized location (0.0-1.0) to sample the fire
* @param scale the 'size' of the fire in world space and time
**/
//// 炎テクスチャからランダムにテクセルをフェッチ
vec4 sampleFire(vec3 loc, vec4 scale) {
// Convert xz to [-1.0, 1.0] range.
loc.xz = loc.xz * 2.0 - 1.0;
// Convert to (radius, height) to sample fire profile texture.
vec2 st = vec2(sqrt(dot(loc.xz, loc.xz)), loc.y);
// Convert loc to 'noise' space
loc.y -= time * scale.w; // Scrolling noise upwards over time.
loc *= scale.xyz; // Scaling noise space.
// Offsetting vertial texture lookup.
// We scale this by the sqrt of the height so that things are
// relatively stable at the base of the fire and volital at the
// top.
//// turbulance = 乱流。おそらく参照点に揺らぎを加味してサンプルしている。
//// st.y == loc.y。st生成時にYは補正していない。意味的にはfire textureの高さ?
float offset = sqrt(st.y) * magnatude * turbulence(loc);
st.y += offset;
// TODO: Update fireProfile texture to have a black row of pixels.
// 高さが1.0を超えた場合はblack pixelにする。
if (st.y > 1.0) {
return vec4(0, 0, 0, 1);
}
//// 計算した結果のポイントをサンプリング。
vec4 result = texture2D(fireProfile, st);
// Fading out bottom so slice clipping isnt obvious
if (st.y < .1) {
result *= st.y / 0.1;
}
return result;
}
varying vec3 texOut;
void main(void) {
// Mapping texture coordinate to -1 => 1 for xy, 0=> 1 for y
vec3 color = sampleFire(texOut, vec4(1.0, 2.0, 1.0, 0.5)).xyz;
gl_FragColor = vec4(color * 1.5, 1);
}
ノイズテクスチャ
おそらく負荷軽減のためかと思いますが、事前に計算して生成しておいたノイズ用のテクスチャを用いてノイズを利用しています。
上記サイトで使われているノイズテクスチャは以下のものが使われています。
実装自体はパーリンノイズに似ているので、パーリンノイズの解説記事を見てみるといいと思います。
Fireテクスチャ
炎に使われているテクスチャは、実はこんなに小さいものです。
このテクスチャから、様々なノイズ計算を経て最終的にフェッチするテクセルを決定します。
その揺らぎ自体を計算することで、リアルな炎を表現しています。
JavaScript側の処理
実は一番重要な処理はJS側での処理です。
こちらは細かいことはわかっていませんが、色々調べてみたところグラフ理論? とかを使って、頂点と頂点を結ぶ線(エッジ)を算出し、視点に応じたボリュームを作り出しているような感じです。
(◯◯理論を使ってるんだよってのを知ってる人いたら教えて下さい( ;´Д`))
ちなみに以下のコードは、Three.js向けに書きなおしたものです。
(BufferGeometryを利用しています。BufferGeometryについてはちょろっと書いたのでこちらを)
大事な点は炎を表現するためのボリュームを計算する部分です。
多分、各頂点と視点を利用したエッジから視点方向に表示すべきエッジと頂点などを生成しているのかなと予想。
エッジを生成している部分は以下です。
slice: {
value: function() {
// 頂点情報
this._points = [];
// テクスチャ位置情報
this._texCoords = [];
// インデックス
this._indexes = [];
// モデルの角への視線距離?
var cornerDistance = [];
cornerDistance[0] = this._posCorners[0].dot(this._viewVector);
// 最大距離の頂点のインデックス
var maxCorner = 0;
// 最小視線距離
var minDistance = cornerDistance[0];
// 最大視線距離
var maxDistance = cornerDistance[0];
// 全頂点数 | this._posCorners.length === 8
// 全頂点に対して距離を算出、最大・最小を検出
for (var i = 1; i < 8; ++i) {
// 距離なら三平方の定理で、x * x + y * yの平方根をとるが、処理負荷軽減のために平方根は取っていない?
// dotはx * x + y * yを実現している。
cornerDistance[i] = this._posCorners[i].dot(this._viewVector);
if (cornerDistance[i] > maxDistance) {
maxCorner = i;
maxDistance = cornerDistance[i];
}
if (cornerDistance[i] < minDistance) {
minDistance = cornerDistance[i];
}
}
// Aligning slices
// 小数点第一位までに切り詰める処理?
var sliceDistance = Math.floor(maxDistance / this._sliceSpacing) * this._sliceSpacing;
var activeEdges = [];
var firstEdge = 0;
var nextEdge = 0;
var expirations = new PriorityQueue();
/**
* エッジ(辺)の生成
*/
var createEdge = function(startIndex, endIndex) {
// 12が最大値?
if (nextEdge >= 12) {
return undefined;
}
var activeEdge = {
expired : false,
startIndex: startIndex,
endIndex : endIndex,
deltaPos : new THREE.Vector3(),
deltaTex : new THREE.Vector3(),
pos : new THREE.Vector3(),
tex : new THREE.Vector3()
};
// start <-> end間の長さ
var range = cornerDistance[startIndex] - cornerDistance[endIndex];
if (range != 0.0) {
// rangeの逆数
var irange = 1.0 / range;
// start <-> end間の差分ベクトルを取得。差分ベクトルなので end - start。
// それに逆数を掛ける。
activeEdge.deltaPos.subVectors(
this._posCorners[endIndex],
this._posCorners[startIndex]
).multiplyScalar(irange);
activeEdge.deltaTex.subVectors(
this._texCorners[endIndex],
this._texCorners[startIndex]
).multiplyScalar(irange);
var step = cornerDistance[startIndex] - sliceDistance;
activeEdge.pos.addVectors(
activeEdge.deltaPos.clone().multiplyScalar(step),
this._posCorners[startIndex]
);
activeEdge.tex.addVectors(
activeEdge.deltaTex.clone().multiplyScalar(step),
this._texCorners[startIndex]
);
activeEdge.deltaPos.multiplyScalar(this._sliceSpacing);
activeEdge.deltaTex.multiplyScalar(this._sliceSpacing);
}
// 距離がプライオリティとして利用される
expirations.push(activeEdge, cornerDistance[endIndex]);
activeEdge.cur = nextEdge;
activeEdges[nextEdge++] = activeEdge;
return activeEdge;
};
// 一番遠い位置にある頂点を基準に3本のエッジをそれぞれprev / nextでつなぐ
for (i = 0; i < 3; ++i) {
var activeEdge = createEdge.call(this, maxCorner, this._cornerNeighbors[maxCorner][i]);
activeEdge.prev = (i + 2) % 3;
activeEdge.next = (i + 1) % 3;
}
// sliceDistanceがminDistanceより小さくなるまで繰り返し
var nextIndex = 0;
while (sliceDistance > minDistance) {
// 視線距離が大きいほうから処理
while (expirations.top().priority >= sliceDistance) {
var edge = expirations.pop().object;
if (edge.expired) {
continue;
}
var isNotEnd = (edge.endIndex !== activeEdges[edge.prev].endIndex &&
edge.endIndex !== activeEdges[edge.next].endIndex);
if (isNotEnd) {
// split this edge.
// 処理済としてフラグを立てる
edge.expired = true;
// create two new edges.
var activeEdge1 = createEdge.call(
this,
edge.endIndex,
this._incomingEdges[edge.endIndex][edge.startIndex]
);
activeEdge1.prev = edge.prev;
activeEdges[edge.prev].next = nextEdge - 1;
activeEdge1.next = nextEdge;
var activeEdge2 = createEdge.call(
this,
edge.endIndex,
this._incomingEdges[edge.endIndex][activeEdge1.endIndex]
);
activeEdge2.prev = nextEdge - 2;
activeEdge2.next = edge.next;
activeEdges[activeEdge2.next].prev = nextEdge - 1;
firstEdge = nextEdge - 1;
}
else {
// merge edge.
var prev;
var next;
if (edge.endIndex === activeEdges[edge.prev].endIndex) {
prev = activeEdges[edge.prev];
next = edge;
}
else {
prev = edge;
next = activeEdges[edge.next];
}
prev.expired = true;
next.expired = true;
// make new edge
var activeEdge = createEdge.call(
this,
edge.endIndex,
this._incomingEdges[edge.endIndex][prev.startIndex]
);
activeEdge.prev = prev.prev;
activeEdges[activeEdge.prev].next = nextEdge - 1;
activeEdge.next = next.next;
activeEdges[activeEdge.next].prev = nextEdge - 1;
firstEdge = nextEdge - 1;
}
}
var cur = firstEdge;
var count = 0;
do {
++count;
// ループ処理中のアクティブなエッジ
var activeEdge = activeEdges[cur];
// 算出した頂点座標
this._points.push(activeEdge.pos.x);
this._points.push(activeEdge.pos.y);
this._points.push(activeEdge.pos.z);
// 算出したUV座標
this._texCoords.push(activeEdge.tex.x);
this._texCoords.push(activeEdge.tex.y);
this._texCoords.push(activeEdge.tex.z);
activeEdge.pos.add(activeEdge.deltaPos);
activeEdge.tex.add(activeEdge.deltaTex);
cur = activeEdge.next;
} while (cur !== firstEdge);
for (i = 2; i < count; ++i) {
this._indexes.push(nextIndex);
this._indexes.push(nextIndex + i - 1);
this._indexes.push(nextIndex + i + 0);
}
nextIndex += count;
sliceDistance -= this._sliceSpacing;
}
}
}
実はこの処理がコードの大部分を占めています。
ここで計算された結果を元に、シェーダに頂点データを送っています。
これ以外のコードがやっているのはちょっとしたアップデートと、上記メソッドで更新され頂点データをシェーダに送っているだけです。
以下に今回のサンプルの全文を載せておきます。
(function () {
'use strict';
/**
* Volume fire element.
*/
function FireElement(width, height, depth, sliceSpacing, camera) {
this.camera = camera;
var halfWidth = width / 2;
var halfHeight = height / 2;
var halfDepth = depth / 2;
this._posCorners = [
new THREE.Vector3(-halfWidth, -halfHeight, -halfDepth),
new THREE.Vector3( halfWidth, -halfHeight, -halfDepth),
new THREE.Vector3(-halfWidth, halfHeight, -halfDepth),
new THREE.Vector3( halfWidth, halfHeight, -halfDepth),
new THREE.Vector3(-halfWidth, -halfHeight, halfDepth),
new THREE.Vector3( halfWidth, -halfHeight, halfDepth),
new THREE.Vector3(-halfWidth, halfHeight, halfDepth),
new THREE.Vector3( halfWidth, halfHeight, halfDepth)
];
this._texCorners = [
new THREE.Vector3(0.0, 0.0, 0.0),
new THREE.Vector3(1.0, 0.0, 0.0),
new THREE.Vector3(0.0, 1.0, 0.0),
new THREE.Vector3(1.0, 1.0, 0.0),
new THREE.Vector3(0.0, 0.0, 1.0),
new THREE.Vector3(1.0, 0.0, 1.0),
new THREE.Vector3(0.0, 1.0, 1.0),
new THREE.Vector3(1.0, 1.0, 1.0)
];
this._viewVector = new THREE.Vector3();
this._sliceSpacing = sliceSpacing;
var vsCode = [ /* 頂点シェーダコードなので省略 */ ].join('\n');
var fsCode = [ /* フラグメントシェーダコードなので省略 */ ].join('\n');
var index = new Uint16Array ((width + height + depth) * 30);
var position = new Float32Array((width + height + depth) * 30 * 3);
var tex = new Float32Array((width + height + depth) * 30 * 3);
var geometry = new THREE.BufferGeometry();
geometry.dynamic = true;
geometry.addAttribute('position', new THREE.BufferAttribute(position, 3));
geometry.addAttribute('index', new THREE.BufferAttribute(index, 1));
geometry.addAttribute('tex', new THREE.BufferAttribute(tex, 3));
var material = this.createMaterial(vsCode, fsCode);
this.mesh = new THREE.Mesh(geometry, material);
this.mesh.updateMatrixWorld();
}
FireElement.prototype = Object.create({}, {
constructor: {
value: FireElement
},
_cornerNeighbors: {
value: [
[1, 2, 4],
[0, 5, 3],
[0, 3, 6],
[1, 7, 2],
[0, 6, 5],
[1, 4, 7],
[2, 7, 4],
[3, 5, 6]
]
},
_incomingEdges: {
value: [
[-1, 2, 4, -1, 1, -1, -1, -1 ],
[ 5, -1, -1, 0, -1, 3, -1, -1 ],
[ 3, -1, -1, 6, -1, -1, 0, -1 ],
[-1, 7, 1, -1, -1, -1, -1, 2 ],
[ 6, -1, -1, -1, -1, 0, 5, -1 ],
[-1, 4, -1, -1, 7, -1, -1, 1 ],
[-1, -1, 7, -1, 2, -1, -1, 4 ],
[-1, -1, -1, 5, -1, 6, 3, -1 ]
]
},
createMaterial: {
value: function (vsCode, fsCode) {
var nzw = THREE.ImageUtils.loadTexture('img/noise.png');
nzw.wrapS = THREE.RepeatWrapping;
nzw.wrapT = THREE.RepeatWrapping;
nzw.magFilter = THREE.LinearFilter;
nzw.minFilter = THREE.LinearFilter;
var fireProfile = THREE.ImageUtils.loadTexture('img/fire.png');
fireProfile.wrapS = THREE.ClampToEdgeWrapping;
fireProfile.wrapT = THREE.ClampToEdgeWrapping;
fireProfile.magFilter = THREE.LinearFilter;
fireProfile.minFilter = THREE.LinearFilter;
var material = new THREE.RawShaderMaterial({
vertexShader : vsCode,
fragmentShader: fsCode,
uniforms: {
time: {
type: 'f',
value: 0
},
nzw: {
type: 't',
value: nzw
},
fireProfile: {
type: 't',
value: fireProfile
}
},
attributes: {
tex: {
type: 'v3',
value: null
}
},
side: THREE.DoubleSide,
blending: THREE.AdditiveBlending,
transparent: true
});
return material;
}
},
getViewVector: {
value: function (matrix) {
var elements = matrix.elements;
return new THREE.Vector3(-elements[2], -elements[6], -elements[10]).normalize();
}
},
update: {
value: function (deltaTime) {
var matrix = new THREE.Matrix4();
matrix.multiplyMatrices(this.camera.matrixWorldInverse, this.mesh.matrixWorld);
// カメラの視点とオブジェクトの位置が変化していたらボリュームを更新
var viewVector = this.getViewVector(matrix);
if (!this._viewVector.equals(viewVector)) {
this._viewVector = viewVector;
this.slice();
// sliceメソッドによって更新された頂点情報をシェーダにアップロード
this.mesh.geometry.attributes.position.array.set(this._points);
this.mesh.geometry.attributes.tex.array.set(this._texCoords);
this.mesh.geometry.attributes.index.array.set(this._indexes);
this.mesh.geometry.attributes.position.needsUpdate = true;
this.mesh.geometry.attributes.tex.needsUpdate = true;
this.mesh.geometry.attributes.index.needsUpdate = true;
}
// 経過時間を更新
this.mesh.material.uniforms.time.value += deltaTime;
}
},
slice: {
value: function() {
// 頂点情報
this._points = [];
// テクスチャ位置情報
this._texCoords = [];
// インデックス
this._indexes = [];
// モデルの角への視線距離?
var cornerDistance = [];
cornerDistance[0] = this._posCorners[0].dot(this._viewVector);
// 最大距離の頂点のインデックス
var maxCorner = 0;
// 最小視線距離
var minDistance = cornerDistance[0];
// 最大視線距離
var maxDistance = cornerDistance[0];
// 全頂点数 | this._posCorners.length === 8
// 全頂点に対して距離を算出、最大・最小を検出
for (var i = 1; i < 8; ++i) {
// 距離なら三平方の定理で、x * x + y * yの平方根をとるが、処理負荷軽減のために平方根は取っていない?
// dotはx * x + y * yを実現している。
cornerDistance[i] = this._posCorners[i].dot(this._viewVector);
if (cornerDistance[i] > maxDistance) {
maxCorner = i;
maxDistance = cornerDistance[i];
}
if (cornerDistance[i] < minDistance) {
minDistance = cornerDistance[i];
}
}
// Aligning slices
// 小数点第一位までに切り詰める処理?
var sliceDistance = Math.floor(maxDistance / this._sliceSpacing) * this._sliceSpacing;
var activeEdges = [];
var firstEdge = 0;
var nextEdge = 0;
var expirations = new PriorityQueue();
/**
* エッジ(辺)の生成
*/
var createEdge = function(startIndex, endIndex) {
// 12が最大値?
if (nextEdge >= 12) {
return undefined;
}
var activeEdge = {
expired : false,
startIndex: startIndex,
endIndex : endIndex,
deltaPos : new THREE.Vector3(),
deltaTex : new THREE.Vector3(),
pos : new THREE.Vector3(),
tex : new THREE.Vector3()
};
// start <-> end間の長さ
var range = cornerDistance[startIndex] - cornerDistance[endIndex];
if (range != 0.0) {
// rangeの逆数
var irange = 1.0 / range;
// start <-> end間の差分ベクトルを取得。差分ベクトルなので end - start。
// それに逆数を掛ける。
activeEdge.deltaPos.subVectors(
this._posCorners[endIndex],
this._posCorners[startIndex]
).multiplyScalar(irange);
activeEdge.deltaTex.subVectors(
this._texCorners[endIndex],
this._texCorners[startIndex]
).multiplyScalar(irange);
var step = cornerDistance[startIndex] - sliceDistance;
activeEdge.pos.addVectors(
activeEdge.deltaPos.clone().multiplyScalar(step),
this._posCorners[startIndex]
);
activeEdge.tex.addVectors(
activeEdge.deltaTex.clone().multiplyScalar(step),
this._texCorners[startIndex]
);
activeEdge.deltaPos.multiplyScalar(this._sliceSpacing);
activeEdge.deltaTex.multiplyScalar(this._sliceSpacing);
}
// 距離がプライオリティとして利用される
expirations.push(activeEdge, cornerDistance[endIndex]);
activeEdge.cur = nextEdge;
activeEdges[nextEdge++] = activeEdge;
return activeEdge;
};
// 3辺を接続?(A <-> B <-> C <-> A)
for (i = 0; i < 3; ++i) {
var activeEdge = createEdge.call(this, maxCorner, this._cornerNeighbors[maxCorner][i]);
activeEdge.prev = (i + 2) % 3;
activeEdge.next = (i + 1) % 3;
}
// sliceDistanceがminDistanceより小さくなるまで繰り返し
var nextIndex = 0;
while (sliceDistance > minDistance) {
// 視線距離が大きいほうが優先度高?
while (expirations.top().priority >= sliceDistance) {
var edge = expirations.pop().object;
if (edge.expired) {
continue;
}
var isNotEnd = (edge.endIndex !== activeEdges[edge.prev].endIndex &&
edge.endIndex !== activeEdges[edge.next].endIndex);
if (isNotEnd) {
// split this edge.
// 処理済としてフラグを立てる?
edge.expired = true;
// create two new edges.
var activeEdge1 = createEdge.call(
this,
edge.endIndex,
this._incomingEdges[edge.endIndex][edge.startIndex]
);
activeEdge1.prev = edge.prev;
activeEdges[edge.prev].next = nextEdge - 1;
activeEdge1.next = nextEdge;
var activeEdge2 = createEdge.call(
this,
edge.endIndex,
this._incomingEdges[edge.endIndex][activeEdge1.endIndex]
);
activeEdge2.prev = nextEdge - 2;
activeEdge2.next = edge.next;
activeEdges[activeEdge2.next].prev = nextEdge - 1;
firstEdge = nextEdge - 1;
}
else {
// merge edge.
var prev;
var next;
if (edge.endIndex === activeEdges[edge.prev].endIndex) {
prev = activeEdges[edge.prev];
next = edge;
}
else {
prev = edge;
next = activeEdges[edge.next];
}
prev.expired = true;
next.expired = true;
// make new edge
var activeEdge = createEdge.call(
this,
edge.endIndex,
this._incomingEdges[edge.endIndex][prev.startIndex]
);
activeEdge.prev = prev.prev;
activeEdges[activeEdge.prev].next = nextEdge - 1;
activeEdge.next = next.next;
activeEdges[activeEdge.next].prev = nextEdge - 1;
firstEdge = nextEdge - 1;
}
}
var cur = firstEdge;
var count = 0;
do {
++count;
// ループ処理中のアクティブなエッジ
var activeEdge = activeEdges[cur];
// 算出した頂点座標
this._points.push(activeEdge.pos.x); // x
this._points.push(activeEdge.pos.y); // y
this._points.push(activeEdge.pos.z); // z
// 算出したUV座標
this._texCoords.push(activeEdge.tex.x); // x
this._texCoords.push(activeEdge.tex.y); // y
this._texCoords.push(activeEdge.tex.z); // z
activeEdge.pos.add(activeEdge.deltaPos);
activeEdge.tex.add(activeEdge.deltaTex);
cur = activeEdge.next;
} while (cur !== firstEdge);
for (i = 2; i < count; ++i) {
this._indexes.push(nextIndex);
this._indexes.push(nextIndex + i - 1);
this._indexes.push(nextIndex + i + 0);
}
nextIndex += count;
sliceDistance -= this._sliceSpacing;
}
}
}
});
/////////////////////////////////////////////////////////////////////////////
function init() {
var renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setClearColor(0x000000);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setViewport(0, 0, window.innerWidth, window.innerHeight);
renderer.setScissor(0, 0, window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 5;
camera.position.y = 5;
camera.lookAt(new THREE.Vector3(0, 0, 0));
var controls = new THREE.OrbitControls(camera);
// light
var light = new THREE.DirectionalLight(0xfffffff);
light.position.set(1, 1, 1);
scene.add(light);
var ambient = new THREE.AmbientLight(0x666666);
scene.add(ambient);
var fireElement = new FireElement(2, 4, 2, 0.5, camera);
scene.add(fireElement.mesh);
var prevTime = Date.now();
(function loop() {
var now = Date.now();
var deltaTime = now - prevTime;
prevTime = now;
fireElement.update(deltaTime / 1000);
renderer.render(scene, camera);
controls.update();
setTimeout(loop, 16);
}());
}
/////////////////////////////////////////////////////////////////////////////
// 起動
init();
}());