<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Godzilla 3D Model</title>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #a39182; /* 煙と夕日を感じるダスティな色に変更 */
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
#canvas-container {
width: 100vw;
height: 100vh;
}
.loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 1.5rem;
}
</style>
</head>
<body>
<div id="canvas-container">
<div class="loading" id="loading-text"></div>
</div>
<!-- Three.js と OrbitControls の読み込み -->
<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>
window.onload = function() {
document.getElementById('loading-text').style.display = 'none';
// 1. シーン、カメラ、レンダラーのセットアップ
const container = document.getElementById('canvas-container');
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0xa39182, 0.006);
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(80, 50, 120);
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
container.appendChild(renderer.domElement);
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.target.set(0, 25, 0);
controls.enableKeys = false; // カメラの矢印キー操作を無効化して首の操作と競合しないようにする
// 2. 照明のセットアップ
const ambientLight = new THREE.AmbientLight(0xffeedd, 0.4);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffccaa, 1.5);
dirLight.position.set(-40, 50, 40);
dirLight.castShadow = true;
dirLight.shadow.mapSize.width = 2048;
dirLight.shadow.mapSize.height = 2048;
dirLight.shadow.camera.near = 0.5;
dirLight.shadow.camera.far = 200;
dirLight.shadow.camera.left = -100;
dirLight.shadow.camera.right = 100;
dirLight.shadow.camera.top = 100;
dirLight.shadow.camera.bottom = -100;
scene.add(dirLight);
const fireLight1 = new THREE.PointLight(0xff5500, 2.0, 150);
fireLight1.position.set(40, 10, 20);
scene.add(fireLight1);
const fireLight2 = new THREE.PointLight(0xff3300, 2.5, 200);
fireLight2.position.set(-30, 5, -40);
scene.add(fireLight2);
// 発光アニメーション用の配列
const spikeMaterials = [];
// 3. ゴジラの構築
const godzilla = new THREE.Group();
const bodyColor = 0x161412;
const bellyColor = 0x221e1a;
const finColor = 0x1a1816;
const mouthColor = 0x882222;
const teethColor = 0xddddcc;
const eyeColor = 0xffaa00;
const bodyMat = new THREE.MeshStandardMaterial({ color: bodyColor, roughness: 0.9, metalness: 0.1 });
const bellyMat = new THREE.MeshStandardMaterial({ color: bellyColor, roughness: 0.9, metalness: 0.1 });
const finMat = new THREE.MeshStandardMaterial({ color: finColor, roughness: 0.8, metalness: 0.2 });
const mouthMat = new THREE.MeshStandardMaterial({ color: mouthColor, roughness: 0.5 });
const teethMat = new THREE.MeshStandardMaterial({ color: teethColor, roughness: 0.5 });
const eyeMat = new THREE.MeshBasicMaterial({ color: eyeColor });
function createBox(w, h, d, mat, castShadow = true) {
const mesh = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), mat);
mesh.castShadow = castShadow;
mesh.receiveShadow = true;
return mesh;
}
// 胴体
const body = new THREE.Group();
const lowerBody = createBox(13, 11, 12, bodyMat);
lowerBody.position.y = 8;
lowerBody.rotation.x = 0.1;
body.add(lowerBody);
const upperBody = createBox(10, 9, 10, bodyMat);
upperBody.position.set(0, 16, 2);
upperBody.rotation.x = 0.2;
body.add(upperBody);
const chest = createBox(8, 7, 4, bellyMat);
chest.position.set(0, 16, 6.5);
chest.rotation.x = 0.3;
body.add(chest);
godzilla.add(body);
// 頭と首
const neckAndHead = new THREE.Group();
const neck = createBox(7, 6.5, 7, bodyMat);
neck.position.set(0, 19.5, 6);
neck.rotation.x = 0.3;
neckAndHead.add(neck);
const head = new THREE.Group();
head.position.set(0, 22.5, 9);
head.rotation.x = -0.1;
head.scale.set(0.85, 0.85, 0.85);
const upperJaw = createBox(4.5, 3, 6, bodyMat);
head.add(upperJaw);
const snout = createBox(3.5, 2, 3, bodyMat);
snout.position.set(0, -0.5, 4);
head.add(snout);
const leftEye = createBox(0.4, 0.4, 0.4, eyeMat, false);
leftEye.position.set(-2.3, 0.5, 1);
head.add(leftEye);
const rightEye = createBox(0.4, 0.4, 0.4, eyeMat, false);
rightEye.position.set(2.3, 0.5, 1);
head.add(rightEye);
for(let i=-1; i<=1; i+=2) {
const fang = new THREE.Mesh(new THREE.ConeGeometry(0.2, 1, 4), teethMat);
fang.position.set(i*1.2, -1.5, 4.5);
fang.rotation.x = Math.PI;
head.add(fang);
}
// 下顎 (デフォルトは口を閉じる)
const jawGroup = new THREE.Group();
jawGroup.position.set(0, -1.5, 1);
jawGroup.rotation.x = 0.05; // 口を閉じた状態
const lowerJaw = createBox(3.8, 1.5, 6.5, bodyMat);
lowerJaw.position.set(0, -0.75, 2);
jawGroup.add(lowerJaw);
const tongue = createBox(2.5, 0.8, 4, mouthMat);
tongue.position.set(0, 0.2, 2.5);
jawGroup.add(tongue);
for(let i=-1; i<=1; i+=2) {
const fang = new THREE.Mesh(new THREE.ConeGeometry(0.2, 1.2, 4), teethMat);
fang.position.set(i*1.2, 0.5, 4.5);
jawGroup.add(fang);
}
head.add(jawGroup);
// --- 放射熱線のビーム生成 ---
const beamGroup = new THREE.Group();
// コア(中心の白い光)
const coreGeo = new THREE.CylinderGeometry(0.6, 2.0, 150, 16);
coreGeo.rotateX(Math.PI / 2); // Z軸方向に向ける
coreGeo.translate(0, 0, 75); // 前方に伸ばす
const coreMat = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, blending: THREE.AdditiveBlending });
const beamCore = new THREE.Mesh(coreGeo, coreMat);
beamGroup.add(beamCore);
// オーラ(周囲の青い光)
const auraGeo = new THREE.CylinderGeometry(2.0, 4.5, 150, 16);
auraGeo.rotateX(Math.PI / 2);
auraGeo.translate(0, 0, 75);
const auraMat = new THREE.MeshBasicMaterial({ color: 0x0066ff, transparent: true, opacity: 0.6, blending: THREE.AdditiveBlending });
const beamAura = new THREE.Mesh(auraGeo, auraMat);
beamGroup.add(beamAura);
// 口をもっと大きく開けるため、発射位置を上顎から離して喉の奥(下方向)へ調整
beamGroup.position.set(0, -1.5, 4.0);
beamGroup.visible = false;
head.add(beamGroup);
// --- 追加:ビームの向きを取得するためのターゲット ---
const beamTarget = new THREE.Object3D();
beamTarget.position.set(0, 0, 100);
beamGroup.add(beamTarget);
// ---------------------------
neckAndHead.add(head);
godzilla.add(neckAndHead);
// 腕
const createArm = (isLeft) => {
const arm = new THREE.Group();
const sign = isLeft ? -1 : 1;
const upperArm = createBox(3.5, 5.5, 4.5, bodyMat);
upperArm.position.set(sign * 6.5, 16, 5);
upperArm.rotation.z = sign * 0.4;
upperArm.rotation.x = -0.5;
arm.add(upperArm);
const lowerArm = createBox(2.8, 5.5, 3.5, bodyMat);
lowerArm.position.set(sign * 7.5, 12, 8);
lowerArm.rotation.x = -1.2;
lowerArm.rotation.y = sign * -0.2;
arm.add(lowerArm);
const hand = createBox(2.5, 2, 3, bodyMat);
hand.position.set(sign * 6.5, 9, 10.5);
hand.rotation.x = -1.5;
arm.add(hand);
for(let i=-1; i<=1; i++) {
const claw = new THREE.Mesh(new THREE.ConeGeometry(0.3, 1.5, 4), teethMat);
claw.position.set(sign * 6.5 + (i*0.8), 8, 11.5);
claw.rotation.x = -1.8;
arm.add(claw);
}
return arm;
};
godzilla.add(createArm(true));
godzilla.add(createArm(false));
// 足
const createLeg = (isLeft) => {
const leg = new THREE.Group();
const sign = isLeft ? -1 : 1;
const thigh = createBox(8.5, 11, 9, bodyMat);
thigh.position.set(sign * 7.5, 8, -1);
thigh.rotation.x = -0.2;
thigh.rotation.z = sign * -0.2;
leg.add(thigh);
const thighMuscle = createBox(6, 8, 5, bodyMat);
thighMuscle.position.set(sign * 7.5, 7, 3);
thighMuscle.rotation.x = -0.4;
leg.add(thighMuscle);
const calf = createBox(6, 6, 7, bodyMat);
calf.position.set(sign * 8, 4, 1.5);
leg.add(calf);
const foot = createBox(7, 3.5, 9, bodyMat);
foot.position.set(sign * 8.5, 1.75, 4.5);
foot.rotation.y = sign * 0.2;
leg.add(foot);
for(let i=-1; i<=1; i++) {
const claw = new THREE.Mesh(new THREE.ConeGeometry(0.6, 3.0, 4), teethMat);
claw.position.set(sign * 8.5 + (i*2.0), 1.0, 9.0);
claw.rotation.x = -1.2;
claw.rotation.y = sign * (i*0.1);
leg.add(claw);
}
if (!isLeft) {
leg.position.z = -2;
} else {
leg.position.z = 1;
}
return leg;
};
godzilla.add(createLeg(true));
godzilla.add(createLeg(false));
// 尻尾
const tail = new THREE.Group();
const tailSegments = 12;
const tailSpikeMats = []; // 尻尾のトゲのマテリアルを一時保存
for (let i = 0; i < tailSegments; i++) {
const size = 9 - (i * 0.7);
const seg = createBox(size, size*0.9, size, bodyMat);
const zPos = -6 - (i * 4);
let yPos = 8 - (i * 0.8);
if (i > 5) yPos = 4 - ((i-5) * 0.3);
if (yPos < size/2) yPos = size/2;
seg.position.set(0, yPos, zPos);
if (i % 2 === 0 && i < 10) {
const sMat = finMat.clone(); // 発光用にクローン
const spike = new THREE.Mesh(new THREE.ConeGeometry(size*0.2, size*0.8, 4), sMat);
spike.position.set(0, size*0.5, 0);
seg.add(spike);
tailSpikeMats.push(sMat);
}
tail.add(seg);
}
godzilla.add(tail);
// 尻尾の先端(配列の最後)から順番にするためリバースして追加
spikeMaterials.push(...tailSpikeMats.reverse());
// 背びれ
const fins = new THREE.Group();
const finPositions = [
{y: 22, z: 6, size: 2.5}, // 首
{y: 19.5, z: 2, size: 4.5},
{y: 16, z: -3, size: 7.5}, // 背中
{y: 11, z: -7, size: 6.0},
{y: 7, z: -10, size: 4.0},
{y: 4, z: -12, size: 2.0}, // 尻尾の付け根
];
function createSpikyFin(baseSize, matArray) {
const group = new THREE.Group();
const mainMat = finMat.clone(); // 発光用にクローン
matArray.push(mainMat);
const main = new THREE.Mesh(new THREE.ConeGeometry(baseSize*0.35, baseSize*3.5, 4), mainMat);
main.castShadow = true;
group.add(main);
const spikeCount = Math.floor(baseSize * 1.5);
for(let j=0; j<spikeCount; j++) {
const sSize = baseSize * (0.3 + Math.random()*0.4);
const subMat = finMat.clone();
matArray.push(subMat);
const spike = new THREE.Mesh(new THREE.ConeGeometry(sSize*0.3, sSize*3, 4), subMat);
spike.position.set(
(Math.random()-0.5) * baseSize * 0.6,
(Math.random()-0.5) * baseSize * 1.5,
(Math.random()-0.5) * baseSize * 0.6
);
spike.rotation.set(
(Math.random()-0.5) * 0.8, 0, (Math.random()-0.5) * 0.8
);
spike.castShadow = true;
group.add(spike);
}
return group;
}
// 尻尾側から首側へ向かって背びれを生成&マテリアル登録
const reversedFins = [...finPositions].reverse();
reversedFins.forEach((pos, i) => {
const cFin = createSpikyFin(pos.size, spikeMaterials);
cFin.position.set(0, pos.y, pos.z);
cFin.rotation.x = -0.3 - ((reversedFins.length - 1 - i)*0.15);
fins.add(cFin);
if (pos.size > 3) {
const lFin = createSpikyFin(pos.size * 0.6, spikeMaterials);
lFin.position.set(-pos.size*0.6, pos.y - pos.size*0.4, pos.z + 0.5);
lFin.rotation.x = -0.3 - ((reversedFins.length - 1 - i)*0.15);
lFin.rotation.z = 0.5;
fins.add(lFin);
const rFin = createSpikyFin(pos.size * 0.6, spikeMaterials);
rFin.position.set(pos.size*0.6, pos.y - pos.size*0.4, pos.z + 0.5);
rFin.rotation.x = -0.3 - ((reversedFins.length - 1 - i)*0.15);
rFin.rotation.z = -0.5;
fins.add(rFin);
}
});
godzilla.add(fins);
godzilla.scale.set(2.5, 2.5, 2.5);
scene.add(godzilla);
// --- 破壊ギミック用の変数 ---
const destructibles = []; // 破壊可能なオブジェクトのリスト
const activeDebris = []; // 飛び散っている最中の破片リスト
const beamRaycaster = new THREE.Raycaster();
// ビーム着弾時に光るライト
const hitLight = new THREE.PointLight(0xff5500, 0, 150);
scene.add(hitLight);
// 4. 破壊された街並み (既存のまま)
const floorGeo = new THREE.PlaneGeometry(300, 300);
const floorMat = new THREE.MeshStandardMaterial({ color: 0x332b22, roughness: 1.0 });
const floor = new THREE.Mesh(floorGeo, floorMat);
floor.rotation.x = -Math.PI / 2;
floor.receiveShadow = true;
scene.add(floor);
const buildings = new THREE.Group();
const buildingMats = [
new THREE.MeshLambertMaterial({ color: 0x555550 }),
new THREE.MeshLambertMaterial({ color: 0x444b55 }),
new THREE.MeshLambertMaterial({ color: 0x665a44 }),
new THREE.MeshLambertMaterial({ color: 0x333333 }),
new THREE.MeshLambertMaterial({ color: 0x4a2a22 })
];
for (let i = -6; i <= 6; i++) {
createBuilding(-25 - Math.random() * 5, i * 20 + (Math.random() - 0.5) * 10);
createBuilding(25 + Math.random() * 5, i * 20 + (Math.random() - 0.5) * 10);
createBuilding(-50 - Math.random() * 15, i * 25);
createBuilding(50 + Math.random() * 15, i * 25);
}
function createBuilding(x, z) {
if (Math.abs(x - (-25)) < 6 && Math.abs(z - 15) < 6) {
const wakoMat = buildingMats[0];
const base = new THREE.Mesh(new THREE.BoxGeometry(16, 15, 16), wakoMat);
base.position.set(x, 7.5, z);
base.castShadow = true;
base.receiveShadow = true;
buildings.add(base);
destructibles.push(base); // 追加:破壊対象に登録
const tower = new THREE.Mesh(new THREE.BoxGeometry(6, 10, 6), wakoMat);
tower.position.set(x + 2, 19, z - 2);
tower.rotation.z = -0.4;
tower.rotation.x = 0.2;
tower.castShadow = true;
buildings.add(tower);
destructibles.push(tower); // 追加
const debris = new THREE.Mesh(new THREE.BoxGeometry(7, 5, 8), wakoMat);
debris.position.set(x + 6, 2.5, z + 6);
debris.rotation.set(0.5, 0.8, -0.2);
debris.castShadow = true;
buildings.add(debris);
destructibles.push(debris); // 追加
return;
}
const width = 10 + Math.random() * 12;
const depth = 10 + Math.random() * 12;
const height = 5 + Math.random() * 15;
const mat = buildingMats[Math.floor(Math.random() * buildingMats.length)];
const bldg = new THREE.Mesh(new THREE.BoxGeometry(width, height, depth), mat);
bldg.position.set(x, height / 2, z);
if (Math.random() > 0.8) {
bldg.rotation.x = (Math.random() - 0.5) * 0.3;
bldg.rotation.z = (Math.random() - 0.5) * 0.3;
}
bldg.castShadow = true;
bldg.receiveShadow = true;
buildings.add(bldg);
destructibles.push(bldg); // 追加
const topDebrisCount = Math.floor(Math.random() * 3) + 1;
for(let j=0; j<topDebrisCount; j++) {
const tWidth = width * (0.3 + Math.random() * 0.5);
const tDepth = depth * (0.3 + Math.random() * 0.5);
const tHeight = 2 + Math.random() * 6;
const topObj = new THREE.Mesh(new THREE.BoxGeometry(tWidth, tHeight, tDepth), mat);
topObj.position.set(
x + (Math.random() - 0.5) * (width - tWidth),
height + tHeight / 2 - 1,
z + (Math.random() - 0.5) * (depth - tDepth)
);
topObj.rotation.set((Math.random()-0.5), (Math.random()-0.5), (Math.random()-0.5));
topObj.castShadow = true;
buildings.add(topObj);
destructibles.push(topObj); // 追加
}
}
scene.add(buildings);
const rubbleGeo = new THREE.BoxGeometry(1, 1, 1);
const rubbleMat = new THREE.MeshStandardMaterial({ color: 0x222222, roughness: 1.0 });
const emberMat = new THREE.MeshBasicMaterial({ color: 0xff4400 });
// --- 追加:ガラガラと崩れて燃える破片を生成する関数 ---
function createExplosion(position, scaleFactor) {
const count = Math.floor(Math.random() * 8) + 5;
for (let i = 0; i < count; i++) {
const mesh = new THREE.Mesh(rubbleGeo, emberMat); // 最初は燃えている
const size = Math.random() * 1.5 + Math.min(scaleFactor * 0.1, 2);
mesh.scale.set(size, size, size);
mesh.position.copy(position);
mesh.position.x += (Math.random() - 0.5) * scaleFactor;
mesh.position.y += (Math.random() - 0.5) * scaleFactor;
mesh.position.z += (Math.random() - 0.5) * scaleFactor;
scene.add(mesh);
activeDebris.push({
mesh: mesh,
velocity: new THREE.Vector3(
(Math.random() - 0.5) * 40,
Math.random() * 30 + 10, // 上方向に吹き飛ぶ
(Math.random() - 0.5) * 40
),
life: 1.5 + Math.random() * 2.0, // 燃え続ける時間(秒)
age: 0,
size: size
});
}
}
// --------------------------------------------------
for (let i = 0; i < 300; i++) {
const isEmber = Math.random() > 0.85;
const mesh = new THREE.Mesh(rubbleGeo, isEmber ? emberMat : rubbleMat);
const size = isEmber ? (Math.random() * 1.5 + 0.5) : (Math.random() * 4 + 1);
mesh.scale.set(size, size, size);
const rx = (Math.random() - 0.5) * 120;
const rz = (Math.random() - 0.5) * 120;
const ry = (isEmber && Math.random() > 0.5) ? Math.random() * 20 : size / 2;
mesh.position.set(rx, ry, rz);
mesh.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, Math.random() * Math.PI);
if (!isEmber) {
mesh.castShadow = true;
mesh.receiveShadow = true;
}
scene.add(mesh);
destructibles.push(mesh); // 追加:既存の瓦礫もビームで吹き飛ぶようにする
}
const trackGroup = new THREE.Group();
const railMat = new THREE.MeshStandardMaterial({ color: 0x555555, metalness: 0.8, roughness: 0.5 });
const sleeperMat = new THREE.MeshStandardMaterial({ color: 0x3a2a1a, roughness: 1.0 });
const trackZ = 35;
for(let x = -100; x <= 100; x += 3) {
if (Math.random() > 0.3) {
const sleeper = new THREE.Mesh(new THREE.BoxGeometry(1.5, 0.5, 6), sleeperMat);
sleeper.position.set(x, 0.25, trackZ + (Math.random()-0.5)*0.5);
sleeper.rotation.y = (Math.random() - 0.5) * 0.2;
sleeper.castShadow = true;
sleeper.receiveShadow = true;
trackGroup.add(sleeper);
destructibles.push(sleeper); // 追加
}
}
for(let x = -100; x <= 100; x += 10) {
if(Math.random() > 0.2) {
const rail1 = new THREE.Mesh(new THREE.BoxGeometry(10, 0.8, 0.5), railMat);
rail1.position.set(x, 0.6, trackZ - 1.5);
if(Math.random() > 0.7) {
rail1.rotation.y = (Math.random()-0.5) * 0.8;
rail1.position.y += Math.random() * 2.5;
rail1.rotation.z = (Math.random()-0.5) * 0.5;
}
rail1.castShadow = true;
trackGroup.add(rail1);
destructibles.push(rail1); // 追加
const rail2 = new THREE.Mesh(new THREE.BoxGeometry(10, 0.8, 0.5), railMat);
rail2.position.set(x, 0.6, trackZ + 1.5);
if(Math.random() > 0.7) {
rail2.rotation.y = (Math.random()-0.5) * 0.8;
rail2.position.y += Math.random() * 2.5;
rail2.rotation.z = (Math.random()-0.5) * 0.5;
}
rail2.castShadow = true;
trackGroup.add(rail2);
destructibles.push(rail2); // 追加
}
}
scene.add(trackGroup);
const planes = [];
const planeMat = new THREE.MeshStandardMaterial({ color: 0x3a4a3a, metalness: 0.6, roughness: 0.5 });
const propMat = new THREE.MeshStandardMaterial({ color: 0x111111 });
function createPlane() {
const plane = new THREE.Group();
const bodyGeo = new THREE.CylinderGeometry(0.5, 1.2, 8, 8);
bodyGeo.rotateX(Math.PI / 2);
const planeBody = new THREE.Mesh(bodyGeo, planeMat);
plane.add(planeBody);
const mainWing = new THREE.Mesh(new THREE.BoxGeometry(14, 0.2, 2.5), planeMat);
mainWing.position.set(0, 0, 1.5);
plane.add(mainWing);
const vWing1 = new THREE.Mesh(new THREE.BoxGeometry(0.2, 2.5, 2), planeMat);
vWing1.position.set(-4.5, 1, 1.5);
plane.add(vWing1);
const vWing2 = new THREE.Mesh(new THREE.BoxGeometry(0.2, 2.5, 2), planeMat);
vWing2.position.set(4.5, 1, 1.5);
plane.add(vWing2);
const frontWing = new THREE.Mesh(new THREE.BoxGeometry(5, 0.2, 1.2), planeMat);
frontWing.position.set(0, 0, -3);
plane.add(frontWing);
const canopyMat = new THREE.MeshStandardMaterial({ color: 0x112233, roughness: 0.1, metalness: 0.9 });
const canopy = new THREE.Mesh(new THREE.BoxGeometry(1.0, 1.2, 2.5), canopyMat);
canopy.position.set(0, 0.8, -0.5);
plane.add(canopy);
const propeller = new THREE.Group();
for(let j=0; j<6; j++) {
const blade = new THREE.Mesh(new THREE.BoxGeometry(3.5, 0.1, 0.3), propMat);
blade.rotation.z = (Math.PI / 3) * j;
propeller.add(blade);
}
propeller.position.set(0, 0, 4.2);
plane.add(propeller);
plane.propeller = propeller;
plane.scale.set(1.5, 1.5, 1.5);
plane.castShadow = true;
return plane;
}
for(let i=0; i<3; i++) {
const p = createPlane();
scene.add(p);
planes.push({
mesh: p,
angle: Math.PI * 0.6 * i,
radius: 70 + i * 15,
speed: 1.2 + i * 0.1,
yOffset: 65 + i * 10
});
}
// --- インタラクションとシークエンス制御 ---
let isFiring = false;
let fireStartTime = 0;
// クリック検知 (Raycaster)
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
window.addEventListener('pointerdown', (event) => {
// 画面クリック時に確実にフォーカスを当ててキーボード入力を受け付けるようにする
window.focus();
if (isFiring) return; // 既に発射中なら無視
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
// ゴジラをクリックしたか判定
const intersects = raycaster.intersectObject(godzilla, true);
if (intersects.length > 0) {
isFiring = true;
fireStartTime = clock.getElapsedTime();
}
});
// キーボード操作 (十字キーで首を動かす)
const keys = { ArrowUp: false, ArrowDown: false, ArrowLeft: false, ArrowRight: false };
let keyRotX = 0; // キーによる縦の回転オフセット
let keyRotY = 0; // キーによる横の回転オフセット
// documentに対してイベントを付けることで、入力を検知しやすくする
document.addEventListener('keydown', (e) => {
if (keys.hasOwnProperty(e.code)) {
keys[e.code] = true;
e.preventDefault(); // 画面のスクロールを防ぐ
}
}, { passive: false });
document.addEventListener('keyup', (e) => {
if (keys.hasOwnProperty(e.code)) {
keys[e.code] = false;
}
});
// 5. アニメーション
const clock = new THREE.Clock();
let lastTime = 0; // 追加:経過時間を正確に取るため
function animate() {
requestAnimationFrame(animate);
const time = clock.getElapsedTime();
let dt = time - lastTime;
if (dt > 0.1) dt = 0.016; // タブ復帰時の経過時間異常を防ぐ
lastTime = time;
// 十字キーによる首の回転オフセットの更新
if (keys.ArrowUp) keyRotX -= 0.03;
if (keys.ArrowDown) keyRotX += 0.03;
if (keys.ArrowLeft) keyRotY += 0.03;
if (keys.ArrowRight) keyRotY -= 0.03;
// 限界角を設定
keyRotX = THREE.MathUtils.clamp(keyRotX, -0.6, 0.6);
keyRotY = THREE.MathUtils.clamp(keyRotY, -0.8, 0.8);
// 基本モーションの変数
let breathY = Math.sin(time * 1.0) * 0.15;
let neckBreathY = Math.sin(time * 1.0 - 0.5) * 0.2;
let animBodyX = 0.2; // 上半身の基本角度
let animHeadX = -0.1; // 頭の基本角度
let animJawX = 0.05; // 基本は口を閉じる
let shakeX = 0; // 振動エフェクト
let shakeY = 0;
if (isFiring) {
const t = time - fireStartTime;
if (t < 3.0) {
// --- フェーズ1:チャージ(背びれが光り、力を込めて下を向く) ---
const progress = t / 3.0; // 0.0 ~ 1.0
// 尻尾から順番に発光させる
const glowIndex = Math.floor(progress * spikeMaterials.length);
spikeMaterials.forEach((mat, index) => {
if (index <= glowIndex) {
// 光った場所は明滅する
const intensity = 0.5 + Math.sin(time * 20) * 0.5;
mat.emissive.setHex(0x0088ff);
mat.emissiveIntensity = intensity * 2.0;
} else {
mat.emissive.setHex(0x000000);
}
});
// 力を込めてゆっくり下を向くモーション (後半から)
if (t > 1.5) {
const dipProgress = (t - 1.5) / 1.5;
animHeadX = -0.1 + dipProgress * 0.7; // 下を向く
animBodyX = 0.2 + dipProgress * 0.25; // 前のめり
// プルプルと力みで震える
shakeX = (Math.random() - 0.5) * 0.15;
shakeY = (Math.random() - 0.5) * 0.15;
}
} else if (t < 3.6) {
// --- フェーズ2:振り上げ(顔を上げ、ガバッと口を開ける) ---
const upProgress = (t - 3.0) / 0.6; // 0.0 ~ 1.0
animHeadX = 0.6 - upProgress * 1.0; // 下から上へ一気に振り上げる
animBodyX = 0.45 - upProgress * 0.2; // 少し起き上がる
animJawX = 0.05 + upProgress * 1.15; // 顎が外れるくらい大きく口を開ける
// 背びれ全体が最大発光
spikeMaterials.forEach(mat => {
mat.emissive.setHex(0x00aaff);
mat.emissiveIntensity = 2.0 + Math.sin(time * 30) * 1.0;
});
} else if (t < 6.5) {
// --- フェーズ3:熱線放射! ---
animHeadX = -0.4; // 放射中の基本角度 (少し上向き)
animJawX = 1.2; // 全開(大きく開いた状態をキープ)
beamGroup.visible = true;
// ビームの揺らぎエフェクト
beamCore.scale.set(1 + Math.random()*0.3, 1, 1 + Math.random()*0.3);
beamAura.scale.set(1 + Math.random()*0.4, 1, 1 + Math.random()*0.4);
beamAura.material.opacity = 0.4 + Math.random()*0.6;
// 激しい発光
spikeMaterials.forEach(mat => {
mat.emissive.setHex(0x00aaff);
mat.emissiveIntensity = 2.5 + Math.random() * 1.5;
});
// 放射の反動で激しく震える
shakeX = (Math.random() - 0.5) * 0.4;
shakeY = (Math.random() - 0.5) * 0.4;
// --- 追加:ビームの当たり判定と破壊処理 ---
const beamStart = new THREE.Vector3();
beamGroup.getWorldPosition(beamStart);
const beamEnd = new THREE.Vector3();
beamTarget.getWorldPosition(beamEnd);
// ビームの進む方向を計算
const beamDir = new THREE.Vector3().subVectors(beamEnd, beamStart).normalize();
beamRaycaster.set(beamStart, beamDir);
const intersects = beamRaycaster.intersectObjects(destructibles, false);
if (intersects.length > 0) {
const hitPoint = intersects[0].point;
hitLight.position.copy(hitPoint);
hitLight.intensity = 3.0; // 着弾点をオレンジ色に強く光らせる
// ヒットポイント周辺のオブジェクトを巻き込んで破壊
const destroyRadius = 15;
for (let i = destructibles.length - 1; i >= 0; i--) {
const obj = destructibles[i];
if (obj.position.distanceTo(hitPoint) < destroyRadius) {
let scaleStr = 5;
if (obj.geometry && obj.geometry.parameters && obj.geometry.parameters.height) {
scaleStr = obj.geometry.parameters.height;
}
// 爆発(破片)生成
createExplosion(obj.position, scaleStr);
// ビルや瓦礫をシーンから削除
if (obj.parent) obj.parent.remove(obj);
destructibles.splice(i, 1);
}
}
}
// ------------------------------------
} else if (t < 8.0) {
// --- フェーズ4:クールダウン ---
const coolProgress = (t - 6.5) / 1.5;
beamGroup.visible = false;
animJawX = 1.2 * (1.0 - coolProgress); // ゆっくり口を閉じる
// 発光がスーッと消えていく
spikeMaterials.forEach(mat => {
mat.emissiveIntensity = (1.0 - coolProgress) * 2.5;
});
} else {
// 終了して元に戻る
isFiring = false;
beamGroup.visible = false;
spikeMaterials.forEach(mat => {
mat.emissive.setHex(0x000000);
mat.emissiveIntensity = 0;
});
}
}
// モーションの適用(アニメーションの基本姿勢 + 揺れ)
body.position.y = breathY;
body.position.x = shakeX;
upperBody.rotation.x = animBodyX;
jawGroup.rotation.x = animJawX;
neckAndHead.position.y = neckBreathY;
neckAndHead.position.x = shakeX;
// 頭の角度は「アニメーションの基本角度 + 十字キーでの操作角度」を滑らかに適用
const targetHeadX = animHeadX + keyRotX;
const targetHeadY = keyRotY;
head.rotation.x += (targetHeadX - head.rotation.x) * 0.15;
head.rotation.y += (targetHeadY - head.rotation.y) * 0.15;
// 尻尾のゆらゆら
tail.children.forEach((seg, index) => {
seg.position.x = Math.sin(time * 1.5 - index * 0.3) * (index * 0.2);
});
// 戦闘機のアニメーション
planes.forEach(planeData => {
const p = planeData.mesh;
const currentAngle = planeData.angle - time * planeData.speed;
p.position.x = Math.cos(currentAngle) * planeData.radius;
p.position.z = Math.sin(currentAngle) * planeData.radius;
p.position.y = planeData.yOffset + Math.sin(time * 3 + planeData.angle) * 3;
p.rotation.y = -currentAngle;
p.rotation.z = -0.4;
p.rotation.x = 0.1;
p.propeller.rotation.z += 0.8;
});
// --- 追加:飛び散る破片の物理挙動と燃焼効果の更新 ---
for (let i = activeDebris.length - 1; i >= 0; i--) {
const debris = activeDebris[i];
debris.age += dt;
debris.velocity.y -= 80 * dt; // 重力で落ちる
debris.mesh.position.addScaledVector(debris.velocity, dt);
debris.mesh.rotation.x += dt * 5;
debris.mesh.rotation.y += dt * 5;
// 床でのバウンドと摩擦
if (debris.mesh.position.y < debris.size / 2) {
debris.mesh.position.y = debris.size / 2;
debris.velocity.y *= -0.4; // 弾む
debris.velocity.x *= 0.7; // 摩擦で減速
debris.velocity.z *= 0.7;
}
// 燃え尽きたら色を黒い瓦礫に戻す
if (debris.age > debris.life && debris.mesh.material === emberMat) {
debris.mesh.material = rubbleMat;
}
// 動かなくなってしばらくしたら更新対象から外す(ただの瓦礫として残す)
if (debris.age > debris.life + 3.0) {
activeDebris.splice(i, 1);
}
}
// 着弾ライトを徐々に暗くする
if (hitLight.intensity > 0) {
hitLight.intensity = Math.max(0, hitLight.intensity - dt * 8.0);
}
// ----------------------------------------------------
controls.update();
renderer.render(scene, camera);
}
animate();
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
};
</script>
</body>
</html>
ゴジラがビームを出します。