<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>King Ghidorah 3D</title>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #050505; /* 暗闇の背景 */
color: #fff;
font-family: 'Courier New', Courier, monospace;
user-select: none;
}
#canvas-container {
width: 100vw;
height: 100vh;
display: block;
position: absolute;
top: 0;
left: 0;
z-index: 1;
}
#ui {
position: absolute;
bottom: 40px;
left: 40px;
pointer-events: none;
z-index: 10;
}
h1 {
margin: 0;
font-size: 3rem;
letter-spacing: 12px;
color: #ffd700;
text-shadow: 0 0 20px #ffaa00;
text-transform: uppercase;
}
p {
margin: 10px 0 0 0;
font-size: 1rem;
letter-spacing: 6px;
color: #aaaaaa;
}
#controls-hint {
position: absolute;
top: 20px;
right: 20px;
font-size: 0.8rem;
color: #888;
z-index: 10;
text-align: right;
letter-spacing: 1px;
}
</style>
<!-- Three.jsとアドオン -->
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
}
}
</script>
</head>
<body>
<div id="ui">
<h1>GHIDORAH</h1>
<p>KING OF THE MONSTERS</p>
</div>
<div id="controls-hint">
[DRAG] ROTATE<br>
[SCROLL] ZOOM
</div>
<div id="canvas-container"></div>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
let scene, camera, renderer, composer;
let ghidorah;
const clock = new THREE.Clock();
// ジオメトリの頂点をランダムに揺らしてゴツゴツさせる関数
function roughGeometry(geo, strength = 0.5) {
const pos = geo.attributes.position;
for(let i=0; i<pos.count; i++) {
pos.setXYZ(
i,
pos.getX(i) + (Math.random()-0.5)*strength,
pos.getY(i) + (Math.random()-0.5)*strength,
pos.getZ(i) + (Math.random()-0.5)*strength
);
}
geo.computeVertexNormals();
return geo;
}
// 鱗のような凹凸を作るためのプロシージャルノイズテクスチャ
function createBumpMap() {
const canvas = document.createElement('canvas');
canvas.width = 512;
canvas.height = 512;
const ctx = canvas.getContext('2d');
const imgData = ctx.createImageData(512, 512);
for(let i=0; i<imgData.data.length; i+=4) {
// 細かいランダムノイズ
const v = Math.random() * 255;
imgData.data[i] = v;
imgData.data[i+1] = v;
imgData.data[i+2] = v;
imgData.data[i+3] = 255;
}
ctx.putImageData(imgData, 0, 0);
const tex = new THREE.CanvasTexture(canvas);
tex.wrapS = THREE.RepeatWrapping;
tex.wrapT = THREE.RepeatWrapping;
tex.repeat.set(10, 10);
return tex;
}
// 共通の黄金マテリアル(重厚な金属質+ゴツゴツした鱗感)
const goldMat = new THREE.MeshStandardMaterial({
color: 0xffd700,
emissive: 0x331a00, // 暗所でのシルエット強調
metalness: 1.0,
roughness: 0.35,
bumpMap: createBumpMap(),
bumpScale: 0.15, // 凹凸の強さ
flatShading: true, // 【追加】ポリゴンの面を際立たせてゴツゴツさせる
side: THREE.DoubleSide
});
function init() {
const container = document.getElementById('canvas-container');
// 1. シーンとカメラの設定
scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x050505, 0.002);
camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 1500);
// さらに全身が収まるようにカメラを調整
camera.position.set(0, 80, 350);
// 2. レンダラーの設定
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.2;
container.appendChild(renderer.domElement);
// 3. ライティング(金属の質感を出すための多方向からの光)
const ambientLight = new THREE.AmbientLight(0x404040, 1.0);
scene.add(ambientLight);
const dirLight1 = new THREE.DirectionalLight(0xffeedd, 3.5);
dirLight1.position.set(100, 200, 100);
scene.add(dirLight1);
const dirLight2 = new THREE.DirectionalLight(0xffaa55, 2.0);
dirLight2.position.set(-100, -50, -100);
scene.add(dirLight2);
const rimLight = new THREE.DirectionalLight(0xffeeaa, 2.0);
rimLight.position.set(0, 100, -200); // 背後からのリムライトでシルエットを強調
scene.add(rimLight);
// 4. ポストプロセッシング(控えめなブルーム)
const renderScene = new RenderPass(scene, camera);
const bloomPass = new UnrealBloomPass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
0.8, // 強さ(控えめに)
0.4, // 半径
0.6 // 閾値(光沢の強い部分だけ光らせる)
);
composer = new EffectComposer(renderer);
composer.addPass(renderScene);
composer.addPass(bloomPass);
// 5. コントロール
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.maxDistance = 600;
controls.target.set(0, 50, 0); // 胴体を中心に
// 6. ギドラの構築
ghidorah = new FullGhidorah();
scene.add(ghidorah.group);
window.addEventListener('resize', onWindowResize);
}
// --- ギドラ全身を管理するクラス ---
class FullGhidorah {
constructor() {
this.group = new THREE.Group();
this.appendages = []; // 首と尻尾
this.wings = []; // 翼
// 全体の基準位置
this.baseY = 20;
this.group.position.y = this.baseY;
this.buildBody();
this.buildLegs();
this.buildWings();
// 首 3本 (中央を長く、左右を少し短く曲げる)
this.appendages.push(new GhidorahAppendage('neck', new THREE.Vector3(0, 25, 10), 0)); // 中央
this.appendages.push(new GhidorahAppendage('neck', new THREE.Vector3(-12, 15, 0), -1)); // 左
this.appendages.push(new GhidorahAppendage('neck', new THREE.Vector3(12, 15, 0), 1)); // 右
// 尻尾 2本
this.appendages.push(new GhidorahAppendage('tail', new THREE.Vector3(-8, -10, -15), -1));
this.appendages.push(new GhidorahAppendage('tail', new THREE.Vector3(8, -10, -15), 1));
this.appendages.forEach(app => this.group.add(app.group));
}
// ゴツゴツした胴体(より生物的なフォルムへ)
buildBody() {
const bodyGroup = new THREE.Group();
// 胸部(張り出したマッシブな胸、頂点を揺らして岩のようにゴツゴツさせる)
const chestGeo = roughGeometry(new THREE.IcosahedronGeometry(18, 2), 2.0);
const chest = new THREE.Mesh(chestGeo, goldMat);
chest.position.set(0, 15, 5);
chest.scale.set(1, 0.9, 1.2);
bodyGroup.add(chest);
// 腹部〜腰
const bellyGeo = roughGeometry(new THREE.IcosahedronGeometry(14, 2), 1.5);
const belly = new THREE.Mesh(bellyGeo, goldMat);
belly.position.set(0, 0, -5);
belly.scale.set(0.9, 1.2, 1.0);
bodyGroup.add(belly);
// 背中の棘(列状に並べる)
for(let i=0; i<7; i++) {
const spikeGeo = new THREE.ConeGeometry(2.5, 18, 4);
const spike = new THREE.Mesh(spikeGeo, goldMat);
spike.position.set(0, 25 - i*6, -5 - i*3);
spike.rotation.x = -Math.PI / 4 - (i*0.1);
bodyGroup.add(spike);
}
this.group.add(bodyGroup);
}
// 力強い2本の足(関節を意識した構造)
buildLegs() {
const legsGroup = new THREE.Group();
for(let i = 0; i < 2; i++) {
const isLeft = i === 0;
const sign = isLeft ? -1 : 1;
const leg = new THREE.Group();
leg.position.set(sign * 14, 0, -5);
// 太もも
const thighGeo = roughGeometry(new THREE.IcosahedronGeometry(9, 1), 1.5);
const thigh = new THREE.Mesh(thighGeo, goldMat);
thigh.scale.set(1, 1.8, 1.2);
thigh.position.set(sign * 2, -10, 5);
thigh.rotation.x = -0.3;
leg.add(thigh);
// すね(六角柱を歪ませる)
const calfGeo = roughGeometry(new THREE.CylinderGeometry(5, 3, 25, 6), 1.0);
const calf = new THREE.Mesh(calfGeo, goldMat);
calf.position.set(sign * 2, -30, 0);
calf.rotation.x = 0.2;
leg.add(calf);
// 足首・足の甲
const footGeo = roughGeometry(new THREE.IcosahedronGeometry(6, 1), 1.0);
const foot = new THREE.Mesh(footGeo, goldMat);
foot.scale.set(1.2, 0.6, 1.5);
foot.position.set(sign * 2, -42, 5);
leg.add(foot);
// 爪
const clawGeo = new THREE.ConeGeometry(1.5, 10, 4);
clawGeo.rotateX(Math.PI / 2);
for(let j=0; j<3; j++) {
const claw = new THREE.Mesh(clawGeo, goldMat);
claw.position.set(sign * 2 + (j-1)*4, -42, 12);
claw.rotation.y = (j-1) * 0.2 * sign;
leg.add(claw);
}
legsGroup.add(leg);
}
this.group.add(legsGroup);
}
// 巨大な翼(ShapeGeometryを用いてコウモリのような皮膜を再現)
buildWings() {
this.wingsGroup = new THREE.Group();
// 翼のシルエット定義
const shape = new THREE.Shape();
shape.moveTo(0, 0); // 付け根
shape.lineTo(40, 30); // 腕の第一関節
shape.lineTo(95, 45); // 翼端1
shape.quadraticCurveTo(80, 10, 110, -20); // 翼端2
shape.quadraticCurveTo(70, -5, 80, -50); // 翼端3
shape.quadraticCurveTo(45, -10, 40, -60); // 翼端4
shape.quadraticCurveTo(20, -15, 0, -30); // 胴体へ戻る
shape.lineTo(0, 0);
const wingGeo = new THREE.ShapeGeometry(shape);
// 皮膜に立体感をつける(波打たせる)
const pos = wingGeo.attributes.position;
for(let j=0; j<pos.count; j++) {
const x = pos.getX(j);
const y = pos.getY(j);
// 大きなうねりに加え、細かいランダムな凹凸を追加してシワっぽくする
pos.setZ(j, Math.sin(x * 0.1) * 8 + Math.cos(y * 0.1) * 4 + (Math.random()-0.5)*1.5);
}
wingGeo.computeVertexNormals();
for (let i = 0; i < 2; i++) {
const isLeft = i === 0;
const sign = isLeft ? -1 : 1;
const wing = new THREE.Group();
wing.position.set(sign * 15, 25, -10);
// 主骨(太い腕、カクカクさせる)
const armGeo = roughGeometry(new THREE.CylinderGeometry(4, 2, 60, 6), 0.5);
const arm = new THREE.Mesh(armGeo, goldMat);
arm.position.set(sign * 25, 15, 0);
arm.rotation.z = sign * -Math.PI / 3;
wing.add(arm);
// 皮膜メッシュ
const membrane = new THREE.Mesh(wingGeo, goldMat);
// 左翼の場合はX軸を反転させる
if (isLeft) {
membrane.scale.x = -1;
}
wing.add(membrane);
wing.userData = { sign: sign, baseRotZ: sign * 0.1, baseRotY: sign * -0.2 };
wing.rotation.z = wing.userData.baseRotZ;
wing.rotation.y = wing.userData.baseRotY;
this.wings.push(wing);
this.wingsGroup.add(wing);
}
this.group.add(this.wingsGroup);
}
update(time) {
// 1. 全体のゆっくりとした上下動(呼吸)
this.group.position.y = this.baseY + Math.sin(time * 1.5) * 2;
// 2. 翼の羽ばたき
this.wings.forEach(wing => {
const flap = Math.sin(time * 2.0);
wing.rotation.z = wing.userData.baseRotZ + (flap * wing.userData.sign * 0.3);
wing.rotation.y = wing.userData.baseRotY + (flap * wing.userData.sign * 0.1);
});
// 3. 首と尻尾のうねり
this.appendages.forEach(app => app.update(time));
}
}
// --- 首と尻尾を生成・管理する汎用クラス ---
class GhidorahAppendage {
constructor(type, startPos, indexSign) {
this.group = new THREE.Group();
this.type = type;
this.indexSign = indexSign;
this.segmentCount = type === 'neck' ? 50 : 60;
this.startPos = startPos;
this.points = [];
this.basePoints = [];
if (type === 'neck') {
// 中央は高く、左右は外側に広がる
const height = indexSign === 0 ? 80 : 60;
const spread = indexSign * 40;
const endPos = new THREE.Vector3(spread, height, 40 + Math.abs(indexSign)*10);
this.basePoints = [
startPos.clone(),
new THREE.Vector3(spread * 0.3, height * 0.4, 10),
new THREE.Vector3(spread * 0.7, height * 0.7, 20),
endPos.clone()
];
} else {
// 尻尾
const endPos = new THREE.Vector3(indexSign * 50, -40, -120);
this.basePoints = [
startPos.clone(),
new THREE.Vector3(indexSign * 15, -20, -40),
new THREE.Vector3(indexSign * 30, -30, -80),
endPos.clone()
];
}
this.basePoints.forEach(p => this.points.push(p.clone()));
this.curve = new THREE.CatmullRomCurve3(this.points);
// 節の形を滑らかな円柱から、ゴツゴツした多面体(八面体)に変更
const segmentGeo = new THREE.OctahedronGeometry(4.5, 0);
this.instancedMesh = new THREE.InstancedMesh(segmentGeo, goldMat, this.segmentCount);
// 背中の棘(少しランダムに歪ませて有機的に)
const spikeGeo = roughGeometry(new THREE.ConeGeometry(2, 12, 4), 0.3);
spikeGeo.translate(0, 6, 0);
this.spikeMesh = new THREE.InstancedMesh(spikeGeo, goldMat, this.segmentCount);
this.group.add(this.instancedMesh);
this.group.add(this.spikeMesh);
if (type === 'neck') {
this.head = this.buildHead();
this.group.add(this.head);
} else {
// 尻尾の先端(トゲ付きメイス)
const tailTipGroup = new THREE.Group();
const tipCore = new THREE.Mesh(new THREE.IcosahedronGeometry(4, 1), goldMat);
tailTipGroup.add(tipCore);
// 周囲のトゲ
for(let i=0; i<4; i++) {
const s = new THREE.Mesh(new THREE.ConeGeometry(1, 8, 4), goldMat);
s.rotation.x = Math.PI/2;
s.rotation.z = (Math.PI/2) * i;
s.position.y = Math.sin((Math.PI/2)*i) * 3;
s.position.x = Math.cos((Math.PI/2)*i) * 3;
tailTipGroup.add(s);
}
this.head = tailTipGroup;
this.group.add(this.head);
}
this.dummy = new THREE.Object3D();
this.spikeDummy = new THREE.Object3D();
this.timeOffset = indexSign * 2.0 + (type === 'tail' ? 5.0 : 0);
}
// 竜の頭部(よりシャープで威圧感のあるシルエットに)
buildHead() {
const headGroup = new THREE.Group();
const matGlow = new THREE.MeshBasicMaterial({ color: 0xff1100 }); // 赤い目
// 頭部全体を少し前に出す
const offsetZ = 5;
// 上顎(細長くシャープに)
const upperJawGeo = new THREE.ConeGeometry(2.0, 16, 6);
upperJawGeo.rotateX(-Math.PI / 2);
upperJawGeo.translate(0, 2, offsetZ + 4);
const upperJaw = new THREE.Mesh(upperJawGeo, goldMat);
headGroup.add(upperJaw);
// 下顎
const lowerJawGeo = new THREE.ConeGeometry(1.2, 14, 6);
lowerJawGeo.rotateX(-Math.PI / 2 + 0.3);
lowerJawGeo.translate(0, -0.5, offsetZ + 3);
const lowerJaw = new THREE.Mesh(lowerJawGeo, goldMat);
headGroup.add(lowerJaw);
// 後頭部・頬
const skullGeo = new THREE.SphereGeometry(3.5, 8, 8);
const skull = new THREE.Mesh(skullGeo, goldMat);
skull.position.set(0, 2, offsetZ - 2);
headGroup.add(skull);
// 放射状に伸びる角(王冠のように)
const hornAngles = [-0.6, -0.3, 0, 0.3, 0.6];
hornAngles.forEach((angle, idx) => {
const length = idx === 2 ? 14 : 10; // 中央を長く
const hornGeo = new THREE.ConeGeometry(1, length, 4);
hornGeo.rotateX(Math.PI / 2 + 0.4);
const horn = new THREE.Mesh(hornGeo, goldMat);
horn.position.set(angle * 5, 4, offsetZ - 4);
horn.rotation.y = angle;
horn.rotation.x -= Math.abs(angle) * 0.5;
headGroup.add(horn);
});
// 目(少し吊り目に配置)
const eyeGeo = new THREE.SphereGeometry(0.7, 8, 8);
const eyeL = new THREE.Mesh(eyeGeo, matGlow);
eyeL.position.set(-1.8, 3.2, offsetZ + 1);
headGroup.add(eyeL);
const eyeR = new THREE.Mesh(eyeGeo, matGlow);
eyeR.position.set(1.8, 3.2, offsetZ + 1);
headGroup.add(eyeR);
return headGroup;
}
update(time) {
const t = time * 2.0 + this.timeOffset;
// 制御点を揺らして有機的なうねりを作る
for(let i = 1; i < this.points.length; i++) {
const waveAmp = this.type === 'neck' ? 8 : 18;
const speedMultiplier = this.type === 'neck' ? 0.8 : 1.2;
this.points[i].x = this.basePoints[i].x + Math.sin(t * speedMultiplier + i) * waveAmp;
this.points[i].y = this.basePoints[i].y + Math.cos(t * 0.7 * speedMultiplier + i) * waveAmp * 0.5;
if(i < this.points.length - 1) {
this.points[i].z = this.basePoints[i].z + Math.sin(t * 1.1 * speedMultiplier + i) * waveAmp;
}
}
const curvePoints = this.curve.getPoints(this.segmentCount - 1);
for(let i = 0; i < this.segmentCount; i++) {
const pt = curvePoints[i];
this.dummy.position.copy(pt);
this.spikeDummy.position.copy(pt);
if (i < this.segmentCount - 1) {
this.dummy.lookAt(curvePoints[i+1]);
this.spikeDummy.lookAt(curvePoints[i+1]);
}
// 節ごとにランダムな角度をつけて、鱗が重なり合ったようなゴツゴツ感を出す
this.dummy.rotateZ(Math.sin(t*0.5 + i*0.1) * 0.2 + (i * 123.45));
this.dummy.rotateX((i * 45.67));
// 太さの変化(滑らかに)
let scale = 1.0;
const norm = i / this.segmentCount;
if (this.type === 'neck') {
// 根元太い(1.2) -> 中間少し細い(0.8) -> 頭付近戻る(1.0)
scale = 0.8 + Math.sin(norm * Math.PI) * 0.1 + (1.0 - norm) * 0.5;
} else {
scale = 1.3 * (1.0 - norm * 0.7);
}
this.dummy.scale.set(scale, scale, 1.0); // Z軸(進行方向)は伸ばさない
this.dummy.updateMatrix();
this.instancedMesh.setMatrixAt(i, this.dummy.matrix);
// 棘の配置(背中側)
this.spikeDummy.rotateX(-Math.PI / 2);
this.spikeDummy.translateY(scale * 3.5);
const spikeScale = scale * (this.type === 'neck' ? 0.9 : 1.3) * (i % 2 === 0 ? 1 : 0.6);
this.spikeDummy.scale.set(spikeScale, spikeScale, spikeScale);
this.spikeDummy.updateMatrix();
this.spikeMesh.setMatrixAt(i, this.spikeDummy.matrix);
}
this.instancedMesh.instanceMatrix.needsUpdate = true;
this.spikeMesh.instanceMatrix.needsUpdate = true;
// 先端パーツの更新
const lastPt = curvePoints[this.segmentCount - 1];
const prevPt = curvePoints[this.segmentCount - 2];
this.head.position.copy(lastPt);
const dir = new THREE.Vector3().subVectors(lastPt, prevPt).normalize();
this.head.lookAt(lastPt.clone().add(dir));
if (this.type === 'neck') {
this.head.rotateY(Math.sin(t) * 0.3);
this.head.rotateZ(Math.cos(t * 0.8) * 0.2);
}
}
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
composer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
requestAnimationFrame(animate);
const time = clock.getElapsedTime();
if (ghidorah) {
ghidorah.update(time);
}
composer.render();
}
init();
animate();
</script>
</body>
</html>
黄金の輝きを表現しました。