🌿
🌿 MidoriPhotoArt.
3D空間と2D空間の融合。試作調査。3D空間にHTMLを配置。CSS3Dレンダラーを使用し、3D空間内でHTML要素を表示。基本的な、文字、表、SVG図 CSSスタイル。試作段階。

Three.jsでHTML要素を3D空間に配置する

1. はじめに

このページでは、Three.jsを使用して、HTML要素を3D空間内に配置し、表示する方法について解説します。 CSS3DRendererを使用することで、通常のHTMLコンテンツを3Dオブジェクトとして扱うことが可能になります。

2. HTMLコンテンツの準備

まず、3D空間に配置したいHTMLコンテンツを用意します。 ここでは、サンプルとして2つのコンテンツを用意しています。 一つは、自己紹介と画像、ボタンを含むコンテンツ、もう一つは、日本の伝統工芸に関するデータ表、グラフ、SVG図を含むコンテンツです。

const contents = [
    {
        html: `
            <div class="content-style">
                <h2 class="neon-text">MidoriPhotArt</h2>
                <p class="fade-in-text">ようこそ、私のデジタルアートギャラリーへ。</p>
                <img src="images/texture1.jpg" width="250" class="image-style glow-image">
                <p class="paragraph-style slide-in-text">ここでは、日本の伝統工芸「組子」をモチーフにしたデジタルアートを展示しています。</p>
                <p class="paragraph-style zoom-in-text">組子の幾何学模様の美しさと、デジタル技術を融合させた作品をお楽しみください。</p>
                <button class="button-style pulse-button" onclick="alert('ギャラリーへようこそ!')">ギャラリーに入る</button>
                <script>
                    console.log('このスクリプトは実行されます!');
                    // ここにJavaScriptコードを追加できます。
                    // 例:document.getElementById('myElement').addEventListener('click', function() { alert('要素がクリックされました!'); });
                </script>
            </div>
            <style>
                .content-style {
                    color: white;
                    font-size: 18px;
                    text-align: center;
                    perspective: 1000px; /* 3D効果のための視点を設定 */
                }
                .neon-text {
                    animation: neon 2s infinite alternate;
                    text-shadow: 0 0 10px #fff, 0 0 20px #fff, 0 0 30px #fff, 0 0 40px #ff00de, 0 0 70px #ff00de, 0 0 80px #ff00de, 0 0 100px #ff00de, 0 0 150px #ff00de;
                }
                .fade-in-text {
                    animation: fadeIn 3s ease-out;
                }
                .image-style {
                    margin-top: 10px;
                    transition: transform 0.5s ease; /* トランジション効果を追加 */
                }
                .image-style:hover {
                    transform: scale(1.1) rotate(5deg); /* ホバー時に拡大と回転 */
                }
                .glow-image {
                    animation: glow 2s infinite alternate;
                }
                .paragraph-style {
                    margin-top: 10px;
                    transform-style: preserve-3d; /* 子要素に3D変換を適用 */
                }
                .slide-in-text {
                    animation: slideIn 3s ease-out;
                    transform: translateZ(50px); /* Z軸方向に移動 */
                }
                .zoom-in-text {
                    animation: zoomIn 3s ease-out;
                    transform: translateZ(50px); /* Z軸方向に移動 */
                }
                .button-style {
                    margin-top: 10px;
                    background-color: #4CAF50;
                    color: white;
                    padding: 10px 20px;
                    border: none;
                    cursor: pointer;
                    transition: transform 0.3s ease; /* トランジション効果を追加 */
                }
                .button-style:hover {
                    transform: scale(1.1); /* ホバー時に拡大 */
                }
                .pulse-button {
                    animation: pulse 2s infinite;
                }
                @keyframes neon {
                    to {
                        text-shadow: 0 0 5px #fff, 0 0 10px #fff, 0 0 15px #fff, 0 0 20px #00ff00, 0 0 35px #00ff00, 0 0 40px #00ff00, 0 0 50px #00ff00, 0 0 75px #00ff00;
                    }
                }
                @keyframes fadeIn {
                    from { opacity: 0; }
                    to { opacity: 1; }
                }
                @keyframes glow {
                    to {
                        box-shadow: 0 0 20px #fff, 0 0 30px #0000ff, 0 0 40px #0000ff, 0 0 50px #0000ff, 0 0 60px #0000ff, 0 0 70px #0000ff, 0 0 80px #0000ff;
                    }
                }
                @keyframes slideIn {
                    from { transform: translateX(-100%) rotateY(-30deg); opacity: 0; }
                    to { transform: translateX(0) rotateY(0); opacity: 1; }
                }
                @keyframes zoomIn {
                    from { transform: scale(0.5); opacity: 0; }
                    to { transform: scale(1); opacity: 1; }
                }
                @keyframes pulse {
                    to {
                        box-shadow: 0 0 0 10px rgba(255, 255, 255, 0);
                    }
                }
            </style>
        `,
        x: 0, y: 0, z: -500, width: 300, height: 400,
        options: { rotationY: 0 }
    },
    {
        // CSSの表現を変更し、表、グラフ、SVG図をコードで生成
        html: `
            <div class="content-style">
                <h2 class="data-title">日本の伝統工芸データ</h2>
                <div id="clock" class="clock-style"></div>
                <table class="data-table">
                    <thead>
                        <tr>
                            <th>工芸名</th>
                            <th>特徴</th>
                            <th>歴史</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr>
                            <td>組子細工</td>
                            <td>釘を使わずに木を組み付ける</td>
                            <td>飛鳥時代から</td>
                        </tr>
                        <tr>
                            <td>陶磁器</td>
                            <td>土を焼いて作る器</td>
                            <td>縄文時代から</td>
                        </tr>
                        <tr>
                            <td>漆器</td>
                            <td>漆を塗って作る器</td>
                            <td>縄文時代から</td>
                        </tr>
                    </tbody>
                </table>
                <div class="chart-container">
                    <svg class="bar-chart" width="400" height="300"></svg>
                </div>
                <div class="svg-container">
                    <svg width="300" height="200">
                        <circle cx="50" cy="50" r="40" stroke="green" stroke-width="4" fill="yellow" />
                        <rect width="150" height="100" x="100" y="100" style="fill:rgb(0,0,255);stroke-width:3;stroke:rgb(0,0,0)" />
                    </svg>
                </div>
                <button class="data-button" onclick="alert('データ詳細!')">詳細データ</button>   
            </div>
            <style>
                .content-style {
                    color: white;
                    font-size: 18px;
                    text-align: center;
                    background: rgba(0, 0, 0, 0.5);
                    padding: 20px;
                    border-radius: 10px;
                }
                .data-title {
                    margin-bottom: 20px;
                    font-size: 24px;
                }
                .clock-style {
                    font-size: 20px;
                    margin-bottom: 20px;
                }
                .data-table {
                    width: 100%;
                    margin-bottom: 20px;
                    border-collapse: collapse;
                }
                .data-table th, .data-table td {
                    border: 1px solid white;
                    padding: 10px;
                }
                .chart-container {
                    width: 100%;
                    height: 300px;
                    margin-bottom: 20px;
                }
                .bar-chart {
                    width: 100%;
                    height: 100%;
                }
                .svg-container {
                    width: 100%;
                    margin-bottom: 20px;
                }
                .data-button {
                    background-color: #008CBA;
                    color: white;
                    padding: 15px 30px;
                    border: none;
                    cursor: pointer;
                    border-radius: 5px;
                }
            </style>
            <script>
                // D3.js ライブラリの読み込み
                <script src="https://d3js.org/d3.v7.min.js"></script>
                // 時計の更新関数
                function updateClock() {
                    const now = new Date();
                    const hours = now.getHours().toString().padStart(2, '0');
                    const minutes = now.getMinutes().toString().padStart(2, '0');
                    const seconds = now.getSeconds().toString().padStart(2, '0');
                    const timeString = \`\${hours}:\${minutes}:\${seconds}\`;
                    // document.getElementById('clock') の代わりに object.element.querySelector('#clock') を使用
                    object.element.querySelector('#clock').innerText = timeString;
                }

                // 時計の初期化
                updateClock();
                setInterval(updateClock, 1000);

                // グラフデータの例
                const chartData = [
                    { label: '組子細工', value: 30 },
                    { label: '陶磁器', value: 50 },
                    { label: '漆器', value: 20 }
                ];

                // 棒グラフの生成
                const svg = d3.select(".bar-chart");
                const margin = { top: 20, right: 20, bottom: 30, left: 40 };
                const width = +svg.attr("width") - margin.left - margin.right;
                const height = +svg.attr("height") - margin.top - margin.bottom;
                const g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");

                const x = d3.scaleBand()
                    .rangeRound([0, width])
                    .padding(0.1)
                    .domain(chartData.map(d => d.label));
                const y = d3.scaleLinear()
                    .rangeRound([height, 0])
                    .domain([0, d3.max(chartData, d => d.value)]);

                g.append("g")
                    .attr("transform", "translate(0," + height + ")")
                    .call(d3.axisBottom(x));

                g.append("g")
                    .call(d3.axisLeft(y))
                    .append("text")
                    .attr("fill", "#000")
                    .attr("transform", "rotate(-90)")
                    .attr("y", 6)
                    .attr("dy", "0.71em")
                    .attr("text-anchor", "end")
                    .text("Value");

                g.selectAll(".bar")
                    .data(chartData)
                    .enter().append("rect")
                    .attr("class", "bar")
                    .attr("x", d => x(d.label))
                    .attr("y", d => y(d.value))
                    .attr("width", x.bandwidth())
                    .attr("height", d => height - y(d.value))
                    .attr("fill", "steelblue");
            </script>
        `,
        x: 0, y: 0, z: -200, width: 100, height: 100,
        options: { rotationY: 0 }
    }
];

