<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>マザーレギオン&ソルジャーレギオン 3D Viewer - ガメラ2</title>
<!-- Tailwind CSS CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- 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>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #030712;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
user-select: none;
}
canvas {
display: block;
width: 100vw;
height: 100vh;
}
.glass-panel {
background: rgba(15, 23, 42, 0.82);
backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.12);
}
.glow-button {
transition: all 0.25s ease;
box-shadow: 0 0 10px rgba(217, 119, 6, 0.2);
}
.glow-button:hover {
box-shadow: 0 0 20px rgba(217, 119, 6, 0.6);
transform: translateY(-2px);
}
</style>
</head>
<body class="text-slate-100">
<!-- UI Header Overlay -->
<div class="absolute top-4 left-4 z-10 glass-panel rounded-xl p-4 max-w-xs md:max-w-sm pointer-events-auto">
<div class="flex items-center space-x-3 mb-2">
<div class="w-3 h-3 rounded-full bg-amber-500 animate-pulse"></div>
<h1 class="text-lg font-bold tracking-wider text-amber-400">レギオン 3Dビューアー</h1>
</div>
<p class="text-xs text-slate-300 leading-relaxed mb-3">
『ガメラ2 レギオン降臨』(1996)<br>
黄金の巨大母体「マザーレギオン」と、前進し蠢く群生甲虫「ソルジャーレギオン」。
</p>
<div class="flex flex-wrap gap-2 text-xs">
<span class="bg-amber-950/80 text-amber-300 border border-amber-700/50 px-2 py-0.5 rounded">マザー: 140m</span>
<span class="bg-purple-950/80 text-purple-300 border border-purple-700/50 px-2 py-0.5 rounded">ソルジャー: 3m (1300体・常時前進)</span>
<span class="bg-slate-800 text-slate-300 px-2 py-0.5 rounded">珪素生命体</span>
</div>
</div>
<!-- Control Panel -->
<div class="absolute bottom-6 left-1/2 -translate-x-1/2 z-10 glass-panel rounded-2xl p-3 flex items-center space-x-3 pointer-events-auto shadow-2xl">
<button id="btn-attack" class="glow-button bg-gradient-to-r from-amber-600 to-red-600 hover:from-amber-500 hover:to-red-500 text-white font-semibold text-xs sm:text-sm px-4 py-2.5 rounded-xl flex items-center space-x-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
<span>マイクロ波照射</span>
</button>
<button id="btn-toggle-swarm" class="bg-slate-800 hover:bg-slate-700 text-slate-200 font-medium text-xs sm:text-sm px-3.5 py-2.5 rounded-xl border border-slate-700 transition">
ソルジャー群: <span id="swarm-status" class="text-purple-400 font-bold">ON (1300体前進中)</span>
</button>
<button id="btn-reset-cam" class="bg-slate-800 hover:bg-slate-700 text-slate-200 font-medium text-xs sm:text-sm px-3.5 py-2.5 rounded-xl border border-slate-700 transition">
視点リセット
</button>
</div>
<!-- Instructions Overlay -->
<div class="absolute bottom-6 right-6 z-10 hidden md:block text-right pointer-events-none">
<p class="text-xs text-slate-400">ドラッグ: 視点回転 | ホイール: ズーム | 右クリック: 平行移動</p>
</div>
<!-- 3D Scene Container Script -->
<script>
let scene, camera, renderer, controls;
let motherGroup, soldierGroup, attackBeam, particleSystem;
let motherHorns = { left: null, right: null };
let motherEggSac, hornCoreLight;
let isAttacking = false;
let attackProgress = 0;
let showSwarm = true;
let soldierUnits = [];
let instancedSoldierMeshes = [];
let soldierComponents = [];
const clock = new THREE.Clock();
// Color & Material Definitions
const materials = {
carapace: new THREE.MeshStandardMaterial({
color: 0xcfb06b,
roughness: 0.35,
metalness: 0.35,
flatShading: true
}),
carapaceAccent: new THREE.MeshStandardMaterial({
color: 0x9e7232,
roughness: 0.4,
metalness: 0.4
}),
carapaceDark: new THREE.MeshStandardMaterial({
color: 0x141210,
roughness: 0.8,
metalness: 0.15
}),
hornCore: new THREE.MeshStandardMaterial({
color: 0xffaa00,
emissive: 0xff5500,
emissiveIntensity: 1.2,
roughness: 0.2
}),
eggSac: new THREE.MeshStandardMaterial({
color: 0xc41414,
emissive: 0x880505,
emissiveIntensity: 1.1,
transparent: true,
opacity: 0.95,
roughness: 0.25
}),
eggCore: new THREE.MeshStandardMaterial({
color: 0xff3300,
emissive: 0xff0000,
emissiveIntensity: 2.2
}),
eyeBlue: new THREE.MeshStandardMaterial({
color: 0x00f0ff,
emissive: 0x00aaff,
emissiveIntensity: 2.5,
roughness: 0.1
}),
// --- Soldier Legion Glossy Deep Violet Material ---
soldierCarapace: new THREE.MeshStandardMaterial({
color: 0x241733,
roughness: 0.28,
metalness: 0.45,
flatShading: true
}),
soldierCarapaceDark: new THREE.MeshStandardMaterial({
color: 0x140a1f,
roughness: 0.5,
metalness: 0.2
}),
soldierJaw: new THREE.MeshStandardMaterial({
color: 0x36234a,
roughness: 0.2,
metalness: 0.6
}),
beam: new THREE.MeshBasicMaterial({
color: 0xffcc33,
transparent: true,
opacity: 0.0,
side: THREE.DoubleSide
})
};
window.onload = function() {
initScene();
buildEnvironment();
buildMotherLegion();
buildSoldierLegionSwarm();
buildParticleSystem();
setupUIEvents();
animate();
};
function initScene() {
scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x030712, 0.012);
camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 14, 34);
renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: "high-performance" });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.25;
document.body.appendChild(renderer.domElement);
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.maxPolarAngle = Math.PI / 2 + 0.05;
controls.minDistance = 5;
controls.maxDistance = 100;
controls.target.set(0, 5, 0);
// Lighting Setup
const ambientLight = new THREE.AmbientLight(0x282338, 1.5);
scene.add(ambientLight);
const mainLight = new THREE.DirectionalLight(0xfffaed, 2.5);
mainLight.position.set(25, 45, 20);
mainLight.castShadow = true;
mainLight.shadow.mapSize.width = 2048;
mainLight.shadow.mapSize.height = 2048;
scene.add(mainLight);
const violetRim = new THREE.DirectionalLight(0x8b5cf6, 2.2);
violetRim.position.set(-25, 25, -20);
scene.add(violetRim);
const warmFillLight = new THREE.DirectionalLight(0xd97706, 1.2);
warmFillLight.position.set(0, -10, 20);
scene.add(warmFillLight);
hornCoreLight = new THREE.PointLight(0xff7700, 2, 25);
scene.add(hornCoreLight);
}
function buildEnvironment() {
const groundGeo = new THREE.PlaneGeometry(140, 140);
const groundMat = new THREE.MeshStandardMaterial({
color: 0x080b14,
roughness: 0.9,
metalness: 0.2
});
const ground = new THREE.Mesh(groundGeo, groundMat);
ground.rotation.x = -Math.PI / 2;
ground.position.y = -0.1;
ground.receiveShadow = true;
scene.add(ground);
const gridHelper = new THREE.GridHelper(140, 70, 0x8b5cf6, 0x1e293b);
gridHelper.position.y = 0;
scene.add(gridHelper);
}
function buildMotherLegion() {
motherGroup = new THREE.Group();
motherGroup.position.set(-2, 0, 0);
const bodyContainer = new THREE.Group();
bodyContainer.rotation.x = -0.22;
motherGroup.add(bodyContainer);
// Spine / Neck
for (let i = 0; i < 7; i++) {
const seg = new THREE.Mesh(
new THREE.CylinderGeometry(1.8 - i * 0.1, 2.2 - i * 0.1, 1.2, 12),
materials.carapaceDark
);
seg.position.set(0, 3.2 + i * 0.8, -1.2 - i * 0.7);
seg.rotation.x = 0.45;
seg.castShadow = true;
bodyContainer.add(seg);
}
// Thorax Shells
const thoraxGroup = new THREE.Group();
for (let i = 0; i < 6; i++) {
const shell = new THREE.Mesh(
new THREE.ConeGeometry(2.6 - i * 0.25, 3.2, 6),
materials.carapace
);
shell.rotation.x = Math.PI / 2.8 + (i * 0.05);
shell.position.set(0, 7.8 + i * 0.5, 0.2 - i * 1.2);
shell.scale.set(1.3, 1.1, 0.85);
shell.castShadow = true;
thoraxGroup.add(shell);
}
// Thoracic Rib Spikes
for (let side of [-1, 1]) {
for (let i = 0; i < 5; i++) {
const spikeGroup = new THREE.Group();
spikeGroup.position.set(side * (1.6 + i * 0.2), 6.2 + i * 0.5, 1.2 - i * 1.1);
const spikeBase = new THREE.Mesh(
new THREE.ConeGeometry(0.4, 3.2 - i * 0.3, 5),
materials.carapace
);
spikeBase.rotation.z = side * -1.25;
spikeBase.rotation.x = 0.25;
spikeBase.rotation.y = side * 0.4;
spikeBase.castShadow = true;
spikeGroup.add(spikeBase);
const spikeTip = new THREE.Mesh(
new THREE.ConeGeometry(0.2, 1.8, 4),
materials.carapaceAccent
);
spikeTip.position.set(side * 1.6, 0.4, 0.4);
spikeTip.rotation.z = side * -0.9;
spikeTip.rotation.x = 0.5;
spikeGroup.add(spikeTip);
thoraxGroup.add(spikeGroup);
}
}
// Chest Red Egg Cluster
const chestEggGroup = new THREE.Group();
chestEggGroup.position.set(0, 6.2, 1.6);
const chestFrame = new THREE.Mesh(
new THREE.TorusGeometry(1.4, 0.35, 8, 16),
materials.carapaceAccent
);
chestFrame.rotation.x = 0.3;
chestFrame.scale.set(1.0, 1.4, 0.8);
chestEggGroup.add(chestFrame);
const chestBase = new THREE.Mesh(
new THREE.SphereGeometry(1.4, 16, 16),
materials.eggSac
);
chestBase.scale.set(0.9, 1.3, 0.7);
chestEggGroup.add(chestBase);
for (let i = 0; i < 40; i++) {
const egg = new THREE.Mesh(
new THREE.SphereGeometry(0.22, 8, 8),
materials.eggCore
);
const phi = Math.random() * Math.PI;
const theta = (Math.random() - 0.5) * Math.PI * 1.2;
const radius = 1.1 + Math.random() * 0.25;
egg.position.set(
radius * Math.sin(phi) * Math.sin(theta) * 0.8,
radius * Math.cos(phi) * 1.2,
radius * Math.sin(phi) * Math.cos(theta) * 0.5 + 0.3
);
chestEggGroup.add(egg);
}
thoraxGroup.add(chestEggGroup);
bodyContainer.add(thoraxGroup);
// Abdomen / Lower Egg Sac
const eggGroup = new THREE.Group();
motherEggSac = new THREE.Mesh(
new THREE.SphereGeometry(3.4, 20, 20),
materials.eggSac
);
motherEggSac.scale.set(0.95, 1.15, 2.2);
motherEggSac.position.set(0, 3.5, -7.5);
motherEggSac.castShadow = true;
eggGroup.add(motherEggSac);
for (let i = 0; i < 20; i++) {
const egg = new THREE.Mesh(
new THREE.SphereGeometry(0.42, 10, 10),
materials.eggCore
);
const angle = Math.random() * Math.PI * 2;
const dist = Math.random() * 1.8;
const z = (Math.random() - 0.5) * 5.0;
egg.position.set(Math.cos(angle) * dist, Math.sin(angle) * dist, z);
motherEggSac.add(egg);
}
bodyContainer.add(eggGroup);
// Head, Mouth Claws & Crest Horn
const headGroup = new THREE.Group();
headGroup.position.set(0, 9.2, 2.8);
for (let side of [-1, 1]) {
const eye = new THREE.Mesh(
new THREE.SphereGeometry(0.35, 12, 12),
materials.eyeBlue
);
eye.position.set(side * 1.0, 0.4, 1.4);
eye.scale.set(1.2, 0.8, 1.5);
headGroup.add(eye);
}
for (let side of [-1, 1]) {
for (let j = 0; j < 5; j++) {
const clawArm = new THREE.Group();
clawArm.position.set(side * (0.3 + j * 0.32), -0.2 - j * 0.35, 1.6 + j * 0.45);
const seg1 = new THREE.Mesh(
new THREE.CylinderGeometry(0.18 - j * 0.02, 0.28 - j * 0.02, 1.6, 6),
materials.carapaceAccent
);
seg1.rotation.x = Math.PI / 2.3;
seg1.rotation.z = side * (-0.3 - j * 0.15);
clawArm.add(seg1);
const tipClaw = new THREE.Mesh(
new THREE.ConeGeometry(0.16 - j * 0.02, 1.6, 5),
materials.carapace
);
tipClaw.position.set(side * 0.2, 0.1, 1.2);
tipClaw.rotation.x = Math.PI / 1.8;
tipClaw.rotation.z = side * (-0.2);
clawArm.add(tipClaw);
headGroup.add(clawArm);
}
}
const hornCoreGeo = new THREE.ConeGeometry(0.7, 5.5, 8);
const hornCoreMesh = new THREE.Mesh(hornCoreGeo, materials.hornCore);
hornCoreMesh.rotation.x = Math.PI / 2.2;
hornCoreMesh.position.set(0, 0.8, 2.2);
headGroup.add(hornCoreMesh);
motherHorns.left = new THREE.Group();
motherHorns.left.position.set(-0.3, 0, 0);
const leftMainShell = new THREE.Mesh(
new THREE.CylinderGeometry(0.25, 0.8, 8.5, 7),
materials.carapace
);
leftMainShell.rotation.x = Math.PI / 3.0;
leftMainShell.rotation.z = -0.18;
leftMainShell.position.set(-0.6, 2.8, 2.8);
leftMainShell.castShadow = true;
motherHorns.left.add(leftMainShell);
const leftUpperTip = new THREE.Mesh(
new THREE.ConeGeometry(0.32, 4.8, 6),
materials.carapaceAccent
);
leftUpperTip.rotation.x = Math.PI / 1.75;
leftUpperTip.rotation.z = -0.1;
leftUpperTip.position.set(-0.75, 5.8, 6.0);
leftUpperTip.castShadow = true;
motherHorns.left.add(leftUpperTip);
motherHorns.right = new THREE.Group();
motherHorns.right.position.set(0.3, 0, 0);
const rightMainShell = leftMainShell.clone();
rightMainShell.rotation.z = 0.18;
rightMainShell.position.x = 0.6;
motherHorns.right.add(rightMainShell);
const rightUpperTip = leftUpperTip.clone();
rightUpperTip.rotation.z = 0.1;
rightUpperTip.position.x = 0.75;
motherHorns.right.add(rightUpperTip);
headGroup.add(motherHorns.left);
headGroup.add(motherHorns.right);
bodyContainer.add(headGroup);
// Front Scythe Arms
for (let side of [-1, 1]) {
const scytheArm = new THREE.Group();
scytheArm.position.set(side * 3.0, 7.8, 2.2);
const joint = new THREE.Mesh(new THREE.SphereGeometry(0.95, 12, 12), materials.carapaceAccent);
scytheArm.add(joint);
const upperArm = new THREE.Mesh(
new THREE.BoxGeometry(0.8, 4.5, 1.2),
materials.carapace
);
upperArm.position.set(side * 1.2, -1.8, 0.8);
upperArm.rotation.z = side * -0.5;
scytheArm.add(upperArm);
const mainBlade = new THREE.Mesh(
new THREE.ConeGeometry(0.7, 9.5, 6),
materials.carapace
);
mainBlade.position.set(side * 2.2, -5.2, 2.8);
mainBlade.rotation.z = side * -0.3;
mainBlade.rotation.x = 0.75;
mainBlade.castShadow = true;
scytheArm.add(mainBlade);
bodyContainer.add(scytheArm);
}
// Vertical Blade Legs
for (let i = 0; i < 3; i++) {
for (let side of [-1, 1]) {
const legGroup = new THREE.Group();
legGroup.position.set(side * 2.5, 4.2, -i * 3.2 + 0.4);
const upperPlate = new THREE.Mesh(
new THREE.BoxGeometry(0.7, 10.0, 2.2),
materials.carapace
);
upperPlate.position.set(side * 2.8, 3.8, 0);
upperPlate.rotation.z = side * -0.32;
upperPlate.rotation.x = -0.15 + i * 0.12;
upperPlate.castShadow = true;
legGroup.add(upperPlate);
const topFin = new THREE.Mesh(
new THREE.ConeGeometry(1.5, 6.5, 4),
materials.carapaceAccent
);
topFin.position.set(side * 3.8, 7.8, -0.6);
topFin.rotation.z = side * -0.45;
topFin.rotation.x = -0.2;
topFin.scale.set(0.4, 1.0, 1.2);
topFin.castShadow = true;
legGroup.add(topFin);
const lowerLeg = new THREE.Mesh(
new THREE.ConeGeometry(0.5, 8.0, 5),
materials.carapaceAccent
);
lowerLeg.position.set(side * 4.4, -2.8, 0.5);
lowerLeg.rotation.z = side * 0.22;
lowerLeg.rotation.x = 0.2;
lowerLeg.castShadow = true;
legGroup.add(lowerLeg);
bodyContainer.add(legGroup);
}
}
// Attack Beam Cone
const beamGeo = new THREE.ConeGeometry(7.0, 42, 16, 1, true);
attackBeam = new THREE.Mesh(beamGeo, materials.beam);
attackBeam.rotation.x = -Math.PI / 2;
attackBeam.position.set(0, 9.8, 23);
bodyContainer.add(attackBeam);
scene.add(motherGroup);
}
function createSingleSoldierModel() {
const soldier = new THREE.Group();
// 1. Thorax / Spine
const headContainer = new THREE.Group();
headContainer.position.set(0, 0.75, 0.8);
headContainer.rotation.x = 0.35;
// Main Head-Thorax Shell Body
const headShellGeo = new THREE.ConeGeometry(0.55, 2.2, 7);
const headShell = new THREE.Mesh(headShellGeo, materials.soldierCarapace);
headShell.rotation.x = Math.PI / 2.3;
headShell.castShadow = true;
headContainer.add(headShell);
// Segmented Ridges along spine
for (let i = 0; i < 4; i++) {
const ridge = new THREE.Mesh(
new THREE.TorusGeometry(0.42 - i * 0.08, 0.1, 6, 10),
materials.soldierCarapaceDark
);
ridge.rotation.x = Math.PI / 2.1;
ridge.position.set(0, 0.1, -i * 0.35 + 0.2);
headContainer.add(ridge);
}
// 2. Large Curved Top Crest Horn
const crestGroup = new THREE.Group();
crestGroup.position.set(0, 0.35, -0.1);
const crestBase = new THREE.Mesh(
new THREE.ConeGeometry(0.28, 1.8, 5),
materials.soldierCarapace
);
crestBase.rotation.x = -Math.PI / 3.2;
crestBase.position.set(0, 0.6, -0.3);
crestBase.scale.set(0.45, 1.2, 1.0);
crestBase.castShadow = true;
crestGroup.add(crestBase);
const crestTip = new THREE.Mesh(
new THREE.ConeGeometry(0.18, 1.2, 4),
materials.soldierJaw
);
crestTip.rotation.x = -Math.PI / 2.2;
crestTip.position.set(0, 1.2, -0.7);
crestTip.scale.set(0.35, 1.0, 0.8);
crestGroup.add(crestTip);
headContainer.add(crestGroup);
// 3. Dual Forked Mouth Jaws / Mandibles
for (let side of [-1, 1]) {
const jawGroup = new THREE.Group();
jawGroup.position.set(side * 0.22, -0.15, 1.1);
const upperJaw = new THREE.Mesh(
new THREE.ConeGeometry(0.12, 1.1, 5),
materials.soldierJaw
);
upperJaw.rotation.x = Math.PI / 1.7;
upperJaw.rotation.z = side * -0.25;
jawGroup.add(upperJaw);
const lowerJaw = new THREE.Mesh(
new THREE.ConeGeometry(0.08, 0.8, 4),
materials.soldierCarapaceDark
);
lowerJaw.position.set(0, -0.18, 0.2);
lowerJaw.rotation.x = Math.PI / 1.5;
lowerJaw.rotation.z = side * -0.1;
jawGroup.add(lowerJaw);
headContainer.add(jawGroup);
}
// 4. Large Front Scythe Legs
for (let side of [-1, 1]) {
const frontScythe = new THREE.Group();
frontScythe.position.set(side * 0.55, 0.1, 0.4);
const joint = new THREE.Mesh(
new THREE.SphereGeometry(0.18, 8, 8),
materials.soldierCarapaceDark
);
frontScythe.add(joint);
const mainBlade = new THREE.Mesh(
new THREE.ConeGeometry(0.16, 2.6, 5),
materials.soldierCarapace
);
mainBlade.position.set(side * 0.35, -0.9, 0.3);
mainBlade.rotation.z = side * -0.4;
mainBlade.rotation.x = 0.35;
mainBlade.castShadow = true;
frontScythe.add(mainBlade);
headContainer.add(frontScythe);
}
soldier.add(headContainer);
// 5. Lateral Walking Legs (3 Pairs)
for (let i = 0; i < 3; i++) {
for (let side of [-1, 1]) {
const leg = new THREE.Group();
leg.position.set(side * 0.45, 0.4, -i * 0.45 + 0.2);
const upperLeg = new THREE.Mesh(
new THREE.CylinderGeometry(0.06, 0.1, 1.2),
materials.soldierCarapaceDark
);
upperLeg.position.set(side * 0.4, -0.1, 0);
upperLeg.rotation.z = side * -0.7;
upperLeg.rotation.x = -0.2 + i * 0.15;
upperLeg.castShadow = true;
leg.add(upperLeg);
const lowerSpike = new THREE.Mesh(
new THREE.ConeGeometry(0.05, 1.1, 4),
materials.soldierCarapace
);
lowerSpike.position.set(side * 0.7, -0.6, 0.1);
lowerSpike.rotation.z = side * 0.25;
leg.add(lowerSpike);
soldier.add(leg);
}
}
soldier.scale.set(0.9, 0.9, 0.9);
return soldier;
}
function buildSoldierLegionSwarm() {
soldierGroup = new THREE.Group();
soldierUnits = [];
instancedSoldierMeshes = [];
soldierComponents = [];
// 1. Template soldier to extract component geometries & local transforms
const templateSoldier = createSingleSoldierModel();
templateSoldier.position.set(0, 0, 0);
templateSoldier.rotation.set(0, 0, 0);
templateSoldier.scale.set(1, 1, 1);
templateSoldier.updateMatrixWorld(true);
templateSoldier.traverse((child) => {
if (child.isMesh) {
soldierComponents.push({
geometry: child.geometry,
material: child.material,
localMatrix: child.matrixWorld.clone()
});
}
});
// 2. Initialize InstancedMeshes
const count = 1300;
soldierComponents.forEach((comp) => {
const im = new THREE.InstancedMesh(comp.geometry, comp.material, count);
im.castShadow = true;
im.receiveShadow = true;
instancedSoldierMeshes.push(im);
soldierGroup.add(im);
});
// 3. Generate swarm units with independent movement vector & crawling parameters
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const distBias = Math.pow(Math.random(), 1.3);
const radius = 3.0 + distBias * 50.0;
const posX = -2 + Math.cos(angle) * radius;
const posZ = Math.sin(angle) * radius;
// Random orientation for forward movement
const moveAngle = Math.random() * Math.PI * 2;
const scale = 0.75 + Math.random() * 0.3;
soldierUnits.push({
posX: posX,
posZ: posZ,
baseY: 0,
angle: moveAngle,
scale: scale,
phase: Math.random() * Math.PI * 2,
crawlSpeed: 9.0 + Math.random() * 6.0, // 足の高速ワサワサ速度
moveSpeed: 2.2 + Math.random() * 3.8 // 実際の前進速度 (m/s)
});
}
scene.add(soldierGroup);
}
function buildParticleSystem() {
const particleCount = 220;
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(particleCount * 3);
for (let i = 0; i < particleCount * 3; i += 3) {
positions[i] = (Math.random() - 0.5) * 70;
positions[i + 1] = Math.random() * 30;
positions[i + 2] = (Math.random() - 0.5) * 70;
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const pMaterial = new THREE.PointsMaterial({
color: 0x8b5cf6,
size: 0.25,
transparent: true,
opacity: 0.5
});
particleSystem = new THREE.Points(geometry, pMaterial);
scene.add(particleSystem);
}
function setupUIEvents() {
document.getElementById('btn-attack').addEventListener('click', () => {
if (!isAttacking) {
isAttacking = true;
attackProgress = 0;
}
});
document.getElementById('btn-toggle-swarm').addEventListener('click', () => {
showSwarm = !showSwarm;
soldierGroup.visible = showSwarm;
document.getElementById('swarm-status').textContent = showSwarm ? 'ON (1300体前進中)' : 'OFF';
document.getElementById('swarm-status').className = showSwarm ? 'text-purple-400 font-bold' : 'text-slate-500 font-bold';
});
document.getElementById('btn-reset-cam').addEventListener('click', () => {
camera.position.set(0, 14, 34);
controls.target.set(0, 5, 0);
controls.update();
});
window.addEventListener('resize', onWindowResize);
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
const time = clock.getElapsedTime();
// Organic breathing motion for Mother Legion
if (motherGroup) {
motherGroup.position.y = Math.sin(time * 1.6) * 0.18;
if (motherEggSac) {
motherEggSac.scale.set(
0.95 + Math.sin(time * 3.0) * 0.04,
1.15 + Math.cos(time * 3.0) * 0.04,
2.2
);
}
}
// Animate 1,300 Instanced Soldier Legions ALWAYS MOVING FORWARD & CRAWLING
if (showSwarm && soldierUnits.length > 0 && instancedSoldierMeshes.length > 0) {
const tempMatrix = new THREE.Matrix4();
const tempPos = new THREE.Vector3();
const tempRot = new THREE.Euler();
const tempQuat = new THREE.Quaternion();
const tempScale = new THREE.Vector3();
const finalMatrix = new THREE.Matrix4();
for (let i = 0; i < soldierUnits.length; i++) {
const u = soldierUnits[i];
// 1. Forward movement calculation (towards facing angle)
u.posX += Math.sin(u.angle) * u.moveSpeed * delta;
u.posZ += Math.cos(u.angle) * u.moveSpeed * delta;
// Respawn near Mother Legion when wandering beyond 58 meters
const distFromMother = Math.hypot(u.posX - (-2), u.posZ);
if (distFromMother > 58.0) {
const respawnAngle = Math.random() * Math.PI * 2;
const respawnRadius = 2.0 + Math.random() * 8.0;
u.posX = -2 + Math.cos(respawnAngle) * respawnRadius;
u.posZ = Math.sin(respawnAngle) * respawnRadius;
u.angle = Math.random() * Math.PI * 2; // New forward angle
}
// 2. Dynamic step crawl motions (synchronized pitch & y-bounce with move speed)
const stepCycle = time * u.crawlSpeed + u.phase;
const yOffset = Math.abs(Math.sin(stepCycle)) * 0.12;
const pitch = Math.sin(stepCycle) * 0.08; // Forward-back tilt per gait
const roll = Math.sin(stepCycle * 0.8) * 0.04;
tempPos.set(u.posX, u.baseY + yOffset, u.posZ);
tempRot.set(pitch, u.angle, roll);
tempQuat.setFromEuler(tempRot);
tempScale.set(u.scale, u.scale, u.scale);
tempMatrix.compose(tempPos, tempQuat, tempScale);
for (let c = 0; c < soldierComponents.length; c++) {
finalMatrix.multiplyMatrices(tempMatrix, soldierComponents[c].localMatrix);
instancedSoldierMeshes[c].setMatrixAt(i, finalMatrix);
}
}
for (let c = 0; c < instancedSoldierMeshes.length; c++) {
instancedSoldierMeshes[c].instanceMatrix.needsUpdate = true;
}
}
// Horn & Microwave Shell Attack Animation
if (isAttacking) {
attackProgress += delta * 1.5;
if (attackProgress < 1.0) {
motherHorns.left.rotation.z = -attackProgress * 0.55;
motherHorns.right.rotation.z = attackProgress * 0.55;
materials.hornCore.emissiveIntensity = 1.2 + attackProgress * 4.0;
materials.beam.opacity = Math.sin(attackProgress * Math.PI) * 0.75;
hornCoreLight.intensity = attackProgress * 8;
hornCoreLight.position.set(-2, 9, 3);
} else {
isAttacking = false;
motherHorns.left.rotation.z = 0;
motherHorns.right.rotation.z = 0;
materials.hornCore.emissiveIntensity = 1.2;
materials.beam.opacity = 0.0;
hornCoreLight.intensity = 2;
}
} else {
materials.hornCore.emissiveIntensity = 1.2 + Math.sin(time * 4) * 0.3;
}
// Animate dust particles
if (particleSystem) {
const positions = particleSystem.geometry.attributes.position.array;
for (let i = 1; i < positions.length; i += 3) {
positions[i] -= delta * 0.8;
if (positions[i] < 0) positions[i] = 30;
}
particleSystem.geometry.attributes.position.needsUpdate = true;
}
controls.update();
renderer.render(scene, camera);
}
</script>
</body>
</html>
集合体恐怖症の人は注意