🔒 サンドボックス内で実行中 ⛶ 全画面で遊ぶ
HTML / CSS / JS
<!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>

eiriankui-n

エイリアンクイーンを作りました

🤖 使用したAI
Gemini
👁 29 回閲覧
📅 投稿:2026年8月9日 🔄 更新:2026年9月11日
応援スタンプを押してみよう!