3. CSS3DRendererの設定

Three.jsのCSS3DRendererを使用すると、HTML要素を3D空間内に配置できます。 まず、CSS3DRendererを初期化し、サイズを設定します。 このレンダラーは、通常のWebGLRendererと併用して、3Dシーン内にHTML要素をレンダリングします。


// CSS3D レンダラーの設定
css3dRenderer = new CSS3DRenderer();
css3dRenderer.setSize(window.innerWidth, window.innerHeight);
css3dRenderer.domElement.style.position = 'absolute';
css3dRenderer.domElement.style.top = 0;
css3dRenderer.domElement.style.pointerEvents = 'none';
document.getElementById('threeContainer').appendChild(css3dRenderer.domElement);

4. HTMLコンテンツを3Dオブジェクトに変換

用意したHTMLコンテンツを、CSS3DObjectに変換します。 これにより、HTML要素がThree.jsのシーン内で3Dオブジェクトとして扱えるようになります。 各コンテンツに対して、位置(x, y, z)、サイズ(width, height)、回転(rotationY)などのオプションを設定できます。


// HTMLコンテンツを3Dオブジェクトに変換する関数
function create3DObject(html, x, y, z, width, height, options) {
    // div要素を作成し、HTMLコンテンツを格納
    const div = document.createElement('div');
    div.innerHTML = html;
    div.style.width = \`\${width}px\`;
    div.style.height = \`\${height}px\`;
    div.style.opacity = 0.8; // 透明度を設定
    div.style.background = 'rgba(0, 0, 0, 0.5)'; // 背景色を設定
    div.style.border = '1px solid white'; // 枠線を設定
    div.style.overflow = 'hidden'; // コンテンツがはみ出さないように設定

    // CSS3DObjectを作成
    const object = new CSS3DObject(div);
    object.position.set(x, y, z);
    object.rotation.y = options.rotationY;

    // シーンに追加
    scene.add(object);
}

5. アニメーションとインタラクション

作成した3Dオブジェクトは、Three.jsのアニメーションループ内で更新できます。 例えば、オブジェクトを回転させたり、マウスの動きに反応させたりすることができます。 また、HTML要素内のJavaScriptコードも実行されるため、通常のWebページと同様にインタラクティブな要素を追加することも可能です。

6. 完全なソースコード

以下は、HTML要素を3D空間に配置する完全なソースコードです。


import * as THREE from 'three';
// Controls
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { DragControls } from 'three/addons/controls/DragControls.js';

// Loaders
import { TextureLoader } from 'three';
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
import { EXRLoader } from 'three/addons/loaders/EXRLoader.js';

// CSS3DRenderer
import { CSS3DRenderer, CSS3DObject } from 'three/addons/renderers/CSS3DRenderer.js';

// Utils
import { GUI } from 'dat.gui';
import Stats from 'three/addons/libs/stats.module.js';
import { PMREMGenerator } from 'three';

let scene, camera, renderer, controls, stats, css3dRenderer;





// パーティクルエフェクトを生成する関数
// パーティクルエフェクトを生成する関数
// パーティクルエフェクトを生成し、アニメーションさせる関数
// パーティクルエフェクトを生成し、アニメーションさせる関数
function createParticleEffect() {
    const particlesGeometry = new THREE.BufferGeometry();
    const particlesCnt = 30000; // パーティクルの数を増やす

    const posArray = new Float32Array(particlesCnt * 3);
    const colors = new Float32Array(particlesCnt * 3);
    const sizes = new Float32Array(particlesCnt);

    for (let i = 0; i < particlesCnt; i++) {
        // 位置を宇宙空間のように広範囲にランダムに設定
        posArray[i * 3] = (Math.random() - 0.5) * 8000;
        posArray[i * 3 + 1] = (Math.random() - 0.5) * 8000;
        posArray[i * 3 + 2] = (Math.random() - 0.5) * 8000;

        // 色を青白く、星のような色合いに設定
        const color = new THREE.Color();
        if (Math.random() > 0.8) {
            // 明るい星
            color.setHSL(0.6, 0.8, 0.9);
        } else {
            // 暗い星
...
// HTMLコンテンツを3D空間に追加する関数
function addHTMLContentTo3DSpace() {
    // サンプルコンテンツの配列
    const contents = [
        {
            html: `
                <div class="content-style">
                    <h2 class="neon-text">MidoriPhotArt</h2>
                    <p class="fade-in-text">ようこそ、私のデジタルアートギャラリーへ。</p>
                    <img src="images/texture1.jpg" width="250" class="image-style glow-image">
                    <p class="paragraph-style slide-in-text">ここでは、日本の伝統工芸「組子」をモチーフにしたデジタルアートを展示しています。</p>
                    <p class="paragraph-style zoom-in-text">組子の幾何学模様の美しさと、デジタル技術を融合させた作品をお楽しみください。</p>
                    <button class="button-style pulse-button" onclick="alert('ギャラリーへようこそ!')">ギャラリーに入る</button>
                    <script>
                        console.log('このスクリプトは実行されます!');
                        // ここにJavaScriptコードを追加できます。
                        // 例:document.getElementById('myElement').addEventListener('click', function() { alert('要素がクリックされました!'); });
                    </script>
                </div>
                <style>
                    .content-style {
                        color: white;
                        font-size: 18px;
                        text-align: center;
                        perspective: 1000px; /* 3D効果のための視点を設定 */
                    }
                    .neon-text {
                        animation: neon 2s infinite alternate;
                        text-shadow: 0 0 10px #fff, 0 0 20px #fff, 0 0 30px #fff, 0 0 40px #ff00de, 0 0 70px #ff00de, 0 0 80px #ff00de, 0 0 100px #ff00de, 0 0 150px #ff00de;
                    }
                    .fade-in-text {
                        animation: fadeIn 3s ease-out;
                    }
                    .image-style {
                        margin-top: 10px;
                        transition: transform 0.5s ease; /* トランジション効果を追加 */
                    }
                    .image-style:hover {
                        transform: scale(1.1) rotate(5deg); /* ホバー時に拡大と回転 */
                    }
                    .glow-image {
                        animation: glow 2s infinite alternate;
                    }
                    .paragraph-style {
                        margin-top: 10px;
                        transform-style: preserve-3d; /* 子要素に3D変換を適用 */
                    }
                    .slide-in-text {
                        animation: slideIn 3s ease-out;
                        transform: translateZ(50px); /* Z軸方向に移動 */
                    }
                    .zoom-in-text {
                        animation: zoomIn 3s ease-out;
                        transform: translateZ(50px); /* Z軸方向に移動 */
                    }
                    .button-style {
                        margin-top: 10px;
                        background-color: #4CAF50;
                        color: white;
                        padding: 10px 20px;
                        border: none;
                        cursor: pointer;
                        transition: transform 0.3s ease; /* トランジション効果を追加 */
                    }
                    .button-style:hover {
                        transform: scale(1.1); /* ホバー時に拡大 */
                    }
                    .pulse-button {
                        animation: pulse 2s infinite;
                    }
                    @keyframes neon {
                        to {
                            text-shadow: 0 0 5px #fff, 0 0 10px #fff, 0 0 15px #fff, 0 0 20px #00ff00, 0 0 35px #00ff00, 0 0 40px #00ff00, 0 0 50px #00ff00, 0 0 75px #00ff00;
                        }
                    }
                    @keyframes fadeIn {
                        from { opacity: 0; }
                        to { opacity: 1; }
                    }
                    @keyframes glow {
                        to {
                            box-shadow: 0 0 20px #fff, 0 0 30px #0000ff, 0 0 40px #0000ff, 0 0 50px #0000ff, 0 0 60px #0000ff, 0 0 70px #0000ff, 0 0 80px #0000ff;
                        }
                    }
                    @keyframes slideIn {
                        from { transform: translateX(-100%) rotateY(-30deg); opacity: 0; }
                        to { transform: translateX(0) rotateY(0); opacity: 1; }
                    }
                    @keyframes zoomIn {
                        from { transform: scale(0.5); opacity: 0; }
                        to { transform: scale(1); opacity: 1; }
                    }
                    @keyframes pulse {
                        to {
                            box-shadow: 0 0 0 10px rgba(255, 255, 255, 0);
                        }
                    }
                </style>
            `,
            x: 0, y: 0, z: -500, width: 300, height: 400,
            options: { rotationY: 0 }
        },
        {
            // CSSの表現を変更し、表、グラフ、SVG図をコードで生成
            html: `
                <div class="content-style">
                    <h2 class="data-title">日本の伝統工芸データ</h2>
                    <div id="clock" class="clock-style"></div>
                    <table class="data-table">
                        <thead>
                            <tr>
                                <th>工芸名</th>
                                <th>特徴</th>
                                <th>歴史</th>
                            </tr>
                        </thead>
                        <tbody>
                            <tr>
                                <td>組子細工</td>
                                <td>釘を使わずに木を組み付ける</td>
                                <td>飛鳥時代から</td>
                            </tr>
                            <tr>
                                <td>陶磁器</td>
                                <td>土を焼いて作る器</td>
                                <td>縄文時代から</td>
                            </tr>
                            <tr>
                                <td>漆器</td>
                                <td>漆を塗って作る器</td>
                                <td>縄文時代から</td>
                            </tr>
                        </tbody>
                    </table>
                    <div class="chart-container">
                        <svg class="bar-chart" width="400" height="300"></svg>
                    </div>
                    <div class="svg-container">
                        <svg width="300" height="200">
                            <circle cx="50" cy="50" r="40" stroke="green" stroke-width="4" fill="yellow" />
                            <rect width="150" height="100" x="100" y="100" style="fill:rgb(0,0,255);stroke-width:3;stroke:rgb(0,0,0)" />
                        </svg>
                    </div>
                    <button class="data-button" onclick="alert('データ詳細!')">詳細データ</button>   
                </div>
                <style>
                    .content-style {
                        color: white;
                        font-size: 18px;
                        text-align: center;
                        background: rgba(0, 0, 0, 0.5);
                        padding: 20px;
                        border-radius: 10px;
                    }
                    .data-title {
                        margin-bottom: 20px;
                        font-size: 24px;
                    }
                    .clock-style {
                        font-size: 20px;
                        margin-bottom: 20px;
                    }
                    .data-table {
                        width: 100%;
                        margin-bottom: 20px;
                        border-collapse: collapse;
                    }
                    .data-table th, .data-table td {
                        border: 1px solid white;
                        padding: 10px;
                    }
                    .chart-container {
                        width: 100%;
                        height: 300px;
                        margin-bottom: 20px;
                    }
                    .bar-chart {
                        width: 100%;
                        height: 100%;
                    }
                    .svg-container {
                        width: 100%;
                        margin-bottom: 20px;
                    }
                    .data-button {
                        background-color: #008CBA;
                        color: white;
                        padding: 15px 30px;
                        border: none;
                        cursor: pointer;
                        border-radius: 5px;
                    }
                </style>
                <script>
                    // D3.js ライブラリの読み込み
                    <script src="https://d3js.org/d3.v7.min.js"></script>
                    // 時計の更新関数
                    function updateClock() {
                        const now = new Date();
                        const hours = now.getHours().toString().padStart(2, '0');
                        const minutes = now.getMinutes().toString().padStart(2, '0');
                        const seconds = now.getSeconds().toString().padStart(2, '0');
                        const timeString = \`\${hours}:\${minutes}:\${seconds}\`;
                        // document.getElementById('clock') の代わりに object.element.querySelector('#clock') を使用
                        object.element.querySelector('#clock').innerText = timeString;
                    }

                    // 時計の初期化
                    updateClock();
                    setInterval(updateClock, 1000);

                    // グラフデータの例
                    const chartData = [
                        { label: '組子細工', value: 30 },
                        { label: '陶磁器', value: 50 },
                        { label: '漆器', value: 20 }
                    ];

                    // 棒グラフの生成
const fireParams = {
    speed: 1.5,        // 炎の動きの速度
    intensity: 0.3,    // 炎の明るさ
    scale: 2.5,        // 炎の全体的なサイズ
    density: 1.2,      // 炎の密度
    turbulence: 1.4,   // 炎のゆらぎの強さ
    flameHeight: 1.3,  // 炎の高さ
    colorIntensity: 1.5, // 炎の色の鮮やかさ
    volumetricLayers: 48, // 炎の3D表現のためのレイヤー数
    coneAngle: 25      // 炎の広がり角度
};
scale: 2.5 flameHeight: 1.3 coneAngle: 25°

パラメータの相互作用

基本パラメータ ・speed ・turbulence 形状パラメータ ・scale ・flameHeight 視覚効果 ・intensity ・colorIntensity
  • 基本パラメータ
    • speed × turbulence → 炎のゆらぎの動きを決定
    • intensity × colorIntensity → 炎の視覚的な強さを制御
  • 形状パラメータ
    • scale + flameHeight → 炎の全体的な大きさを決定
    • density + volumetricLayers → 炎の立体感を表現

2. GUIコントロールの実装

dat.GUIを使用して、ユーザーが各パラメータをリアルタイムで調整できるインターフェースを実装しています。 GUIパネルはドラッグ可能で、画面上の任意の位置に移動できます。

// GUIコントロールの初期化
const gui = new dat.GUI();
gui.domElement.style.opacity = "0.8";

// パラメータの追加
gui.add(fireParams, 'speed', 0, 3).name('炎の速度');
gui.add(fireParams, 'intensity', 0, 1).name('明るさ');
gui.add(fireParams, 'scale', 0.5, 5).name('サイズ');
gui.add(fireParams, 'density', 0.5, 2).name('密度');
gui.add(fireParams, 'turbulence', 0, 3).name('ゆらぎ');
                    
dat.GUI初期化 パラメータコントロール追加 リアルタイム更新処理

実装のポイント

  • 各パラメータにスライダーを実装:min/max値の適切な設定
  • 日本語のラベル表示:name()メソッドで設定
  • ドラッグ機能の実装:CSSでdraggable設定
  • 透明度を持つ背景色の設定:opacity = 0.8

3. シェーダーによる炎の表現

複数の2D平面を円錐状に配置し、シェーダーを使用して炎のような見た目を実現しています。 以下のコードとフローチャートで実装方法を説明します。

// フラグメントシェーダー
uniform float time;
uniform float intensity;
uniform float turbulence;

varying vec2 vUv;

void main() {
    // ノイズ関数による揺らぎの生成
    float noise = snoise(vec3(vUv * 4.0, time));
    
    // グラデーションの計算
    vec3 color = mix(
        vec3(1.0, 0.5, 0.0),  // オレンジ
        vec3(1.0, 0.8, 0.0),  // 黄色
        noise * turbulence
    );
    
    // 透明度の計算
    float alpha = (1.0 - vUv.y) * intensity;
    
    gl_FragColor = vec4(color, alpha);
}
                    
シェーダー初期化 ノイズ生成 レイヤー配置 炎エフェクトの合成

実装の主なポイント

  • ノイズ関数(Simplex Noise)を使用して自然な揺らぎを表現
  • オレンジから黄色へのグラデーションで炎らしい色彩を実現
  • 透明度をy座標に応じて変化させ、上部ほど薄くなるように制御
  • 複数のレイヤーを重ねることで立体的な炎の表現を実現
  • uniformパラメータでリアルタイムに炎の様子を調整可能

4. アニメーションの更新処理

各フレームごとに炎エフェクトを更新し、リアルタイムでダイナミックな動きを実現します。 主な更新内容は以下の通りです:

  • 時間パラメータの更新による炎の揺らぎ制御
  • 速度・強度などの動的パラメータの反映
  • 各レイヤーの空間的な配置の計算
  • 全体的なスケールの調整
フレーム開始 時間パラメータ更新 レイヤー位置計算 スケール適用

// 炎エフェクトの更新処理
const updateFire = () => {
    // 円錐の半径を計算(角度に基づく)
    const radius = Math.sin(fireParams.coneAngle * Math.PI / 180) * 0.5;
    
    // 各レイヤーの更新処理
    layers.forEach((layer, index) => {
        // 時間経過による動的な変化
        layer.material.uniforms.time.value += 0.016;  // 約60FPSに相当
        
        // エフェクトパラメータの更新
        layer.material.uniforms.speed.value = fireParams.speed;      // 炎の速度
        layer.material.uniforms.intensity.value = fireParams.intensity;  // 炎の強度
        // ... その他のパラメータ更新
        
        // 円周上にレイヤーを均等配置
        const angle = (index / (fireParams.volumetricLayers - 1)) * Math.PI * 2;
        layer.position.x = Math.cos(angle) * radius;  // X座標
        layer.position.z = Math.sin(angle) * radius;  // Z座標
        layer.rotation.y = angle;  // レイヤーの向き
    });
    
    // 全体的なスケールの適用
    fireGroup.scale.set(fireParams.scale, fireParams.scale, fireParams.scale);
};
                    

実装のポイント解説

  • 時間値を少しずつ増加させることで、連続的な炎の動きを実現
  • 円錐状の配置により、立体的な炎の表現を実現
  • 各レイヤーを円周上に均等配置することで、自然な体積感を表現
  • スケールパラメータにより、炎の大きさを動的に調整可能

3. 炎エフェクトの完全なソースコード

以下は、Three.jsを使用した炎エフェクトの完全な実装コードです。コードの理解を深めるため、主要な部分ごとに分けて解説します。

3.1 パラメータとGUIの設定


// 炎のマテリアルのパラメータ
const fireParams = {
    speed: 1.5,         // より自然な動きのスピード
    intensity: 0.3,     // より明るい炎の強度
    scale: 2.5,         // より大きなサイズ
    density: 1.2,       // より密度の高い炎
    turbulence: 1.4,    // より自然な乱流
    flameHeight: 1.3,   // より高い炎
    colorIntensity: 1.5, // より鮮やかな色
    volumetricLayers: 48, // より滑らかな見た目のレイヤー数
    coneAngle: 25       // より自然な広がり角度
};

// GUIコントロールの設定
const gui = new GUI({ autoPlace: false });
const fireFolder = gui.addFolder('炎のパラメータ');
fireFolder.add(fireParams, 'speed', 0.1, 3.0).name('速度');
fireFolder.add(fireParams, 'intensity', 0.1, 3.0).name('強度');
// ... その他のGUIパラメータ設定 ...

3.2 ドラッグ可能なGUIコントロール実装


// GUIコンテナの作成と配置
const guiContainer = document.createElement('div');
guiContainer.style.position = 'absolute';
guiContainer.style.right = '10px';
guiContainer.style.top = '50%';
// ... スタイル設定 ...

// ドラッグ機能の実装
let isDragging = false;
let currentX, currentY, initialX, initialY;

dragHandle.addEventListener('mousedown', (e) => {
    isDragging = true;
    initialX = e.clientX - guiContainer.offsetLeft;
    initialY = e.clientY - guiContainer.offsetTop;
    e.preventDefault();
});

3.3 炎のジオメトリとマテリアル生成


// 3D炎エフェクトの作成
const fireGroup = new THREE.Group();
const layers = [];

for(let i = 0; i < fireParams.volumetricLayers; i++) {
    const segments = 64;
    const planeGeometry = new THREE.PlaneGeometry(1, 4, segments, 128);
    
    // 台形状の変形処理
    const positions = planeGeometry.attributes.position.array;
    for(let j = 0; j < positions.length; j += 3) {
        const y = positions[j + 1];
        const widthScale = 1.0 + (y / 4.0) * 0.5;
        positions[j] *= widthScale;
    }
    planeGeometry.attributes.position.needsUpdate = true;

3.4 シェーダーコード


// バーテックスシェーダー
varying vec2 vUv;
varying vec3 vPosition;
void main() {
    vUv = uv;
    vPosition = position;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

// フラグメントシェーダー
uniform float time;
uniform float speed;
uniform float intensity;
// ... その他のユニフォーム変数 ...

void main() {
    vec2 uv = vUv;
    float n = 0.0;
    float amplitude = 1.0;
    float frequency = 1.0;
    
    // ノイズ生成処理
    for(int i = 0; i < 4; i++) {
        vec2 noisePos = uv * noiseScale * frequency * density + 
            vec2(sin(time * speed * 0.1 + layerOffset * 6.28) * turbulence, 
                 -time * speed * (0.5 + float(i) * 0.1));
        n += noise(noisePos) * amplitude;
        amplitude *= 0.5;
        frequency *= 2.0;
    }

    // 炎の形状と色の計算
    float flame = smoothstep(0.1, 0.5, uv.y) * 
        (1.0 - pow(uv.y, flameHeight)) * 2.0;
    flame *= n * intensity;

3.5 アニメーション更新処理


const updateFire = () => {
    const radius = Math.sin(fireParams.coneAngle * Math.PI / 180) * 0.5;
    
    layers.forEach((layer, index) => {
        // 時間更新
        layer.material.uniforms.time.value += 0.016;
        
        // パラメータ更新
        layer.material.uniforms.speed.value = fireParams.speed;
        layer.material.uniforms.intensity.value = fireParams.intensity;
        // ... その他のパラメータ更新 ...
        
        // 位置更新
        const angle = (index / (fireParams.volumetricLayers - 1)) * Math.PI * 2;
        layer.position.x = Math.cos(angle) * radius;
        layer.position.z = Math.sin(angle) * radius;
        layer.rotation.y = angle;
    });
    
    fireGroup.scale.set(fireParams.scale, fireParams.scale, fireParams.scale);
};

実装のポイント

  • コードを機能ごとに分割し、保守性と可読性を向上
  • 各セクションに適切なコメントを追加し、理解を促進
  • シェーダーコードを独立したセクションとして表示
  • 重要なパラメータと更新ロジックを明確に区分