<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>シン・ゴジラ 3D シミュレーター | Shin Godzilla 3D Simulator</title>
<!-- Tailwind CSS (UI用) -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- FontAwesome (アイコン用) -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
body {
margin: 0;
overflow: hidden;
background-color: #050505;
user-select: none;
-webkit-user-select: none;
touch-action: manipulation;
}
/* スクロールバー非表示 */
::-webkit-scrollbar {
display: none;
}
/* 巨災対風のUI装飾 */
.neon-border {
box-shadow: 0 0 15px rgba(239, 68, 68, 0.5);
border: 1px solid rgba(239, 68, 68, 0.8);
}
.neon-border-purple {
box-shadow: 0 0 15px rgba(168, 85, 247, 0.5);
border: 1px solid rgba(168, 85, 247, 0.8);
}
/* モバイル用の快適なジェスチャー無効化 */
#canvas-container canvas {
touch-action: none;
}
</style>
</head>
<body class="text-gray-100 font-mono">
<!-- メイン3Dキャンバス -->
<div id="canvas-container" class="absolute inset-0 z-0"></div>
<!-- UIオーバーレイ -->
<div class="absolute inset-0 flex flex-col justify-between p-3 sm:p-4 pointer-events-none z-10">
<!-- ヘッダー: 巨災対風ステータスモニター -->
<header class="w-full flex justify-between items-start pointer-events-auto gap-2">
<div id="status-panel" class="bg-black/80 p-3 sm:p-4 rounded neon-border transition-all duration-300 transform w-full max-w-[280px] sm:max-w-sm md:max-w-md">
<div class="flex items-center space-x-2 mb-1">
<span class="inline-block w-2.5 h-2.5 rounded-full bg-red-600 animate-pulse" id="status-dot"></span>
<h1 class="text-red-500 font-black text-sm sm:text-base md:text-xl tracking-wider truncate">巨大不明生物災害対策本部</h1>
</div>
<p class="text-[9px] sm:text-xs text-gray-400 border-b border-red-900/50 pb-1.5 mb-1.5">GIANT UNIDENTIFIED LIFE FORM MONITOR</p>
<div class="grid grid-cols-2 gap-x-2 sm:gap-x-4 gap-y-0.5 sm:gap-y-1 text-[10px] sm:text-xs">
<div class="truncate">識別名: <span class="text-red-400 font-bold">ゴジラ</span></div>
<div class="truncate">型式: <span class="text-red-400 font-bold" id="code-type-text">第4形態</span></div>
<div class="truncate">全長: <span class="text-gray-300" id="height-text">118.5 m</span></div>
<div class="truncate">状態: <span id="state-text" class="text-red-500 font-bold animate-pulse">警戒監視中</span></div>
<div class="col-span-2 mt-1">エネルギー蓄積率:
<div class="w-full bg-gray-800 h-1.5 mt-1 rounded-full overflow-hidden">
<div id="energy-bar" class="bg-red-600 h-full transition-all duration-300" style="width: 45%"></div>
</div>
</div>
</div>
</div>
<!-- 右上操作ガイド & UI非表示スイッチ -->
<div class="flex flex-col items-end space-y-2 pointer-events-auto shrink-0">
<button id="btn-toggle-ui" class="bg-red-950/80 hover:bg-red-900 border border-red-500 text-red-200 px-2.5 py-1.5 sm:px-3 sm:py-2 rounded text-[10px] sm:text-xs transition active:scale-95 flex items-center space-x-1 shadow-lg">
<i class="fa-solid fa-eye-slash" id="toggle-ui-icon"></i>
<span id="toggle-ui-text">UI非表示</span>
</button>
<!-- PC向け操作ガイド -->
<div class="hidden md:block bg-black/60 p-3 rounded border border-gray-800 text-right text-[10px] sm:text-xs text-gray-400 max-w-[200px]">
<p class="mb-0.5"><i class="fa-solid fa-arrows-to-eye mr-1"></i> ドラッグ: 回転</p>
<p class="mb-0.5"><i class="fa-solid fa-arrows-up-down mr-1"></i> スクロール: ズーム</p>
<p><i class="fa-solid fa-hand-pointer mr-1"></i> 右ドラッグ: 平行移動</p>
</div>
<!-- モバイル向け操作ガイド -->
<div class="block md:hidden bg-black/60 p-2 rounded border border-gray-800 text-right text-[9px] text-gray-400">
<p class="mb-0.5"><i class="fa-solid fa-finger mr-1"></i> 1本指スワイプ: 回転</p>
<p><i class="fa-solid fa-arrows-pinch mr-1"></i> 2本指ピンチ: ズーム/移動</p>
</div>
</div>
</header>
<!-- 下部: コントロールパネル -->
<footer class="w-full flex flex-col justify-end items-center gap-1 sm:gap-2 pointer-events-auto transition-all duration-300 transform">
<!-- モバイル専用タブ切り替えバー (画面が小さい時だけ出現) -->
<div id="mobile-tabs" class="flex md:hidden w-full max-w-sm bg-black/95 border border-gray-800 rounded-t-lg overflow-hidden shadow-lg transition-all duration-300">
<button id="tab-actions" class="flex-1 py-2 text-center text-red-500 border-b-2 border-red-500 font-black text-[11px] sm:text-xs transition">
<i class="fa-solid fa-gamepad mr-1"></i>指令ユニット
</button>
<button id="tab-env" class="flex-1 py-2 text-center text-gray-500 border-b-2 border-transparent text-[11px] sm:text-xs transition">
<i class="fa-solid fa-sliders mr-1"></i>環境設定
</button>
</div>
<!-- パネルコンテナ -->
<div class="w-full flex flex-col md:flex-row justify-between items-end gap-3 md:gap-4">
<!-- 左下: カメラ・環境コントロール -->
<div id="env-panel" class="bg-black/80 p-3 sm:p-4 rounded border border-gray-800 w-full md:w-80 text-[11px] sm:text-xs space-y-2 sm:space-y-3 hidden md:block transition-all duration-300">
<h2 class="text-gray-400 font-bold uppercase border-b border-gray-800 pb-1 mb-1 sm:mb-2">環境設定 / シミュレーション</h2>
<!-- 尻尾のうねり速度 -->
<div>
<div class="flex justify-between mb-1">
<span>尾部細胞活性度 (運動速度)</span>
<span id="tail-speed-val" class="text-red-400">1.0x</span>
</div>
<input type="range" id="tail-speed" min="0" max="3" step="0.1" value="1" class="w-full accent-red-600 bg-gray-800 h-1 sm:h-1.5 rounded-lg appearance-none cursor-pointer">
</div>
<!-- 霧の濃さ -->
<div>
<div class="flex justify-between mb-1">
<span>放射性物質濃度 (フォグ)</span>
<span id="fog-val" class="text-red-400">標準</span>
</div>
<input type="range" id="fog-density" min="0" max="10" step="1" value="3" class="w-full accent-red-600 bg-gray-800 h-1 sm:h-1.5 rounded-lg appearance-none cursor-pointer">
</div>
<!-- 明るさ(ライティング) -->
<div>
<div class="flex justify-between mb-1">
<span>環境の明るさ (照明)</span>
<span id="light-val" class="text-red-400">明るい</span>
</div>
<input type="range" id="scene-brightness" min="1" max="15" step="1" value="8" class="w-full accent-red-600 bg-gray-800 h-1 sm:h-1.5 rounded-lg appearance-none cursor-pointer">
</div>
<!-- 昼夜切り替え -->
<div class="grid grid-cols-2 gap-2 pt-1">
<button id="btn-env-night" class="bg-gray-900 hover:bg-gray-800 text-gray-400 py-1.5 px-2 rounded border border-gray-700 text-[10px] sm:text-xs font-bold transition flex items-center justify-center space-x-1 active:scale-95">
<i class="fa-solid fa-moon"></i>
<span>夜間作戦</span>
</button>
<button id="btn-env-day" class="bg-amber-950/40 hover:bg-amber-900/60 text-amber-400 py-1.5 px-2 rounded border border-amber-800/80 text-[10px] sm:text-xs font-bold transition flex items-center justify-center space-x-1 active:scale-95">
<i class="fa-solid fa-sun"></i>
<span>昼間作戦</span>
</button>
</div>
<!-- カメラリセット -->
<button id="btn-reset-cam" class="w-full bg-gray-900 hover:bg-gray-800 text-gray-300 py-1.5 px-3 rounded border border-gray-700 transition active:scale-95 flex items-center justify-center space-x-1 text-[10px] sm:text-xs">
<i class="fa-solid fa-video"></i>
<span>視点をリセット</span>
</button>
</div>
<!-- 右下: 兵器・ゴジラアクションコントロール -->
<div id="action-panel" class="bg-black/80 p-3 sm:p-4 rounded neon-border transition-all duration-500 w-full md:w-96 space-y-3 sm:space-y-4 block md:block">
<h2 class="text-red-500 font-bold text-xs sm:text-sm tracking-wider uppercase border-b border-red-900/50 pb-1 flex items-center justify-between">
<span>活動指令シミュレーター</span>
<span class="text-[9px] sm:text-xs text-gray-500">CONTROL UNIT</span>
</h2>
<!-- 形態選択トグル (第2・第3・第4) -->
<div class="grid grid-cols-3 gap-1">
<button id="btn-phase-2" class="bg-gray-900 hover:bg-gray-800 text-gray-400 border border-gray-700 font-bold py-1.5 px-1 rounded text-[10px] sm:text-xs transition flex flex-col items-center justify-center active:scale-95">
<span class="w-2 h-2 rounded-full bg-amber-600 mb-1"></span>
<span>第2形態</span>
</button>
<button id="btn-phase-3" class="bg-gray-900 hover:bg-gray-800 text-gray-400 border border-gray-700 font-bold py-1.5 px-1 rounded text-[10px] sm:text-xs transition flex flex-col items-center justify-center active:scale-95">
<span class="w-2 h-2 rounded-full bg-red-800 mb-1"></span>
<span>第3形態</span>
</button>
<button id="btn-phase-4" class="bg-red-950/80 hover:bg-red-900 text-red-200 border border-red-500 font-bold py-1.5 px-1 rounded text-[10px] sm:text-xs transition flex flex-col items-center justify-center active:scale-95">
<span class="w-2 h-2 rounded-full bg-red-500 mb-1 animate-pulse"></span>
<span>第4形態</span>
</button>
</div>
<!-- 第4形態専用: 通常/熱線 変更トグル -->
<div id="phase4-glow-toggle" class="grid grid-cols-2 gap-2 transition-all duration-300">
<button id="btn-state-red" class="bg-red-950/80 hover:bg-red-900 text-red-200 border border-red-500 font-bold py-1.5 px-2 rounded text-[10px] sm:text-xs transition flex items-center justify-center space-x-1.5 active:scale-95">
<span class="w-2 h-2 rounded-full bg-red-500 animate-pulse"></span>
<span>通常 (赤)</span>
</button>
<button id="btn-state-purple" class="bg-gray-900 hover:bg-gray-800 text-gray-400 border border-gray-700 font-bold py-1.5 px-2 rounded text-[10px] sm:text-xs transition flex items-center justify-center space-x-1.5 active:scale-95">
<span class="w-2 h-2 rounded-full bg-purple-500"></span>
<span>熱線 (紫)</span>
</button>
</div>
<!-- 形態適応アクションボタン -->
<div class="grid grid-cols-2 gap-2">
<button id="btn-roar" class="bg-amber-950/50 hover:bg-amber-900/80 text-amber-200 border border-amber-600 py-2 sm:py-2.5 px-2 rounded text-[11px] sm:text-xs font-bold transition flex items-center justify-center space-x-1.5 active:scale-95">
<i id="icon-action-1" class="fa-solid fa-volume-high"></i>
<span id="lbl-action-1">咆哮モーション</span>
</button>
<button id="btn-beam" class="bg-purple-950/50 hover:bg-purple-900/80 text-purple-200 border border-purple-600 py-2 sm:py-2.5 px-2 rounded text-[11px] sm:text-xs font-bold transition flex items-center justify-center space-x-1.5 relative overflow-hidden group active:scale-95">
<span class="absolute inset-0 bg-purple-600/20 translate-y-full group-hover:translate-y-0 transition-transform"></span>
<i id="icon-action-2" class="fa-solid fa-bolt-lightning"></i>
<span id="lbl-action-2">放射線流 発射</span>
</button>
</div>
<!-- ミニ解説・警報 -->
<div class="bg-red-950/20 border border-red-900/30 p-2 sm:p-2.5 rounded text-[10px] text-red-400/90 leading-relaxed" id="warning-box">
<i class="fa-solid fa-circle-exclamation mr-1 animate-ping text-red-500"></i>
<strong>警告:</strong> 熱線放出の際、体内温度が急上昇し、周囲の全システムが融解・破壊される恐れがあります。
</div>
</div>
</div>
</footer>
</div>
<!-- 音響効果用Web Audio API、3Dライブラリ読み込み -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
<script>
// --- 1. グローバル変数と初期設定 ---
let scene, camera, renderer, controls;
let godzilla, tailSegments = [], backPlates = [], tailPlates = [];
let bodyMesh, headMesh, jawMesh, leftArm, rightArm;
let glowLights = [], ambientLight, dirLight;
let particles, particleGeometry;
let beamsGroup; // 熱線ビームグループ
let beamParticles; // ビームのスパーク
// エラ排熱と第2形態用の追加オブジェクト・変数
let currentPhase = 4; // デフォルト:第4形態
let gills = []; // 首元のエラ
let gillLights = [];
let isGillEjecting = false; // エラ排熱アクションフラグ
let gillEjectTimer = 0;
let gillParticles; // エラから出る血液・熱水風パーティクル
let isSquirming = false; // 急速蛇行フラグ
let squirmTimer = 0;
let isStandingUp = false; // 直立試行フラグ
let standUpTimer = 0;
// アニメーション用状態
let animationSpeed = 1.0;
let isPurpleState = false;
let isRoaring = false;
let isBeaming = false;
let roarTimer = 0;
let beamTimer = 0;
let isDayMode = true; // デフォルトを昼間モードにして明るく
let brightnessMultiplier = 0.8; // 明るさスライダー倍率
let uiHidden = false; // UI隠しフラグ
// 音声効果
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
// カラーパレット
const COLORS = {
skin: 0x111111, // 不気味な濃い黒
redGlow: 0xff1100, // 第4形態の赤発光
purpleGlow: 0xaa00ff, // 熱線の紫発光
ground: 0x0a0a0a, // 焦土
fogRed: 0x0f0505, // 赤フォグ
fogPurple: 0x09050f, // 紫フォグ
dayGround: 0x2c2d30, // 灰色のガレキ地面
dayFogRed: 0x6e727a, // 煙が立ち込める明るい昼フォグ
dayFogPurple: 0x5a556b // 昼間の熱線時フォグ
};
// --- 2. 起動処理 (window.onload) ---
window.onload = function() {
init3D();
buildGodzilla(4); // デフォルトは第4形態
buildEnvironment();
setupInteractions();
animate();
};
// --- 3. 3Dシーンの初期化 ---
function init3D() {
const container = document.getElementById('canvas-container');
// シーン
scene = new THREE.Scene();
// 初期カラーの決定
const initialEnvColor = isDayMode ? COLORS.dayFogRed : COLORS.fogRed;
scene.background = new THREE.Color(initialEnvColor);
scene.fog = new THREE.FogExp2(initialEnvColor, 0.03);
// カメラ
camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 1000);
resetCamera();
// レンダラー (モバイルのGPU負荷を抑えるために設定を適正化)
renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: "high-performance" });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5)); // 1.5に制限して発熱防止
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
container.appendChild(renderer.domElement);
// コントロール
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.maxPolarAngle = Math.PI / 2 - 0.02; // 地面以下に行かないように
controls.minDistance = 5;
controls.maxDistance = 45;
controls.target.set(0, 5, 0);
// 照明
ambientLight = new THREE.AmbientLight(0x0f0f15, 1.2);
scene.add(ambientLight);
// メイン指向性ライト
dirLight = new THREE.DirectionalLight(0x445566, 1.5);
dirLight.position.set(15, 30, 15);
dirLight.castShadow = true;
dirLight.shadow.mapSize.width = 1024;
dirLight.shadow.mapSize.height = 1024;
dirLight.shadow.bias = -0.001;
scene.add(dirLight);
// 昼間を明るく照らす補助半球ライト
const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444, 0.3);
hemiLight.position.set(0, 50, 0);
hemiLight.name = "hemiLight";
scene.add(hemiLight);
// 体内発光再現用ポイントライト
const light1 = new THREE.PointLight(COLORS.redGlow, 5, 15);
light1.position.set(0, 5, -1);
scene.add(light1);
glowLights.push(light1);
const light2 = new THREE.PointLight(COLORS.redGlow, 3, 10);
light2.position.set(0, 3, -4);
scene.add(light2);
glowLights.push(light2);
// エラ排熱用の赤いパーティクルシステムを構築
initGillParticles();
// 初期明るさの適用
updateLights();
// リサイズ対応
window.addEventListener('resize', onWindowResize, false);
}
function initGillParticles() {
const pCount = 80;
const geom = new THREE.BufferGeometry();
const positions = new Float32Array(pCount * 3);
const velocities = [];
for (let i = 0; i < pCount; i++) {
positions[i * 3] = 0;
positions[i * 3 + 1] = -999; // 最初は画面外に隠す
positions[i * 3 + 2] = 0;
velocities.push(new THREE.Vector3(0, 0, 0));
}
geom.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const mat = new THREE.PointsMaterial({
color: 0xcc1100, // 血の赤
size: 0.22,
transparent: true,
opacity: 0.85,
blending: THREE.AdditiveBlending
});
gillParticles = new THREE.Points(geom, mat);
gillParticles.userData = { velocities: velocities };
scene.add(gillParticles);
}
// カメラ視点のリセット (モバイル/タブレットのアスペクト比に合わせて見切れを防止)
function resetCamera() {
const isMobile = window.innerWidth < 768;
if (controls) {
if (currentPhase === 2) {
controls.target.set(0, 1.2, 1.0);
camera.position.set(0, isMobile ? 5 : 4, isMobile ? 15 : 12); // モバイルは一歩引く
} else if (currentPhase === 3) {
controls.target.set(0, 1.8, 1.0);
camera.position.set(0, isMobile ? 6 : 5, isMobile ? 18 : 14);
} else {
controls.target.set(0, 5, 0);
camera.position.set(0, isMobile ? 10 : 8, isMobile ? 28 : 22);
}
}
}
// ライティングの動的更新
function updateLights() {
const baseAmbient = isDayMode ? 1.8 : 0.8;
const baseDir = isDayMode ? 2.5 : 1.2;
const hemiLight = scene.getObjectByName("hemiLight");
// スライダー倍率を乗算
ambientLight.intensity = baseAmbient * brightnessMultiplier;
dirLight.intensity = baseDir * brightnessMultiplier;
if (hemiLight) {
hemiLight.intensity = (isDayMode ? 1.0 : 0.2) * brightnessMultiplier;
hemiLight.color.setHex(isDayMode ? 0xddeeff : 0x445566);
}
// 皮膚マテリアルの調整
if (godzilla) {
godzilla.traverse((child) => {
if (child.isMesh && child.material) {
if (child.material.color && child.material.color.getHex() === COLORS.skin) {
child.material.roughness = isDayMode ? 0.75 : 0.9;
child.material.metalness = isDayMode ? 0.25 : 0.15;
}
}
});
}
// 背景色とフォグ色の更新
let envColor;
if (isDayMode) {
envColor = isPurpleState ? COLORS.dayFogPurple : COLORS.dayFogRed;
} else {
envColor = isPurpleState ? COLORS.fogPurple : COLORS.fogRed;
}
scene.background.set(envColor);
scene.fog.color.set(envColor);
}
// --- 4. シン・ゴジラプロシージャルモデルの構築 (形態分岐版) ---
function buildGodzilla(phase = 4) {
currentPhase = phase;
// 既存オブジェクトのクリーンアップ
if (godzilla) {
scene.remove(godzilla);
}
godzilla = new THREE.Group();
scene.add(godzilla);
tailSegments = [];
backPlates = [];
tailPlates = [];
gills = [];
gillLights = [];
// マテリアル定義
let skinColor, emissiveColor, emissiveIntensity;
let roughness = 0.9;
let metalness = 0.15;
if (phase === 2) {
skinColor = 0xb59e7f; // 画像のフィギュアに近いオリーブベージュ
emissiveColor = 0xff3300; // オレンジ寄りの赤発光
emissiveIntensity = 1.0;
} else if (phase === 3) {
skinColor = 0x221110; // 品川くんの赤黒い焦げた肌色
emissiveColor = 0xff0000; // 鮮血のような強烈な赤発光
emissiveIntensity = 2.0;
} else {
// 第4形態
skinColor = COLORS.skin;
emissiveColor = isPurpleState ? COLORS.purpleGlow : COLORS.redGlow;
emissiveIntensity = isPurpleState ? 3.5 : 2.5;
}
const skinMat = new THREE.MeshStandardMaterial({
color: skinColor,
roughness: roughness,
metalness: metalness,
flatShading: true
});
const glowMat = new THREE.MeshStandardMaterial({
color: 0x110000,
emissive: emissiveColor,
emissiveIntensity: emissiveIntensity,
roughness: 0.4,
flatShading: true
});
// 胴体の構築
const bodyGroup = new THREE.Group();
godzilla.add(bodyGroup);
if (phase === 2) {
// --- 第2形態 (蒲田くん / アップロード画像フィギュア準拠) の構築 ---
bodyGroup.position.set(0, 0.8, 0.8);
bodyGroup.rotation.x = Math.PI / 12;
// 扁平で前後に長い胴体
const hipsGeo = new THREE.CylinderGeometry(0.8, 1.4, 4.0, 10);
hipsGeo.scale(1.2, 0.8, 1.0);
const hips = new THREE.Mesh(hipsGeo, skinMat);
hips.position.set(0, 0.2, -0.6);
hips.rotation.x = Math.PI / 2.6;
hips.castShadow = true;
hips.receiveShadow = true;
bodyGroup.add(hips);
// 胸部
const chestGeo = new THREE.CylinderGeometry(0.6, 1.0, 3.0, 10);
chestGeo.scale(1.1, 0.7, 1.0);
bodyMesh = new THREE.Mesh(chestGeo, skinMat);
bodyMesh.position.set(0, 0.8, 0.8);
bodyMesh.rotation.x = Math.PI / 2.8;
bodyMesh.castShadow = true;
bodyGroup.add(bodyMesh);
// 4連スリットエラ
const gillSlitMat = new THREE.MeshStandardMaterial({
color: 0x880000,
emissive: 0xff0000,
emissiveIntensity: 1.5,
flatShading: true
});
for (let j = 0; j < 4; j++) {
const gillSiltGeo = new THREE.BoxGeometry(0.06, 0.5, 0.12);
const zPos = 1.1 + j * 0.22;
const yPos = 0.6 - j * 0.04;
const lG = new THREE.Mesh(gillSiltGeo, gillSlitMat);
lG.position.set(0.68 + j * 0.01, yPos, zPos);
lG.rotation.set(0.1, 0.35, -0.05);
bodyGroup.add(lG);
gills.push(lG);
const rG = lG.clone();
rG.position.x = -(0.68 + j * 0.01);
rG.rotation.y = -0.35;
bodyGroup.add(rG);
gills.push(rG);
}
// 前肢
const armGeo = new THREE.BoxGeometry(0.12, 0.4, 0.6);
leftArm = new THREE.Mesh(armGeo, skinMat);
leftArm.position.set(0.9, 0.3, 0.1);
leftArm.rotation.set(0.3, 0.2, -0.5);
bodyGroup.add(leftArm);
rightArm = leftArm.clone();
rightArm.position.x = -0.9;
rightArm.rotation.y = -0.2;
rightArm.rotation.z = 0.5;
bodyGroup.add(rightArm);
// 頭部と目
const headGroup = new THREE.Group();
headGroup.position.set(0, 0.9, 2.3);
bodyGroup.add(headGroup);
const skullGeo = new THREE.BoxGeometry(0.65, 0.45, 1.4);
headMesh = new THREE.Mesh(skullGeo, skinMat);
headMesh.position.set(0, 0.1, 0.1);
headMesh.castShadow = true;
headGroup.add(headMesh);
jawMesh = new THREE.Group();
jawMesh.position.set(0, -0.15, 0.3);
headGroup.add(jawMesh);
const jawBaseGeo = new THREE.BoxGeometry(0.6, 0.15, 1.2);
const jawBase = new THREE.Mesh(jawBaseGeo, skinMat);
jawMesh.add(jawBase);
// 巨大ギョロ目
const eyeWhiteGeo = new THREE.SphereGeometry(0.16, 12, 12);
const eyeBlackGeo = new THREE.SphereGeometry(0.06, 8, 8);
const eyeWhiteMat = new THREE.MeshBasicMaterial({ color: 0xfafafa });
const eyeBlackMat = new THREE.MeshBasicMaterial({ color: 0x000000 });
const lEyeWhite = new THREE.Mesh(eyeWhiteGeo, eyeWhiteMat);
lEyeWhite.position.set(0.32, 0.1, 0.4);
const lEyeBlack = new THREE.Mesh(eyeBlackGeo, eyeBlackMat);
lEyeBlack.position.set(0.04, 0, 0.13);
lEyeWhite.add(lEyeBlack);
headMesh.add(lEyeWhite);
const rEyeWhite = lEyeWhite.clone();
rEyeWhite.position.x = -0.32;
rEyeWhite.children[0].position.x = -0.04;
headMesh.add(rEyeWhite);
// 脚
const legGroup = new THREE.Group();
godzilla.add(legGroup);
const lLegGeo = new THREE.CylinderGeometry(0.35, 0.6, 1.2, 8);
const lLeg = new THREE.Mesh(lLegGeo, skinMat);
lLeg.position.set(1.0, 0.4, -0.8);
lLeg.rotation.set(0.8, 0, -0.4);
legGroup.add(lLeg);
const lFootGeo = new THREE.BoxGeometry(0.6, 0.3, 1.2);
const lFoot = new THREE.Mesh(lFootGeo, skinMat);
lFoot.position.set(0, -0.6, 0.3);
lFoot.rotation.x = -0.5;
lLeg.add(lFoot);
const rLeg = lLeg.clone();
rLeg.position.x = -1.0;
rLeg.rotation.z = 0.4;
legGroup.add(rLeg);
// 背鰭
const platesGroup = new THREE.Group();
bodyGroup.add(platesGroup);
for (let i = 0; i < 11; i++) {
const ratio = i / 10;
const size = 0.7 * (1.0 - ratio * 0.4);
const pGlowMat = new THREE.MeshStandardMaterial({
color: 0x6e4e3b,
emissive: 0xff3300,
emissiveIntensity: 1.0,
flatShading: true
});
const plateGeo = new THREE.ConeGeometry(0.18 * size, 0.9 * size, 4);
const plate = new THREE.Mesh(plateGeo, pGlowMat);
plate.position.set(0, 1.1 - (i * 0.22), -0.1 - (i * 0.28));
plate.rotation.x = -Math.PI / 3.2;
platesGroup.add(plate);
backPlates.push(plate);
}
} else if (phase === 3) {
// --- 第3形態 (品川くん / アップロード画像準拠の赤黒・極太首仕様) の構築 ---
bodyGroup.position.set(0, 1.6, 0.4);
bodyGroup.rotation.x = Math.PI / 10;
// 胴体
const hipsGeo = new THREE.CylinderGeometry(1.2, 2.2, 3.8, 10);
const hips = new THREE.Mesh(hipsGeo, skinMat);
hips.position.set(0, -0.4, -0.4);
hips.rotation.x = Math.PI / 6;
hips.castShadow = true;
hips.receiveShadow = true;
bodyGroup.add(hips);
// 胸部
const chestGeo = new THREE.CylinderGeometry(0.9, 1.5, 3.2, 10);
bodyMesh = new THREE.Mesh(chestGeo, skinMat);
bodyMesh.position.set(0, 0.9, 0.6);
bodyMesh.rotation.x = Math.PI / 4;
bodyMesh.castShadow = true;
bodyGroup.add(bodyMesh);
// 首 (太い蛇腹仕様)
const neckGroup = new THREE.Group();
neckGroup.position.set(0, 1.8, 1.4);
neckGroup.rotation.x = -Math.PI / 10;
bodyGroup.add(neckGroup);
for (let j = 0; j < 5; j++) {
const neckSegGeo = new THREE.CylinderGeometry(0.6 - j*0.04, 0.7 - j*0.04, 0.4, 8);
const neckSeg = new THREE.Mesh(neckSegGeo, skinMat);
neckSeg.position.set(0, j * 0.35, j * 0.15);
neckSeg.rotation.x = Math.PI / 4;
neckGroup.add(neckSeg);
const ringGeo = new THREE.CylinderGeometry(0.61 - j*0.04, 0.71 - j*0.04, 0.08, 8);
const ring = new THREE.Mesh(ringGeo, new THREE.MeshStandardMaterial({
color: 0x660000,
emissive: 0xff1100,
emissiveIntensity: 2.0,
flatShading: true
}));
ring.position.set(0, j * 0.35, j * 0.15);
ring.rotation.x = Math.PI / 4;
neckGroup.add(ring);
}
// エラ
const gillGeo = new THREE.BoxGeometry(0.2, 0.8, 0.4);
const gillMat = new THREE.MeshStandardMaterial({
color: 0x440000,
emissive: 0xff0000,
emissiveIntensity: 1.8,
flatShading: true
});
const leftGill = new THREE.Mesh(gillGeo, gillMat);
leftGill.position.set(0.7, 0.8, 1.2);
leftGill.rotation.set(0.2, 0.4, -0.2);
bodyGroup.add(leftGill);
gills.push(leftGill);
const rightGill = leftGill.clone();
rightGill.position.x = -0.7;
rightGill.rotation.y = -0.4;
rightGill.rotation.z = 0.2;
bodyGroup.add(rightGill);
gills.push(rightGill);
// 前腕
leftArm = new THREE.Group();
leftArm.position.set(0.7, 0.8, 0.9);
const lShoulder = new THREE.Mesh(new THREE.CylinderGeometry(0.12, 0.16, 0.6, 5), skinMat);
lShoulder.rotation.z = -0.3;
lShoulder.rotation.x = 0.5;
leftArm.add(lShoulder);
const lForearm = new THREE.Mesh(new THREE.CylinderGeometry(0.1, 0.12, 0.5, 5), skinMat);
lForearm.position.set(0.05, -0.4, 0.15);
lForearm.rotation.x = 0.8;
leftArm.add(lForearm);
const clawMat = new THREE.MeshStandardMaterial({ color: 0xdddddd, roughness: 0.3 });
for(let c = -1; c <= 1; c++) {
const claw = new THREE.Mesh(new THREE.ConeGeometry(0.04, 0.15, 4), clawMat);
claw.position.set(c * 0.06, -0.3, 0.1);
claw.rotation.x = Math.PI / 3;
lForearm.add(claw);
}
bodyGroup.add(leftArm);
rightArm = leftArm.clone();
rightArm.position.x = -0.7;
rightArm.children[0].rotation.z = 0.3;
rightArm.children[1].position.x = -0.05;
bodyGroup.add(rightArm);
// 頭部、目、歯
const headGroup = new THREE.Group();
headGroup.position.set(0, 1.7, 2.1);
neckGroup.add(headGroup);
const skullGeo = new THREE.BoxGeometry(0.65, 0.5, 1.2);
headMesh = new THREE.Mesh(skullGeo, skinMat);
headMesh.position.set(0, 0, 0);
headMesh.castShadow = true;
headGroup.add(headMesh);
jawMesh = new THREE.Group();
jawMesh.position.set(0, -0.22, 0.15);
headGroup.add(jawMesh);
const jawBase = new THREE.Mesh(new THREE.BoxGeometry(0.6, 0.2, 1.0), skinMat);
jawMesh.add(jawBase);
const toothGeo = new THREE.ConeGeometry(0.03, 0.12, 4);
const toothMat = new THREE.MeshBasicMaterial({ color: 0xeeeeee });
for (let t = 0; t < 6; t++) {
const toothU = new THREE.Mesh(toothGeo, toothMat);
toothU.position.set((t - 2.5) * 0.08, -0.2, 0.4 - Math.abs(t - 2.5)*0.02);
toothU.rotation.x = Math.PI;
headMesh.add(toothU);
const toothL = new THREE.Mesh(toothGeo, toothMat);
toothL.position.set((t - 2.5) * 0.08, 0.1, 0.4 - Math.abs(t - 2.5)*0.02);
jawMesh.add(toothL);
}
// 黄色い目
const eyeWhiteGeo = new THREE.SphereGeometry(0.13, 10, 10);
const eyeBlackGeo = new THREE.SphereGeometry(0.05, 6, 6);
const eyeWhiteMat = new THREE.MeshBasicMaterial({ color: 0xffdd11 });
const eyeBlackMat = new THREE.MeshBasicMaterial({ color: 0x000000 });
const lEyeWhite = new THREE.Mesh(eyeWhiteGeo, eyeWhiteMat);
lEyeWhite.position.set(0.33, 0.1, 0.2);
const lEyeBlack = new THREE.Mesh(eyeBlackGeo, eyeBlackMat);
lEyeBlack.position.set(0.03, 0, 0.09);
lEyeWhite.add(lEyeBlack);
headMesh.add(lEyeWhite);
const rEyeWhite = lEyeWhite.clone();
rEyeWhite.position.x = -0.33;
rEyeWhite.children[0].position.x = -0.03;
headMesh.add(rEyeWhite);
// 脚
const legGroup = new THREE.Group();
godzilla.add(legGroup);
const lLegUpperGeo = new THREE.CylinderGeometry(0.55, 1.0, 2.4, 8);
const lLegUpper = new THREE.Mesh(lLegUpperGeo, skinMat);
lLegUpper.position.set(1.1, 1.4, -0.2);
lLegUpper.rotation.set(0.4, 0, -0.1);
lLegUpper.castShadow = true;
legGroup.add(lLegUpper);
const lLegLowerGeo = new THREE.CylinderGeometry(0.8, 1.0, 1.6, 8);
const lLegLower = new THREE.Mesh(lLegLowerGeo, skinMat);
lLegLower.position.set(0, -1.0, 0.2);
lLegLower.rotation.x = -0.6;
lLegLower.castShadow = true;
lLegUpper.add(lLegLower);
const lFoot = new THREE.Mesh(new THREE.BoxGeometry(1.0, 0.5, 1.6), skinMat);
lFoot.position.set(0, -0.8, 0.2);
lFoot.castShadow = true;
lLegLower.add(lFoot);
for(let c = -1; c <= 1; c++) {
const claw = new THREE.Mesh(new THREE.ConeGeometry(0.14, 0.4, 4), clawMat);
claw.position.set(c * 0.35, -0.15, 0.8);
claw.rotation.x = Math.PI / 2.5;
lFoot.add(claw);
}
const rLegUpper = lLegUpper.clone();
rLegUpper.position.x = -1.1;
rLegUpper.rotation.z = 0.1;
legGroup.add(rLegUpper);
// 灰白色の骨っぽ背鰭
const platesGroup = new THREE.Group();
bodyGroup.add(platesGroup);
for (let i = 0; i < 11; i++) {
const ratio = i / 10;
const size = 0.95 * (1.0 - ratio * 0.4);
const pGlowMat = new THREE.MeshStandardMaterial({
color: 0x888888,
emissive: 0xff2200,
emissiveIntensity: 1.8,
roughness: 0.8,
flatShading: true
});
const plateGeo = new THREE.ConeGeometry(0.24 * size, 1.3 * size, 4);
const plate = new THREE.Mesh(plateGeo, pGlowMat);
plate.position.set(0, 1.4 - (i * 0.3), -0.5 - (i * 0.24));
plate.rotation.x = -Math.PI / 3;
platesGroup.add(plate);
backPlates.push(plate);
}
} else {
// --- 第4形態 (通常完全体) の構築 ---
bodyGroup.position.set(0, 4.5, 0);
// 下半身
const hipsGeo = new THREE.CylinderGeometry(1.6, 2.8, 4, 10);
const hips = new THREE.Mesh(hipsGeo, skinMat);
hips.position.y = -1;
hips.castShadow = true;
hips.receiveShadow = true;
bodyGroup.add(hips);
// 胸部
const chestGeo = new THREE.CylinderGeometry(1.0, 1.8, 3.5, 10);
bodyMesh = new THREE.Mesh(chestGeo, skinMat);
bodyMesh.position.y = 1.8;
bodyMesh.castShadow = true;
bodyGroup.add(bodyMesh);
// 突起
for(let i=0; i<30; i++) {
const detailGeo = new THREE.DodecahedronGeometry(0.3 + Math.random()*0.3);
const detail = new THREE.Mesh(detailGeo, skinMat);
detail.position.set(
(Math.random() - 0.5) * 2.5,
(Math.random() - 0.5) * 4 + 1,
(Math.random() - 0.5) * 2.5
);
bodyGroup.add(detail);
}
// 腕
leftArm = new THREE.Mesh(new THREE.CylinderGeometry(0.15, 0.25, 1.2, 5), skinMat);
leftArm.position.set(1.1, 2.3, 0.8);
leftArm.rotation.set(0.5, 0.3, -0.6);
bodyGroup.add(leftArm);
rightArm = leftArm.clone();
rightArm.position.x = -1.1;
rightArm.rotation.set(0.5, -0.3, 0.6);
bodyGroup.add(rightArm);
const handGeo = new THREE.ConeGeometry(0.15, 0.6, 4);
const leftHand = new THREE.Mesh(handGeo, skinMat);
leftHand.position.set(0, 0.6, 0.2);
leftHand.rotation.x = 0.5;
leftArm.add(leftHand);
const rightHand = leftHand.clone();
rightArm.add(rightHand);
// 頭部
const headGroup = new THREE.Group();
headGroup.position.set(0, 3.8, 0.8);
bodyGroup.add(headGroup);
const skullGeo = new THREE.BoxGeometry(0.9, 0.7, 1.6);
headMesh = new THREE.Mesh(skullGeo, skinMat);
headMesh.position.set(0, 0.4, 0.6);
headMesh.castShadow = true;
headGroup.add(headMesh);
// 目
const eyeGeo = new THREE.SphereGeometry(0.08, 6, 6);
const eyeMat = new THREE.MeshBasicMaterial({ color: 0xdddddd });
const leftEye = new THREE.Mesh(eyeGeo, eyeMat);
leftEye.position.set(0.48, 0.4, 0.8);
headMesh.add(leftEye);
const rightEye = leftEye.clone();
rightEye.position.x = -0.48;
headMesh.add(rightEye);
// 下顎
jawMesh = new THREE.Group();
jawMesh.position.set(0, 0.05, 0.6);
headGroup.add(jawMesh);
const jawBaseGeo = new THREE.BoxGeometry(0.85, 0.3, 1.4);
const jawBase = new THREE.Mesh(jawBaseGeo, skinMat);
jawBase.position.y = -0.15;
jawMesh.add(jawBase);
const mouthGlowGeo = new THREE.BoxGeometry(0.6, 0.1, 1.1);
const mouthGlow = new THREE.Mesh(mouthGlowGeo, glowMat);
mouthGlow.position.set(0, 0.05, 0.1);
jawMesh.add(mouthGlow);
// 脚
const legGroup = new THREE.Group();
godzilla.add(legGroup);
const lLegUpperGeo = new THREE.CylinderGeometry(0.9, 1.6, 3.5, 8);
const lLegUpper = new THREE.Mesh(lLegUpperGeo, skinMat);
lLegUpper.position.set(1.9, 3.5, -0.5);
lLegUpper.rotation.z = -0.1;
lLegUpper.castShadow = true;
legGroup.add(lLegUpper);
const lLegLowerGeo = new THREE.CylinderGeometry(1.4, 1.8, 3.0, 8);
const lLegLower = new THREE.Mesh(lLegLowerGeo, skinMat);
lLegLower.position.set(0, -1.8, 0.5);
lLegLower.rotation.x = 0.2;
lLegLower.castShadow = true;
lLegUpper.add(lLegLower);
const footGeo = new THREE.BoxGeometry(1.8, 1.0, 2.8);
const lFoot = new THREE.Mesh(footGeo, skinMat);
lFoot.position.set(0, -1.5, 0.4);
lFoot.castShadow = true;
lLegLower.add(lFoot);
const clawGeo = new THREE.ConeGeometry(0.25, 0.8, 4);
const clawMat = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.5 });
for(let c = -1; c <= 1; c++) {
const claw = new THREE.Mesh(clawGeo, clawMat);
claw.position.set(c * 0.6, -0.3, 1.4);
claw.rotation.x = Math.PI / 2.5;
lFoot.add(claw);
}
const rLegUpper = lLegUpper.clone();
rLegUpper.position.x = -1.9;
rLegUpper.rotation.z = 0.1;
legGroup.add(rLegUpper);
// 背鰭
const platesGroup = new THREE.Group();
bodyGroup.add(platesGroup);
const numPlates = 14;
for (let i = 0; i < numPlates; i++) {
const ratio = i / (numPlates - 1);
const size = 1.3 * (1.0 - ratio * 0.4);
const plateGeo = new THREE.ConeGeometry(0.35 * size, 1.8 * size, 4);
const mainPlate = new THREE.Mesh(plateGeo, glowMat);
mainPlate.position.set(0, 2.5 - (i * 0.45), -1.1 - (i * 0.15));
mainPlate.rotation.x = -Math.PI / 3 - (ratio * 0.2);
platesGroup.add(mainPlate);
backPlates.push(mainPlate);
if (i < 12) {
const lPlate = mainPlate.clone();
lPlate.scale.set(0.6, 0.8, 0.6);
lPlate.position.x = 0.45;
lPlate.position.z += 0.1;
lPlate.rotation.z = 0.2;
platesGroup.add(lPlate);
backPlates.push(lPlate);
const rPlate = lPlate.clone();
rPlate.position.x = -0.45;
rPlate.rotation.z = -0.2;
platesGroup.add(rPlate);
backPlates.push(rPlate);
}
}
}
// --- 尻尾 (連結関節構造、形態別サイズスケール) ---
const totalTailSegments = phase === 2 ? 18 : (phase === 3 ? 19 : 20);
let lastSegment = bodyGroup;
for (let i = 0; i < totalTailSegments; i++) {
const ratio = i / totalTailSegments;
let thickness = 2.0 * (1.0 - ratio * 0.88);
let segLength = 1.2 - (ratio * 0.4);
if (phase === 2) {
thickness *= 0.50; // 画像の細い尻尾に補正
segLength *= 0.70;
} else if (phase === 3) {
thickness *= 0.75;
segLength *= 0.88;
}
const segGeo = new THREE.CylinderGeometry(thickness * 0.82, thickness, segLength, 8);
const segMesh = new THREE.Mesh(segGeo, skinMat);
segMesh.castShadow = true;
segMesh.rotation.x = Math.PI / 2.0;
const bone = new THREE.Group();
if (i === 0) {
if (phase === 2) {
bone.position.set(0, -0.1, -1.8);
} else if (phase === 3) {
bone.position.set(0, -0.5, -2.0);
} else {
bone.position.set(0, -0.8, -2.4);
}
} else {
bone.position.set(0, 0, -segLength * 0.95);
}
bone.add(segMesh);
lastSegment.add(bone);
lastSegment = bone;
tailSegments.push(bone);
if (i < totalTailSegments - 2) {
const tPlateGeo = new THREE.ConeGeometry(thickness * 0.2, segLength * 1.5, 4);
const tGlowMat = phase === 2 ? new THREE.MeshStandardMaterial({
color: 0x6e4e3b,
emissive: 0xff3300,
emissiveIntensity: 1.0,
flatShading: true
}) : (phase === 3 ? new THREE.MeshStandardMaterial({
color: 0x888888,
emissive: 0xff2200,
emissiveIntensity: 1.8,
flatShading: true
}) : glowMat);
const tPlate = new THREE.Mesh(tPlateGeo, tGlowMat);
tPlate.position.set(0, thickness * 0.6, 0);
tPlate.rotation.x = -Math.PI / 3;
segMesh.add(tPlate);
tailPlates.push(tPlate);
}
if (i === totalTailSegments - 1) {
const tipScale = (phase === 2 ? 0.45 : (phase === 3 ? 0.75 : 1.0));
const tipGlowGeo = new THREE.SphereGeometry(0.35 * tipScale, 8, 8);
const tipGlowMat = phase === 2 ? new THREE.MeshStandardMaterial({
color: 0x6e4e3b,
emissive: 0xff3300,
emissiveIntensity: 1.0,
flatShading: true
}) : (phase === 3 ? new THREE.MeshStandardMaterial({
color: 0x888888,
emissive: 0xff2200,
emissiveIntensity: 1.8,
flatShading: true
}) : glowMat);
const tipGlow = new THREE.Mesh(tipGlowGeo, tipGlowMat);
tipGlow.position.set(0, 0, -0.4);
segMesh.add(tipGlow);
tailPlates.push(tipGlow);
for(let k=0; k<6; k++) {
const microSpikeGeo = new THREE.ConeGeometry(0.06, 0.4, 4);
const microSpike = new THREE.Mesh(microSpikeGeo, skinMat);
microSpike.position.set(
(Math.random()-0.5)*0.3,
(Math.random()-0.5)*0.3,
-0.5 - Math.random()*0.3
);
microSpike.rotation.set(Math.random(), Math.random(), 0);
segMesh.add(microSpike);
}
}
}
if (phase === 4) {
initBeamEffects();
}
updateLights();
}
// --- 5. 熱線エフェクトの初期化 ---
function initBeamEffects() {
beamsGroup = new THREE.Group();
scene.add(beamsGroup);
beamsGroup.visible = false;
const mouthBeamGeo = new THREE.CylinderGeometry(0.05, 0.4, 60, 8);
mouthBeamGeo.translate(0, 30, 0);
const mouthBeamMat = new THREE.MeshBasicMaterial({
color: 0xee88ff,
transparent: true,
opacity: 0.95
});
const mouthBeam = new THREE.Mesh(mouthBeamGeo, mouthBeamMat);
mouthBeam.rotation.x = Math.PI / 2;
mouthBeam.name = "mouthBeam";
beamsGroup.add(mouthBeam);
const flareGeo = new THREE.SphereGeometry(1.2, 16, 16);
const flareMat = new THREE.MeshBasicMaterial({
color: 0xcc44ff,
transparent: true,
opacity: 0.8
});
const flare = new THREE.Mesh(flareGeo, flareMat);
flare.name = "mouthFlare";
beamsGroup.add(flare);
const tailBeamGeo = new THREE.CylinderGeometry(0.03, 0.3, 50, 8);
tailBeamGeo.translate(0, 25, 0);
const tailBeam = new THREE.Mesh(tailBeamGeo, mouthBeamMat);
tailBeam.rotation.x = -Math.PI / 4;
tailBeam.name = "tailBeam";
beamsGroup.add(tailBeam);
const backBeams = new THREE.Group();
backBeams.name = "backBeams";
for (let i = 0; i < 12; i++) {
const subBeamGeo = new THREE.CylinderGeometry(0.01, 0.08, 40, 4);
subBeamGeo.translate(0, 20, 0);
const subBeam = new THREE.Mesh(subBeamGeo, new THREE.MeshBasicMaterial({
color: 0xaa22ff,
transparent: true,
opacity: 0.6
}));
subBeam.rotation.set(
-Math.PI/3 + (Math.random() - 0.5) * 0.5,
(Math.random() - 0.5) * 2,
(Math.random() - 0.5) * 0.5
);
subBeam.position.set(0, 5 - i*0.4, -1.5 - i*0.2);
backBeams.add(subBeam);
}
beamsGroup.add(backBeams);
const sparkGeo = new THREE.BufferGeometry();
const sparkCount = 100;
const positions = new Float32Array(sparkCount * 3);
const velocities = [];
for(let i=0; i<sparkCount; i++) {
positions[i*3] = (Math.random() - 0.5) * 2;
positions[i*3+1] = 6 + (Math.random() - 0.5) * 2;
positions[i*3+2] = 2 + (Math.random() - 0.5) * 2;
velocities.push(new THREE.Vector3(
(Math.random() - 0.5) * 0.3,
(Math.random() - 0.5) * 0.3,
(Math.random() + 0.1) * 0.5
));
}
sparkGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const sparkMat = new THREE.PointsMaterial({
color: 0xdd66ff,
size: 0.25,
transparent: true,
blending: THREE.AdditiveBlending
});
beamParticles = new THREE.Points(sparkGeo, sparkMat);
beamParticles.userData = { velocities: velocities };
beamsGroup.add(beamParticles);
}
// --- 6. 環境の構築 ---
function buildEnvironment() {
const floorGeo = new THREE.PlaneGeometry(300, 300, 30, 30);
const pos = floorGeo.attributes.position;
for (let i = 0; i < pos.count; i++) {
const vx = pos.getX(i);
const vy = pos.getY(i);
const z = Math.sin(vx * 0.05) * Math.cos(vy * 0.05) * 1.5 + (Math.random() - 0.5) * 0.2;
pos.setZ(i, z);
}
floorGeo.computeVertexNormals();
const floorMat = new THREE.MeshStandardMaterial({
color: isDayMode ? COLORS.dayGround : COLORS.ground,
roughness: 0.95,
metalness: 0.1,
flatShading: true
});
const floor = new THREE.Mesh(floorGeo, floorMat);
floor.rotation.x = -Math.PI / 2;
floor.receiveShadow = true;
scene.add(floor);
const lavaGridGeo = new THREE.PlaneGeometry(60, 60);
const lavaGridMat = new THREE.MeshBasicMaterial({
color: 0x1a0500,
transparent: true,
opacity: 0.3
});
const lavaGrid = new THREE.Mesh(lavaGridGeo, lavaGridMat);
lavaGrid.rotation.x = -Math.PI / 2;
lavaGrid.position.y = 0.01;
scene.add(lavaGrid);
const particleCount = 250;
particleGeometry = new THREE.BufferGeometry();
const positions = new Float32Array(particleCount * 3);
for (let i = 0; i < particleCount; i++) {
positions[i * 3] = (Math.random() - 0.5) * 60;
positions[i * 3 + 1] = Math.random() * 25;
positions[i * 3 + 2] = (Math.random() - 0.5) * 60;
}
particleGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const particleMaterial = new THREE.PointsMaterial({
color: 0xff3300,
size: 0.15,
transparent: true,
opacity: 0.6,
blending: THREE.AdditiveBlending
});
particles = new THREE.Points(particleGeometry, particleMaterial);
scene.add(particles);
}
// --- 7. UIインタラクション設定 ---
function setupInteractions() {
// 形態選択ボタン
const btnPhase2 = document.getElementById('btn-phase-2');
const btnPhase3 = document.getElementById('btn-phase-3');
const btnPhase4 = document.getElementById('btn-phase-4');
const glowToggle = document.getElementById('phase4-glow-toggle');
const warningBox = document.getElementById('warning-box');
function setPhase(phase) {
if (currentPhase === phase && godzilla) return;
isRoaring = false;
isBeaming = false;
isGillEjecting = false;
isSquirming = false;
isStandingUp = false;
if (beamsGroup) beamsGroup.visible = false;
currentPhase = phase;
btnPhase2.className = "bg-gray-900 hover:bg-gray-800 text-gray-400 border border-gray-700 font-bold py-1.5 px-1 rounded text-[10px] sm:text-xs transition flex flex-col items-center justify-center active:scale-95";
btnPhase3.className = "bg-gray-900 hover:bg-gray-800 text-gray-400 border border-gray-700 font-bold py-1.5 px-1 rounded text-[10px] sm:text-xs transition flex flex-col items-center justify-center active:scale-95";
btnPhase4.className = "bg-gray-900 hover:bg-gray-800 text-gray-400 border border-gray-700 font-bold py-1.5 px-1 rounded text-[10px] sm:text-xs transition flex flex-col items-center justify-center active:scale-95";
const stateText = document.getElementById('state-text');
const energyBar = document.getElementById('energy-bar');
if (phase === 2) {
btnPhase2.className = "bg-amber-950/80 hover:bg-amber-900 text-amber-200 border border-amber-500 font-bold py-1.5 px-1 rounded text-[10px] sm:text-xs transition flex flex-col items-center justify-center active:scale-95";
document.querySelector('#status-panel h1').innerText = "巨大不明生物第2形態監視モニター";
document.querySelector('#status-panel span').className = "inline-block w-2.5 h-2.5 rounded-full bg-amber-500 animate-pulse";
stateText.innerText = "這行進中 (EVADING)";
stateText.className = "text-amber-400 font-bold animate-pulse";
document.getElementById('code-type-text').innerText = "第2形態 (蒲田くん)";
document.getElementById('code-type-text').className = "text-amber-400 font-bold";
document.getElementById('height-text').innerText = "約 28.0 m";
energyBar.style.width = "20%";
energyBar.className = "bg-amber-500 h-full transition-all duration-300";
document.getElementById('lbl-action-1').innerText = "エラ排熱 (出血)";
document.getElementById('icon-action-1').className = "fa-solid fa-water";
document.getElementById('lbl-action-2').innerText = "急速蛇行進";
document.getElementById('icon-action-2').className = "fa-solid fa-gauge-high";
glowToggle.style.display = "none";
warningBox.style.display = "none";
} else if (phase === 3) {
btnPhase3.className = "bg-red-950/80 hover:bg-red-900 text-red-200 border border-red-500 font-bold py-1.5 px-1 rounded text-[10px] sm:text-xs transition flex flex-col items-center justify-center active:scale-95";
document.querySelector('#status-panel h1').innerText = "巨大不明生物第3形態監視モニター";
document.querySelector('#status-panel span').className = "inline-block w-2.5 h-2.5 rounded-full bg-red-600 animate-pulse";
stateText.innerText = "急速進化中 (EVOLVING)";
stateText.className = "text-red-400 font-bold animate-pulse";
document.getElementById('code-type-text').innerText = "第3形態 (品川くん)";
document.getElementById('code-type-text').className = "text-red-400 font-bold";
document.getElementById('height-text').innerText = "約 57.0 m";
energyBar.style.width = "40%";
energyBar.className = "bg-red-600 h-full transition-all duration-300";
document.getElementById('lbl-action-1').innerText = "未完の咆哮";
document.getElementById('icon-action-1').className = "fa-solid fa-volume-high";
document.getElementById('lbl-action-2').innerText = "直立試行";
document.getElementById('icon-action-2').className = "fa-solid fa-arrow-up-from-ground";
glowToggle.style.display = "none";
warningBox.style.display = "none";
} else {
btnPhase4.className = "bg-red-950/80 hover:bg-red-900 text-red-200 border border-red-500 font-bold py-1.5 px-1 rounded text-[10px] sm:text-xs transition flex flex-col items-center justify-center active:scale-95";
document.querySelector('#status-panel h1').innerText = "巨大不明生物災害対策本部";
const dotColor = isPurpleState ? "bg-purple-500 animate-ping" : "bg-red-600 animate-pulse";
document.querySelector('#status-panel span').className = "inline-block w-2.5 h-2.5 rounded-full " + dotColor;
stateText.innerText = isPurpleState ? "熱線充填中 (ENERGY CRITICAL)" : "警戒監視中 (STANDBY)";
stateText.className = isPurpleState ? "text-purple-400 font-bold animate-pulse" : "text-red-500 font-bold animate-pulse";
document.getElementById('code-type-text').innerText = "第4形態 (鎌倉さん)";
document.getElementById('code-type-text').className = "text-red-400 font-bold";
document.getElementById('height-text').innerText = "118.5 m";
energyBar.style.width = isPurpleState ? "95%" : "45%";
energyBar.className = isPurpleState ? "bg-purple-500 h-full" : "bg-red-600 h-full";
document.getElementById('lbl-action-1').innerText = "咆哮モーション";
document.getElementById('icon-action-1').className = "fa-solid fa-volume-high";
document.getElementById('lbl-action-2').innerText = "放射線流 発射";
document.getElementById('icon-action-2').className = "fa-solid fa-bolt-lightning";
glowToggle.style.display = "grid";
warningBox.style.display = "block";
}
buildGodzilla(phase);
resetCamera(); // 形態変更時の自動ズーム補正
playBeepSound(150 + phase * 100, 0.15);
}
btnPhase2.addEventListener('click', () => setPhase(2));
btnPhase3.addEventListener('click', () => setPhase(3));
btnPhase4.addEventListener('click', () => setPhase(4));
// 通常/熱線 変更トグル (第4形態用)
const btnRed = document.getElementById('btn-state-red');
const btnPurple = document.getElementById('btn-state-purple');
btnRed.addEventListener('click', () => {
if (currentPhase === 4) setGodzillaState(false);
});
btnPurple.addEventListener('click', () => {
if (currentPhase === 4) setGodzillaState(true);
});
// 昼夜切り替えボタン
const btnNight = document.getElementById('btn-env-night');
const btnDay = document.getElementById('btn-env-day');
btnNight.addEventListener('click', () => setEnvironmentDayMode(false));
btnDay.addEventListener('click', () => setEnvironmentDayMode(true));
// アクションボタン分岐
document.getElementById('btn-roar').addEventListener('click', () => {
if (currentPhase === 2) {
triggerGillEject();
} else if (currentPhase === 3) {
triggerRoarPhase3();
} else {
triggerRoar();
}
});
document.getElementById('btn-beam').addEventListener('click', () => {
if (currentPhase === 2) {
triggerSquirm();
} else if (currentPhase === 3) {
triggerStandUp();
} else {
triggerBeam();
}
});
document.getElementById('btn-reset-cam').addEventListener('click', () => resetCamera());
// 尾部速度スライダー
const speedSlider = document.getElementById('tail-speed');
const speedVal = document.getElementById('tail-speed-val');
speedSlider.addEventListener('input', (e) => {
animationSpeed = parseFloat(e.target.value);
speedVal.innerText = animationSpeed.toFixed(1) + "x";
});
// フォグスライダー
const fogSlider = document.getElementById('fog-density');
const fogVal = document.getElementById('fog-val');
fogSlider.addEventListener('input', (e) => {
const val = parseInt(e.target.value);
const density = val * 0.01;
scene.fog.density = density;
if (val === 0) fogVal.innerText = "なし";
else if (val < 4) fogVal.innerText = "薄い";
else if (val < 8) fogVal.innerText = "標準";
else fogVal.innerText = "濃霧";
});
// 明るさスライダー
const brightSlider = document.getElementById('scene-brightness');
const brightVal = document.getElementById('light-val');
brightSlider.addEventListener('input', (e) => {
const val = parseInt(e.target.value);
brightnessMultiplier = val * 0.1;
updateLights();
if (val <= 3) brightVal.innerText = "暗い";
else if (val <= 7) brightVal.innerText = "通常";
else if (val <= 11) brightVal.innerText = "明るい";
else brightVal.innerText = "極光";
});
// ---------------- モバイル専用タブ切り替えロジック ----------------
const tabActions = document.getElementById('tab-actions');
const tabEnv = document.getElementById('tab-env');
const envPanel = document.getElementById('env-panel');
const actionPanel = document.getElementById('action-panel');
function switchMobileTab(tab) {
if (tab === 'actions') {
tabActions.className = "flex-1 py-2 text-center text-red-500 border-b-2 border-red-500 font-black text-xs transition";
tabEnv.className = "flex-1 py-2 text-center text-gray-500 border-b-2 border-transparent text-xs transition";
actionPanel.classList.remove('hidden');
envPanel.classList.add('hidden');
} else {
tabEnv.className = "flex-1 py-2 text-center text-red-500 border-b-2 border-red-500 font-black text-xs transition";
tabActions.className = "flex-1 py-2 text-center text-gray-500 border-b-2 border-transparent text-xs transition";
envPanel.classList.remove('hidden');
actionPanel.classList.add('hidden');
}
playBeepSound(300, 0.05);
}
tabActions.addEventListener('click', () => switchMobileTab('actions'));
tabEnv.addEventListener('click', () => switchMobileTab('env'));
// ---------------- UI表示/非表示 トグル ----------------
const btnToggleUi = document.getElementById('btn-toggle-ui');
const toggleUiIcon = document.getElementById('toggle-ui-icon');
const toggleUiText = document.getElementById('toggle-ui-text');
const statusPanel = document.getElementById('status-panel');
const footer = document.querySelector('footer');
btnToggleUi.addEventListener('click', () => {
uiHidden = !uiHidden;
if (uiHidden) {
// UI隠しモード
statusPanel.classList.add('opacity-0', 'pointer-events-none', '-translate-y-4');
footer.classList.add('opacity-0', 'pointer-events-none', 'translate-y-4');
toggleUiIcon.className = "fa-solid fa-eye";
toggleUiText.innerText = "UI表示";
btnToggleUi.classList.remove('bg-red-950/80', 'border-red-500', 'text-red-200');
btnToggleUi.classList.add('bg-black/80', 'border-gray-700', 'text-gray-300');
} else {
// UI通常モード
statusPanel.classList.remove('opacity-0', 'pointer-events-none', '-translate-y-4');
footer.classList.remove('opacity-0', 'pointer-events-none', 'translate-y-4');
toggleUiIcon.className = "fa-solid fa-eye-slash";
toggleUiText.innerText = "UI非表示";
btnToggleUi.classList.add('bg-red-950/80', 'border-red-500', 'text-red-200');
btnToggleUi.classList.remove('bg-black/80', 'border-gray-700', 'text-gray-300');
}
playBeepSound(400, 0.08);
});
}
// 環境モード(昼夜)切り替え
function setEnvironmentDayMode(toDay) {
if (isDayMode === toDay) return;
isDayMode = toDay;
const btnNight = document.getElementById('btn-env-night');
const btnDay = document.getElementById('btn-env-day');
if (isDayMode) {
btnDay.className = "bg-amber-950/40 hover:bg-amber-900/60 text-amber-400 py-1.5 px-2 rounded border border-amber-800/80 text-[10px] sm:text-xs font-bold transition flex items-center justify-center space-x-1 active:scale-95";
btnNight.className = "bg-gray-900 hover:bg-gray-800 text-gray-400 py-1.5 px-2 rounded border border-gray-700 text-[10px] sm:text-xs font-bold transition flex items-center justify-center space-x-1 active:scale-95";
} else {
btnNight.className = "bg-red-950/40 hover:bg-red-900/60 text-red-400 py-1.5 px-2 rounded border border-red-800/80 text-[10px] sm:text-xs font-bold transition flex items-center justify-center space-x-1 active:scale-95";
btnDay.className = "bg-gray-900 hover:bg-gray-800 text-gray-400 py-1.5 px-2 rounded border border-gray-700 text-[10px] sm:text-xs font-bold transition flex items-center justify-center space-x-1 active:scale-95";
}
// 地面の色の変更
scene.traverse((child) => {
if (child.isMesh && child.geometry && child.geometry.type === "PlaneGeometry") {
if (child.material && child.material.color) {
child.material.color.setHex(isDayMode ? COLORS.dayGround : COLORS.ground);
}
}
});
updateLights();
playBeepSound(300, 0.1);
}
// 第4形態用状態切り替え (赤 ⇔ 紫)
function setGodzillaState(toPurple) {
if (isPurpleState === toPurple) return;
isPurpleState = toPurple;
const btnRed = document.getElementById('btn-state-red');
const btnPurple = document.getElementById('btn-state-purple');
const statusPanel = document.getElementById('status-panel');
const actionPanel = document.getElementById('action-panel');
const statusDot = document.getElementById('status-dot');
const stateText = document.getElementById('state-text');
const energyBar = document.getElementById('energy-bar');
const emissiveColor = isPurpleState ? COLORS.purpleGlow : COLORS.redGlow;
const targetColor = new THREE.Color(emissiveColor);
backPlates.concat(tailPlates).forEach(plate => {
plate.material.emissive.set(targetColor);
plate.material.emissiveIntensity = isPurpleState ? 3.5 : 2.5;
});
glowLights.forEach(light => {
light.color.set(targetColor);
});
particles.material.color.set(isPurpleState ? 0x9955ff : 0xff3300);
updateLights();
if (isPurpleState) {
btnPurple.className = "bg-purple-950/80 hover:bg-purple-900 text-purple-200 border border-purple-500 font-bold py-1.5 px-2 rounded text-[10px] sm:text-xs transition flex items-center justify-center space-x-1.5 active:scale-95";
btnRed.className = "bg-gray-900 hover:bg-gray-800 text-gray-400 border border-gray-700 font-bold py-1.5 px-2 rounded text-[10px] sm:text-xs transition flex items-center justify-center space-x-1.5 active:scale-95";
statusPanel.className = "bg-black/80 p-3 sm:p-4 rounded neon-border-purple transition-all duration-300 transform w-full max-w-[280px] sm:max-w-sm md:max-w-md";
actionPanel.className = "bg-black/80 p-3 sm:p-4 rounded neon-border-purple transition-all duration-500 w-full md:w-96 space-y-3 sm:space-y-4";
statusDot.className = "inline-block w-2.5 h-2.5 rounded-full bg-purple-500 animate-ping";
stateText.innerText = "熱線充填中 (ENERGY CRITICAL)";
stateText.className = "text-purple-400 font-bold animate-pulse";
energyBar.style.width = "95%";
energyBar.className = "bg-purple-500 h-full transition-all duration-300";
} else {
btnRed.className = "bg-red-950/80 hover:bg-red-900 text-red-200 border border-red-500 font-bold py-1.5 px-2 rounded text-[10px] sm:text-xs transition flex items-center justify-center space-x-1.5 active:scale-95";
btnPurple.className = "bg-gray-900 hover:bg-gray-800 text-gray-400 border border-gray-700 font-bold py-1.5 px-2 rounded text-[10px] sm:text-xs transition flex items-center justify-center space-x-1.5 active:scale-95";
statusPanel.className = "bg-black/80 p-3 sm:p-4 rounded neon-border transition-all duration-300 transform w-full max-w-[280px] sm:max-w-sm md:max-w-md";
actionPanel.className = "bg-black/80 p-3 sm:p-4 rounded neon-border transition-all duration-500 w-full md:w-96 space-y-3 sm:space-y-4";
statusDot.className = "inline-block w-2.5 h-2.5 rounded-full bg-red-600 animate-pulse";
stateText.innerText = "警戒監視中 (STANDBY)";
stateText.className = "text-red-500 font-bold animate-pulse";
energyBar.style.width = "45%";
energyBar.className = "bg-red-600 h-full transition-all duration-300";
if (isBeaming) {
isBeaming = false;
beamsGroup.visible = false;
}
}
playBeepSound(isPurpleState ? 440 : 220, 0.15);
}
// --- 8. ゴジラ特殊アクション ---
// アクション: 第2形態 エラ排熱 (血液ブシャー)
function triggerGillEject() {
if (isGillEjecting) return;
isGillEjecting = true;
gillEjectTimer = 0;
const stateText = document.getElementById('state-text');
stateText.innerText = "エラ排熱 (EJECTING)";
stateText.className = "text-red-500 font-bold animate-pulse";
try {
if (audioCtx.state === 'suspended') audioCtx.resume();
const duration = 1.5;
const bufferSize = audioCtx.sampleRate * duration;
const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = (Math.random() * 2 - 1) * 0.4;
}
const noise = audioCtx.createBufferSource();
noise.buffer = buffer;
const filter = audioCtx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.setValueAtTime(320, audioCtx.currentTime);
filter.frequency.exponentialRampToValueAtTime(70, audioCtx.currentTime + duration);
const gain = audioCtx.createGain();
gain.gain.setValueAtTime(0.6, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + duration);
noise.connect(filter);
filter.connect(gain);
gain.connect(audioCtx.destination);
noise.start();
} catch(e){}
setTimeout(() => {
isGillEjecting = false;
stateText.innerText = "這行進中 (EVADING)";
stateText.className = "text-amber-400 font-bold animate-pulse";
}, 2500);
}
// アクション: 第2形態 急速蛇行
function triggerSquirm() {
if (isSquirming) return;
isSquirming = true;
squirmTimer = 0;
const stateText = document.getElementById('state-text');
stateText.innerText = "急速蛇行進 (SQUIRMING)";
stateText.className = "text-amber-500 font-bold animate-bounce";
try {
if (audioCtx.state === 'suspended') audioCtx.resume();
const duration = 2.5;
const osc = audioCtx.createOscillator();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(35, audioCtx.currentTime);
osc.frequency.linearRampToValueAtTime(80, audioCtx.currentTime + 1.0);
osc.frequency.linearRampToValueAtTime(15, audioCtx.currentTime + 2.5);
const filter = audioCtx.createBiquadFilter();
filter.type = 'bandpass';
filter.frequency.setValueAtTime(120, audioCtx.currentTime);
const gain = audioCtx.createGain();
gain.gain.setValueAtTime(0.4, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + duration);
osc.connect(filter);
filter.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + duration);
} catch(e){}
setTimeout(() => {
isSquirming = false;
stateText.innerText = "這行進中 (EVADING)";
stateText.className = "text-amber-400 font-bold animate-pulse";
}, 3000);
}
// アクション: 第3形態 未完の咆哮
function triggerRoarPhase3() {
if (isRoaring) return;
isRoaring = true;
roarTimer = 0;
const stateText = document.getElementById('state-text');
stateText.innerText = "未完の咆哮 (ROARING)";
stateText.className = "text-red-400 font-bold animate-bounce";
try {
if (audioCtx.state === 'suspended') audioCtx.resume();
const duration = 1.8;
const osc = audioCtx.createOscillator();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(110, audioCtx.currentTime);
osc.frequency.linearRampToValueAtTime(180, audioCtx.currentTime + 0.3);
osc.frequency.exponentialRampToValueAtTime(55, audioCtx.currentTime + 1.5);
const filter = audioCtx.createBiquadFilter();
filter.type = 'bandpass';
filter.frequency.setValueAtTime(280, audioCtx.currentTime);
const gain = audioCtx.createGain();
gain.gain.setValueAtTime(0.4, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + duration);
osc.connect(filter);
filter.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + duration);
} catch(e){}
setTimeout(() => {
isRoaring = false;
stateText.innerText = "急速進化中 (EVOLVING)";
stateText.className = "text-red-400 font-bold animate-pulse";
}, 2000);
}
// アクション: 第3形態 直立試行
function triggerStandUp() {
if (isStandingUp) return;
isStandingUp = true;
standUpTimer = 0;
const stateText = document.getElementById('state-text');
stateText.innerText = "直立試行 (ERECTING)";
stateText.className = "text-red-500 font-bold animate-pulse";
try {
if (audioCtx.state === 'suspended') audioCtx.resume();
const osc = audioCtx.createOscillator();
osc.type = 'triangle';
osc.frequency.setValueAtTime(30, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(8, audioCtx.currentTime + 1.0);
const gain = audioCtx.createGain();
gain.gain.setValueAtTime(0.9, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 1.5);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + 1.5);
} catch(e){}
setTimeout(() => {
isStandingUp = false;
stateText.innerText = "急速進化中 (EVOLVING)";
stateText.className = "text-red-400 font-bold animate-pulse";
}, 4000);
}
// アクション: 第4形態 咆哮モーション
function triggerRoar() {
if (isRoaring) return;
isRoaring = true;
roarTimer = 0;
const stateText = document.getElementById('state-text');
const originalText = stateText.innerText;
const originalClass = stateText.className;
stateText.innerText = "咆哮検知 (ROARING)";
stateText.className = "text-amber-400 font-bold animate-bounce";
playRoarSound();
setTimeout(() => {
isRoaring = false;
stateText.innerText = originalText;
stateText.className = originalClass;
}, 2500);
}
// アクション: 第4形態 放射線流発射
function triggerBeam() {
if (isBeaming) return;
if (!isPurpleState) {
setGodzillaState(true);
setTimeout(startBeamFiring, 800);
} else {
startBeamFiring();
}
}
function startBeamFiring() {
isBeaming = true;
beamTimer = 0;
beamsGroup.visible = true;
const stateText = document.getElementById('state-text');
stateText.innerText = "放射線流発射 (THERMAL BEAM)";
stateText.className = "text-purple-400 font-bold animate-ping";
playBeamSound();
setTimeout(() => {
isBeaming = false;
beamsGroup.visible = false;
const originalText = isPurpleState ? "熱線充填中 (ENERGY CRITICAL)" : "警戒監視中 (STANDBY)";
const originalClass = isPurpleState ? "text-purple-400 font-bold animate-pulse" : "text-red-500 font-bold animate-pulse";
stateText.innerText = originalText;
stateText.className = originalClass;
}, 4500);
}
// --- 9. シンセサイザー効果音生成 (Web Audio API) ---
function playBeepSound(freq, duration) {
try {
if (audioCtx.state === 'suspended') audioCtx.resume();
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(freq, audioCtx.currentTime);
gain.gain.setValueAtTime(0.1, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + duration);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + duration);
} catch (e) {}
}
function playRoarSound() {
try {
if (audioCtx.state === 'suspended') audioCtx.resume();
const duration = 2.0;
const bufferSize = audioCtx.sampleRate * duration;
const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = Math.random() * 2 - 1;
}
const noise = audioCtx.createBufferSource();
noise.buffer = buffer;
const filter = audioCtx.createBiquadFilter();
filter.type = 'bandpass';
filter.frequency.setValueAtTime(80, audioCtx.currentTime);
filter.frequency.exponentialRampToValueAtTime(350, audioCtx.currentTime + 0.3);
filter.frequency.exponentialRampToValueAtTime(100, audioCtx.currentTime + 1.2);
filter.Q.setValueAtTime(2.0, audioCtx.currentTime);
const osc = audioCtx.createOscillator();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(65, audioCtx.currentTime);
osc.frequency.linearRampToValueAtTime(120, audioCtx.currentTime + 0.4);
osc.frequency.exponentialRampToValueAtTime(45, audioCtx.currentTime + 1.5);
const oscGain = audioCtx.createGain();
oscGain.gain.setValueAtTime(0.3, audioCtx.currentTime);
oscGain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 1.8);
const gain = audioCtx.createGain();
gain.gain.setValueAtTime(0.4, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + duration);
noise.connect(filter);
filter.connect(gain);
osc.connect(oscGain);
oscGain.connect(gain);
gain.connect(audioCtx.destination);
noise.start();
osc.start();
noise.stop(audioCtx.currentTime + duration);
osc.stop(audioCtx.currentTime + duration);
} catch (e) {}
}
function playBeamSound() {
try {
if (audioCtx.state === 'suspended') audioCtx.resume();
const duration = 4.0;
const osc = audioCtx.createOscillator();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(40, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(1200, audioCtx.currentTime + 0.8);
osc.frequency.setValueAtTime(800, audioCtx.currentTime + 1.2);
const filter = audioCtx.createBiquadFilter();
filter.type = 'peaking';
filter.frequency.setValueAtTime(1000, audioCtx.currentTime);
filter.Q.setValueAtTime(5, audioCtx.currentTime);
const gain = audioCtx.createGain();
gain.gain.setValueAtTime(0.01, audioCtx.currentTime);
gain.gain.linearRampToValueAtTime(0.4, audioCtx.currentTime + 0.4);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + duration);
osc.connect(filter);
filter.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + duration);
} catch (e) {}
}
// --- 10. メイン・アニメーションループ ---
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
const time = clock.getElapsedTime();
// 発光脈動 (全形態共通)
const glowSpeed = isPurpleState ? 5.0 : 2.5;
const glowBase = isPurpleState ? 3.0 : 2.0;
const pulse = glowBase + Math.sin(time * glowSpeed) * 1.2;
backPlates.concat(tailPlates).forEach((plate, idx) => {
const individualPulse = pulse + Math.sin(time * 3 + idx * 0.2) * 0.5;
plate.material.emissiveIntensity = Math.max(0.5, individualPulse);
});
glowLights.forEach((light, index) => {
light.intensity = pulse * (index === 0 ? 3.5 : 2.0);
});
// ==================== 形態別アニメーション定義 ====================
if (currentPhase === 2) {
// ------------------ 第2形態 (蒲田くん) ------------------
const currentSpeed = isSquirming ? 2.8 : animationSpeed;
const squirmAmp = isSquirming ? 1.6 : 1.0;
// 蛇行:這いずりながら体を大きく左右にクネクネ振る
if (godzilla.children[0]) {
godzilla.children[0].rotation.y = Math.sin(time * 3.5 * currentSpeed) * 0.16 * squirmAmp;
godzilla.children[0].position.x = Math.sin(time * 3.5 * currentSpeed) * 0.55 * squirmAmp;
}
// ギョロ目のピクピクとした不揃いな視線の動き
if (headMesh && headMesh.children[0] && headMesh.children[1]) {
headMesh.children[0].children[0].position.x = 0.04 + Math.sin(time * 8) * 0.01;
headMesh.children[1].children[0].position.x = -0.04 + Math.sin(time * 8 + 3) * 0.01;
}
// エラスリットのひくひく脈動
gills.forEach((gill, idx) => {
const individualDelay = Math.floor(idx / 2) * 0.5;
const breath = 1.0 + Math.sin(time * 4.0 - individualDelay) * 0.18;
gill.scale.set(breath, 1.0, 1.0);
});
// 首の開口部 (出血排熱アクション時)
if (isGillEjecting) {
jawMesh.position.y = -0.22 - Math.sin(time * 20.0) * 0.04;
} else {
jawMesh.position.y += (-0.12 - jawMesh.position.y) * 0.08;
}
// 尻尾の大きな横振りのうねり (完全水平を保ち、画像準拠のまっすぐな美シルエット)
tailSegments.forEach((bone, index) => {
const delay = index * 0.25;
const waveY = Math.sin(time * 3.0 * currentSpeed - delay) * (0.12 + index * 0.009) * squirmAmp;
bone.rotation.y = waveY;
let baseAngleX = 0;
if (index === 0) {
baseAngleX = -Math.PI / 12; // 胴体の前傾をキャンセル
} else {
baseAngleX = 0.02; // 水平直線
}
bone.rotation.x = baseAngleX + Math.sin(time * 1.5 - delay) * 0.01;
});
// エラ排熱スプラッシュパーティクル制御
if (isGillEjecting && gillParticles) {
const posAttr = gillParticles.geometry.attributes.position;
const vels = gillParticles.userData.velocities;
const leftGillWorld = new THREE.Vector3();
const rightGillWorld = new THREE.Vector3();
if (gills[2] && gills[3]) {
gills[2].getWorldPosition(leftGillWorld);
gills[3].getWorldPosition(rightGillWorld);
}
for (let i = 0; i < posAttr.count; i++) {
let px = posAttr.getX(i) + vels[i].x;
let py = posAttr.getY(i) + vels[i].y;
let pz = posAttr.getZ(i) + vels[i].z;
vels[i].y -= 0.008; // 重力
if (py < 0.15) {
const useLeft = Math.random() > 0.5;
const src = useLeft ? leftGillWorld : rightGillWorld;
px = src.x;
py = src.y;
pz = src.z;
const dirX = useLeft ? (0.4 + Math.random()*0.5) : (-0.4 - Math.random()*0.5);
vels[i].set(dirX, 0.06 + Math.random()*0.1, (Math.random() - 0.5)*0.15);
}
posAttr.setXYZ(i, px, py, pz);
}
posAttr.needsUpdate = true;
gillParticles.visible = true;
} else if (gillParticles) {
gillParticles.visible = false;
}
} else if (currentPhase === 3) {
// ------------------ 第3形態 (品川くん) ------------------
const currentSpeed = animationSpeed;
// 直立試行ポーズの制御 (立ち上がる動き)
if (isStandingUp) {
standUpTimer += delta;
const targetRotX = -Math.PI / 8;
if (godzilla.children[0]) {
godzilla.children[0].rotation.x += (targetRotX - godzilla.children[0].rotation.x) * 0.06;
if (headMesh) headMesh.parent.rotation.x = -0.5;
if (jawMesh) {
jawMesh.position.y = -0.32;
jawMesh.rotation.x = 0.25;
}
}
} else {
if (godzilla.children[0]) {
godzilla.children[0].rotation.x += (Math.PI / 10 - godzilla.children[0].rotation.x) * 0.06;
}
if (isRoaring) {
roarTimer += delta;
const progress = Math.min(roarTimer / 0.5, 1.0);
const decay = roarTimer > 1.1 ? Math.max(0, 1 - (roarTimer - 1.1) / 0.7) : 1.0;
if (headMesh) headMesh.parent.rotation.x = -0.4 * progress * decay;
if (jawMesh) {
jawMesh.position.y = -0.25 * progress * decay;
jawMesh.rotation.x = 0.18 * progress * decay;
}
} else {
if (headMesh && jawMesh) {
headMesh.parent.rotation.x *= 0.92;
jawMesh.position.y += (-0.05 - jawMesh.position.y) * 0.08;
jawMesh.rotation.x *= 0.92;
}
}
}
// 呼吸ゆらぎ
if (bodyMesh) {
bodyMesh.scale.x = 1.0 + Math.sin(time * 1.5) * 0.02;
bodyMesh.scale.z = 1.0 + Math.sin(time * 1.5) * 0.02;
}
// 前腕のぶらぶらスイング
if (leftArm && rightArm) {
leftArm.rotation.x = 0.7 + Math.sin(time * 2.5) * 0.12;
rightArm.rotation.x = 0.7 - Math.sin(time * 2.5) * 0.12;
}
// 尻尾のうねり (ツンと先が上を向くシルエット)
tailSegments.forEach((bone, index) => {
const delay = index * 0.25;
const waveY = Math.sin(time * 2.0 * currentSpeed - delay) * (0.07 + index * 0.006);
const waveX = Math.cos(time * 1.0 * currentSpeed - delay) * 0.015;
bone.rotation.y = waveY;
let baseAngleX = 0;
if (index === 0) {
baseAngleX = -Math.PI / 10; // 前傾キャンセル
} else if (index > 13) {
baseAngleX = -0.08; // 先端のみツンと反らせる
} else {
baseAngleX = 0.01;
}
bone.rotation.x = baseAngleX + waveX;
});
if (gillParticles) gillParticles.visible = false;
} else {
// ------------------ 第4形態 (鎌倉さん) ------------------
if (bodyMesh) {
bodyMesh.scale.x = 1.0 + Math.sin(time * 1.2) * 0.015;
bodyMesh.scale.z = 1.0 + Math.sin(time * 1.2) * 0.015;
}
if (leftArm && rightArm) {
leftArm.rotation.x = 0.5 + Math.sin(time * 3.0) * 0.04;
rightArm.rotation.x = 0.5 - Math.sin(time * 3.0) * 0.04;
}
// 尻尾 (浮遊J型そり立ち)
const tailSpeedMultiplier = animationSpeed;
tailSegments.forEach((bone, index) => {
const delay = index * 0.28;
const waveY = Math.sin(time * 2.2 * tailSpeedMultiplier - delay) * (0.06 + index * 0.005);
const waveX = Math.cos(time * 1.1 * tailSpeedMultiplier - delay) * 0.02;
bone.rotation.y = waveY;
let baseAngleX = -0.03;
if (index > 3 && index <= 12) {
baseAngleX = -0.08;
} else if (index > 12) {
baseAngleX = -0.11;
}
bone.rotation.x = baseAngleX + waveX;
});
// 咆哮
if (isRoaring) {
roarTimer += delta;
if (headMesh && jawMesh) {
const progress = Math.min(roarTimer / 0.5, 1.0);
const decay = roarTimer > 1.8 ? Math.max(0, 1 - (roarTimer - 1.8) / 0.7) : 1.0;
const angle = 0.6 * Math.sin(progress * Math.PI / 2) * decay;
headMesh.parent.rotation.x = -angle;
jawMesh.position.y = -0.4 * Math.sin(progress * Math.PI / 2) * decay;
jawMesh.rotation.x = 0.3 * Math.sin(progress * Math.PI / 2) * decay;
}
} else if (!isBeaming) {
if (headMesh && jawMesh) {
headMesh.parent.rotation.x *= 0.92;
jawMesh.position.y += (-0.05 - jawMesh.position.y) * 0.08;
jawMesh.rotation.x *= 0.92;
}
}
// 熱線ビーム
if (isBeaming) {
beamTimer += delta;
beamsGroup.visible = true;
const progress = Math.min(beamTimer / 0.6, 1.0);
if (headMesh && jawMesh) {
headMesh.parent.rotation.x = -0.3 * progress;
jawMesh.position.y = -0.55 * progress;
jawMesh.rotation.x = 0.45 * progress;
}
const mBeam = beamsGroup.getObjectByName("mouthBeam");
const mFlare = beamsGroup.getObjectByName("mouthFlare");
const tBeam = beamsGroup.getObjectByName("tailBeam");
const bBeams = beamsGroup.getObjectByName("backBeams");
if (mBeam && mFlare && tBeam && bBeams) {
const beamPulse = 1.0 + Math.sin(time * 25.0) * 0.2;
mBeam.scale.set(beamPulse, 1, beamPulse);
tBeam.scale.set(beamPulse * 0.8, 1, beamPulse * 0.8);
mBeam.rotation.z = Math.sin(time * 15.0) * 0.01;
mBeam.rotation.y = Math.cos(time * 18.0) * 0.01;
mFlare.scale.setScalar(beamPulse * (1.2 + Math.random() * 0.3));
const headWorldPos = new THREE.Vector3();
headMesh.getWorldPosition(headWorldPos);
mBeam.position.copy(headWorldPos);
mBeam.position.y -= 0.2;
mBeam.position.z += 0.4;
mFlare.position.copy(mBeam.position);
const tailTipWorldPos = new THREE.Vector3();
const lastBone = tailSegments[tailSegments.length - 1];
if (lastBone && lastBone.children[0]) {
lastBone.children[0].getWorldPosition(tailTipWorldPos);
tBeam.position.copy(tailTipWorldPos);
}
bBeams.children.forEach((subBeam, idx) => {
const subPulse = 0.8 + Math.sin(time * 30.0 + idx) * 0.3;
subBeam.scale.set(subPulse, 1, subPulse);
});
}
if (beamParticles) {
const posAttr = beamParticles.geometry.attributes.position;
const vels = beamParticles.userData.velocities;
const mouthPos = new THREE.Vector3();
headMesh.getWorldPosition(mouthPos);
for(let i=0; i<posAttr.count; i++) {
let px = posAttr.getX(i) + vels[i].x;
let py = posAttr.getY(i) + vels[i].y;
let pz = posAttr.getZ(i) + vels[i].z;
const dist = Math.sqrt((px - mouthPos.x)**2 + (py - mouthPos.y)**2 + (pz - mouthPos.z)**2);
if (dist > 8.0) {
px = mouthPos.x + (Math.random()-0.5)*1.0;
py = mouthPos.y + (Math.random()-0.5)*1.0;
pz = mouthPos.z + (Math.random()-0.5)*1.0;
vels[i].set(
(Math.random() - 0.5) * 0.2,
(Math.random() - 0.5) * 0.2,
(Math.random() + 0.2) * 0.4
);
}
posAttr.setXYZ(i, px, py, pz);
}
posAttr.needsUpdate = true;
}
} else {
if (beamsGroup && beamsGroup.visible) {
beamsGroup.visible = false;
}
}
if (gillParticles) gillParticles.visible = false;
}
// 火の粉
if (particleGeometry) {
const posAttr = particleGeometry.attributes.position;
const speed = isPurpleState ? 0.08 : 0.03;
for (let i = 0; i < posAttr.count; i++) {
let y = posAttr.getY(i) + speed;
let x = posAttr.getX(i) + Math.sin(time * 0.5 + i) * 0.01;
if (y > 25) {
y = 0;
x = (Math.random() - 0.5) * 60;
}
posAttr.setY(i, y);
posAttr.setX(i, x);
}
posAttr.needsUpdate = true;
}
// カメラのゆらぎ
if (controls && controls.state === -1) {
camera.position.y += Math.sin(time * 0.3) * 0.001;
}
controls.update();
renderer.render(scene, camera);
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
resetCamera(); // リサイズ時も見切れ防止を再評価
}
</script>
</body>
</html>
ゴジラ ゴジラ ゴジラがやってきた