<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>T-REX & TRICERATOPS - Jungle Discovery</title>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #1a2e1a;
font-family: 'Helvetica Neue', Arial, sans-serif;
color: #fff;
}
#ui-overlay {
position: absolute;
top: 40px;
left: 40px;
pointer-events: none;
z-index: 10;
}
.discovery-label {
background: rgba(0, 40, 0, 0.6);
padding: 20px;
border-left: 5px solid #2ecc71;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
backdrop-filter: blur(8px);
}
h1 { margin: 0; font-size: 24px; letter-spacing: 2px; color: #2ecc71; }
p { margin: 5px 0; font-size: 14px; color: #eee; }
.controls-hint {
position: absolute;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.7);
color: #fff;
padding: 10px 25px;
border-radius: 30px;
font-size: 12px;
pointer-events: none;
}
#loading {
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
background: #1a2e1a;
display: flex;
justify-content: center;
align-items: center;
z-index: 100;
transition: opacity 1s;
}
</style>
</head>
<body>
<div id="loading">古代の対峙を再現中...</div>
<div id="ui-overlay">
<div class="discovery-label">
<h1>APEX PREDATOR vs DEFENDER</h1>
<p>Discovery: T-Rex & Triceratops "Lane" Style</p>
<p>Location: Hell Creek Formation (Deep Jungle)</p>
<p style="font-style: italic; color: #ffcc00; font-weight: bold;">[ ROARING POSE vs DEFENSIVE STANCE ]</p>
</div>
</div>
<div class="controls-hint">
マウスで回転 / スクロールでズーム / 迫力の対決シーンを鑑賞
</div>
<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>
let scene, camera, renderer, controls, trexGroup, triceratopsGroup;
function init() {
scene = new THREE.Scene();
const forestGreen = 0x1a2e1a;
scene.background = new THREE.Color(forestGreen);
scene.fog = new THREE.Fog(forestGreen, 40, 160);
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(45, 25, 50);
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.target.set(8, 6, 0);
const ambient = new THREE.AmbientLight(0xffffff, 1.0);
scene.add(ambient);
const sunLight = new THREE.DirectionalLight(0xfffdf5, 1.6);
sunLight.position.set(40, 60, 30);
sunLight.castShadow = true;
sunLight.shadow.mapSize.width = 2048;
sunLight.shadow.mapSize.height = 2048;
scene.add(sunLight);
const hemiLight = new THREE.HemisphereLight(0x88ccff, 0x224422, 0.4);
scene.add(hemiLight);
createForestEnv();
createTREX();
createTriceratops();
document.getElementById('loading').style.opacity = '0';
setTimeout(() => document.getElementById('loading').remove(), 1000);
window.addEventListener('resize', onWindowResize, false);
animate();
}
function createForestEnv() {
const groundGeo = new THREE.PlaneGeometry(400, 400, 32, 32);
const groundMat = new THREE.MeshStandardMaterial({ color: 0x2d422d, roughness: 1.0 });
const ground = new THREE.Mesh(groundGeo, groundMat);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
scene.add(ground);
for (let i = 0; i < 120; i++) {
const tree = createTree();
const angle = Math.random() * Math.PI * 2;
const dist = 50 + Math.random() * 120;
const x = Math.cos(angle) * dist;
const z = Math.sin(angle) * dist;
if (Math.abs(x) < 30 && Math.abs(z) < 30) continue;
tree.position.set(x, 0, z);
tree.rotation.y = Math.random() * Math.PI;
const scale = 1.0 + Math.random() * 2.0;
tree.scale.set(scale, scale, scale);
scene.add(tree);
}
}
function createTree() {
const group = new THREE.Group();
const trunkGeo = new THREE.CylinderGeometry(0.4, 0.7, 14, 8);
const trunkMat = new THREE.MeshStandardMaterial({ color: 0x4d382d });
const trunk = new THREE.Mesh(trunkGeo, trunkMat);
trunk.position.y = 7;
trunk.castShadow = true;
group.add(trunk);
const leafMat = new THREE.MeshStandardMaterial({ color: 0x163d16 });
for (let i = 0; i < 4; i++) {
const leafGeo = new THREE.ConeGeometry(4 - i * 0.8, 8, 8);
const leaves = new THREE.Mesh(leafGeo, leafMat);
leaves.position.y = 11 + i * 3;
leaves.castShadow = true;
group.add(leaves);
}
return group;
}
function createBonePart(geo, x, y, z, rx=0, ry=0, rz=0, color=0x5d4632) {
const mat = new THREE.MeshStandardMaterial({ color: color, roughness: 0.6, metalness: 0.1 });
const mesh = new THREE.Mesh(geo, mat);
mesh.position.set(x, y, z);
mesh.rotation.set(rx, ry, rz);
mesh.castShadow = true;
mesh.receiveShadow = true;
return mesh;
}
function createCurvedBone(path, radius, radialSegments, tubularSegments, color=0x5d4632) {
const geo = new THREE.TubeGeometry(path, tubularSegments, radius, radialSegments, false);
const mat = new THREE.MeshStandardMaterial({ color: color, roughness: 0.6 });
const mesh = new THREE.Mesh(geo, mat);
mesh.castShadow = true;
mesh.receiveShadow = true;
return mesh;
}
function createTREX() {
trexGroup = new THREE.Group();
trexGroup.position.set(0, 0.5, 0);
const skullGroup = new THREE.Group();
const snout = createBonePart(new THREE.BoxGeometry(3.5, 1.3, 1.5), 1.8, 0.4, 0);
skullGroup.add(snout);
const backSkull = createBonePart(new THREE.BoxGeometry(1.6, 1.9, 1.9), 0, 0.7, 0);
skullGroup.add(backSkull);
for(let i=0; i<8; i++) {
const tooth = createBonePart(new THREE.ConeGeometry(0.08, 0.5, 8), -1.5 + i * 0.45, -0.65, 0.6, Math.PI);
snout.add(tooth);
const toothR = tooth.clone(); toothR.position.z = -0.6; snout.add(toothR);
}
const lowerJawGroup = new THREE.Group();
const jawBone = createBonePart(new THREE.BoxGeometry(3.8, 0.4, 1.4), 1.9, 0, 0);
lowerJawGroup.add(jawBone);
for(let i=0; i<7; i++) {
const tooth = createBonePart(new THREE.ConeGeometry(0.07, 0.4, 8), -1.4 + i * 0.5, 0.4, 0.5);
jawBone.add(tooth);
const toothR = tooth.clone(); toothR.position.z = -0.5; jawBone.add(toothR);
}
lowerJawGroup.position.set(-0.5, -0.3, 0); lowerJawGroup.rotation.z = -0.8;
skullGroup.add(lowerJawGroup);
skullGroup.position.set(11, 9, 0); skullGroup.rotation.z = 0.6;
trexGroup.add(skullGroup);
for (let i = 0; i < 55; i++) {
const t = i / 55;
const x = 10 - t * 45;
const y = 6.5 + Math.sin(t * 3.5 + 0.6) * 2.5;
const size = (t < 0.1) ? 0.5 : (1.4 * (1 - t));
trexGroup.add(createBonePart(new THREE.BoxGeometry(1.0, size, size*0.8), x, y, 0, 0, 0, Math.cos(t * 3.5 + 0.6) * 0.5));
if (i > 6 && i < 20) {
const ribSize = 4.8 * (1 - (i-7)/25);
trexGroup.add(createBonePart(new THREE.CylinderGeometry(0.14, 0.07, ribSize), x, y-ribSize/2, 1.4, 0.3, 0, 0.5));
trexGroup.add(createBonePart(new THREE.CylinderGeometry(0.14, 0.07, ribSize), x, y-ribSize/2, -1.4, -0.3, 0, -0.5));
}
}
const pelvis = createBonePart(new THREE.BoxGeometry(4.2, 3.0, 3.5), -3, 7.0, 0); trexGroup.add(pelvis);
const legR = createLeg(); legR.position.set(-2, 9.0, 2.5); legR.rotation.z = 0.3; trexGroup.add(legR);
const legL = createLeg(); legL.position.set(1.5, 9.0, -2.5); legL.rotation.z = -0.4; trexGroup.add(legL);
const armR = createBonePart(new THREE.CylinderGeometry(0.2, 0.15, 2.0), 9, 5.5, 1.5, 0, 0, 0.6); trexGroup.add(armR);
const armL = createBonePart(new THREE.CylinderGeometry(0.2, 0.15, 2.0), 9, 5.5, -1.5, 0, 0, -0.6); trexGroup.add(armL);
scene.add(trexGroup);
}
function createLeg() {
const leg = new THREE.Group();
leg.add(createBonePart(new THREE.CylinderGeometry(0.7, 0.6, 6), 0, -3, 0));
leg.add(createBonePart(new THREE.CylinderGeometry(0.5, 0.4, 6), 0, -8.5, 0.5, 0.5));
leg.add(createBonePart(new THREE.BoxGeometry(2.8, 0.6, 2.0), 1.0, -11.5, 0.5));
return leg;
}
function createTriceratops() {
triceratopsGroup = new THREE.Group();
const boneColor = 0x4a3a2a;
triceratopsGroup.position.set(24, 0.5, 3);
triceratopsGroup.rotation.y = -Math.PI / 1.1;
// --- 頭部ユニット ---
const skullGroup = new THREE.Group();
// 1. 吻部 (Snout)
const snout = createBonePart(new THREE.BoxGeometry(3.8, 2.2, 2.0), 0.8, 0.5, 0, 0, 0, 0, boneColor);
skullGroup.add(snout);
// 2. 後頭部 (Back of the head)
const backHead = createBonePart(new THREE.BoxGeometry(1.6, 1.8, 2.0), -1.0, 0.8, 0, 0, 0, 0, boneColor);
skullGroup.add(backHead);
// 3. フリルの再構築 (より幅広で扇状の盾に)
const frillShape = new THREE.Shape();
const frillSegments = 32;
const frillRadius = 5.2; // さらに大きく
const scallopHeight = 0.35;
// 縁のギザギザ(エポキシピタル)を持つ扇型
for(let i = 0; i <= frillSegments; i++) {
const angle = (i / frillSegments) * Math.PI - Math.PI / 2;
const r = frillRadius + (i % 2 === 0 ? scallopHeight : 0);
const x = Math.cos(angle) * r;
const y = Math.sin(angle) * r * 1.2;
if(i === 0) frillShape.moveTo(x, y);
else frillShape.lineTo(x, y);
}
frillShape.lineTo(0, 0); // 中心へ戻る
// フェネストラ (穴)
const holeL = new THREE.Path();
holeL.absellipse(1.4, 2.6, 0.9, 1.6, 0, Math.PI * 2, true);
frillShape.holes.push(holeL);
const holeR = new THREE.Path();
holeR.absellipse(-1.4, 2.6, 0.9, 1.6, 0, Math.PI * 2, true);
frillShape.holes.push(holeR);
const frillGeo = new THREE.ExtrudeGeometry(frillShape, {
depth: 0.3,
bevelEnabled: true,
bevelThickness: 0.1,
bevelSize: 0.1
});
// フリルの位置と角度を再調整 (後頭部から斜め後ろに流れるように)
const frillMesh = createBonePart(frillGeo, -1.0, 1.2, 0, Math.PI / 2, Math.PI, 0.5, boneColor);
skullGroup.add(frillMesh);
// 4. 嘴 (Beak)
const beak = createBonePart(new THREE.ConeGeometry(0.8, 2.2, 8), 3.2, -0.4, 0, 0, 0, -Math.PI / 2, boneColor);
beak.scale.z = 0.7;
skullGroup.add(beak);
// 5. 角 (前方へ鋭く)
const hornPathL = new THREE.CatmullRomCurve3([
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(2.5, 1.2, 0.5),
new THREE.Vector3(6.5, 1.8, 0.4)
]);
const hornL = createCurvedBone(hornPathL, 0.42, 12, 30, boneColor);
hornL.position.set(0.8, 2.2, 1.1);
hornL.rotation.set(0.3, 0.1, 0);
skullGroup.add(hornL);
const hornPathR = new THREE.CatmullRomCurve3([
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(2.5, 1.2, -0.5),
new THREE.Vector3(6.5, 1.8, -0.4)
]);
const hornR = createCurvedBone(hornPathR, 0.42, 12, 30, boneColor);
hornR.position.set(0.8, 2.2, -1.1);
hornR.rotation.set(-0.3, -0.1, 0);
skullGroup.add(hornR);
const hornN = createBonePart(new THREE.ConeGeometry(0.45, 1.6, 12), 2.8, 1.8, 0, 0, 0, 0.3, boneColor);
skullGroup.add(hornN);
// 6. 眼窩
const eyeL = createBonePart(new THREE.BoxGeometry(1.0, 1.0, 0.3), -0.2, 1.6, 1.1, 0, 0, 0, 0x111111);
skullGroup.add(eyeL);
const eyeR = eyeL.clone(); eyeR.position.z = -1.1;
skullGroup.add(eyeR);
skullGroup.position.set(6, 4.5, 0);
skullGroup.rotation.z = 0.1;
triceratopsGroup.add(skullGroup);
// --- 胴体 ---
const spinePoints = [];
for(let i=0; i<=12; i++) {
const t = i/12;
spinePoints.push(new THREE.Vector3(5 - t * 25, 5 + Math.sin(t * Math.PI) * 1.6, 0));
}
const spineCurve = new THREE.CatmullRomCurve3(spinePoints);
triceratopsGroup.add(createCurvedBone(spineCurve, 0.8, 12, 60, boneColor));
for(let i=0; i<16; i++) {
const t = i / 16;
const p = spineCurve.getPoint(t);
const ribW = 5.5 * Math.sin(t * Math.PI * 0.8 + 0.35);
if(ribW < 1.8) continue;
const ribLPath = new THREE.CatmullRomCurve3([
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(1.4, -ribW*0.6, ribW*0.6),
new THREE.Vector3(0.4, -ribW, 0.4)
]);
const ribL = createCurvedBone(ribLPath, 0.25, 8, 20, boneColor);
ribL.position.set(p.x, p.y - 0.5, 1.2);
triceratopsGroup.add(ribL);
const ribRPath = new THREE.CatmullRomCurve3([
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(1.4, -ribW*0.6, -ribW*0.6),
new THREE.Vector3(0.4, -ribW, -0.4)
]);
const ribR = createCurvedBone(ribRPath, 0.25, 8, 20, boneColor);
ribR.position.set(p.x, p.y - 0.5, -1.2);
triceratopsGroup.add(ribR);
}
const createLimb = (isFront) => {
const group = new THREE.Group();
const h1 = isFront ? 4.0 : 5.0;
const h2 = isFront ? 3.0 : 4.0;
const w = isFront ? 0.95 : 1.2;
group.add(createBonePart(new THREE.CylinderGeometry(w, w*0.8, h1, 12), 0, -h1/2, 0, 0, 0, 0, boneColor));
group.add(createBonePart(new THREE.CylinderGeometry(w*0.8, w*0.6, h2, 12), 0, -h1 - h2/2, 0, 0.15, 0, 0, boneColor));
group.add(createBonePart(new THREE.BoxGeometry(2.5, 0.8, 2.5), 0.8, -h1 - h2 - 0.4, 0, 0, 0, 0, boneColor));
return group;
};
const fL = createLimb(true); fL.position.set(2.8, 4.5, 3.0); triceratopsGroup.add(fL);
const fR = createLimb(true); fR.position.set(2.8, 4.5, -3.0); triceratopsGroup.add(fR);
const bL = createLimb(false); bL.position.set(-10, 5.2, 3.5); triceratopsGroup.add(bL);
const bR = createLimb(false); bR.position.set(-10, 5.2, -3.5); triceratopsGroup.add(bR);
scene.add(triceratopsGroup);
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
window.onload = init;
</script>
</body>
</html>
2匹の恐竜が戦っているシーンを再現しました