-
-
Save kobitoDevelopment/f87fefd23667dd0a35c9abe4f0a53f95 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| .canvas { | |
| display: block; | |
| width: 100%; | |
| height: 100vh; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <canvas class="canvas"></canvas> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ============================================================================ | |
| // 1. キャンバスとWebGLの基本設定 | |
| // ============================================================================ | |
| // HTMLから取得したcanvas要素とWebGLコンテキスト | |
| const canvas = document.querySelector(".canvas"); | |
| let gl; | |
| // ============================================================================ | |
| // 2. 流体シミュレーションの設定パラメータ | |
| // ============================================================================ | |
| // 流体の物理的特性と計算品質を制御するパラメータ | |
| const config = { | |
| SIM_RESOLUTION: 128, // シミュレーション解像度 | |
| DYE_RESOLUTION: 512, // 染料の描画解像度 | |
| DENSITY_DISSIPATION: 1, // 染料の消散率(大きいほど早く消える) | |
| VELOCITY_DISSIPATION: 0.2, // 速度の減衰率 | |
| PRESSURE_ITERATIONS: 20, // 圧力計算の反復回数 | |
| SPLAT_RADIUS: 0.25, // 流体発生時の半径 | |
| SPLAT_FORCE: 6000, // 流体発生時の力の強さ | |
| COLORFUL: true, // カラフルな色を使用するか | |
| COLOR_UPDATE_SPEED: 10, // 色の更新速度 | |
| PAUSED: false, // シミュレーションの一時停止フラグ | |
| }; | |
| // ============================================================================ | |
| // 3. マウス・タッチ入力の管理 | |
| // ============================================================================ | |
| // ポインター(マウス・タッチ)の状態を作成する関数 | |
| function createPointer() { | |
| return { | |
| id: -1, // ポインターID | |
| texcoordX: 0, // テクスチャ座標X(0~1) | |
| texcoordY: 0, // テクスチャ座標Y(0~1) | |
| prevTexcoordX: 0, // 前フレームのX座標 | |
| prevTexcoordY: 0, // 前フレームのY座標 | |
| deltaX: 0, // X方向の移動量 | |
| deltaY: 0, // Y方向の移動量 | |
| down: false, // 押下状態 | |
| moved: false, // 移動フラグ | |
| color: [30, 0, 300] // 流体の色 | |
| }; | |
| } | |
| const pointers = []; // ポインターの配列 | |
| const splatStack = []; // 流体発生のキュー | |
| pointers.push(createPointer()); // メインポインターを初期化 | |
| // ============================================================================ | |
| // 4. WebGLコンテキストの初期化と拡張機能の検出 | |
| // ============================================================================ | |
| // WebGL2/WebGL1の判別と必要な拡張機能の取得 | |
| function getWebGLContext(canvas) { | |
| const params = { | |
| alpha: true, // アルファブレンドを有効化 | |
| depth: false, // 深度バッファは不要 | |
| stencil: false, // ステンシルバッファは不要 | |
| antialias: false, // アンチエイリアシングは無効 | |
| preserveDrawingBuffer: false, // 描画バッファの保持は不要 | |
| }; | |
| gl = canvas.getContext("webgl2", params); | |
| const isWebGL2 = !!gl; // WebGL2のサポートチェック | |
| if (!isWebGL2) { | |
| // WebGL2がサポートされていない場合はWebGL1を使用 | |
| gl = canvas.getContext("webgl", params) || canvas.getContext("experimental-webgl", params); | |
| } | |
| let halfFloat; | |
| let supportLinearFiltering; | |
| if (isWebGL2) { | |
| // WebGL2用の浮動小数点テクスチャサポート | |
| gl.getExtension("EXT_color_buffer_float"); | |
| supportLinearFiltering = gl.getExtension("OES_texture_float_linear"); | |
| } else { | |
| // WebGL1用のハーフフロートテクスチャサポート | |
| halfFloat = gl.getExtension("OES_texture_half_float"); | |
| supportLinearFiltering = gl.getExtension("OES_texture_half_float_linear"); | |
| } | |
| gl.clearColor(0.0, 0.0, 0.0, 1.0); // 背景色を黒に設定 | |
| const halfFloatTexType = isWebGL2 ? gl.HALF_FLOAT : halfFloat.HALF_FLOAT_OES; | |
| let formatRGBA, formatRG, formatR; // テクスチャフォーマットの変数 | |
| if (isWebGL2) { | |
| formatRGBA = getSupportedFormat(gl, gl.RGBA16F, gl.RGBA, halfFloatTexType); | |
| formatRG = getSupportedFormat(gl, gl.RG16F, gl.RG, halfFloatTexType); | |
| formatR = getSupportedFormat(gl, gl.R16F, gl.RED, halfFloatTexType); | |
| } else { | |
| formatRGBA = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType); | |
| formatRG = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType); | |
| formatR = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType); | |
| } | |
| return { | |
| gl: gl, | |
| ext: { | |
| formatRGBA: formatRGBA, | |
| formatRG: formatRG, | |
| formatR: formatR, | |
| halfFloatTexType: halfFloatTexType, | |
| supportLinearFiltering: supportLinearFiltering, | |
| }, | |
| }; | |
| } | |
| function getSupportedFormat(gl, internalFormat, format, type) { | |
| if (!supportRenderTextureFormat(gl, internalFormat, format, type)) { | |
| if (internalFormat === gl.R16F) { | |
| return getSupportedFormat(gl, gl.RG16F, gl.RG, type); | |
| } else if (internalFormat === gl.RG16F) { | |
| return getSupportedFormat(gl, gl.RGBA16F, gl.RGBA, type); | |
| } else { | |
| return null; | |
| } | |
| } | |
| return { | |
| internalFormat: internalFormat, | |
| format: format, | |
| }; | |
| } | |
| function supportRenderTextureFormat(gl, internalFormat, format, type) { | |
| const texture = gl.createTexture(); | |
| gl.bindTexture(gl.TEXTURE_2D, texture); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); | |
| gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, 4, 4, 0, format, type, null); | |
| const fbo = gl.createFramebuffer(); | |
| gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); | |
| gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); | |
| const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER); | |
| return status == gl.FRAMEBUFFER_COMPLETE; | |
| } | |
| // ============================================================================ | |
| // 5. シェーダープログラムの管理クラス | |
| // ============================================================================ | |
| // シェーダーのコンパイル・リンクとユニフォーム変数の管理 | |
| function createShaderProgram(vertexShader, fragmentShader) { | |
| const program = createProgram(vertexShader, fragmentShader); | |
| const uniforms = getUniforms(program); | |
| return { | |
| program: program, | |
| uniforms: uniforms, | |
| bind() { | |
| gl.useProgram(program); | |
| } | |
| }; | |
| } | |
| function createProgram(vertexShader, fragmentShader) { | |
| const program = gl.createProgram(); | |
| gl.attachShader(program, vertexShader); | |
| gl.attachShader(program, fragmentShader); | |
| gl.linkProgram(program); | |
| if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { | |
| throw gl.getProgramInfoLog(program); | |
| } | |
| return program; | |
| } | |
| function getUniforms(program) { | |
| const uniforms = []; | |
| const uniformCount = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS); | |
| for (let i = 0; i < uniformCount; i++) { | |
| const uniformName = gl.getActiveUniform(program, i).name; | |
| uniforms[uniformName] = gl.getUniformLocation(program, uniformName); | |
| } | |
| return uniforms; | |
| } | |
| function compileShader(type, source) { | |
| const shader = gl.createShader(type); | |
| gl.shaderSource(shader, source); | |
| gl.compileShader(shader); | |
| if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { | |
| throw gl.getShaderInfoLog(shader); | |
| } | |
| return shader; | |
| } | |
| // ============================================================================ | |
| // 6. WebGL初期化とシェーダーのコンパイル | |
| // ============================================================================ | |
| // WebGLコンテキストを最初に初期化(シェーダーコンパイルに必要) | |
| resizeCanvas(); | |
| const ref = getWebGLContext(canvas); | |
| gl = ref.gl; | |
| const ext = ref.ext; | |
| // ------------------------------------------------------------------------ | |
| // 6.1 基本頂点シェーダー(全シェーダー共通) | |
| // ------------------------------------------------------------------------ | |
| // 正規化座標(-1~1)をテクスチャ座標(0~1)に変換し、隣接ピクセル座標も計算 | |
| const baseVertexShader = compileShader( | |
| gl.VERTEX_SHADER, | |
| ` | |
| precision highp float; | |
| attribute vec2 aPosition; // 頂点座標 | |
| varying vec2 vUv; // メインテクスチャ座標 | |
| varying vec2 vL; // 左隣接ピクセル座標 | |
| varying vec2 vR; // 右隣接ピクセル座標 | |
| varying vec2 vT; // 上隣接ピクセル座標 | |
| varying vec2 vB; // 下隣接ピクセル座標 | |
| uniform vec2 texelSize; // 1ピクセルのサイズ | |
| void main () { | |
| vUv = aPosition * 0.5 + 0.5; // -1~1 を 0~1 に変換 | |
| vL = vUv - vec2(texelSize.x, 0.0); // 左隣接ピクセル | |
| vR = vUv + vec2(texelSize.x, 0.0); // 右隣接ピクセル | |
| vT = vUv + vec2(0.0, texelSize.y); // 上隣接ピクセル | |
| vB = vUv - vec2(0.0, texelSize.y); // 下隣接ピクセル | |
| gl_Position = vec4(aPosition, 0.0, 1.0); // 頂点座標を設定 | |
| } | |
| ` | |
| ); | |
| // 染料テクスチャをスクリーンに描画するシェーダー | |
| const displayShader = compileShader( | |
| gl.FRAGMENT_SHADER, | |
| ` | |
| precision highp float; | |
| precision highp sampler2D; | |
| varying vec2 vUv; // テクスチャ座標 | |
| uniform sampler2D uTexture; // 染料テクスチャ | |
| void main () { | |
| vec3 c = texture2D(uTexture, vUv).rgb; // 染料の色を取得 | |
| float a = max(c.r, max(c.g, c.b)); // 最大色値をアルファ値として使用 | |
| gl_FragColor = vec4(c, a); // 最終色を出力 | |
| } | |
| ` | |
| ); | |
| // ガウシアン分布で流体を発生させるシェーダー | |
| const splatShader = compileShader( | |
| gl.FRAGMENT_SHADER, | |
| ` | |
| precision highp float; | |
| precision highp sampler2D; | |
| varying vec2 vUv; // テクスチャ座標 | |
| uniform sampler2D uTarget; // 対象テクスチャ(速度または染料) | |
| uniform float aspectRatio; // アスペクト比補正 | |
| uniform vec3 color; // 添加する色または力 | |
| uniform vec2 point; // 発生位置 | |
| uniform float radius; // 発生範囲の半径 | |
| void main () { | |
| vec2 p = vUv - point.xy; // 発生位置からの距離ベクトル | |
| p.x *= aspectRatio; // アスペクト比補正 | |
| vec3 splat = exp(-dot(p, p) / radius) * color; // ガウシアン分布で強度計算 | |
| vec3 base = texture2D(uTarget, vUv).xyz; // 既存の値を取得 | |
| gl_FragColor = vec4(base + splat, 1.0); // 既存値に新しい値を加算 | |
| } | |
| ` | |
| ); | |
| // 移流(物質を速度場に沿って運搬)するシェーダー | |
| const advectionShader = compileShader( | |
| gl.FRAGMENT_SHADER, | |
| ` | |
| precision highp float; | |
| precision highp sampler2D; | |
| varying vec2 vUv; // テクスチャ座標 | |
| uniform sampler2D uVelocity; // 速度場テクスチャ | |
| uniform sampler2D uSource; // 移流させる対象テクスチャ | |
| uniform vec2 texelSize; // 1ピクセルのサイズ | |
| uniform float dt; // フレーム間の時間 | |
| uniform float dissipation; // 減衰率 | |
| void main () { | |
| // 速度を逆に辿って前フレームの物質の位置を推定 | |
| vec2 coord = vUv - dt * texture2D(uVelocity, vUv).xy * texelSize; | |
| vec4 result = texture2D(uSource, coord); // 推定位置から値をサンプル | |
| float decay = 1.0 + dissipation * dt; // 減衰係数 | |
| gl_FragColor = result / decay; // 減衰を適用して出力 | |
| } | |
| ` | |
| ); | |
| // 発散(速度場の圧縮性)を計算するシェーダー | |
| const divergenceShader = compileShader( | |
| gl.FRAGMENT_SHADER, | |
| ` | |
| precision mediump float; | |
| precision mediump sampler2D; | |
| varying highp vec2 vUv; // メインテクスチャ座標 | |
| varying highp vec2 vL; // 左隣接ピクセル座標 | |
| varying highp vec2 vR; // 右隣接ピクセル座標 | |
| varying highp vec2 vT; // 上隣接ピクセル座標 | |
| varying highp vec2 vB; // 下隣接ピクセル座標 | |
| uniform sampler2D uVelocity; // 速度場テクスチャ | |
| void main () { | |
| float L = texture2D(uVelocity, vL).x; // 左の速度X成分 | |
| float R = texture2D(uVelocity, vR).x; // 右の速度X成分 | |
| float T = texture2D(uVelocity, vT).y; // 上の速度Y成分 | |
| float B = texture2D(uVelocity, vB).y; // 下の速度Y成分 | |
| vec2 C = texture2D(uVelocity, vUv).xy; // 中央の速度 | |
| // 境界条件の処理(外側は速度を反転) | |
| if (vL.x < 0.0) { L = -C.x; } | |
| if (vR.x > 1.0) { R = -C.x; } | |
| if (vT.y > 1.0) { T = -C.y; } | |
| if (vB.y < 0.0) { B = -C.y; } | |
| float div = 0.5 * (R - L + T - B); // 発散を計算 | |
| gl_FragColor = vec4(div, 0.0, 0.0, 1.0); | |
| } | |
| ` | |
| ); | |
| // Jacobi反復法で圧力場を計算するシェーダー | |
| const pressureShader = compileShader( | |
| gl.FRAGMENT_SHADER, | |
| ` | |
| precision mediump float; | |
| precision mediump sampler2D; | |
| varying highp vec2 vUv; // メインテクスチャ座標 | |
| varying highp vec2 vL; // 左隣接ピクセル座標 | |
| varying highp vec2 vR; // 右隣接ピクセル座標 | |
| varying highp vec2 vT; // 上隣接ピクセル座標 | |
| varying highp vec2 vB; // 下隣接ピクセル座標 | |
| uniform sampler2D uPressure; // 現在の圧力場 | |
| uniform sampler2D uDivergence; // 発散値 | |
| void main () { | |
| float L = texture2D(uPressure, vL).x; // 左隣接の圧力 | |
| float R = texture2D(uPressure, vR).x; // 右隣接の圧力 | |
| float T = texture2D(uPressure, vT).x; // 上隣接の圧力 | |
| float B = texture2D(uPressure, vB).x; // 下隣接の圧力 | |
| float divergence = texture2D(uDivergence, vUv).x; // 発散値 | |
| // Jacobi反復法で圧力を更新 | |
| float pressure = (L + R + B + T - divergence) * 0.25; | |
| gl_FragColor = vec4(pressure, 0.0, 0.0, 1.0); | |
| } | |
| ` | |
| ); | |
| const gradientSubtractShader = compileShader( | |
| gl.FRAGMENT_SHADER, | |
| ` | |
| precision mediump float; | |
| precision mediump sampler2D; | |
| varying highp vec2 vUv; | |
| varying highp vec2 vL; | |
| varying highp vec2 vR; | |
| varying highp vec2 vT; | |
| varying highp vec2 vB; | |
| uniform sampler2D uPressure; | |
| uniform sampler2D uVelocity; | |
| void main () { | |
| float L = texture2D(uPressure, vL).x; | |
| float R = texture2D(uPressure, vR).x; | |
| float T = texture2D(uPressure, vT).x; | |
| float B = texture2D(uPressure, vB).x; | |
| vec2 velocity = texture2D(uVelocity, vUv).xy; | |
| velocity.xy -= vec2(R - L, T - B); | |
| gl_FragColor = vec4(velocity, 0.0, 1.0); | |
| } | |
| ` | |
| ); | |
| const clearShader = compileShader( | |
| gl.FRAGMENT_SHADER, | |
| ` | |
| precision mediump float; | |
| precision mediump sampler2D; | |
| varying highp vec2 vUv; | |
| uniform sampler2D uTexture; | |
| uniform float value; | |
| void main () { | |
| gl_FragColor = value * texture2D(uTexture, vUv); | |
| } | |
| ` | |
| ); | |
| // シェーダープログラム | |
| const displayProgram = createShaderProgram(baseVertexShader, displayShader); | |
| const splatProgram = createShaderProgram(baseVertexShader, splatShader); | |
| const advectionProgram = createShaderProgram(baseVertexShader, advectionShader); | |
| const divergenceProgram = createShaderProgram(baseVertexShader, divergenceShader); | |
| const pressureProgram = createShaderProgram(baseVertexShader, pressureShader); | |
| const gradientSubtractProgram = createShaderProgram(baseVertexShader, gradientSubtractShader); | |
| const clearProgram = createShaderProgram(baseVertexShader, clearShader); | |
| // フレームバッファ | |
| let dye; | |
| let velocity; | |
| let divergence; | |
| let pressure; | |
| // フルスクリーン四角形のジオメトリ | |
| const blit = (() => { | |
| gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); | |
| gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, -1, 1, 1, 1, 1, -1]), gl.STATIC_DRAW); | |
| gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.createBuffer()); | |
| gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array([0, 1, 2, 0, 2, 3]), gl.STATIC_DRAW); | |
| gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0); | |
| gl.enableVertexAttribArray(0); | |
| return (destination) => { | |
| gl.bindFramebuffer(gl.FRAMEBUFFER, destination); | |
| gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0); | |
| }; | |
| })(); | |
| // FBOユーティリティ | |
| function createFBO(w, h, internalFormat, format, type, param) { | |
| gl.activeTexture(gl.TEXTURE0); | |
| const texture = gl.createTexture(); | |
| gl.bindTexture(gl.TEXTURE_2D, texture); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, param); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, param); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); | |
| gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, w, h, 0, format, type, null); | |
| const fbo = gl.createFramebuffer(); | |
| gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); | |
| gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); | |
| gl.viewport(0, 0, w, h); | |
| gl.clear(gl.COLOR_BUFFER_BIT); | |
| const texelSizeX = 1.0 / w; | |
| const texelSizeY = 1.0 / h; | |
| return { | |
| texture: texture, | |
| fbo: fbo, | |
| width: w, | |
| height: h, | |
| texelSizeX: texelSizeX, | |
| texelSizeY: texelSizeY, | |
| attach(id) { | |
| gl.activeTexture(gl.TEXTURE0 + id); | |
| gl.bindTexture(gl.TEXTURE_2D, texture); | |
| return id; | |
| }, | |
| }; | |
| } | |
| function createDoubleFBO(w, h, internalFormat, format, type, param) { | |
| const fbo1 = createFBO(w, h, internalFormat, format, type, param); | |
| const fbo2 = createFBO(w, h, internalFormat, format, type, param); | |
| const api = { | |
| width: w, | |
| height: h, | |
| texelSizeX: fbo1.texelSizeX, | |
| texelSizeY: fbo1.texelSizeY, | |
| read: fbo1, | |
| write: fbo2, | |
| swap() { | |
| const temp = api.read; | |
| api.read = api.write; | |
| api.write = temp; | |
| }, | |
| }; | |
| return api; | |
| } | |
| function initFramebuffers() { | |
| const simRes = getResolution(config.SIM_RESOLUTION); | |
| const dyeRes = getResolution(config.DYE_RESOLUTION); | |
| const texType = ext.halfFloatTexType; | |
| const rgba = ext.formatRGBA; | |
| const rg = ext.formatRG; | |
| const r = ext.formatR; | |
| const filtering = ext.supportLinearFiltering ? gl.LINEAR : gl.NEAREST; | |
| dye = createDoubleFBO(dyeRes.width, dyeRes.height, rgba.internalFormat, rgba.format, texType, filtering); | |
| velocity = createDoubleFBO(simRes.width, simRes.height, rg.internalFormat, rg.format, texType, filtering); | |
| divergence = createFBO(simRes.width, simRes.height, r.internalFormat, r.format, texType, gl.NEAREST); | |
| pressure = createDoubleFBO(simRes.width, simRes.height, r.internalFormat, r.format, texType, gl.NEAREST); | |
| } | |
| // ------------------------------------------------------------------------ | |
| // 7. 流体シミュレーションのメイン計算ステップ | |
| // ------------------------------------------------------------------------ | |
| // Navier-Stokes方程式に基づく流体の物理シミュレーション | |
| function step(dt) { | |
| // ブレンド無効化(計算フェーズのため) | |
| gl.disable(gl.BLEND); | |
| // 速度場の解像度でビューポート設定 | |
| gl.viewport(0, 0, velocity.width, velocity.height); | |
| // 7.1 発散(Divergence)計算 - 流体の圧縮性を測定 | |
| divergenceProgram.bind(); | |
| gl.uniform2f(divergenceProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); | |
| gl.uniform1i(divergenceProgram.uniforms.uVelocity, velocity.read.attach(0)); | |
| blit(divergence.fbo); | |
| // 7.2 圧力場の初期化(前回の圧力に減衰係数を適用) | |
| clearProgram.bind(); | |
| gl.uniform1i(clearProgram.uniforms.uTexture, pressure.read.attach(0)); | |
| gl.uniform1f(clearProgram.uniforms.value, 0.8); | |
| blit(pressure.write.fbo); | |
| pressure.swap(); | |
| // 7.3 圧力場の計算(Jacobi反復法でPoisson方程式を解く) | |
| pressureProgram.bind(); | |
| gl.uniform2f(pressureProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); | |
| gl.uniform1i(pressureProgram.uniforms.uDivergence, divergence.attach(0)); | |
| // 反復計算により圧力場を収束させる | |
| for (let i = 0; i < config.PRESSURE_ITERATIONS; i++) { | |
| gl.uniform1i(pressureProgram.uniforms.uPressure, pressure.read.attach(1)); | |
| blit(pressure.write.fbo); | |
| pressure.swap(); | |
| } | |
| // 7.4 圧力勾配除去 - 速度場から圧力成分を取り除き非圧縮性を実現 | |
| gradientSubtractProgram.bind(); | |
| gl.uniform2f(gradientSubtractProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); | |
| gl.uniform1i(gradientSubtractProgram.uniforms.uPressure, pressure.read.attach(0)); | |
| gl.uniform1i(gradientSubtractProgram.uniforms.uVelocity, velocity.read.attach(1)); | |
| blit(velocity.write.fbo); | |
| velocity.swap(); | |
| // 7.5 速度の移流(Advection) - 速度場を自分自身で運搬 | |
| advectionProgram.bind(); | |
| gl.uniform2f(advectionProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); | |
| const velocityId = velocity.read.attach(0); | |
| gl.uniform1i(advectionProgram.uniforms.uVelocity, velocityId); | |
| gl.uniform1i(advectionProgram.uniforms.uSource, velocityId); | |
| gl.uniform1f(advectionProgram.uniforms.dt, dt); | |
| gl.uniform1f(advectionProgram.uniforms.dissipation, config.VELOCITY_DISSIPATION); | |
| blit(velocity.write.fbo); | |
| velocity.swap(); | |
| // 7.6 染料の移流 - 色素を速度場に沿って運搬・減衰 | |
| gl.viewport(0, 0, dye.width, dye.height); | |
| gl.uniform1i(advectionProgram.uniforms.uVelocity, velocity.read.attach(0)); | |
| gl.uniform1i(advectionProgram.uniforms.uSource, dye.read.attach(1)); | |
| gl.uniform1f(advectionProgram.uniforms.dissipation, config.DENSITY_DISSIPATION); | |
| blit(dye.write.fbo); | |
| dye.swap(); | |
| } | |
| // ------------------------------------------------------------------------ | |
| // 8. 最終描画処理 | |
| // ------------------------------------------------------------------------ | |
| // 染料テクスチャをスクリーンに描画 | |
| function render() { | |
| // アルファブレンドで透明度を考慮した描画 | |
| gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); | |
| gl.enable(gl.BLEND); | |
| // キャンバス全体をビューポートとして設定 | |
| const width = gl.drawingBufferWidth; | |
| const height = gl.drawingBufferHeight; | |
| gl.viewport(0, 0, width, height); | |
| // 染料テクスチャをスクリーンに描画 | |
| displayProgram.bind(); | |
| gl.uniform1i(displayProgram.uniforms.uTexture, dye.read.attach(0)); | |
| blit(null); | |
| } | |
| // ------------------------------------------------------------------------ | |
| // 9. 流体の新規発生処理(Splat) | |
| // ------------------------------------------------------------------------ | |
| // 指定座標に速度と色素をガウシアン分布で添加 | |
| function splat(x, y, dx, dy, color) { | |
| // 9.1 速度場への力の付与 | |
| gl.viewport(0, 0, velocity.width, velocity.height); | |
| splatProgram.bind(); | |
| gl.uniform1i(splatProgram.uniforms.uTarget, velocity.read.attach(0)); | |
| gl.uniform1f(splatProgram.uniforms.aspectRatio, canvas.width / canvas.height); | |
| gl.uniform2f(splatProgram.uniforms.point, x, y); | |
| gl.uniform3f(splatProgram.uniforms.color, dx, dy, 0.0); | |
| gl.uniform1f(splatProgram.uniforms.radius, correctRadius(config.SPLAT_RADIUS / 100.0)); | |
| blit(velocity.write.fbo); | |
| velocity.swap(); | |
| // 9.2 染料場への色素の添加 | |
| gl.viewport(0, 0, dye.width, dye.height); | |
| gl.uniform1i(splatProgram.uniforms.uTarget, dye.read.attach(0)); | |
| gl.uniform3f(splatProgram.uniforms.color, color.r, color.g, color.b); | |
| blit(dye.write.fbo); | |
| dye.swap(); | |
| } | |
| // ユーティリティ関数 | |
| function resizeCanvas() { | |
| const width = scaleByPixelRatio(canvas.clientWidth); | |
| const height = scaleByPixelRatio(canvas.clientHeight); | |
| if (canvas.width != width || canvas.height != height) { | |
| canvas.width = width; | |
| canvas.height = height; | |
| return true; | |
| } | |
| return false; | |
| } | |
| function correctRadius(radius) { | |
| const aspectRatio = canvas.width / canvas.height; | |
| if (aspectRatio > 1) radius *= aspectRatio; | |
| return radius; | |
| } | |
| function generateColor() { | |
| const c = HSVtoRGB(Math.random(), 1.0, 1.0); | |
| c.r *= 0.15; | |
| c.g *= 0.15; | |
| c.b *= 0.15; | |
| return c; | |
| } | |
| function HSVtoRGB(h, s, v) { | |
| let r, g, b, i, f, p, q, t; | |
| i = Math.floor(h * 6); | |
| f = h * 6 - i; | |
| p = v * (1 - s); | |
| q = v * (1 - f * s); | |
| t = v * (1 - (1 - f) * s); | |
| const colorIndex = i % 6; | |
| if (colorIndex === 0) { | |
| (r = v), (g = t), (b = p); | |
| } else if (colorIndex === 1) { | |
| (r = q), (g = v), (b = p); | |
| } else if (colorIndex === 2) { | |
| (r = p), (g = v), (b = t); | |
| } else if (colorIndex === 3) { | |
| (r = p), (g = q), (b = v); | |
| } else if (colorIndex === 4) { | |
| (r = t), (g = p), (b = v); | |
| } else if (colorIndex === 5) { | |
| (r = v), (g = p), (b = q); | |
| } | |
| return { r: r, g: g, b: b }; | |
| } | |
| function getResolution(resolution) { | |
| let aspectRatio = gl.drawingBufferWidth / gl.drawingBufferHeight; | |
| if (aspectRatio < 1) aspectRatio = 1.0 / aspectRatio; | |
| const min = Math.round(resolution); | |
| const max = Math.round(resolution * aspectRatio); | |
| if (gl.drawingBufferWidth > gl.drawingBufferHeight) return { width: max, height: min }; | |
| else return { width: min, height: max }; | |
| } | |
| function scaleByPixelRatio(input) { | |
| const pixelRatio = window.devicePixelRatio || 1; | |
| return Math.floor(input * pixelRatio); | |
| } | |
| // 入力処理 | |
| function updatePointerDownData(pointer, id, posX, posY) { | |
| pointer.id = id; | |
| pointer.down = true; | |
| pointer.moved = false; | |
| pointer.texcoordX = posX / canvas.width; | |
| pointer.texcoordY = 1.0 - posY / canvas.height; | |
| pointer.prevTexcoordX = pointer.texcoordX; | |
| pointer.prevTexcoordY = pointer.texcoordY; | |
| pointer.deltaX = 0; | |
| pointer.deltaY = 0; | |
| pointer.color = generateColor(); | |
| } | |
| function updatePointerMoveData(pointer, posX, posY) { | |
| pointer.prevTexcoordX = pointer.texcoordX; | |
| pointer.prevTexcoordY = pointer.texcoordY; | |
| pointer.texcoordX = posX / canvas.width; | |
| pointer.texcoordY = 1.0 - posY / canvas.height; | |
| pointer.deltaX = correctDeltaX(pointer.texcoordX - pointer.prevTexcoordX); | |
| pointer.deltaY = correctDeltaY(pointer.texcoordY - pointer.prevTexcoordY); | |
| pointer.moved = Math.abs(pointer.deltaX) > 0 || Math.abs(pointer.deltaY) > 0; | |
| } | |
| function correctDeltaX(delta) { | |
| const aspectRatio = canvas.width / canvas.height; | |
| if (aspectRatio < 1) delta *= aspectRatio; | |
| return delta; | |
| } | |
| function correctDeltaY(delta) { | |
| const aspectRatio = canvas.width / canvas.height; | |
| if (aspectRatio > 1) delta /= aspectRatio; | |
| return delta; | |
| } | |
| function splatPointer(pointer) { | |
| const dx = pointer.deltaX * config.SPLAT_FORCE; | |
| const dy = pointer.deltaY * config.SPLAT_FORCE; | |
| splat(pointer.texcoordX, pointer.texcoordY, dx, dy, pointer.color); | |
| } | |
| // イベントリスナー | |
| canvas.addEventListener("mousedown", (e) => { | |
| const posX = scaleByPixelRatio(e.offsetX); | |
| const posY = scaleByPixelRatio(e.offsetY); | |
| let pointer = pointers.find((p) => p.id == -1); | |
| if (pointer == null) pointer = createPointer(); | |
| updatePointerDownData(pointer, -1, posX, posY); | |
| }); | |
| canvas.addEventListener("mousemove", (e) => { | |
| const pointer = pointers[0]; | |
| const posX = scaleByPixelRatio(e.offsetX); | |
| const posY = scaleByPixelRatio(e.offsetY); | |
| if (!pointer.down) { | |
| // マウスが動いている場合は新しいポインターを作成 | |
| updatePointerDownData(pointer, -1, posX, posY); | |
| } | |
| updatePointerMoveData(pointer, posX, posY); | |
| }); | |
| window.addEventListener("mouseup", () => { | |
| pointers[0].down = false; | |
| }); | |
| canvas.addEventListener("touchstart", (e) => { | |
| e.preventDefault(); | |
| const touches = e.targetTouches; | |
| while (touches.length >= pointers.length) { | |
| pointers.push(createPointer()); | |
| } | |
| for (let i = 0; i < touches.length; i++) { | |
| const posX = scaleByPixelRatio(touches[i].pageX); | |
| const posY = scaleByPixelRatio(touches[i].pageY); | |
| updatePointerDownData(pointers[i + 1], touches[i].identifier, posX, posY); | |
| } | |
| }); | |
| canvas.addEventListener( | |
| "touchmove", | |
| (e) => { | |
| e.preventDefault(); | |
| const touches = e.targetTouches; | |
| for (let i = 0; i < touches.length; i++) { | |
| const pointer = pointers[i + 1]; | |
| if (!pointer.down) continue; | |
| const posX = scaleByPixelRatio(touches[i].pageX); | |
| const posY = scaleByPixelRatio(touches[i].pageY); | |
| updatePointerMoveData(pointer, posX, posY); | |
| } | |
| }, | |
| false | |
| ); | |
| window.addEventListener("touchend", (e) => { | |
| const touches = e.changedTouches; | |
| for (let i = 0; i < touches.length; i++) { | |
| const pointer = pointers.find((p) => p.id == touches[i].identifier); | |
| if (pointer == null) continue; | |
| pointer.down = false; | |
| } | |
| }); | |
| // ------------------------------------------------------------------------ | |
| // 10. アニメーションループとメイン更新処理 | |
| // ------------------------------------------------------------------------ | |
| let lastUpdateTime = Date.now(); | |
| // 毎フレーム呼び出されるメイン更新処理 | |
| function update() { | |
| // 10.1 フレームレート計算とキャンバササイズ変更のチェック | |
| const dt = calcDeltaTime(); | |
| if (resizeCanvas()) { | |
| initFramebuffers(); | |
| } | |
| // 10.2 ポインター入力の処理(マウスやタッチ) | |
| pointers.forEach((p) => { | |
| if (p.moved) { | |
| p.moved = false; | |
| splatPointer(p); | |
| } | |
| }); | |
| // 10.3 物理シミュレーションの実行と描画 | |
| if (!config.PAUSED) { | |
| step(dt); | |
| } | |
| render(); | |
| // 10.4 次のフレームをリクエスト | |
| requestAnimationFrame(update); | |
| } | |
| function calcDeltaTime() { | |
| const now = Date.now(); | |
| let dt = (now - lastUpdateTime) / 1000; | |
| dt = Math.min(dt, 0.016666); | |
| lastUpdateTime = now; | |
| return dt; | |
| } | |
| // ------------------------------------------------------------------------ | |
| // 11. シミュレーションの開始 | |
| // ------------------------------------------------------------------------ | |
| // フレームバッファを初期化し、アニメーションループを開始 | |
| initFramebuffers(); | |
| update(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment