<!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モデル</title>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
background-color: #050505;
font-family: 'Courier New', Courier, monospace;
touch-action: none;
}
#canvas-container {
width: 100vw;
height: 100vh;
display: block;
cursor: pointer;
}
#ui-layer {
position: absolute;
top: 20px;
left: 20px;
color: #4488ff;
pointer-events: none;
text-shadow: 0 0 5px rgba(68, 136, 255, 0.5);
z-index: 10;
}
h1 {
margin: 0 0 10px 0;
font-size: 24px;
letter-spacing: 2px;
text-transform: uppercase;
transition: color 0.1s;
}
p {
margin: 5px 0;
font-size: 14px;
color: #ccc;
}
.hud-line {
width: 200px;
height: 1px;
background: linear-gradient(90deg, #4488ff, transparent);
margin: 10px 0;
}
#loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #fff;
font-size: 20px;
letter-spacing: 5px;
transition: opacity 0.5s;
z-index: 20;
text-align: center;
}
.highlight {
color: #ff3333;
font-weight: bold;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { opacity: 0.7; text-shadow: 0 0 5px #ff3333; }
50% { opacity: 1; text-shadow: 0 0 15px #ff3333; }
100% { opacity: 0.7; text-shadow: 0 0 5px #ff3333; }
}
@media (max-width: 768px) {
#ui-layer { top: 15px; left: 15px; }
h1 { font-size: 18px; }
p { font-size: 11px; }
.hud-line { width: 150px; }
#loading { font-size: 16px; }
}
</style>
</head>
<body>
<div id="loading">SYSTEM INITIALIZING...<br><span style="font-size:12px; color:#888;">LOADING HIVE DATA</span></div>
<div id="ui-layer" style="display:none;">
<h1 id="title-text">XENOMORPH QUEEN</h1>
<div class="hud-line"></div>
<p>CLASSIFICATION: XX121 - MATRIARCH</p>
<p>LOCATION: LV-426 ATMOSPHERE PROCESSOR</p>
<p class="highlight">▶ WASD / 矢印キー:移動</p>
<p>▶ ドラッグ / スワイプ:カメラ回転</p>
<p>▶ スクロール / ピンチ:ズーム</p>
<p>▶ クイーンをタップ:威嚇音</p>
</div>
<div id="canvas-container"></div>
<script type="module">
import * as THREE from 'https://esm.sh/three@0.128.0';
import { OrbitControls } from 'https://esm.sh/three@0.128.0/examples/jsm/controls/OrbitControls.js';
let scene, camera, renderer, controls;
let queenGroup, tailSegments = [], chest, headGroup, lowerJaw;
// 歩行アニメーション制御用の関節グループを保存する変数
let leftLegGroup, rightLegGroup;
let leftCalf, rightCalf;
let leftFoot, rightFoot;
let leftMainArm, rightMainArm;
let clock = new THREE.Clock();
let sporeParticles;
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
let isScreaming = false;
let screamStartTime = 0;
let audioCtx;
// ★キーボード入力と移動用の変数
const keys = { w: false, a: false, s: false, d: false };
let isWalking = false;
let walkCycle = 0; // 歩行アニメーションの位相
let currentTargetRotation = 0; // クイーンが向くべき角度
const moveSpeed = 15.0; // 移動速度
const carapaceMaterial = new THREE.MeshPhysicalMaterial({
color: 0x050b14,
metalness: 0.7,
roughness: 0.2,
clearcoat: 1.0,
clearcoatRoughness: 0.1,
flatShading: false
});
const boneMaterial = new THREE.MeshStandardMaterial({
color: 0x5a3a1a,
roughness: 0.6,
metalness: 0.3
});
const fleshMaterial = new THREE.MeshStandardMaterial({
color: 0x4a1010,
roughness: 0.3,
metalness: 0.1
});
const loader = document.getElementById('loading');
const ui = document.getElementById('ui-layer');
if (loader) {
loader.style.opacity = '0';
setTimeout(() => {
loader.style.display = 'none';
if (ui) ui.style.display = 'block';
}, 500);
}
init();
animate();
function init() {
scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x050805, 0.012);
camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 0.1, 1000);
if (window.innerWidth <= 768) {
camera.position.set(50, 25, 65);
} else {
camera.position.set(35, 20, 45);
}
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0x050805);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.2;
document.getElementById('canvas-container').appendChild(renderer.domElement);
// コントロールのターゲットを固定せず、後でクイーンに追従させる
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.maxPolarAngle = Math.PI / 2;
controls.enablePan = false;
setupLighting();
setupEnvironment();
buildQueen();
window.addEventListener('resize', onWindowResize, false);
window.addEventListener('pointerdown', onPointerDown, false);
// ★キーボードイベントの登録
window.addEventListener('keydown', onKeyDown, false);
window.addEventListener('keyup', onKeyUp, false);
}
function onKeyDown(event) {
switch(event.code) {
case 'KeyW': case 'ArrowUp': keys.w = true; break;
case 'KeyA': case 'ArrowLeft': keys.a = true; break;
case 'KeyS': case 'ArrowDown': keys.s = true; break;
case 'KeyD': case 'ArrowRight': keys.d = true; break;
}
}
function onKeyUp(event) {
switch(event.code) {
case 'KeyW': case 'ArrowUp': keys.w = false; break;
case 'KeyA': case 'ArrowLeft': keys.a = false; break;
case 'KeyS': case 'ArrowDown': keys.s = false; break;
case 'KeyD': case 'ArrowRight': keys.d = false; break;
}
}
function playScreamSound() {
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
if (audioCtx.state === 'suspended') audioCtx.resume();
const masterGain = audioCtx.createGain();
masterGain.gain.setValueAtTime(0, audioCtx.currentTime);
masterGain.gain.linearRampToValueAtTime(0.8, audioCtx.currentTime + 0.1);
masterGain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 2.5);
masterGain.connect(audioCtx.destination);
const osc1 = audioCtx.createOscillator();
osc1.type = 'sawtooth';
osc1.frequency.setValueAtTime(3000, audioCtx.currentTime);
osc1.frequency.exponentialRampToValueAtTime(800, audioCtx.currentTime + 1.5);
const lfo = audioCtx.createOscillator();
lfo.type = 'sine';
lfo.frequency.value = 40;
const lfoGain = audioCtx.createGain();
lfoGain.gain.value = 500;
lfo.connect(lfoGain);
lfoGain.connect(osc1.frequency);
const osc2 = audioCtx.createOscillator();
osc2.type = 'square';
osc2.frequency.setValueAtTime(150, audioCtx.currentTime);
osc2.frequency.linearRampToValueAtTime(50, audioCtx.currentTime + 2.0);
const filter = audioCtx.createBiquadFilter();
filter.type = 'bandpass';
filter.frequency.setValueAtTime(500, audioCtx.currentTime);
filter.frequency.linearRampToValueAtTime(4000, audioCtx.currentTime + 0.2);
filter.Q.value = 5;
osc1.connect(filter);
osc2.connect(filter);
filter.connect(masterGain);
osc1.start(audioCtx.currentTime);
osc2.start(audioCtx.currentTime);
lfo.start(audioCtx.currentTime);
osc1.stop(audioCtx.currentTime + 2.5);
osc2.stop(audioCtx.currentTime + 2.5);
lfo.stop(audioCtx.currentTime + 2.5);
}
function onPointerDown(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(queenGroup.children, true);
if (intersects.length > 0 && !isScreaming) {
isScreaming = true;
screamStartTime = clock.getElapsedTime();
playScreamSound();
const title = document.getElementById('title-text');
if (title) {
title.style.color = '#ff3333';
title.style.textShadow = '0 0 15px #ff0000';
setTimeout(() => {
title.style.color = '#4488ff';
title.style.textShadow = '0 0 5px rgba(68, 136, 255, 0.5)';
}, 2500);
}
}
}
function setupLighting() {
const ambientLight = new THREE.AmbientLight(0x405060, 1.5);
scene.add(ambientLight);
const mainLight = new THREE.SpotLight(0x88bbff, 4.5);
mainLight.position.set(20, 50, 30);
mainLight.angle = Math.PI / 3;
mainLight.penumbra = 0.5;
mainLight.castShadow = true;
mainLight.shadow.bias = -0.001;
mainLight.shadow.mapSize.width = 2048;
mainLight.shadow.mapSize.height = 2048;
scene.add(mainLight);
const amberLight = new THREE.PointLight(0xff7733, 2.5, 80);
amberLight.position.set(-20, 5, 20);
scene.add(amberLight);
const bottomLight = new THREE.PointLight(0x33aa55, 1.5, 40);
bottomLight.position.set(0, -2, 5);
scene.add(bottomLight);
const backLight = new THREE.DirectionalLight(0xaaccff, 3.0);
backLight.position.set(-15, 30, -35);
scene.add(backLight);
}
function setupEnvironment() {
const floorGeo = new THREE.PlaneGeometry(300, 300, 128, 128);
const pos = floorGeo.attributes.position;
for(let i=0; i<pos.count; i++) {
let x = pos.getX(i);
let y = pos.getY(i);
let zNoise = Math.sin(x*0.15)*Math.cos(y*0.15)*1.5 + Math.sin(x*0.05 + y*0.08)*2.5 + Math.random()*0.3;
pos.setZ(i, zNoise);
}
floorGeo.computeVertexNormals();
const hiveMat = new THREE.MeshStandardMaterial({
color: 0x0a0c0a, roughness: 0.3, metalness: 0.6, bumpScale: 0.05
});
const floor = new THREE.Mesh(floorGeo, hiveMat);
floor.rotation.x = -Math.PI / 2;
floor.receiveShadow = true;
scene.add(floor);
const hiveGroup = new THREE.Group();
scene.add(hiveGroup);
const wallGeo = new THREE.CylinderGeometry(90, 80, 80, 32, 16, true);
const wPos = wallGeo.attributes.position;
for(let i=0; i<wPos.count; i++) {
let x = wPos.getX(i);
let y = wPos.getY(i);
let z = wPos.getZ(i);
let ribNoise = Math.sin(Math.atan2(z, x) * 20) * 2;
let waveNoise = Math.sin(y * 0.2) * 5;
let length = Math.sqrt(x*x + z*z);
let factor = (length + ribNoise + waveNoise) / length;
wPos.setX(i, x * factor);
wPos.setZ(i, z * factor);
}
wallGeo.computeVertexNormals();
const wallMat = new THREE.MeshStandardMaterial({ color: 0x080a08, roughness: 0.4, metalness: 0.4, side: THREE.DoubleSide });
const wall = new THREE.Mesh(wallGeo, wallMat);
wall.position.y = 20;
wall.receiveShadow = true;
hiveGroup.add(wall);
const particleGeo = new THREE.BufferGeometry();
const particleCount = 2000;
const particlePos = new Float32Array(particleCount * 3);
for(let i=0; i<particleCount*3; i++) {
particlePos[i] = (Math.random() - 0.5) * 180;
if(i%3 === 1) particlePos[i] = Math.random() * 60;
}
particleGeo.setAttribute('position', new THREE.BufferAttribute(particlePos, 3));
const particleMat = new THREE.PointsMaterial({
color: 0x88ccaa, size: 0.12, transparent: true, opacity: 0.3, blending: THREE.AdditiveBlending
});
sporeParticles = new THREE.Points(particleGeo, particleMat);
scene.add(sporeParticles);
}
function buildQueen() {
queenGroup = new THREE.Group();
queenGroup.position.y = 21.0;
scene.add(queenGroup);
// --- 胴体 ---
chest = new THREE.Mesh(new THREE.CylinderGeometry(1.6, 0.7, 8, 16), carapaceMaterial);
chest.rotation.x = Math.PI / 3.0;
chest.position.z = 2.0;
chest.castShadow = true;
queenGroup.add(chest);
for(let i=0; i<5; i++) {
const ribGeo = new THREE.TorusGeometry(1.7 - i*0.2, 0.15, 8, 16, Math.PI * 1.2);
const rib = new THREE.Mesh(ribGeo, boneMaterial);
rib.position.set(0, 3 - i*1.3, 0);
rib.rotation.x = -Math.PI/2 + 0.3;
chest.add(rib);
}
const pelvis = new THREE.Mesh(new THREE.CylinderGeometry(0.7, 1.5, 4, 16), carapaceMaterial);
pelvis.position.set(0, -5, -1);
pelvis.rotation.x = -Math.PI / 6;
pelvis.castShadow = true;
chest.add(pelvis);
for(let i=0; i<6; i++) {
const isLeft = i % 2 === 0;
const row = Math.floor(i / 2);
const sign = isLeft ? 1 : -1;
const path = new THREE.CatmullRomCurve3([
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(sign * 1.2, 3.5, 0.0),
new THREE.Vector3(sign * (1.8 + row*0.6), 10.0 - row*1.2, -5.0 - row*1.5)
]);
const spikeGeo = new THREE.TubeGeometry(path, 16, 0.18 - row*0.04, 8, false);
const spike = new THREE.Mesh(spikeGeo, carapaceMaterial);
spike.position.set(sign * 0.8, 1.5 - row*1.5, -1.0);
spike.castShadow = true;
chest.add(spike);
}
// --- 頭部 ---
headGroup = new THREE.Group();
headGroup.position.set(0, 4.5, 1.5);
headGroup.rotation.x = -Math.PI / 3;
chest.add(headGroup);
const crestGroup = new THREE.Group();
crestGroup.position.set(0, 0.8, -1.5);
crestGroup.rotation.x = -Math.PI / 2 + 0.2;
headGroup.add(crestGroup);
const crestShape = new THREE.Shape();
crestShape.moveTo(0, -1);
crestShape.bezierCurveTo(4, -0.5, 6, 8, 5, 24);
crestShape.bezierCurveTo(3, 22, 1.5, 25, 0, 23);
crestShape.bezierCurveTo(-1.5, 25, -3, 22, -5, 24);
crestShape.bezierCurveTo(-6, 8, -4, -0.5, 0, -1);
const extrudeSettings = { depth: 0.3, bevelEnabled: true, bevelSegments: 4, steps: 1, bevelSize: 0.1, bevelThickness: 0.2 };
const crestGeo = new THREE.ExtrudeGeometry(crestShape, extrudeSettings);
crestGeo.translate(0, 0, -0.15);
function getBulge(x, y) {
let curveX = Math.cos(Math.min((Math.abs(x) / 8.0) * (Math.PI / 2), Math.PI / 2));
if (curveX <= 0 || y <= -1) return 0;
let normalizedY = Math.max(0, Math.min(1, (y + 1) / 26.0));
let curveY = Math.sin(Math.pow(normalizedY, 0.7) * Math.PI);
return curveX * curveY * 4.5;
}
const cPos = crestGeo.attributes.position;
for(let i=0; i<cPos.count; i++) {
let x = cPos.getX(i);
let y = cPos.getY(i);
let z = cPos.getZ(i);
if (z > 0) {
cPos.setZ(i, z + getBulge(x, y));
} else {
cPos.setZ(i, z + getBulge(x, y) * 0.4);
}
}
crestGeo.computeVertexNormals();
const crestMesh = new THREE.Mesh(crestGeo, carapaceMaterial);
crestMesh.castShadow = true;
crestGroup.add(crestMesh);
const numRibs = 9;
for(let i = 0; i < numRibs; i++) {
let index = i - Math.floor(numRibs / 2);
let isCenter = (index === 0);
let length = isCenter ? 24 : 24 - Math.abs(index) * 2.2;
let angle = index * -0.14;
let radius = isCenter ? 0.35 : 0.1;
let mat = (Math.abs(index) === 2 || isCenter) ? boneMaterial : carapaceMaterial;
let points = [];
let segments = 20;
for(let j=0; j<=segments; j++) {
let t = j / segments;
let lx = -Math.sin(angle) * length * t;
let ly = Math.cos(angle) * length * t - 1.0;
let lz = getBulge(lx, ly) + (isCenter ? 0.1 : 0.15);
points.push(new THREE.Vector3(lx, ly, lz));
}
let path = new THREE.CatmullRomCurve3(points);
let ribGeo = new THREE.TubeGeometry(path, segments, radius, 6, false);
let rib = new THREE.Mesh(ribGeo, mat);
if (isCenter) rib.scale.set(1.5, 1, 0.6);
crestGroup.add(rib);
}
for(let i = -3; i <= 3; i++) {
if (i === 0) continue;
const spikeGeo = new THREE.ConeGeometry(0.15, 2.0, 4);
spikeGeo.translate(0, 1.0, 0);
const spike = new THREE.Mesh(spikeGeo, carapaceMaterial);
const angle = i * -0.3;
const length = 14.5 - Math.abs(i) * 0.8;
let sx = -Math.sin(angle) * length;
let sy = Math.cos(angle) * length - 0.5;
let sz = getBulge(sx, sy);
spike.position.set(sx, sy, sz);
spike.rotation.z = angle;
spike.rotation.x = 0.2;
crestGroup.add(spike);
}
const headDomeGeo = new THREE.SphereGeometry(1.2, 32, 24);
const dPos = headDomeGeo.attributes.position;
for(let i=0; i<dPos.count; i++) {
let z = dPos.getZ(i);
let y = dPos.getY(i);
if (y < 0) dPos.setY(i, y * 0.2);
if (z > 0) dPos.setX(i, dPos.getX(i) * (1 - z * 0.2));
}
headDomeGeo.computeVertexNormals();
const headDome = new THREE.Mesh(headDomeGeo, carapaceMaterial);
headDome.scale.set(0.85, 2.8, 4.5);
headDome.position.set(0, 0.5, 0.5);
headDome.rotation.x = -0.25;
headDome.castShadow = true;
headGroup.add(headDome);
const snoutGeo = new THREE.CylinderGeometry(0.25, 0.7, 4.5, 16);
snoutGeo.rotateX(Math.PI / 2);
snoutGeo.translate(0, -0.2, 2.4);
snoutGeo.scale(1.0, 0.7, 1.0);
const sPos = snoutGeo.attributes.position;
for(let i=0; i<sPos.count; i++) {
let z = sPos.getZ(i);
let y = sPos.getY(i);
let x = sPos.getX(i);
if (z > 0) {
let drop = Math.pow(z / 4.5, 2) * 0.7;
sPos.setY(i, y - drop);
}
if (sPos.getY(i) < -0.35) {
sPos.setY(i, -0.35);
sPos.setX(i, x * 0.85);
}
}
snoutGeo.computeVertexNormals();
const snout = new THREE.Mesh(snoutGeo, carapaceMaterial);
snout.castShadow = true;
headGroup.add(snout);
for(let i=0; i<3; i++) {
const tendonGeo = new THREE.CylinderGeometry(0.03, 0.03, 1.6, 6);
const tendonL = new THREE.Mesh(tendonGeo, fleshMaterial);
tendonL.position.set(-0.35 + i*0.04, -0.5, 1.0 + i*0.15);
tendonL.rotation.x = -Math.PI / 4 + i*0.05;
headGroup.add(tendonL);
const tendonR = new THREE.Mesh(tendonGeo, fleshMaterial);
tendonR.position.set(0.35 - i*0.04, -0.5, 1.0 + i*0.15);
tendonR.rotation.x = -Math.PI / 4 + i*0.05;
headGroup.add(tendonR);
}
const lowerJawGeo = new THREE.CylinderGeometry(0.15, 0.5, 4.0, 16);
lowerJawGeo.rotateX(Math.PI / 2);
lowerJawGeo.translate(0, 0, 2.0);
lowerJawGeo.scale(0.9, 0.4, 1.0);
const lPos = lowerJawGeo.attributes.position;
for(let i=0; i<lPos.count; i++) {
let z = lPos.getZ(i);
let y = lPos.getY(i);
if (z > 0) {
let rise = Math.pow(z / 4.0, 2) * 0.4;
lPos.setY(i, y + rise);
}
if (lPos.getY(i) > 0.05) {
lPos.setY(i, 0.05);
}
}
lowerJawGeo.computeVertexNormals();
lowerJaw = new THREE.Mesh(lowerJawGeo, carapaceMaterial);
lowerJaw.position.set(0, -0.5, 0.4);
lowerJaw.rotation.x = 0.15;
lowerJaw.castShadow = true;
headGroup.add(lowerJaw);
const toothGeo = new THREE.ConeGeometry(0.035, 0.6, 6);
const toothMat = new THREE.MeshPhysicalMaterial({
color: 0xddddcc, metalness: 0.1, roughness: 0.1,
transparent: true, opacity: 0.8, transmission: 0.5
});
for(let i=0; i<10; i++) {
let zPos = 1.0 + i * 0.32;
let progress = i / 9;
let xOffset = 0.35 - (progress * 0.2);
let yBase = -0.35 - Math.pow(zPos / 4.5, 2) * 0.7;
const toothL = new THREE.Mesh(toothGeo, toothMat);
toothL.position.set(-xOffset, yBase - 0.05, zPos);
toothL.rotation.x = Math.PI - 0.15;
toothL.rotation.z = 0.15;
snout.add(toothL);
const toothR = new THREE.Mesh(toothGeo, toothMat);
toothR.position.set(xOffset, yBase - 0.05, zPos);
toothR.rotation.x = Math.PI - 0.15;
toothR.rotation.z = -0.15;
snout.add(toothR);
}
for(let i=0; i<9; i++) {
let zPos = 0.8 + i * 0.32;
let progress = i / 8;
let xOffset = 0.28 - (progress * 0.15);
let yBase = 0.05 + Math.pow(zPos / 4.0, 2) * 0.4;
const toothL = new THREE.Mesh(toothGeo, toothMat);
toothL.position.set(-xOffset, yBase + 0.1, zPos);
toothL.rotation.x = 0.15;
toothL.rotation.z = -0.1;
lowerJaw.add(toothL);
const toothR = new THREE.Mesh(toothGeo, toothMat);
toothR.position.set(xOffset, yBase + 0.1, zPos);
toothR.rotation.x = 0.15;
toothR.rotation.z = 0.1;
lowerJaw.add(toothR);
}
// --- 腕 ---
function createMainArm(isLeft) {
const armGroup = new THREE.Group();
const sign = isLeft ? 1 : -1;
armGroup.position.set(sign * 2.2, 2.5, 0);
const upperArmGeo = new THREE.CylinderGeometry(0.25, 0.15, 6, 8);
upperArmGeo.translate(0, -3, 0);
const upperArm = new THREE.Mesh(upperArmGeo, carapaceMaterial);
upperArm.rotation.z = sign * Math.PI / 6;
upperArm.rotation.x = -Math.PI / 8;
armGroup.add(upperArm);
const lowerArmGeo = new THREE.CylinderGeometry(0.15, 0.1, 7, 8);
lowerArmGeo.translate(0, -3.5, 0);
const lowerArm = new THREE.Mesh(lowerArmGeo, carapaceMaterial);
lowerArm.position.set(0, -6, 0);
lowerArm.rotation.z = sign * -Math.PI / 12;
lowerArm.rotation.x = -Math.PI / 2.0;
upperArm.add(lowerArm);
const hand = new THREE.Group();
hand.position.set(0, -7, 0);
lowerArm.add(hand);
for(let f=0; f<3; f++) {
const fingerGeo = new THREE.CylinderGeometry(0.05, 0.01, 4.5, 6);
fingerGeo.translate(0, -2.25, 0);
const finger = new THREE.Mesh(fingerGeo, carapaceMaterial);
finger.rotation.x = -Math.PI / 4;
finger.rotation.z = (f-1)*0.2;
hand.add(finger);
}
const thumbGeo = new THREE.CylinderGeometry(0.05, 0.01, 3.0, 6);
thumbGeo.translate(0, -1.5, 0);
const thumb = new THREE.Mesh(thumbGeo, carapaceMaterial);
thumb.rotation.x = Math.PI / 4;
thumb.rotation.z = sign * 0.5;
hand.add(thumb);
return armGroup;
}
// ★腕をアニメーション制御用の変数に保存
leftMainArm = createMainArm(true);
rightMainArm = createMainArm(false);
chest.add(leftMainArm);
chest.add(rightMainArm);
function createSmallArm(isLeft) {
const armGroup = new THREE.Group();
const sign = isLeft ? 1 : -1;
armGroup.position.set(sign * 0.8, -1, 1.4);
const upperGeo = new THREE.CylinderGeometry(0.12, 0.08, 2.5, 6);
upperGeo.translate(0, -1.25, 0);
const upper = new THREE.Mesh(upperGeo, carapaceMaterial);
upper.rotation.z = sign * Math.PI / 4;
upper.rotation.x = Math.PI / 8;
armGroup.add(upper);
const lowerGeo = new THREE.CylinderGeometry(0.08, 0.04, 2.5, 6);
lowerGeo.translate(0, -1.25, 0);
const lower = new THREE.Mesh(lowerGeo, carapaceMaterial);
lower.position.set(0, -2.5, 0);
lower.rotation.x = -Math.PI / 1.5;
upper.add(lower);
return armGroup;
}
chest.add(createSmallArm(true));
chest.add(createSmallArm(false));
// --- 足 ---
function createLeg(isLeft) {
const legGroup = new THREE.Group();
const sign = isLeft ? 1 : -1;
legGroup.position.set(sign * 1.5, -2.0, 0);
const thighGeo = new THREE.CylinderGeometry(1.2, 0.6, 8.5, 12);
thighGeo.translate(0, -4.25, 0);
const thigh = new THREE.Mesh(thighGeo, carapaceMaterial);
thigh.rotation.x = Math.PI / 8;
thigh.rotation.z = sign * Math.PI / 12;
legGroup.add(thigh);
const thighDetailGeo = new THREE.CylinderGeometry(0.9, 0.4, 7.5, 6);
thighDetailGeo.translate(0, -4.25, 0);
const thighDetail = new THREE.Mesh(thighDetailGeo, boneMaterial);
thighDetail.position.z = -0.35;
thighDetail.scale.set(0.8, 1, 1.2);
thigh.add(thighDetail);
const calfGeo = new THREE.CylinderGeometry(0.7, 0.35, 9.5, 8);
calfGeo.translate(0, -4.75, 0);
const calf = new THREE.Mesh(calfGeo, carapaceMaterial);
calf.position.set(0, -8.5, 0);
calf.rotation.x = -Math.PI / 2.0;
calf.rotation.z = sign * -Math.PI / 16;
thigh.add(calf);
const footGeo = new THREE.CylinderGeometry(0.4, 0.15, 6.5, 8);
footGeo.translate(0, -3.25, 0);
const foot = new THREE.Mesh(footGeo, carapaceMaterial);
foot.position.set(0, -9.5, 0);
foot.rotation.x = Math.PI / 1.8;
calf.add(foot);
const heelGeo = new THREE.ConeGeometry(0.2, 3.0, 6);
heelGeo.translate(0, 1.5, 0);
const heel = new THREE.Mesh(heelGeo, carapaceMaterial);
heel.position.set(0, 0, -0.6);
heel.rotation.x = Math.PI / 3;
foot.add(heel);
const toe = new THREE.Mesh(new THREE.ConeGeometry(0.15, 2.0, 6), carapaceMaterial);
toe.position.set(0, -6.5, 0.3);
toe.rotation.x = -Math.PI / 6;
foot.add(toe);
// ★足全体と関節をオブジェクトとして返す(アニメーション用)
return { group: legGroup, thigh: thigh, calf: calf, foot: foot };
}
// ★歩行アニメーション用に生成した足をグローバル変数に格納
const leftLeg = createLeg(true);
leftLegGroup = leftLeg.thigh;
leftCalf = leftLeg.calf;
leftFoot = leftLeg.foot;
pelvis.add(leftLeg.group);
const rightLeg = createLeg(false);
rightLegGroup = rightLeg.thigh;
rightCalf = rightLeg.calf;
rightFoot = rightLeg.foot;
pelvis.add(rightLeg.group);
// --- 尻尾 ---
let currentParent = pelvis;
const numSegments = 35;
tailSegments = [];
for(let i=0; i<numSegments; i++) {
const segGroup = new THREE.Group();
segGroup.position.set(0, i===0 ? -2.0 : -1.5, i===0 ? -1 : 0);
const radius = 0.9 * (1 - i/numSegments) + 0.1;
const length = 1.6;
const segMesh = new THREE.Mesh(new THREE.CylinderGeometry(radius*1.2, radius, length, 8), carapaceMaterial);
segMesh.position.set(0, -length/2, 0);
segMesh.castShadow = true;
segGroup.add(segMesh);
if (i < numSegments - 4) {
const finGeo = new THREE.ConeGeometry(0.15, radius*3.0, 4);
finGeo.translate(0, radius*1.5, 0);
const fin = new THREE.Mesh(finGeo, boneMaterial);
fin.position.set(0, -length/2, -radius - 0.1);
fin.rotation.x = -Math.PI / 2 - 0.3;
fin.scale.set(0.3, 1, 1);
segGroup.add(fin);
}
currentParent.add(segGroup);
tailSegments.push(segGroup);
currentParent = segGroup;
if (i === numSegments - 1) {
const spikeGeo = new THREE.ConeGeometry(0.5, 7, 6);
spikeGeo.translate(0, -3.5, 0);
const spike = new THREE.Mesh(spikeGeo, carapaceMaterial);
spike.scale.set(0.1, 1, 1);
segGroup.add(spike);
}
}
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
if (window.innerWidth <= 768) {
camera.position.z = Math.max(camera.position.z, 65);
}
}
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
const time = clock.getElapsedTime();
// ★ 1. 移動ロジックの追加
let moveDirection = new THREE.Vector3(0, 0, 0);
if (keys.w) moveDirection.z -= 1;
if (keys.s) moveDirection.z += 1;
if (keys.a) moveDirection.x -= 1;
if (keys.d) moveDirection.x += 1;
isWalking = moveDirection.lengthSq() > 0;
if (isWalking) {
// カメラの向きに合わせて移動方向を計算
moveDirection.normalize();
const cameraEuler = new THREE.Euler().setFromQuaternion(camera.quaternion);
moveDirection.applyAxisAngle(new THREE.Vector3(0, 1, 0), cameraEuler.y);
// 座標の更新
queenGroup.position.x += moveDirection.x * moveSpeed * delta;
queenGroup.position.z += moveDirection.z * moveSpeed * delta;
// 進行方向を向く
currentTargetRotation = Math.atan2(moveDirection.x, moveDirection.z);
let rotDiff = currentTargetRotation - queenGroup.rotation.y;
while (rotDiff > Math.PI) rotDiff -= Math.PI * 2;
while (rotDiff < -Math.PI) rotDiff += Math.PI * 2;
queenGroup.rotation.y += rotDiff * 5.0 * delta;
walkCycle += delta * 6.0; // 歩行アニメーションのスピード
} else {
// 停止時は足をニュートラルに戻す
walkCycle = THREE.MathUtils.lerp(walkCycle, 0, delta * 3.0);
}
// 2. アニメーション(歩行を反映)
const bounce = isWalking ? Math.abs(Math.sin(walkCycle)) * 1.5 : 0;
const breathSpeed = isScreaming ? 4.0 : 1.5;
const breathAmp = isScreaming ? 0.05 : 0.02;
if (chest) {
chest.scale.set(1 + Math.sin(time * breathSpeed) * breathAmp, 1 + Math.sin(time * breathSpeed) * breathAmp, 1 + Math.sin(time * breathSpeed) * (breathAmp*2));
// 歩行中は重心(バウンド)を下げる
queenGroup.position.y = 22.0 + Math.sin(time * breathSpeed) * 0.15 - bounce;
}
// ★足と腕の歩行モーション
if (leftLegGroup && rightLegGroup) {
// 左足
const leftStride = Math.sin(walkCycle);
leftLegGroup.rotation.x = Math.PI / 8 + leftStride * 0.8;
leftCalf.rotation.x = -Math.PI / 2.0 - Math.max(0, leftStride * 0.5); // 膝の曲げ
leftFoot.rotation.x = Math.PI / 1.8 + leftStride * 0.3; // 足首の返し
// 右足(逆位相)
const rightStride = Math.sin(walkCycle + Math.PI);
rightLegGroup.rotation.x = Math.PI / 8 + rightStride * 0.8;
rightCalf.rotation.x = -Math.PI / 2.0 - Math.max(0, rightStride * 0.5);
rightFoot.rotation.x = Math.PI / 1.8 + rightStride * 0.3;
// 腕振り(足と逆)
leftMainArm.rotation.x = -Math.PI / 8 + rightStride * 0.5;
rightMainArm.rotation.x = -Math.PI / 8 + leftStride * 0.5;
}
if (headGroup) {
if (isScreaming) {
const elapsedScream = time - screamStartTime;
headGroup.rotation.y = Math.sin(elapsedScream * 15) * 0.1;
headGroup.rotation.x = -Math.PI / 3 - 0.3 + Math.sin(elapsedScream * 5) * 0.1;
if (lowerJaw) lowerJaw.rotation.x = 0.8 + Math.sin(elapsedScream * 20) * 0.1;
if (elapsedScream > 2.5) isScreaming = false;
} else {
const headBob = isWalking ? Math.sin(walkCycle) * 0.1 : 0;
headGroup.rotation.y = Math.sin(time * 0.8) * 0.3 + headBob;
headGroup.rotation.x = -Math.PI / 3 + Math.sin(time * 1.2) * 0.1;
headGroup.rotation.z = Math.cos(time * 0.8) * 0.05;
if (lowerJaw) lowerJaw.rotation.x = 0.15;
}
}
if (tailSegments.length > 0) {
for(let i=0; i<tailSegments.length; i++) {
const segment = tailSegments[i];
const delay = i * 0.15;
const tailSpeed = isScreaming ? 3.0 : (isWalking ? 2.5 : 1.5);
const wagAmp = isWalking ? 0.3 : 0.15;
segment.rotation.x = Math.sin(time * tailSpeed - delay) * 0.1 + (i === 0 ? Math.PI/2.2 : 0);
segment.rotation.z = Math.cos(time * (tailSpeed*0.8) - delay) * wagAmp;
}
}
if (sporeParticles) {
sporeParticles.rotation.y = time * 0.03;
sporeParticles.position.y = Math.sin(time * 0.5) * 1.5;
}
// ★カメラがクイーンに追従するようにする
controls.target.copy(queenGroup.position).add(new THREE.Vector3(0, -10, 0));
controls.update();
renderer.render(scene, camera);
}
</script>
</body>
</html>
エイリアンクイーンを作りました