GLSLシェーダーで遊ぶ「Glsl Viewer」


画像

・ソースコードはこちら。↓

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Effect Gem Crafter - Master</title>
    <style>
        body { margin: 0; display: flex; height: 100vh; background: #0a0a10; color: #fff; font-family: 'Segoe UI', sans-serif; overflow: hidden; }
        
        #editor { width: 50%; display: flex; flex-direction: column; border-right: 4px solid #222; position: relative; }
        #toolbar { padding: 8px; background: #151520; display: flex; gap: 6px; flex-wrap: wrap; z-index: 10; box-shadow: 0 4px 10px rgba(0,0,0,0.8);}
        .btn-group { display: flex; gap: 4px; border: 1px solid #333; padding: 4px; border-radius: 6px; background: #111; align-items: center;}
        .btn-group span { font-size: 10px; font-weight: bold; color: #888; writing-mode: vertical-lr; text-align: center; margin-right: 2px;}
        button { font-size: 18px; cursor: pointer; background: #222; border: 1px solid #444; border-radius: 6px; transition: 0.1s; padding: 4px 6px;}
        button:hover { background: #333; transform: scale(1.1); }
        button:active { transform: scale(0.9); }
        
        #ui-canvas { flex-grow: 1; background: #111118; cursor: default; touch-action: none; }
        
        #preview { width: 50%; position: relative; background: #000; }
        #log-panel { position: absolute; bottom: 0; left: 0; width: 100%; height: 200px; background: rgba(0,0,0,0.85); color: #00ffcc; font-family: 'Courier New', monospace; font-size: 11px; padding: 10px; box-sizing: border-box; overflow-y: auto; pointer-events: none; border-top: 1px solid #333; }

        /* パラメータパネル */
        #param-panel {
            position: absolute; display: none; background: rgba(15, 15, 20, 0.95);
            border: 1px solid #555; border-radius: 8px; padding: 12px;
            color: white; z-index: 100; pointer-events: auto;
            box-shadow: 0 10px 30px rgba(0,0,0,0.8); width: 180px;
            transform: translate(-50%, 20px);
        }
        #param-title { font-weight: bold; margin-bottom: 8px; border-bottom: 1px solid #444; padding-bottom: 4px; text-align: center; font-size: 12px; color: #aaa;}
        .param-row { display: flex; flex-direction: column; margin-bottom: 6px; }
        .param-row label { font-size: 10px; color: #ccc; margin-bottom: 2px; display: flex; justify-content: space-between;}
        .param-row input[type="range"] { width: 100%; cursor: pointer; margin: 0; }

        /* コンテキストメニュー (右クリック) */
        #ctx-menu {
            position: absolute; display: none; background: rgba(25, 25, 30, 0.95);
            border: 1px solid #555; border-radius: 6px; padding: 4px 0;
            color: white; z-index: 200; box-shadow: 0 5px 15px rgba(0,0,0,0.8);
            min-width: 120px; font-size: 12px;
        }
        .ctx-item { padding: 8px 16px; cursor: pointer; transition: background 0.1s; }
        .ctx-item:hover { background: #00ffcc; color: #000; font-weight: bold; }
        .ctx-sep { height: 1px; background: #444; margin: 4px 0; }
    </style>
</head>
<body>

    <div id="editor">
        <div id="toolbar">
            <div class="btn-group"><span>ベース</span>
                <button onclick="addNode('BASE')" title="魔法陣を配置">🎇</button>
            </div>
            <div class="btn-group"><span>見た目</span>
                <button onclick="addNode('COLOR')" title="色(HSV)">🎨</button>
                <button onclick="addNode('SHAPE')" title="形(多角形)">📐</button>
            </div>
            <div class="btn-group"><span>動き</span>
                <button onclick="addNode('M_LINEAR')" title="直線移動">➡️</button>
                <button onclick="addNode('M_SINE')" title="反復移動">↔️</button>
                <button onclick="addNode('M_BOUNCE')" title="バウンド">🪀</button>
                <button onclick="addNode('M_PARABOLA')" title="放物線">⤴️</button>
                <button onclick="addNode('M_SPIRAL')" title="渦巻き">🌀</button>
            </div>
            <div class="btn-group"><span>特殊</span>
                <button onclick="addNode('TIME')" title="時間制御(寿命/フェード)">⏳</button>
                <button onclick="addNode('FILTER')" title="フィルター(発光/モザイク)">🌟</button>
                <button onclick="addNode('PARTICLE')" title="パーティクル(爆発/破片)">✨</button>
            </div>
            <button onclick="clearNodes()" style="margin-left:auto; font-size:12px;">🗑️ リセット</button>
        </div>
        <canvas id="ui-canvas"></canvas>
        <div id="param-panel"><div id="param-title">調整</div><div id="param-content"></div></div>
        
        <div id="ctx-menu">
            <div class="ctx-item" id="ctx-cut">✂️ カット</div>
            <div class="ctx-item" id="ctx-copy">📄 コピー</div>
            <div class="ctx-item" id="ctx-paste">📋 ペースト</div>
            <div class="ctx-sep" id="ctx-sep-1"></div>
            <div class="ctx-item" id="ctx-delete" style="color:#ff6666;">🗑️ 削除</div>
        </div>
    </div>
    
    <div id="preview">
        <div id="log-panel">// ✨特殊宝石(時間・パーティクル等)が追加されました!</div>
    </div>

    <script type="importmap">
        { "imports": { "three": "https://unpkg.com/three@0.160.0/build/three.module.js" } }
    </script>

    <script type="module">
        import * as THREE from 'three';

        // ==========================================
        // 1. ノードとパラメータの定義
        // ==========================================
        const NODE_DEFS = {
            BASE: { type: 'BASE', emoji: '🎇', color: '#444455', radiusClosed: 16, radiusOpen: 60 },
            COLOR: { type: 'PARAM', subtype: 'COLOR', emoji: '🎨', color: '#ff55aa', radius: 14, params: [
                { key: 'h', label: '色相 (Hue)', min: 0, max: 1, def: 0.1, step: 0.01 },
                { key: 's', label: '彩度 (Sat)', min: 0, max: 1, def: 0.8, step: 0.01 },
                { key: 'v', label: '明度 (Val)', min: 0, max: 2, def: 1.5, step: 0.01 }
            ]},
            SHAPE: { type: 'PARAM', subtype: 'SHAPE', emoji: '📐', color: '#55aaff', radius: 14, params: [
                { key: 'n', label: '何角形? (0=円)', min: 0, max: 8, def: 4, step: 1 },
                { key: 'size', label: '大きさ', min: 0.01, max: 0.5, def: 0.1, step: 0.01 }
            ]},
            // 動きの定義
            M_LINEAR: { type: 'PARAM', subtype: 'MOTION_LINEAR', emoji: '➡️', color: '#aaff55', radius: 14, params: [
                { key: 'vx', label: 'X速度', min: -2, max: 2, def: 0.5, step: 0.01 },
                { key: 'vy', label: 'Y速度', min: -2, max: 2, def: 0.0, step: 0.01 }
            ]},
            M_SINE: { type: 'PARAM', subtype: 'MOTION_SINE', emoji: '↔️', color: '#55ffaa', radius: 14, params: [
                { key: 'ampX', label: 'X振幅', min: 0, max: 1, def: 0.5, step: 0.01 },
                { key: 'ampY', label: 'Y振幅', min: 0, max: 1, def: 0.0, step: 0.01 },
                { key: 'freq', label: '速さ', min: 0, max: 10, def: 2.0, step: 0.1 }
            ]},
            M_BOUNCE: { type: 'PARAM', subtype: 'MOTION_BOUNCE', emoji: '🪀', color: '#ddff55', radius: 14, params: [
                { key: 'height', label: '跳ね高さ', min: 0, max: 1, def: 0.3, step: 0.01 },
                { key: 'speed', label: '速さ', min: 0, max: 10, def: 4.0, step: 0.1 }
            ]},
            M_PARABOLA: { type: 'PARAM', subtype: 'MOTION_PARABOLA', emoji: '⤴️', color: '#aaddff', radius: 14, params: [
                { key: 'vx', label: 'X速度', min: -2, max: 2, def: 0.5, step: 0.01 },
                { key: 'vy', label: '初期Yジャンプ', min: 0, max: 3, def: 1.5, step: 0.01 },
                { key: 'g', label: '重力', min: 0, max: 5, def: 3.0, step: 0.1 }
            ]},
            M_SPIRAL: { type: 'PARAM', subtype: 'MOTION_SPIRAL', emoji: '🌀', color: '#55ffdd', radius: 14, params: [
                { key: 'radius', label: '広がり幅', min: 0, max: 1, def: 0.3, step: 0.01 },
                { key: 'spin', label: '回転速度', min: -10, max: 10, def: 5.0, step: 0.1 }
            ]},
            // 新規追加:特殊系(時間・フィルター・パーティクル)
            TIME: { type: 'PARAM', subtype: 'TIME', emoji: '⏳', color: '#ffaa00', radius: 14, params: [
                { key: 'delay', label: '発生遅延(秒)', min: 0, max: 5, def: 0.0, step: 0.1 },
                { key: 'life', label: '寿命(ループ秒数)', min: 0.1, max: 10, def: 3.0, step: 0.1 },
                { key: 'fade', label: '時間で消滅(0=無 1=有)', min: 0, max: 1, def: 1.0, step: 1 }
            ]},
            FILTER: { type: 'PARAM', subtype: 'FILTER', emoji: '🌟', color: '#ddddff', radius: 14, params: [
                { key: 'pixel', label: 'ドット化(粗さ)', min: 0, max: 50, def: 0, step: 1 },
                { key: 'glow', label: '発光(Glow)強度', min: 0, max: 2, def: 0.0, step: 0.01 }
            ]},
            PARTICLE: { type: 'PARAM', subtype: 'PARTICLE', emoji: '✨', color: '#ffcc55', radius: 14, params: [
                { key: 'count', label: '破片の数', min: 1, max: 30, def: 10, step: 1 },
                { key: 'spread', label: '散らばり幅', min: 0, max: 2, def: 0.5, step: 0.01 },
                { key: 'speed', label: '弾ける速さ', min: 0, max: 5, def: 1.0, step: 0.1 }
            ]}
        };

        // ==========================================
        // 2. UI キャンバスロジック
        // ==========================================
        const canvas = document.getElementById('ui-canvas');
        const ctx = canvas.getContext('2d');
        const paramPanel = document.getElementById('param-panel');
        let width, height;
        
        let nodes = [];
        let draggedNode = null;
        let selectedParam = null;
        let activeBase = null; 

        // クリップボード用変数
        let clipboardData = null;
        let ctxTarget = null;
        let ctxMousePos = {x:0, y:0};

        function resize() {
            width = canvas.parentElement.clientWidth; height = canvas.parentElement.clientHeight - document.getElementById('toolbar').clientHeight;
            canvas.width = width; canvas.height = height;
            if(window.uniforms) window.uniforms.u_aspect.value = width / height;
            updateShader();
        }
        window.addEventListener('resize', resize); 

        window.addNode = (defKey) => {
            const def = NODE_DEFS[defKey];
            const vals = {};
            if(def.params) def.params.forEach(p => vals[p.key] = p.def);
            
            let n = {
                id: Math.random().toString(36).substr(2, 9),
                ...def, vals: vals,
                x: width/2 + (Math.random()-0.5)*50, y: height/2 + (Math.random()-0.5)*50,
                parent: null, children: []
            };
            nodes.push(n);
            if(n.type === 'BASE') activeBase = n;
            updateShader();
        };
        window.clearNodes = () => { nodes = []; activeBase = null; closeParamPanel(); updateShader(); };

        // --- パラメータパネルUI ---
        function openParamPanel(node) {
            if (node.type === 'BASE') return;
            selectedParam = node;
            paramPanel.style.display = 'block';
            document.getElementById('param-title').innerText = `${node.emoji} の調整`;
            const content = document.getElementById('param-content');
            content.innerHTML = '';
            
            node.params.forEach(p => {
                let row = document.createElement('div'); row.className = 'param-row';
                let labelDiv = document.createElement('label');
                let nameSpan = document.createElement('span'); nameSpan.innerText = p.label;
                let valSpan = document.createElement('span'); valSpan.innerText = node.vals[p.key];
                labelDiv.appendChild(nameSpan); labelDiv.appendChild(valSpan);
                
                let input = document.createElement('input'); input.type = 'range';
                input.min = p.min; input.max = p.max; input.step = p.step; input.value = node.vals[p.key];
                
                input.addEventListener('input', (e) => {
                    let val = parseFloat(e.target.value);
                    node.vals[p.key] = val; valSpan.innerText = val.toFixed(2);
                    updateShader(); 
                });
                
                row.appendChild(labelDiv); row.appendChild(input); content.appendChild(row);
            });
            updateParamPanelPos();
        }
        function closeParamPanel() { paramPanel.style.display = 'none'; selectedParam = null; }
        function updateParamPanelPos() {
            if(!selectedParam || paramPanel.style.display === 'none') return;
            let px = selectedParam.parent ? selectedParam.parent.x : selectedParam.x;
            let py = selectedParam.parent ? selectedParam.parent.y : selectedParam.y;
            paramPanel.style.left = px + 'px'; paramPanel.style.top = (py + (selectedParam.parent ? NODE_DEFS.BASE.radiusOpen : 20) + 10) + 'px';
        }

        const getPos = (e) => { const rect = canvas.getBoundingClientRect(); return { x: (e.touches ? e.touches[0].clientX : e.clientX) - rect.left, y: (e.touches ? e.touches[0].clientY : e.clientY) - rect.top }; };

        // クリックした対象を判定する共通関数
        function getHitTarget(pos) {
            let target = null;
            if(activeBase) {
                activeBase.children.forEach((child, i) => {
                    let angle = (i / activeBase.children.length) * Math.PI * 2;
                    let cx = activeBase.x + Math.cos(angle) * (activeBase.radiusOpen * 0.65);
                    let cy = activeBase.y + Math.sin(angle) * (activeBase.radiusOpen * 0.65);
                    if(Math.hypot(cx - pos.x, cy - pos.y) < child.radius + 5) target = child;
                });
            }
            if(!target) target = nodes.find(n => n.type === 'PARAM' && !n.parent && Math.hypot(n.x - pos.x, n.y - pos.y) < n.radius + 5);
            if(!target) target = nodes.find(n => n.type === 'BASE' && Math.hypot(n.x - pos.x, n.y - pos.y) < (n === activeBase ? n.radiusOpen : n.radiusClosed));
            return target;
        }

        canvas.addEventListener('pointerdown', (e) => {
            if(e.button === 2) return; 
            canvas.setPointerCapture(e.pointerId);
            document.getElementById('ctx-menu').style.display = 'none'; 
            
            const pos = getPos(e);
            draggedNode = getHitTarget(pos);

            if(draggedNode) {
                if(draggedNode.type === 'BASE') {
                    activeBase = draggedNode;
                    closeParamPanel();
                } else if(draggedNode.type === 'PARAM') {
                    if(draggedNode.parent) {
                        draggedNode.parent.children = draggedNode.parent.children.filter(c => c !== draggedNode);
                        draggedNode.parent = null;
                        draggedNode.x = pos.x; draggedNode.y = pos.y;
                        updateShader();
                    }
                    openParamPanel(draggedNode);
                }
            } else {
                activeBase = null;
                closeParamPanel();
            }
        });

        canvas.addEventListener('pointermove', (e) => {
            if(!draggedNode) return;
            const pos = getPos(e); draggedNode.x = pos.x; draggedNode.y = pos.y; 
            updateParamPanelPos();
            if(draggedNode.type === 'BASE') updateShader(); 
        });

        canvas.addEventListener('pointerup', () => {
            if(draggedNode && draggedNode.type === 'PARAM' && activeBase) {
                if(Math.hypot(activeBase.x - draggedNode.x, activeBase.y - draggedNode.y) < activeBase.radiusOpen + 20) {
                    draggedNode.parent = activeBase;
                    activeBase.children.push(draggedNode);
                    updateShader();
                    updateParamPanelPos();
                }
            }
            draggedNode = null;
        });

        // ==========================================
        // 3. 右クリックコンテキストメニュー & コピペ機能
        // ==========================================
        const ctxMenu = document.getElementById('ctx-menu');

        canvas.addEventListener('contextmenu', (e) => {
            e.preventDefault();
            const pos = getPos(e);
            ctxMousePos = { x: pos.x, y: pos.y };
            ctxTarget = getHitTarget(pos);

            ctxMenu.style.display = 'block';
            ctxMenu.style.left = e.clientX + 'px';
            ctxMenu.style.top = e.clientY + 'px';

            document.getElementById('ctx-cut').style.display = ctxTarget ? 'block' : 'none';
            document.getElementById('ctx-copy').style.display = ctxTarget ? 'block' : 'none';
            document.getElementById('ctx-delete').style.display = ctxTarget ? 'block' : 'none';
            document.getElementById('ctx-sep-1').style.display = (ctxTarget && clipboardData) ? 'block' : 'none';
            document.getElementById('ctx-paste').style.display = clipboardData ? 'block' : 'none';
            
            if(!ctxTarget && !clipboardData) ctxMenu.style.display = 'none';
        });

        window.addEventListener('click', (e) => {
            if(!e.target.closest('#ctx-menu')) ctxMenu.style.display = 'none';
        });

        function serializeNode(node) {
            let clone = JSON.parse(JSON.stringify({
                type: node.type, subtype: node.subtype, emoji: node.emoji, color: node.color,
                radiusClosed: node.radiusClosed, radiusOpen: node.radiusOpen, radius: node.radius,
                params: node.params, vals: node.vals
            }));
            if(node.type === 'BASE') {
                clone.children = node.children.map(c => serializeNode(c));
            }
            return clone;
        }

        function deserializeNode(data, x, y) {
            let n = {
                id: Math.random().toString(36).substr(2, 9),
                ...data, x: x, y: y, parent: null, children: []
            };
            nodes.push(n);
            if(data.type === 'BASE' && data.children) {
                data.children.forEach(cData => {
                    let child = deserializeNode(cData, x, y);
                    child.parent = n;
                    n.children.push(child);
                });
            }
            return n;
        }

        function removeNode(node) {
            if(node.type === 'BASE') {
                node.children.forEach(c => { nodes = nodes.filter(n => n !== c); });
                if(activeBase === node) activeBase = null;
            } else if(node.type === 'PARAM' && node.parent) {
                node.parent.children = node.parent.children.filter(c => c !== node);
            }
            nodes = nodes.filter(n => n !== node);
            if(selectedParam === node) closeParamPanel();
        }

        document.getElementById('ctx-delete').onclick = () => { if(!ctxTarget) return; removeNode(ctxTarget); ctxMenu.style.display = 'none'; updateShader(); };
        document.getElementById('ctx-copy').onclick = () => { if(ctxTarget) clipboardData = serializeNode(ctxTarget); ctxMenu.style.display = 'none'; };
        document.getElementById('ctx-cut').onclick = () => { if(ctxTarget) { clipboardData = serializeNode(ctxTarget); removeNode(ctxTarget); updateShader(); } ctxMenu.style.display = 'none'; };
        document.getElementById('ctx-paste').onclick = () => {
            if(clipboardData) {
                let pasteX = ctxMousePos.x + (Math.random()*20 - 10);
                let pasteY = ctxMousePos.y + (Math.random()*20 - 10);
                let newNode = deserializeNode(clipboardData, pasteX, pasteY);
                if(newNode.type === 'BASE') activeBase = newNode; 
                updateShader();
            }
            ctxMenu.style.display = 'none';
        };

        // ==========================================
        // 4. GLSL生成エンジン (時間・パーティクル対応)
        // ==========================================
        function updateShader() {
            let glsl = `
#define PI 3.14159265359
#define TWO_PI 6.28318530718

uniform float u_time;
uniform float u_aspect;
varying vec2 vUv;

vec3 hsv2rgb(vec3 c) {
    vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
    vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
    return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}

float sdPolygon(vec2 p, int n, float size) {
    float a = atan(p.x, p.y) + PI;
    float r = TWO_PI / float(n);
    return cos(floor(0.5 + a / r) * r - a) * length(p) - size;
}

// ランダムハッシュ関数 (パーティクル散布用)
vec2 hash2(float n) {
    float x = fract(sin(n) * 43758.5453123);
    float y = fract(sin(n + 1.0) * 43758.5453123);
    return vec2(x, y);
}

void main() {
    vec3 finalColor = vec3(0.0);
    vec2 st = vUv;
`;
            let bases = nodes.filter(n => n.type === 'BASE');
            
            bases.forEach((base, idx) => {
                let uvX = base.x / width;
                let uvY = 1.0 - (base.y / height);
                
                glsl += `\n    // --- Base Object ${idx} ---\n    {\n`;
                glsl += `        vec2 p = st - vec2(${uvX.toFixed(3)}, ${uvY.toFixed(3)});\n`;
                glsl += `        p.x *= u_aspect;\n`;
                
                // --- 時間制御 (TIME) ---
                let timeNode = base.children.find(c => c.subtype === 'TIME');
                glsl += `        float t = u_time;\n`;
                glsl += `        float alpha = 1.0;\n`;
                if(timeNode) {
                    let v = timeNode.vals;
                    glsl += `        t = max(0.0, u_time - ${v.delay.toFixed(2)});\n`;
                    glsl += `        t = mod(t, ${v.life.toFixed(2)} + 0.01);\n`; // 寿命でループ
                    if(v.fade > 0.5) {
                        glsl += `        alpha = clamp(1.0 - (t / ${v.life.toFixed(2)}), 0.0, 1.0);\n`; // フェードアウト
                    }
                }

                // --- フィルター (FILTER: モザイク/ドット化) ---
                let filterNode = base.children.find(c => c.subtype === 'FILTER');
                if(filterNode && filterNode.vals.pixel > 0) {
                    // pixelパラメータ(1~50)からグリッド分割数を計算
                    let pScale = 200.0 / filterNode.vals.pixel;
                    glsl += `        p = floor(p * ${pScale.toFixed(2)}) / ${pScale.toFixed(2)};\n`;
                }

                // --- 動き (MOTION) ---
                glsl += `        vec2 offset = vec2(0.0);\n`;
                let motions = base.children.filter(c => c.subtype.startsWith('MOTION_'));
                motions.forEach(m => {
                    let v = m.vals;
                    if(m.subtype === 'MOTION_LINEAR') {
                        glsl += `        offset += vec2(${v.vx.toFixed(2)}, ${-v.vy.toFixed(2)}) * t;\n`;
                    }
                    else if(m.subtype === 'MOTION_SINE') {
                        glsl += `        offset += vec2(sin(t * ${v.freq.toFixed(2)}) * ${v.ampX.toFixed(2)}, cos(t * ${v.freq.toFixed(2)}) * ${v.ampY.toFixed(2)});\n`;
                    }
                    else if(m.subtype === 'MOTION_BOUNCE') {
                        glsl += `        offset.y += abs(sin(t * ${v.speed.toFixed(2)})) * ${v.height.toFixed(2)};\n`;
                    }
                    else if(m.subtype === 'MOTION_PARABOLA') {
                        glsl += `        offset += vec2(${v.vx.toFixed(2)} * t, ${v.vy.toFixed(2)} * t - 0.5 * ${v.g.toFixed(2)} * t * t);\n`;
                    }
                    else if(m.subtype === 'MOTION_SPIRAL') {
                        glsl += `        offset += vec2(cos(t * ${v.spin.toFixed(2)}), sin(t * ${v.spin.toFixed(2)})) * (${v.radius.toFixed(2)} * t);\n`;
                    }
                });
                glsl += `        p -= offset;\n`;
                
                // --- 形 (SHAPE) と パーティクル (PARTICLE) ---
                let shape = base.children.find(c => c.subtype === 'SHAPE');
                let n_gon = shape ? shape.vals.n : 0;
                let size = shape ? shape.vals.size : 0.05;
                let particleNode = base.children.find(c => c.subtype === 'PARTICLE');
                
                glsl += `        float d = 999.0;\n`;
                if(particleNode) {
                    let pv = particleNode.vals;
                    glsl += `        for(int i=0; i<30; i++) {\n`;
                    glsl += `            if(float(i) >= ${pv.count.toFixed(1)}) break;\n`; // WebGLのループ制約のため固定回数でbreak
                    glsl += `            vec2 h = hash2(float(i)) * 2.0 - 1.0;\n`; // -1 ~ 1のランダムベクトル
                    glsl += `            vec2 poff = h * (${pv.spread.toFixed(2)} + t * ${pv.speed.toFixed(2)});\n`; // 拡散ベクトル
                    glsl += `            vec2 pp = p - poff;\n`;
                    if(n_gon < 3) glsl += `            d = min(d, length(pp) - ${size.toFixed(3)});\n`;
                    else glsl += `            d = min(d, sdPolygon(pp, ${n_gon}, ${size.toFixed(3)}));\n`;
                    glsl += `        }\n`;
                } else {
                    if(n_gon < 3) glsl += `        d = length(p) - ${size.toFixed(3)};\n`;
                    else glsl += `        d = sdPolygon(p, ${n_gon}, ${size.toFixed(3)});\n`;
                }

                // --- 色 (COLOR) と 発光 (GLOW) ---
                let color = base.children.find(c => c.subtype === 'COLOR');
                if(color) glsl += `        vec3 col = hsv2rgb(vec3(${color.vals.h.toFixed(2)}, ${color.vals.s.toFixed(2)}, ${color.vals.v.toFixed(2)}));\n`;
                else glsl += `        vec3 col = vec3(1.0);\n`;

                glsl += `        float mask = smoothstep(0.015, 0.0, d);\n`;

                // フィルターのGlow(発光)を加算
                if(filterNode && filterNode.vals.glow > 0) {
                    glsl += `        mask += ${filterNode.vals.glow.toFixed(3)} * (0.01 / (d*d + 0.001));\n`;
                }

                // フェードアウト(alpha)を乗算して最終色に加算
                glsl += `        finalColor += col * mask * alpha;\n    }\n`;
            });

            glsl += `\n    gl_FragColor = vec4(finalColor, 1.0);\n}`;
            
            document.getElementById('log-panel').innerText = glsl.trim();

            if(mesh) {
                const newMat = new THREE.ShaderMaterial({
                    vertexShader: `varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`,
                    fragmentShader: glsl,
                    uniforms: window.uniforms,
                    blending: THREE.AdditiveBlending, transparent: true
                });
                if(mesh.material) mesh.material.dispose();
                mesh.material = newMat;
            }
        }

        // ==========================================
        // 5. キャンバス描画ループ
        // ==========================================
        function animateCanvas() {
            ctx.clearRect(0, 0, width, height);
            ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
            
            nodes.filter(n => n.type === 'BASE').forEach(b => {
                let isOpen = (b === activeBase);
                let r = isOpen ? b.radiusOpen : b.radiusClosed;
                
                ctx.beginPath(); ctx.arc(b.x, b.y, r, 0, Math.PI * 2);
                ctx.fillStyle = isOpen ? '#1a1a25' : b.color; ctx.fill();
                ctx.strokeStyle = isOpen ? '#00ffcc' : '#555'; ctx.lineWidth = isOpen ? 2 : 1;
                if(isOpen) ctx.setLineDash([5, 5]); else ctx.setLineDash([]);
                ctx.stroke(); ctx.setLineDash([]);
                
                if(!isOpen) { ctx.font = '14px sans-serif'; ctx.fillText(b.emoji, b.x, b.y + 1); }
                else {
                    ctx.beginPath(); ctx.arc(b.x, b.y, 4, 0, Math.PI*2); ctx.fillStyle='#00ffcc'; ctx.fill();
                    b.children.forEach((child, i) => {
                        let angle = (i / b.children.length) * Math.PI * 2;
                        let cx = b.x + Math.cos(angle) * (r * 0.65);
                        let cy = b.y + Math.sin(angle) * (r * 0.65);
                        
                        ctx.beginPath(); ctx.arc(cx, cy, child.radius, 0, Math.PI*2);
                        ctx.fillStyle = child.color; ctx.fill();
                        ctx.strokeStyle = (child === selectedParam) ? '#fff' : '#222'; ctx.lineWidth = 2; ctx.stroke();
                        ctx.font = '14px sans-serif'; ctx.fillText(child.emoji, cx, cy + 1);
                    });
                }
            });

            nodes.filter(n => n.type === 'PARAM' && !n.parent).forEach(p => {
                ctx.beginPath(); ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
                ctx.fillStyle = p.color; ctx.fill();
                ctx.strokeStyle = (p === selectedParam) ? '#fff' : '#222'; ctx.lineWidth = 2; ctx.stroke();
                ctx.font = '14px sans-serif'; ctx.fillText(p.emoji, p.x, p.y + 1);
            });

            requestAnimationFrame(animateCanvas);
        }
        animateCanvas();

        // ==========================================
        // 6. Three.js セットアップ
        // ==========================================
        const previewContainer = document.getElementById('preview');
        const scene = new THREE.Scene();
        const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.1, 10); camera.position.z = 1;
        const renderer = new THREE.WebGLRenderer({ antialias: true });
        
        window.uniforms = { u_time: { value: 0.0 }, u_aspect: { value: 1.0 } };
        
        renderer.setSize(previewContainer.clientWidth, previewContainer.clientHeight);
        previewContainer.appendChild(renderer.domElement);
        const geometry = new THREE.PlaneGeometry(2, 2);
        let mesh = new THREE.Mesh(geometry, new THREE.MeshBasicMaterial({color: 0x000000}));
        scene.add(mesh);
        
        resize();

        const clock = new THREE.Clock();
        function animateThree() { requestAnimationFrame(animateThree); window.uniforms.u_time.value = clock.getElapsedTime(); renderer.render(scene, camera); }
        animateThree();
    </script>
</body>
</html>

いいなと思ったら応援しよう!

コメント

コメントするには、 ログイン または 会員登録 をお願いします。
GLSLシェーダーで遊ぶ「Glsl Viewer」|古井和雄
word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word

mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1