<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ミズダコ 3Dモデル</title>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #011125; /* 深海の背景色 */
font-family: 'Helvetica Neue', Arial, 'Hiragino Kaku Gothic ProN', 'Hiragino Sans', Meiryo, sans-serif;
color: white;
user-select: none;
}
#canvas-container {
width: 100vw;
height: 100vh;
display: block;
}
#ui-layer {
position: absolute;
top: 20px;
left: 20px;
pointer-events: none; /* UIの裏側のキャンバスを操作できるようにする */
z-index: 10;
}
h1 {
margin: 0 0 10px 0;
font-size: 24px;
text-shadow: 0 2px 4px rgba(0,0,0,0.5);
letter-spacing: 2px;
}
p {
margin: 0;
font-size: 14px;
color: #a0c4ff;
text-shadow: 0 1px 2px rgba(0,0,0,0.8);
}
#loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 20px;
transition: opacity 0.5s ease;
}
</style>
</head>
<body>
<div id="ui-layer">
<h1>ミズダコ & タラバガニ & オニカマス</h1>
<p>左ドラッグ: 回転 | 右ドラッグ: 移動 | スクロール: ズーム</p>
</div>
<div id="loading">海に潜っています...</div>
<div id="canvas-container"></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() {
init();
document.getElementById('loading').style.opacity = 0;
setTimeout(() => document.getElementById('loading').style.display = 'none', 500);
};
let scene, camera, renderer, controls;
let octopusGroup;
let crabGroup;
let barracudaGroup;
const tentacles = [];
let bubbles = [];
let clock = new THREE.Clock();
function createSeabed() {
const seabedGeo = new THREE.PlaneGeometry(100, 100, 64, 64);
const vertices = seabedGeo.attributes.position.array;
for (let i = 0; i < vertices.length; i += 3) {
vertices[i + 2] += Math.random() * 0.5 - 0.25;
vertices[i + 2] += Math.sin(vertices[i] * 0.5) * 0.5;
vertices[i + 2] += Math.cos(vertices[i+1] * 0.5) * 0.5;
}
seabedGeo.computeVertexNormals();
const seabedMat = new THREE.MeshStandardMaterial({
color: 0x5a4d3c,
roughness: 0.9,
metalness: 0.1
});
const seabed = new THREE.Mesh(seabedGeo, seabedMat);
seabed.rotation.x = -Math.PI / 2;
seabed.position.y = -0.5;
seabed.receiveShadow = true;
scene.add(seabed);
}
function init() {
scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a4a7c);
scene.fog = new THREE.FogExp2(0x0a4a7c, 0.015);
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 8, 12);
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;
document.getElementById('canvas-container').appendChild(renderer.domElement);
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.maxDistance = 30;
controls.minDistance = 3;
controls.target.set(0, 0, 0);
const ambientLight = new THREE.AmbientLight(0x6b8eb3, 1.0);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffeedd, 1.5);
directionalLight.position.set(5, 15, 10);
directionalLight.castShadow = true;
directionalLight.shadow.camera.top = 10;
directionalLight.shadow.camera.bottom = -10;
directionalLight.shadow.camera.left = -10;
directionalLight.shadow.camera.right = 10;
scene.add(directionalLight);
const bottomLight = new THREE.DirectionalLight(0x2a3a4a, 0.6);
bottomLight.position.set(-5, -10, -5);
scene.add(bottomLight);
createSeabed();
createOctopus();
createRealCrab();
createBarracuda();
createBubbles();
window.addEventListener('resize', onWindowResize, false);
animate();
}
function createOctopus() {
octopusGroup = new THREE.Group();
scene.add(octopusGroup);
const skinColor = 0xb84b39;
const octopusMaterial = new THREE.MeshStandardMaterial({
color: skinColor,
roughness: 0.8,
metalness: 0.05,
});
const headGeometry = new THREE.SphereGeometry(2.2, 32, 32);
const head = new THREE.Mesh(headGeometry, octopusMaterial);
head.position.set(0, 1.6, -2.5);
head.scale.set(1.2, 0.7, 1.6);
head.rotation.x = -0.1;
head.castShadow = true;
head.receiveShadow = true;
octopusGroup.add(head);
const bodyGeo = new THREE.SphereGeometry(1.5, 32, 32);
const body = new THREE.Mesh(bodyGeo, octopusMaterial);
body.position.set(0, 1.0, -1.0);
body.scale.set(1.2, 0.5, 1.2);
body.castShadow = true;
body.receiveShadow = true;
octopusGroup.add(body);
const eyeGeometry = new THREE.SphereGeometry(0.3, 16, 16);
const eyeMaterial = new THREE.MeshStandardMaterial({ color: 0xffffee, roughness: 0.1 });
const pupilGeometry = new THREE.SphereGeometry(0.15, 16, 16);
const pupilMaterial = new THREE.MeshStandardMaterial({ color: 0x111111, roughness: 0 });
const eyelidGeo = new THREE.SphereGeometry(0.45, 16, 16);
eyelidGeo.scale(1, 0.8, 1);
const rightEyeGroup = new THREE.Group();
rightEyeGroup.position.set(1.4, 1.7, -0.6);
const rightEyelid = new THREE.Mesh(eyelidGeo, octopusMaterial);
rightEyeGroup.add(rightEyelid);
const rightEye = new THREE.Mesh(eyeGeometry, eyeMaterial);
rightEye.position.set(0.1, 0.1, 0.2);
const rightPupil = new THREE.Mesh(pupilGeometry, pupilMaterial);
rightPupil.scale.set(1, 0.3, 1);
rightPupil.position.set(0.15, 0.05, 0.2);
rightPupil.rotation.y = Math.PI / 4;
rightEye.add(rightPupil);
rightEyeGroup.add(rightEye);
octopusGroup.add(rightEyeGroup);
const leftEyeGroup = new THREE.Group();
leftEyeGroup.position.set(-1.4, 1.7, -0.6);
const leftEyelid = new THREE.Mesh(eyelidGeo, octopusMaterial);
leftEyeGroup.add(leftEyelid);
const leftEye = new THREE.Mesh(eyeGeometry, eyeMaterial);
leftEye.position.set(-0.1, 0.1, 0.2);
const leftPupil = new THREE.Mesh(pupilGeometry, pupilMaterial);
leftPupil.scale.set(1, 0.3, 1);
leftPupil.position.set(-0.15, 0.05, 0.2);
leftPupil.rotation.y = -Math.PI / 4;
leftEye.add(leftPupil);
leftEyeGroup.add(leftEye);
octopusGroup.add(leftEyeGroup);
const funnelGeo = new THREE.CylinderGeometry(0.15, 0.3, 0.8, 16);
const funnel = new THREE.Mesh(funnelGeo, octopusMaterial);
funnel.position.set(-1.2, 0.8, -1.0);
funnel.rotation.x = Math.PI / 2;
funnel.rotation.z = Math.PI / 4;
octopusGroup.add(funnel);
const beakGeo = new THREE.ConeGeometry(0.35, 0.6, 16);
const beakMat = new THREE.MeshStandardMaterial({ color: 0x111111, roughness: 0.3 });
const beak = new THREE.Mesh(beakGeo, beakMat);
beak.position.set(0, 0.2, 0);
beak.rotation.x = Math.PI;
beak.castShadow = true;
octopusGroup.add(beak);
const webSegments = 9;
const radialDiv = 4;
const xSegments = 8 * radialDiv;
const webGeo = new THREE.PlaneGeometry(1, 1, xSegments, webSegments - 1);
const webMat = new THREE.MeshStandardMaterial({
color: skinColor,
roughness: 0.8,
metalness: 0.05,
side: THREE.DoubleSide
});
const webMesh = new THREE.Mesh(webGeo, webMat);
webMesh.castShadow = true;
webMesh.receiveShadow = true;
octopusGroup.add(webMesh);
octopusGroup.userData.webMesh = webMesh;
octopusGroup.userData.webSegments = webSegments;
octopusGroup.userData.radialDiv = radialDiv;
const numTentacles = 8;
const segmentsPerTentacle = 20;
for (let i = 0; i < numTentacles; i++) {
const angle = (i / numTentacles) * Math.PI * 2;
const tentacleBase = new THREE.Group();
const radius = 1.4;
tentacleBase.position.set(Math.cos(angle) * radius, 0.5, Math.sin(angle) * radius);
tentacleBase.rotation.set(Math.PI / 2, -angle + Math.PI / 2, 0, 'YXZ');
octopusGroup.add(tentacleBase);
let currentParent = tentacleBase;
const segmentArray = [];
for (let j = 0; j < segmentsPerTentacle; j++) {
const ratioTop = 1 - (j / segmentsPerTentacle);
const ratioBottom = 1 - ((j + 1) / segmentsPerTentacle);
const radiusTop = 0.85 * ratioTop;
const radiusBottom = 0.85 * ratioBottom;
const length = 0.8;
const segmentGeo = new THREE.CylinderGeometry(radiusTop, radiusBottom, length, 16);
segmentGeo.translate(0, -length / 2, 0);
const segment = new THREE.Mesh(segmentGeo, octopusMaterial);
segment.castShadow = true;
segment.receiveShadow = true;
segment.position.y = (j === 0) ? 0 : -length;
segment.rotation.set(0, 0, 0);
if (j < segmentsPerTentacle - 2) {
const suckerGeo = new THREE.SphereGeometry(radiusBottom * 0.4, 8, 8);
const suckerMat = new THREE.MeshStandardMaterial({ color: 0xe6ccba, roughness: 0.9 });
const sucker1 = new THREE.Mesh(suckerGeo, suckerMat);
sucker1.scale.set(1, 0.3, 1);
sucker1.position.set(radiusBottom * 0.4, -length/2, radiusBottom * 0.7);
segment.add(sucker1);
const sucker2 = new THREE.Mesh(suckerGeo, suckerMat);
sucker2.scale.set(1, 0.3, 1);
sucker2.position.set(-radiusBottom * 0.4, -length/2, radiusBottom * 0.7);
segment.add(sucker2);
}
currentParent.add(segment);
segmentArray.push(segment);
currentParent = segment;
}
tentacles.push(segmentArray);
}
}
function createRealCrab() {
crabGroup = new THREE.Group();
scene.add(crabGroup);
const shellColor = 0x3d2828;
const jointColor = 0xc2a689;
const spikeColor = 0xa38c7a;
const crabMaterial = new THREE.MeshStandardMaterial({
color: shellColor,
roughness: 0.8,
metalness: 0.1
});
const jointMaterial = new THREE.MeshStandardMaterial({
color: jointColor,
roughness: 0.6
});
const spikeMaterial = new THREE.MeshStandardMaterial({
color: spikeColor,
roughness: 0.9
});
const shellGeo = new THREE.SphereGeometry(0.55, 32, 16);
const pos = shellGeo.attributes.position;
for (let i = 0; i < pos.count; i++) {
let x = pos.getX(i);
let y = pos.getY(i);
let z = pos.getZ(i);
if (z > 0) {
x *= (1 - z * 0.8);
} else {
x *= (1 - z * 0.4);
}
if (y > 0) {
y += Math.sin(x * 12) * Math.cos(z * 12) * 0.02;
y *= 0.6;
} else {
y *= 0.2;
}
pos.setXYZ(i, x, y, z);
}
shellGeo.computeVertexNormals();
const shell = new THREE.Mesh(shellGeo, crabMaterial);
shell.position.y = 0.4;
shell.castShadow = true;
shell.receiveShadow = true;
crabGroup.add(shell);
const regions = [
{x: 0, z: -0.1, size: 0.08}, {x: 0.15, z: 0, size: 0.06}, {x: -0.15, z: 0, size: 0.06},
{x: 0, z: 0.2, size: 0.07}, {x: 0.25, z: -0.15, size: 0.05}, {x: -0.25, z: -0.15, size: 0.05}
];
regions.forEach(r => {
const s = new THREE.Mesh(new THREE.ConeGeometry(r.size*0.4, r.size, 5), spikeMaterial);
s.position.set(r.x, 0.6, r.z);
crabGroup.add(s);
});
for(let i=0; i<20; i++) {
const angle = (i/20) * Math.PI * 2;
const r = 0.5;
const x = Math.cos(angle) * r * (Math.sin(angle)>0 ? 0.8 : 1.1);
const z = Math.sin(angle) * r;
const s = new THREE.Mesh(new THREE.ConeGeometry(0.015, 0.06, 4), spikeMaterial);
s.position.set(x, 0.45, z);
s.rotation.x = Math.PI/2;
s.rotation.y = -angle + Math.PI/2;
crabGroup.add(s);
}
const eyeStalkGeo = new THREE.CylinderGeometry(0.015, 0.02, 0.1);
const eyeGeo = new THREE.SphereGeometry(0.04, 16, 16);
const eyeMat = new THREE.MeshStandardMaterial({ color: 0x050505, roughness: 0.2 });
const rightEyeGroup = new THREE.Group();
rightEyeGroup.position.set(0.12, 0.5, -0.45);
rightEyeGroup.rotation.x = -Math.PI/4;
const rightEyeStalk = new THREE.Mesh(eyeStalkGeo, crabMaterial);
const rightEye = new THREE.Mesh(eyeGeo, eyeMat);
rightEye.position.y = 0.05;
rightEyeGroup.add(rightEyeStalk);
rightEyeGroup.add(rightEye);
crabGroup.add(rightEyeGroup);
const leftEyeGroup = new THREE.Group();
leftEyeGroup.position.set(-0.12, 0.5, -0.45);
leftEyeGroup.rotation.x = -Math.PI/4;
const leftEyeStalk = new THREE.Mesh(eyeStalkGeo, crabMaterial);
const leftEye = new THREE.Mesh(eyeGeo, eyeMat);
leftEye.position.y = 0.05;
leftEyeGroup.add(leftEyeStalk);
leftEyeGroup.add(leftEye);
crabGroup.add(leftEyeGroup);
const legs = [];
function addSpikeRow(parent, radius, length, count, size, angleOffset) {
for (let i = 0; i < count; i++) {
const spike = new THREE.Mesh(new THREE.ConeGeometry(size*0.3, size, 4), spikeMaterial);
const yPos = (i / (count - 1)) * length * 0.8 + length * 0.1;
spike.position.set(0, yPos, 0);
spike.rotation.y = angleOffset;
spike.rotation.x = Math.PI / 2;
spike.translateY(radius * 0.9);
parent.add(spike);
}
}
function createJoint(size) {
const joint = new THREE.Mesh(new THREE.SphereGeometry(size, 8, 8), jointMaterial);
joint.scale.set(1, 0.8, 1);
return joint;
}
function createRealCrabLeg(sideSign, yRot, len, isClaw, clawScale) {
const root = new THREE.Group();
root.position.set(sideSign * 0.3, 0.3, yRot * 0.5);
root.rotation.y = yRot * sideSign;
const coxa = new THREE.Group();
const baseZRot = sideSign * (isClaw ? -0.2 : -0.3);
coxa.rotation.z = baseZRot;
root.add(coxa);
const thick = isClaw ? 0.08 * clawScale : 0.05;
const merusLen = len * 0.45;
const merus = new THREE.Group();
const merusGeo = new THREE.CylinderGeometry(thick, thick*0.7, merusLen, 8);
merusGeo.translate(0, merusLen/2, 0);
const merusMesh = new THREE.Mesh(merusGeo, crabMaterial);
merusMesh.scale.z = 0.7;
merusMesh.castShadow = true;
merus.add(merusMesh);
addSpikeRow(merus, thick, merusLen, 6, 0.08, 0);
addSpikeRow(merus, thick, merusLen, 6, 0.08, Math.PI);
addSpikeRow(merus, thick, merusLen, 4, 0.06, Math.PI/2);
coxa.add(merus);
const carpus = new THREE.Group();
carpus.position.y = merusLen;
carpus.rotation.z = sideSign * (isClaw ? -0.6 : -1.2);
carpus.add(createJoint(thick*0.9));
merus.add(carpus);
if (isClaw) {
const propodusLen = len * 0.4;
const propodusGeo = new THREE.CylinderGeometry(thick*1.1, thick*1.6, propodusLen, 6);
propodusGeo.translate(0, propodusLen/2, 0);
const propodus = new THREE.Mesh(propodusGeo, crabMaterial);
propodus.scale.z = 0.6;
propodus.castShadow = true;
addSpikeRow(propodus, thick*1.1, propodusLen, 5, 0.08, Math.PI/2);
addSpikeRow(propodus, thick*1.1, propodusLen, 5, 0.08, -Math.PI/2);
carpus.add(propodus);
const fixedFinger = new THREE.Mesh(new THREE.ConeGeometry(thick*0.5, propodusLen*0.8, 8), crabMaterial);
fixedFinger.position.set(thick*0.6 * sideSign, propodusLen, 0);
fixedFinger.rotation.z = -0.15 * sideSign;
fixedFinger.castShadow = true;
carpus.add(fixedFinger);
const dactylus = new THREE.Group();
dactylus.position.set(-thick*0.5 * sideSign, propodusLen*0.9, 0);
const movableFinger = new THREE.Mesh(new THREE.ConeGeometry(thick*0.4, propodusLen*0.8, 8), jointMaterial);
movableFinger.position.y = propodusLen*0.3;
movableFinger.rotation.z = 0.2 * sideSign;
movableFinger.castShadow = true;
dactylus.add(movableFinger);
carpus.add(dactylus);
root.userData = { coxa, carpus, dactylus, baseYRot: yRot * sideSign, baseZRot, side: sideSign };
} else {
const carpusLen = len * 0.15;
const carpusMeshGeo = new THREE.CylinderGeometry(thick*0.8, thick*0.7, carpusLen, 8);
carpusMeshGeo.translate(0, carpusLen/2, 0);
const carpusMesh = new THREE.Mesh(carpusMeshGeo, crabMaterial);
carpusMesh.castShadow = true;
addSpikeRow(carpusMesh, thick*0.7, carpusLen, 2, 0.06, 0);
carpus.add(carpusMesh);
const propodusGroup = new THREE.Group();
propodusGroup.position.y = carpusLen;
propodusGroup.rotation.z = sideSign * 0.2;
propodusGroup.add(createJoint(thick*0.7));
carpus.add(propodusGroup);
const propodusLen = len * 0.35;
const propodusMeshGeo = new THREE.CylinderGeometry(thick*0.7, thick*0.4, propodusLen, 8);
propodusMeshGeo.translate(0, propodusLen/2, 0);
const propodusMesh = new THREE.Mesh(propodusMeshGeo, crabMaterial);
propodusMesh.castShadow = true;
addSpikeRow(propodusMesh, thick*0.5, propodusLen, 4, 0.05, 0);
propodusGroup.add(propodusMesh);
const dactylusGroup = new THREE.Group();
dactylusGroup.position.y = propodusLen;
dactylusGroup.rotation.z = sideSign * -0.3;
dactylusGroup.add(createJoint(thick*0.4));
propodusGroup.add(dactylusGroup);
const dactylusLen = len * 0.25;
const dactylusGeo = new THREE.ConeGeometry(thick*0.4, dactylusLen, 8);
dactylusGeo.translate(0, dactylusLen/2, 0);
const dacPos = dactylusGeo.attributes.position;
for(let i=0; i<dacPos.count; i++) {
let y = dacPos.getY(i);
let ratio = y / dactylusLen;
dacPos.setX(i, dacPos.getX(i) + Math.pow(ratio, 2) * dactylusLen * 0.4 * sideSign);
}
dactylusGeo.computeVertexNormals();
const dactylusMesh = new THREE.Mesh(dactylusGeo, jointMaterial);
dactylusMesh.castShadow = true;
dactylusGroup.add(dactylusMesh);
root.userData = { coxa, carpus: propodusGroup, dactylus: null, baseYRot: yRot * sideSign, baseZRot, side: sideSign };
}
crabGroup.add(root);
return root;
}
crabGroup.userData.clawR = createRealCrabLeg(1, -0.6, 1.8, true, 1.4);
crabGroup.userData.clawL = createRealCrabLeg(-1, -0.6, 1.5, true, 0.8);
legs.push(createRealCrabLeg(1, -0.1, 2.5, false));
legs.push(createRealCrabLeg(-1, -0.1, 2.5, false));
legs.push(createRealCrabLeg(1, 0.4, 2.7, false));
legs.push(createRealCrabLeg(-1, 0.4, 2.7, false));
legs.push(createRealCrabLeg(1, 0.9, 2.2, false));
legs.push(createRealCrabLeg(-1, 0.9, 2.2, false));
crabGroup.userData.legs = legs;
crabGroup.scale.set(1.5, 1.5, 1.5);
}
// ====== リアルに修正したオニカマス ======
function createBarracuda() {
barracudaGroup = new THREE.Group();
scene.add(barracudaGroup);
const silverMat = new THREE.MeshStandardMaterial({ color: 0xd0d8e0, metalness: 0.9, roughness: 0.4 });
const darkMat = new THREE.MeshStandardMaterial({ color: 0x4a5a6a, metalness: 0.7, roughness: 0.5 });
const eyeMat = new THREE.MeshStandardMaterial({ color: 0x111111, roughness: 0.1 });
const finMat = new THREE.MeshStandardMaterial({ color: 0x778899, transparent: true, opacity: 0.8, side: THREE.DoubleSide });
const stripeMat = new THREE.MeshStandardMaterial({ color: 0x223344 });
// 魚全体を組み立てるコンテナ
// -90度倒すことで、+Y(頭)を-Z(進行方向)に向け、+Z(背中)を+Y(上)に向ける
const fishRig = new THREE.Group();
fishRig.rotation.x = -Math.PI / 2;
barracudaGroup.add(fishRig);
const fishBody = new THREE.Group();
fishRig.add(fishBody);
// --- 胴体 ---
const torsoGeo = new THREE.CylinderGeometry(0.4, 0.25, 5.0, 16);
const torso = new THREE.Mesh(torsoGeo, silverMat);
torso.castShadow = true;
fishBody.add(torso);
// 背中(暗い色)
const backGeo = new THREE.CylinderGeometry(0.41, 0.26, 5.0, 16, 1, false, -Math.PI/2, Math.PI);
const back = new THREE.Mesh(backGeo, darkMat);
fishBody.add(back);
// 側面の黒い縞模様
const stripeGeo = new THREE.BoxGeometry(0.85, 0.15, 0.3);
for(let i=0; i<6; i++) {
const stripe = new THREE.Mesh(stripeGeo, stripeMat);
stripe.position.set(0, 1.5 - i*0.8, 0);
stripe.rotation.x = Math.PI/8;
fishBody.add(stripe);
}
// --- 頭部 ---
const headGeo = new THREE.ConeGeometry(0.4, 2.0, 16);
const head = new THREE.Mesh(headGeo, silverMat);
head.position.y = 3.5;
head.castShadow = true;
fishBody.add(head);
// 頭部の背中
const headBackGeo = new THREE.ConeGeometry(0.41, 2.0, 16, 1, false, -Math.PI/2, Math.PI);
const headBack = new THREE.Mesh(headBackGeo, darkMat);
headBack.position.y = 3.5;
fishBody.add(headBack);
// 上顎(追加して、頭らしさを強調)
const upperJawGeo = new THREE.ConeGeometry(0.2, 1.8, 16);
const upperJaw = new THREE.Mesh(upperJawGeo, silverMat);
upperJaw.position.set(0, 4.0, 0.05);
upperJaw.rotation.x = -0.05;
fishBody.add(upperJaw);
const upperJawBack = new THREE.Mesh(new THREE.ConeGeometry(0.21, 1.8, 16, 1, false, -Math.PI/2, Math.PI), darkMat);
upperJawBack.position.set(0, 4.0, 0.05);
upperJawBack.rotation.x = -0.05;
fishBody.add(upperJawBack);
// 下顎(鋭く突き出ている)
const jawGeo = new THREE.ConeGeometry(0.2, 2.2, 16);
const jaw = new THREE.Mesh(jawGeo, silverMat);
jaw.position.set(0, 4.2, -0.15);
jaw.rotation.x = 0.05;
jaw.castShadow = true;
fishBody.add(jaw);
// 目
const eyeGeo = new THREE.SphereGeometry(0.08, 16, 16);
const eyeR = new THREE.Mesh(eyeGeo, eyeMat);
eyeR.position.set(0.3, 3.2, 0.15);
fishBody.add(eyeR);
const eyeL = new THREE.Mesh(eyeGeo, eyeMat);
eyeL.position.set(-0.3, 3.2, 0.15);
fishBody.add(eyeL);
// 歯
const toothGeo = new THREE.ConeGeometry(0.02, 0.2, 4);
for(let i=0; i<7; i++) {
const yPos = 3.6 + i * 0.2;
// 下顎の歯(上向き)
const tR = new THREE.Mesh(toothGeo, silverMat);
tR.position.set(0.12, yPos, -0.1);
tR.rotation.x = Math.PI * 0.6;
fishBody.add(tR);
const tL = new THREE.Mesh(toothGeo, silverMat);
tL.position.set(-0.12, yPos, -0.1);
tL.rotation.x = Math.PI * 0.6;
fishBody.add(tL);
// 上顎の歯(下向き)
if (i < 5) {
const tuR = new THREE.Mesh(toothGeo, silverMat);
tuR.position.set(0.12, yPos, 0.05);
tuR.rotation.x = -Math.PI * 0.6;
fishBody.add(tuR);
const tuL = new THREE.Mesh(toothGeo, silverMat);
tuL.position.set(-0.12, yPos, 0.05);
tuL.rotation.x = -Math.PI * 0.6;
fishBody.add(tuL);
}
}
// --- 尾部(尻尾を振るために別グループ化) ---
const tailGroup = new THREE.Group();
tailGroup.position.y = -2.5;
fishRig.add(tailGroup);
const tailBodyGeo = new THREE.CylinderGeometry(0.25, 0.08, 2.0, 16);
const tailBody = new THREE.Mesh(tailBodyGeo, silverMat);
tailBody.position.y = -1.0;
tailBody.castShadow = true;
tailGroup.add(tailBody);
const tailBackGeo = new THREE.CylinderGeometry(0.26, 0.09, 2.0, 16, 1, false, -Math.PI/2, Math.PI);
const tailBack = new THREE.Mesh(tailBackGeo, darkMat);
tailBack.position.y = -1.0;
tailGroup.add(tailBack);
// --- ヒレ(★修正: 横幅ではなく縦方向(X軸)を細くして自然なヒレにする) ---
const dFinGeo = new THREE.ConeGeometry(0.4, 1.5, 3);
const dFin1 = new THREE.Mesh(dFinGeo, finMat);
dFin1.position.set(0, 0.5, 0.4);
dFin1.rotation.x = Math.PI * 0.6;
dFin1.scale.x = 0.05; // 縦方向に薄くする
fishBody.add(dFin1);
const dFin2 = new THREE.Mesh(dFinGeo, finMat);
dFin2.position.set(0, -1.0, 0.25);
dFin2.rotation.x = Math.PI * 0.6;
dFin2.scale.x = 0.05;
tailGroup.add(dFin2);
const aFin = new THREE.Mesh(dFinGeo, finMat);
aFin.position.set(0, -1.0, -0.25);
aFin.rotation.x = -Math.PI * 0.6;
aFin.scale.x = 0.05;
tailGroup.add(aFin);
// 胸ビレ
const pFinGeo = new THREE.ConeGeometry(0.3, 1.2, 3);
const pFinR = new THREE.Mesh(pFinGeo, finMat);
pFinR.position.set(0.4, 1.5, -0.2);
pFinR.rotation.set(-Math.PI * 0.8, 0, -Math.PI/6);
pFinR.scale.z = 0.05; // 胸ビレは横向きでOK
fishBody.add(pFinR);
const pFinL = new THREE.Mesh(pFinGeo, finMat);
pFinL.position.set(-0.4, 1.5, -0.2);
pFinL.rotation.set(-Math.PI * 0.8, 0, Math.PI/6);
pFinL.scale.z = 0.05;
fishBody.add(pFinL);
// 尾ビレ
const tailFinGroup = new THREE.Group();
tailFinGroup.position.y = -2.0;
tailGroup.add(tailFinGroup);
const tFinGeo = new THREE.ConeGeometry(0.8, 2.5, 3);
const tFinTop = new THREE.Mesh(tFinGeo, finMat);
tFinTop.position.set(0, -0.8, 0.5);
tFinTop.rotation.x = Math.PI * 0.6;
tFinTop.scale.x = 0.05; // 縦方向に薄く
tailFinGroup.add(tFinTop);
const tFinBot = new THREE.Mesh(tFinGeo, finMat);
tFinBot.position.set(0, -0.8, -0.5);
tFinBot.rotation.x = -Math.PI * 0.6;
tFinBot.scale.x = 0.05;
tailFinGroup.add(tFinBot);
barracudaGroup.scale.set(2.2, 2.2, 2.2);
barracudaGroup.userData.tail = tailGroup;
barracudaGroup.userData.fishBody = fishBody;
barracudaGroup.userData.fishRig = fishRig;
}
function createBubbles() {
const bubbleGeo = new THREE.SphereGeometry(0.1, 8, 8);
const bubbleMat = new THREE.MeshStandardMaterial({
color: 0xffffff,
transparent: true,
opacity: 0.3,
roughness: 0,
transmission: 0.9
});
for (let i = 0; i < 50; i++) {
const bubble = new THREE.Mesh(bubbleGeo, bubbleMat);
bubble.position.set(
(Math.random() - 0.5) * 40,
(Math.random() - 0.5) * 40 - 10,
(Math.random() - 0.5) * 40
);
const scale = Math.random() * 1.5 + 0.5;
bubble.scale.set(scale, scale, scale);
bubble.userData.speed = Math.random() * 0.05 + 0.02;
scene.add(bubble);
bubbles.push(bubble);
}
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
requestAnimationFrame(animate);
const elapsedTime = clock.getElapsedTime();
// --- タコのアニメーション ---
if (octopusGroup) {
const head = octopusGroup.children[0];
head.scale.y = 0.7 + Math.sin(elapsedTime * 1.5) * 0.05;
head.scale.x = 1.2 + Math.sin(elapsedTime * 1.5 + Math.PI) * 0.05;
const body = octopusGroup.children[1];
body.scale.y = 0.6 + Math.sin(elapsedTime * 1.5 + 0.5) * 0.02;
}
tentacles.forEach((segmentArray, tIndex) => {
segmentArray.forEach((segment, sIndex) => {
const waveOffset = tIndex * 0.2 - sIndex * 0.4;
const tipFactor = sIndex / segmentArray.length;
const amplitude = 0.05 + tipFactor * 0.2;
segment.rotation.z = Math.sin(elapsedTime * 2.0 + waveOffset) * amplitude;
segment.rotation.x = 0.02 + (tipFactor * 0.1);
});
});
if (octopusGroup) {
octopusGroup.updateMatrixWorld(true);
}
if (octopusGroup && octopusGroup.userData.webMesh) {
const web = octopusGroup.userData.webMesh;
const pos = web.geometry.attributes.position;
const webSegs = octopusGroup.userData.webSegments;
const rDiv = octopusGroup.userData.radialDiv;
const xSegments = 8 * rDiv;
const invMat = new THREE.Matrix4().copy(octopusGroup.matrixWorld).invert();
for (let y = 0; y < webSegs; y++) {
for (let i = 0; i < 8; i++) {
const segA = tentacles[i][y];
const segB = tentacles[(i+1)%8][y];
const posA = new THREE.Vector3();
segA.getWorldPosition(posA);
posA.applyMatrix4(invMat);
posA.y -= 0.15;
const posB = new THREE.Vector3();
segB.getWorldPosition(posB);
posB.applyMatrix4(invMat);
posB.y -= 0.15;
for (let j = 0; j < rDiv; j++) {
const x = i * rDiv + j;
const t = j / rDiv;
const idx = y * (xSegments + 1) + x;
const vec = new THREE.Vector3().lerpVectors(posA, posB, t);
if (j > 0) {
const valley = Math.sin(t * Math.PI);
const factor = y / (webSegs - 1);
vec.y -= valley * factor * 0.6;
vec.x *= (1 - valley * factor * 0.3);
vec.z *= (1 - valley * factor * 0.3);
}
if (y === 0) {
vec.y = 0.4;
vec.x *= 0.25;
vec.z *= 0.25;
}
pos.setXYZ(idx, vec.x, vec.y, vec.z);
}
}
const pos0 = new THREE.Vector3();
tentacles[0][y].getWorldPosition(pos0);
pos0.applyMatrix4(invMat);
pos0.y -= 0.15;
if (y === 0) {
pos0.y = 0.4;
pos0.x *= 0.25;
pos0.z *= 0.25;
}
const idxEnd = y * (xSegments + 1) + xSegments;
pos.setXYZ(idxEnd, pos0.x, pos0.y, pos0.z);
}
pos.needsUpdate = true;
web.geometry.computeVertexNormals();
}
// --- カニ(タラバガニ)のアニメーション ---
if (crabGroup) {
const speed = elapsedTime * 3.0;
const clawR = crabGroup.userData.clawR.userData;
const clawL = crabGroup.userData.clawL.userData;
clawR.coxa.rotation.z = clawR.baseZRot + Math.sin(speed * 0.5) * 0.1;
clawL.coxa.rotation.z = clawL.baseZRot + Math.sin(speed * 0.5 + Math.PI) * 0.1;
clawR.dactylus.rotation.z = Math.abs(Math.sin(speed)) * 0.4;
clawL.dactylus.rotation.z = -Math.abs(Math.sin(speed + 1.0)) * 0.4;
crabGroup.userData.legs.forEach((legObj, i) => {
const leg = legObj.userData;
const offset = (i % 2 === 0) ? 0 : Math.PI;
leg.coxa.rotation.z = leg.baseZRot + Math.max(0, Math.sin(speed + offset)) * 0.2 * leg.side;
legObj.rotation.y = leg.baseYRot + Math.cos(speed + offset) * 0.15;
});
const crabSpeed = elapsedTime * 0.12;
const crabRadius = 11.0;
crabGroup.position.x = Math.cos(crabSpeed) * crabRadius;
crabGroup.position.z = Math.sin(crabSpeed) * crabRadius;
crabGroup.lookAt(0, 0, 0);
crabGroup.position.y = Math.sin(crabGroup.position.x * 0.5) * 0.3 + 0.3;
}
// --- オニカマスのアニメーション ---
if (barracudaGroup) {
const bSpeed = elapsedTime * 0.4;
const bRadius = 16.0;
// 軌道上を前進する
const bx = Math.cos(bSpeed) * bRadius;
const bz = Math.sin(bSpeed) * bRadius;
const by = 12 + Math.sin(elapsedTime * 0.5) * 2.0;
barracudaGroup.position.set(bx, by, bz);
// 少し先の未来の座標を見て、顔を進行方向に向ける
const nextX = Math.cos(bSpeed + 0.1) * bRadius;
const nextZ = Math.sin(bSpeed + 0.1) * bRadius;
const nextY = 12 + Math.sin((elapsedTime + 0.1) * 0.5) * 2.0;
barracudaGroup.lookAt(nextX, nextY, nextZ);
// 体から尾ビレにかけて力が伝わる(波打つ)自然なアニメーション
barracudaGroup.userData.fishBody.rotation.z = Math.sin(elapsedTime * 8) * 0.05;
barracudaGroup.userData.tail.rotation.z = Math.sin(elapsedTime * 8 - 1.0) * 0.2;
barracudaGroup.userData.fishRig.rotation.z = Math.sin(elapsedTime * 4) * 0.05;
}
// --- 泡のアニメーション ---
bubbles.forEach(bubble => {
bubble.position.y += bubble.userData.speed;
bubble.position.x += Math.sin(elapsedTime * 2 + bubble.position.y) * 0.01;
if (bubble.position.y > 20) {
bubble.position.y = -20;
bubble.position.x = (Math.random() - 0.5) * 40;
}
});
controls.update();
renderer.render(scene, camera);
}
</script>
</body>
</html>
眺める