以下は、前回のコード全体に対して、以下の2点の修正を加えた例です。
- 丘陵の起伏をもっと緩やかに
・全体の高さスケールを従来の 25 から 15 に変更し、getHeightAt 関数内の各ノイズの重みも低めに設定しています。これにより、地形はより穏やかな丘陵状になります。 - 樹木の表現をよりリアルに
・幹は CylinderGeometry の分割数を増やし(例:8→12)さらに各頂点にランダムなずれを加えることで、僅かな曲がりを表現しています。
・また、針葉樹の場合はレベル数を増やし、広葉樹の場合も球状ジオメトリに対してノイズを強めることで、葉の塊がより不規則な印象になるよう調整しています。 以下のコード全体をお使いください。
html
無限に広がる3D地形探索 body { margin: 0; overflow: hidden; font-family: Arial, sans-serif; } canvas { display: block; } .info { position: absolute; top: 10px; left: 10px; background: rgba(0, 0, 0, 0.7); color: white; padding: 10px; border-radius: 5px; font-size: 14px; } .loading { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0, 0, 0, 0.8); color: white; padding: 20px; border-radius: 10px; font-size: 18px; text-align: center; } .coordinates { position: absolute; bottom: 10px; left: 10px; background: rgba(0, 0, 0, 0.7); color: white; padding: 10px; border-radius: 5px; font-size: 14px; } 読み込み中... 初期地形を生成しています 操作方法 W: 前進(※ペダル操作により自動前進します) S: 後退 A: 左へ D: 右へ マウス: 視点移動 スペース: ジャンプ Enter: ペダリング入力 数字キー 1~9: ギア変更 ペダル速度: 0.0 km/h 座標: X: 0, Z: 0 // ── ペダルシミュレーターのクラス ───────────────────────────── class PedalSimulator { constructor() { this.tapTimes = []; this.virtualSpeed = 0; // 推定速度 (km/h) this.gear = 1; // 初期ギアは1 this.maxHistory = 5; // 過去5回分のタップタイムを利用 this.baseFactor = 0.2; // 1rpmあたりの速度係数(ギア1の場合) } // Enterキーが押されたときに呼び出す registerTap() { const now = performance.now(); this.tapTimes.push(now); if (this.tapTimes.length > this.maxHistory) { this.tapTimes.shift(); } this.calculateCadence(); } // タップ間隔からケイデンスと推定速度を計算する calculateCadence() { if (this.tapTimes.length a + b, 0) / intervals.length; // ケイデンス (rpm) = 60000 / 平均タップ間隔(ミリ秒) const cadence = 60000 / avgInterval; // ギアに応じた変換係数を掛ける(ギアが大きいほど速度が高くなる) this.virtualSpeed = cadence * this.gear * this.baseFactor; console.log(`ケイデンス: ${cadence.toFixed(1)} rpm, ギア: ${this.gear}, 推定速度: ${this.virtualSpeed.toFixed(1)} km/h`); } // 現在の推定速度を返す getSpeed() { return this.virtualSpeed; } // ギアの設定(1~9) setGear(newGear) { newGear = Math.max(1, Math.min(9, newGear)); this.gear = newGear; console.log(`ギアを ${this.gear} に設定しました`); // ギア変更後に再計算(直近のタップ情報がある場合) this.calculateCadence(); } } // ペダルシミュレーターのインスタンス作成とキーイベント設定 const pedalSimulator = new PedalSimulator(); document.addEventListener('keydown', (e) => { if (e.key === 'Enter') { pedalSimulator.registerTap(); } if (e.key >= '1' && e.key 0 ? heightData[y * width + (x - 1)] : heightData[y * width + x]; const right = x 0 ? heightData[(y - 1) * width + x] : heightData[y * width + x]; const bottom = y 0.5 ? 0 : 1; const trunkHeight = height * (3 + Math.random() * 2); const trunkRadius = 0.1 + Math.random() * 0.2; // 幹の生成(より多くの分割数とランダムなずれを加える) const trunkGeometry = new THREE.CylinderGeometry(trunkRadius * 0.7, trunkRadius * 1.3, trunkHeight, 12); // 各頂点に微小な乱れを与え、幹に不規則性を追加 const posAttr = trunkGeometry.attributes.position; for (let i = 0; i 2 && h 0.7 ? 1 : 0; const baseColor = { r: 40 + random() * 40, g: 100 + random() * 70, b: 20 + random() * 40 }; for (let i = 0; i 0.7) { const flowerX = endX; const flowerY = endY + 5; const flowerSize = 2 + random() * 3; const flowerColors = ['#FFFFFF', '#FFD700', '#FF6347', '#9370DB', '#FF69B4']; const flowerColor = flowerColors[Math.floor(random() * flowerColors.length)]; ctx.fillStyle = flowerColor; for (let j = 0; j 0.5 && h removeDistance) { scene.remove(chunkInfo.mesh); chunkInfo.trees.forEach(tree => scene.remove(tree)); chunkInfo.grass.forEach(grass => scene.remove(grass)); chunks.delete(key); debugInfo.chunksActive--; } } } } } // ── 水面、ライト、空・雲の設定 ───────────────────────────── function addWater() { const waterGeometry = new THREE.PlaneGeometry(chunkSize * (viewDistance * 4), chunkSize * (viewDistance * 4)); const waterMaterial = new THREE.MeshStandardMaterial({ color: 0x2389da, transparent: true, opacity: 0.6, roughness: 0.1, metalness: 0.1 }); const water = new THREE.Mesh(waterGeometry, waterMaterial); water.rotation.x = -Math.PI / 2; water.position.y = 0.5; function updateWater() { water.position.x = camera.position.x; water.position.z = camera.position.z; } scene.add(water); return updateWater; } function setupLights() { const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); scene.add(ambientLight); const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); directionalLight.position.set(50, 100, 50); directionalLight.castShadow = true; directionalLight.shadow.mapSize.width = 2048; directionalLight.shadow.mapSize.height = 2048; directionalLight.shadow.camera.left = -100; directionalLight.shadow.camera.right = 100; directionalLight.shadow.camera.top = 100; directionalLight.shadow.camera.bottom = -100; directionalLight.shadow.camera.far = 250; directionalLight.shadow.bias = -0.0005; scene.add(directionalLight); const fillLight = new THREE.DirectionalLight(0x90a0ff, 0.15); fillLight.position.set(-50, 80, -50); scene.add(fillLight); function updateLights() { directionalLight.position.x = camera.position.x + 50; directionalLight.position.z = camera.position.z + 50; fillLight.position.x = camera.position.x - 50; fillLight.position.z = camera.position.z - 50; } return updateLights; } function addSkyAndClouds() { const skyGeometry = new THREE.SphereGeometry(500, 32, 32); skyGeometry.scale(-1, 1, 1); const skyCanvas = document.createElement('canvas'); skyCanvas.width = 1024; skyCanvas.height = 1024; const skyCtx = skyCanvas.getContext('2d'); const skyGradient = skyCtx.createLinearGradient(0, 0, 0, 1024); skyGradient.addColorStop(0, '#1e90ff'); skyGradient.addColorStop(0.4, '#87ceeb'); skyGradient.addColorStop(1, '#e0f2f7'); skyCtx.fillStyle = skyGradient; skyCtx.fillRect(0, 0, 1024, 1024); const skyTexture = new THREE.CanvasTexture(skyCanvas); const skyMaterial = new THREE.MeshBasicMaterial({ map: skyTexture, side: THREE.BackSide }); const sky = new THREE.Mesh(skyGeometry, skyMaterial); function updateSky() { sky.position.x = camera.position.x; sky.position.z = camera.position.z; } scene.add(sky); const cloudParticles = []; const cloudCount = 50; for (let i = 0; i { cloud.userData.angle += cloud.userData.speed * delta; cloud.position.x = camera.position.x + Math.cos(cloud.userData.angle) * cloud.userData.radius; cloud.position.z = camera.position.z + Math.sin(cloud.userData.angle) * cloud.userData.radius; }); } return { updateSky, updateClouds, cloudParticles }; } camera.position.set(0, 5, 0); camera.lookAt(0, 5, -10); // ── プレイヤー操作設定 ───────────────────────────── let verticalVelocity = 0; let isJumping = false; let moveBackward = false, moveRight = false, moveLeft = false, jump = false; // Wキーはペダル操作により自動前進するので無効、その他は従来通り document.addEventListener('keydown', function(event) { switch (event.key.toLowerCase()) { case 's': moveBackward = true; break; case 'a': moveLeft = true; break; case 'd': moveRight = true; break; case ' ': if (!isJumping) jump = true; break; } }); document.addEventListener('keyup', function(event) { switch (event.key.toLowerCase()) { case 's': moveBackward = false; break; case 'a': moveLeft = false; break; case 'd': moveRight = false; break; case ' ': jump = false; break; } }); let mouseX = 0, mouseY = 0; let isMouseDown = false; document.addEventListener('mousedown', function() { isMouseDown = true; }); document.addEventListener('mouseup', function() { isMouseDown = false; }); document.addEventListener('mousemove', function(event) { if (isMouseDown) { const movementX = event.movementX || 0; const movementY = event.movementY || 0; mouseX -= movementX * 0.002; mouseY -= movementY * 0.002; mouseY = Math.max(-Math.PI/2+0.1, Math.min(Math.PI/2-0.1, mouseY)); } }); // 擬似乱数生成器 Math.seedrandom = function(seed) { function mulberry32(a) { return function() { let t = a += 0x6D2B79F5; t = Math.imul(t ^ t >>> 15, t | 1); t ^= t + Math.imul(t ^ t >>> 7, t | 61); return ((t ^ t >>> 14) >>> 0) / 4294967296; }; } const seedNumber = seed.split('').reduce((a, b) => { a = ((a m/s: 1km/h = 1/3.6 m/s, 1 frame(約16ms)に換算すると1/216 m/frame const conversionFactor = 1 / 216; let effectiveSpeed = pedalSpeed * conversionFactor; // 勾配補正:前方2m先の地形高さをサンプリング const direction = new THREE.Vector3(); camera.getWorldDirection(direction); direction.y = 0; direction.normalize(); const sampleDistance = 2; // サンプル距離 2m const currentHeight = getHeightAt(camera.position.x, camera.position.z); const sampleX = camera.position.x + direction.x * sampleDistance; const sampleZ = camera.position.z + direction.z * sampleDistance; const aheadHeight = getHeightAt(sampleX, sampleZ); const slope = (aheadHeight - currentHeight) / sampleDistance; const slopeCoefficient = 0.5; const slopeFactor = Math.max(0.2, 1 - slopeCoefficient * slope); effectiveSpeed *= slopeFactor; // 速度ベクトル計算(自動前進+左右・後退操作) const velocity = new THREE.Vector3(); const sideDirection = new THREE.Vector3(-direction.z, 0, direction.x); velocity.add(direction.multiplyScalar(effectiveSpeed * delta)); if (moveBackward) velocity.sub(direction.clone().multiplyScalar(0.4 * delta)); if (moveRight) velocity.add(sideDirection.multiplyScalar(0.4 * delta)); if (moveLeft) velocity.sub(sideDirection.clone().multiplyScalar(0.4 * delta)); // ジャンプ処理 if (jump && !isJumping) { verticalVelocity = 0.15; isJumping = true; } verticalVelocity -= 0.006 * delta; camera.position.x += velocity.x; camera.position.z += velocity.z; camera.position.y += verticalVelocity * delta; const terrainHeight = getHeightAt(camera.position.x, camera.position.z); if (camera.position.y