3D Mode7 – ハイトマップ&深度テスト(バーチャルジョイスティック実装例) ~ 疑似3Dと比較した完全3D処理の高速性
.jpg)
3D Mode7 – ハイトマップ&深度テスト レースゲーム3D実装例その3 ~ 疑似3Dと比較した高速3D処理
本実装例は、従来のCPU主体の疑似3D方式と比較して、WebGL2 を利用した完全な3D描画処理を実現する方法を示しています。ここでは、GPUの並列処理やインスタンシング技術により、複雑なシーンでも高速かつ滑らかな描画が可能になっている点に注目してください。特に、モバイルデバイスでも高いパフォーマンスを発揮できる点が大きなメリットです。
フル画面は こちらのリンク からアクセスできます。
1. HTML と CSS の構成
プロジェクトはシンプルなHTMLとCSSで構成されています。
HTMLでは、3D描画のためのキャンバス、操作用の仮想ジョイスティック、及び各種パラメータを調整する設定ポップアップが配置されています。
CSSは、キャンバスの全画面表示、UI部品の配置、及びレスポンシブデザインを実現しており、PCやスマートフォンなど様々なデバイスで快適な操作性を提供します。
2. JavaScript の詳しい解説
2.1 WebGL2 の初期化とキャンバスリサイズ
WebGL2 コンテキストを取得することで、最新のハードウェアアクセラレーションを利用し、GPU上での並列処理による高速な3D描画を実現しています。
・canvas.getContext("webgl2")
によりWebGL2コンテキストを取得。
・ウィンドウサイズ変更に応じたキャンバスのリサイズと、gl.viewport()
の更新により、常に最適な描画環境を維持します。
2.2 ユーザーインターフェースと操作
ユーザー入力は、仮想ジョイスティックとキーボード操作を通じて受け取られます。
・左下のジョイスティックはステアリング操作用、右下はアクセル/ブレーキ操作用に配置。タッチやマウス操作に対応して直感的に車両の動きを制御します。
・設定ポップアップでは、カメラの高さ、視野角、ピッチ、シェイク、ハイトマップの高さなどをスライダーで調整し、シーンの見た目をリアルタイムで変更できます。
2.3 シェーダーの定義とコンパイル
GPU上でのリアルタイム描画処理は、シェーダーによって行われます。
・Vertex Shaderでは、各頂点の位置情報とテクスチャ座標を受け取り、モデルビュー射影行列(uMVP)を適用して3D空間に変換。さらに、高さマップ(uHeightTex)をサンプリングし、頂点の高さを調整することで地形の凹凸を表現します。
・Fragment Shaderは、頂点シェーダーから渡されたテクスチャ座標に基づき、テクスチャ(uTexture)から色をサンプリングし、最終的なピクセル色(fragColor)を出力します。
これらのシェーダーは、compileShader
関数を通じてコンパイルされ、GPU上での高速な並列計算が実現されます。
2.4 グリッドジオメトリとインスタンシング
地形は、指定したサイズと分割数に基づいて生成されるグリッドジオメトリを用いて描画されます。
・generateGrid
関数により、平面状の頂点データとインデックスデータを生成。
・インスタンシング技術により、各レイヤーやスライスを1回の描画呼び出しで同時にレンダリングするため、通信オーバーヘッドが大幅に削減され、非常に効率的な描画が可能となっています。
2.5 テクスチャの非同期読み込み
loadTexture
関数を使用して、基本テクスチャ(uBaseTex)と高さマップ(uHeightTex)を外部から非同期に読み込みます。
読み込み完了後、テクスチャパラメータが設定され、レンダリングループが開始されることで、スムーズな描画が保証されます。
2.6 レンダーループとカメラ制御
requestAnimationFrame
を利用したレンダーループにより、毎フレームユーザー入力を反映したシーン更新と描画が行われます。
・ユーザーの仮想ジョイスティックやキーボード入力により、カメラの位置、角度、そしてシェイク効果がリアルタイムに変更され、動的なシーン再構成が実現されます。
・更新されたカメラパラメータは、uniform変数としてシェーダーに送られ、GPU上での高速な並列計算により、滑らかな描画が達成されます。
3. 疑似3Dと比較した完全3D実装の優位性
従来の疑似3D方式では、CPUで2Dスプライトの合成や透視変換を行うため、シーンが複雑になると処理負荷が急増し、特に低スペックのモバイル環境ではパフォーマンスに限界がありました。
本実装例では、以下の技術により高速かつ滑らかな描画が可能となっています:
- GPU の並列処理: WebGL2 と GLSL ES 3.00 により、頂点およびフラグメントの処理が数百~数千のGPUコアで並列に実行され、従来のCPU主体の疑似3D方式と比べて大幅な高速化が実現されます。
- インスタンシング技術: 1回の描画呼び出しで複数のオブジェクトを同時にレンダリングするため、通信オーバーヘッドが削減され、複雑なシーンでも高いフレームレートが維持されます。
- シェーダー最適化: シェーダー内での計算処理が効率的に最適化され、透視変換やテクスチャサンプリングの処理が迅速に行われるため、全体として非常に高速な描画が可能です。
これらの技術により、従来の疑似3D手法に比べ、スマートフォンなどの低スペックデバイスでも快適に動作する、高速な3Dレンダリングが実現されています。
まとめ
「3D Mode7 – ハイトマップ&深度テスト レースゲーム3D実装例その3 ~ 疑似3Dと比較した高速3D処理」は、従来の疑似3D方式から進化し、WebGL2を用いた完全な3D描画によって、GPUの並列処理やインスタンシング技術を最大限に活用しています。
仮想ジョイスティックや設定ポップアップによる直感的な操作と、最適化されたシェーダー処理により、ユーザーはリアルタイムでシーンのパラメータを調整でき、レースゲームならではの迫力ある走行シーンが実現されます。
この実装例を参考に、最新のWebGL2技術による3D描画の可能性と、その高速性をぜひ体験してください。
最終版ソースコード
下記のコードをコピーして、自由にお使いください。各パラメータはリアルタイムに変更可能なため、動作や見た目のカスタマイズも容易です。
<!DOCTYPE html>
<html lang="ja"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<!-- スマホ・タブレット表示用 -->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Mode7 – ハイトマップ&深度テスト(バーチャルジョイスティック版)</title>
<style>
html,
body {
margin: 0;
padding: 0;
overflow: hidden;
}
canvas {
display: block;
}
/* 左下:ステアリング用ジョイスティック */
#joystickLeft {
position: absolute;
bottom: 10px;
left: 10px;
width: 120px;
height: 120px;
background: rgba(200, 200, 200, 0.5);
border-radius: 50%;
touch-action: none;
z-index: 100;
}
/* 右下:アクセル/ブレーキ用ジョイスティック */
#joystickRight {
position: absolute;
bottom: 10px;
right: 10px;
width: 120px;
height: 120px;
background: rgba(200, 200, 200, 0.5);
border-radius: 50%;
touch-action: none;
z-index: 100;
}
/* ジョイスティックのノブ(共通) */
.joystick-knob {
position: absolute;
left: 50%;
top: 50%;
width: 48px;
height: 48px;
background: #666;
border-radius: 50%;
transform: translate(-50%, -50%);
touch-action: none;
}
/* 設定ボタン(右上) */
#settingsButton {
position: absolute;
top: 10px;
right: 70px;
z-index: 100;
font-size: 20px;
padding: 10px 15px;
border: none;
border-radius: 5px;
background: #444;
color: #fff;
cursor: pointer;
user-select: none;
-webkit-touch-callout: none;
}
/* 設定ポップアップ */
#settingsPopup {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #fff3;
border: 1px solid #ccc;
padding: 20px;
z-index: 200;
display: none;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
font-family: sans-serif;
}
#settingsPopup h3 {
margin-top: 0;
}
#settingsPopup label {
display: block;
margin: 10px 0 5px;
}
#settingsPopup input[type="range"] {
width: 100%;
}
/* スライダー(設定用)のサムは大きく */
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
height: 32px;
width: 32px;
border-radius: 50%;
background: #666;
cursor: pointer;
}
input[type="range"]::-moz-range-thumb {
height: 32px;
width: 32px;
border-radius: 50%;
background: #666;
cursor: pointer;
}
</style>
</head>
<body>
<canvas id="glCanvas" width="2085" height="1176"></canvas>
<!-- 左下:ステアリング用ジョイスティック -->
<div id="joystickLeft">
<div id="joystickLeftKnob" class="joystick-knob"></div>
</div>
<!-- 右下:アクセル/ブレーキ用ジョイスティック -->
<div id="joystickRight">
<div id="joystickRightKnob" class="joystick-knob"></div>
</div>
<!-- 設定ボタン -->
<button id="settingsButton">Settings</button>
<!-- 設定ポップアップ -->
<div id="settingsPopup">
<h3>カメラ設定</h3>
<label for="sliderCamY">カメラ高さ: <span id="valCamY">1.5</span></label>
<input type="range" id="sliderCamY" min="0.5" max="10" step="0.1" value="1.5">
<label for="sliderFOV">視野角 (FOV°): <span id="valFOV">90</span></label>
<input type="range" id="sliderFOV" min="45" max="120" step="1" value="90">
<label for="sliderPitch">カメラピッチ: <span id="valPitch">0.1</span></label>
<input type="range" id="sliderPitch" min="0" max="0.5" step="0.01" value="0.1">
<label for="sliderShake">カメラシェイク: <span id="valShake">0.1</span></label>
<input type="range" id="sliderShake" min="0" max="0.2" step="0.01" value="0.1">
<label for="sliderHeight">ハイトマップ高さ: <span id="valHeight">0.5</span></label>
<input type="range" id="sliderHeight" min="0" max="3.5" step="0.01" value="0.5">
<button id="closeSettings">閉じる</button>
</div>
<script>
(function () {
// ---------------- 仮想ジョイスティック処理 ----------------
const maxDistance = 50; // 最大移動距離(px)
// 左側ジョイスティック(水平移動・ステアリング用)の初期化関数
function initHorizontalJoystick(containerId, knobId, callback) {
const container = document.getElementById(containerId);
const knob = document.getElementById(knobId);
let dragging = false;
let centerX = 0;
container.addEventListener("pointerdown", e => {
dragging = true;
const rect = container.getBoundingClientRect();
centerX = rect.left + rect.width / 2;
updateJoystick(e.clientX);
});
container.addEventListener("pointermove", e => {
if (!dragging) return;
updateJoystick(e.clientX);
});
container.addEventListener("pointerup", () => {
dragging = false;
knob.style.transform = "translate(-50%, -50%)";
callback(0);
});
container.addEventListener("pointercancel", () => {
dragging = false;
knob.style.transform = "translate(-50%, -50%)";
callback(0);
});
function updateJoystick(clientX) {
let dx = clientX - centerX;
dx = Math.max(-maxDistance, Math.min(dx, maxDistance));
const value = dx / maxDistance; // -1 ~ 1
knob.style.transform = `translate(${dx - 24}px, -50%)`;
callback(value);
}
}
// 右側ジョイスティック(垂直移動・アクセル/ブレーキ用)の初期化関数
function initVerticalJoystick(containerId, knobId, callback) {
const container = document.getElementById(containerId);
const knob = document.getElementById(knobId);
let dragging = false;
let centerY = 0;
container.addEventListener("pointerdown", e => {
dragging = true;
const rect = container.getBoundingClientRect();
centerY = rect.top + rect.height / 2;
updateJoystick(e.clientY);
});
container.addEventListener("pointermove", e => {
if (!dragging) return;
updateJoystick(e.clientY);
});
container.addEventListener("pointerup", () => {
dragging = false;
knob.style.transform = "translate(-50%, -50%)";
callback(0);
});
container.addEventListener("pointercancel", () => {
dragging = false;
knob.style.transform = "translate(-50%, -50%)";
callback(0);
});
function updateJoystick(clientY) {
let dy = clientY - centerY;
dy = Math.max(-maxDistance, Math.min(dy, maxDistance));
// 上方向 (dy negative) → 正の値、下方向 (dy positive) → 負の値(反転)
const value = -dy / maxDistance;
knob.style.transform = `translate(-50%, ${dy - 24}px)`;
callback(value);
}
}
// ---------------- ジョイスティックの値保持 ----------------
// 左側:ステアリング用
let steerValue = 0;
// 右側:アクセル/ブレーキ用
let accelInput = 0;
// 初期化
initHorizontalJoystick("joystickLeft", "joystickLeftKnob", v => { steerValue = v; });
initVerticalJoystick("joystickRight", "joystickRightKnob", v => { accelInput = v; });
// ---------------- カメラ・物理制御用の変数 ----------------
let moveSpeed = 0;
let accel = 0.1;
let brakeDecel = 0.3;
let maxSpeed = 2; // 全開時の最高速度
let minMaxSpeed = 1; // 部分的なアクセル/ブレーキ時の最高速度下限
let camX = 0, camY = 1.5, camZ = 0;
let camRotY = 0;
let camPitch = 0.1;
let fov = 90 * Math.PI / 180;
let uHeightScale = 0.5;
let keyboardAccel = 0;
let keyboardSteer = 0;
// ---------------- 設定ポップアップ&スライダー処理 ----------------
const settingsButton = document.getElementById("settingsButton");
const settingsPopup = document.getElementById("settingsPopup");
const closeSettings = document.getElementById("closeSettings");
const sliderCamY = document.getElementById("sliderCamY");
const sliderFOV = document.getElementById("sliderFOV");
const sliderPitch = document.getElementById("sliderPitch");
const sliderShake = document.getElementById("sliderShake");
const sliderHeight = document.getElementById("sliderHeight");
const valCamY = document.getElementById("valCamY");
const valFOV = document.getElementById("valFOV");
const valPitch = document.getElementById("valPitch");
const valShake = document.getElementById("valShake");
const valHeight = document.getElementById("valHeight");
settingsButton.addEventListener("click", () => {
settingsPopup.style.display = "block";
});
closeSettings.addEventListener("click", () => {
settingsPopup.style.display = "none";
});
// 各スライダー変更時に対応する変数と表示を更新
sliderCamY.addEventListener("input", () => {
camY = parseFloat(sliderCamY.value);
valCamY.textContent = sliderCamY.value;
});
sliderFOV.addEventListener("input", () => {
// fovはラジアンで管理
fov = parseFloat(sliderFOV.value) * Math.PI / 180;
valFOV.textContent = sliderFOV.value;
});
sliderPitch.addEventListener("input", () => {
camPitch = parseFloat(sliderPitch.value);
valPitch.textContent = sliderPitch.value;
});
sliderShake.addEventListener("input", () => {
valShake.textContent = sliderShake.value;
});
sliderHeight.addEventListener("input", () => {
uHeightScale = parseFloat(sliderHeight.value);
valHeight.textContent = sliderHeight.value;
});
// ---------------- WebGL 初期化 ----------------
const canvas = document.getElementById("glCanvas");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const gl = canvas.getContext("webgl2");
if (!gl) {
alert("WebGL2がサポートされていません");
return;
}
gl.viewport(0, 0, canvas.width, canvas.height);
gl.clearColor(0.53, 0.81, 0.92, 1.0);
gl.enable(gl.DEPTH_TEST);
gl.clearDepth(1.0);
gl.depthFunc(gl.LEQUAL);
// ---------------- テクスチャ読み込み ----------------
function loadTexture(url, textureUnit, callback) {
const texture = gl.createTexture();
gl.activeTexture(gl.TEXTURE0 + textureUnit);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0,
gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([255, 255, 255, 255]));
const img = new Image();
img.crossOrigin = "Anonymous";
img.onload = function () {
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);
gl.generateMipmap(gl.TEXTURE_2D);
if (callback) callback(img.width, img.height);
};
img.src = url;
return texture;
}
// ※URLの拡張子重複を修正(必要に応じてURLを調整)
const baseTexURL = "https://doiworks.com/wp-content/uploads/2025/03/race_track_original.png.png";
const baseTexture = loadTexture(baseTexURL, 0);
const heightMapURL = "https://doiworks.com/wp-content/uploads/2025/03/height_map_grayscale_s.png";
const heightTexture = loadTexture(heightMapURL, 1);
// ---------------- シェーダー関連 ----------------
const vsSource = `#version 300 es
precision mediump float;
in vec3 aPosition;
in vec2 aTexCoord;
uniform mat4 uMVP;
uniform sampler2D uHeightTex;
uniform float uHeightScale;
uniform float uGridSize;
out vec2 vTexCoord;
void main() {
vec2 dispUV = (aPosition.xz + vec2(uGridSize * 0.5)) / uGridSize;
float rawHeight = texture(uHeightTex, dispUV).r;
float height = smoothstep(0.3, 0.7, rawHeight);
vec3 displacedPos = vec3(aPosition.x, height * uHeightScale, aPosition.z);
gl_Position = uMVP * vec4(displacedPos, 1.0);
vTexCoord = aTexCoord;
}
`;
const fsSource = `#version 300 es
precision mediump float;
in vec2 vTexCoord;
out vec4 fragColor;
uniform sampler2D uTexture;
void main() {
fragColor = texture(uTexture, vTexCoord);
}
`;
function compileShader(src, type) {
const shader = gl.createShader(type);
gl.shaderSource(shader, src);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error("Shader compile error:", gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
const vs = compileShader(vsSource, gl.VERTEX_SHADER);
const fs = compileShader(fsSource, gl.FRAGMENT_SHADER);
const program = gl.createProgram();
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error("Program linking error:", gl.getProgramInfoLog(program));
}
gl.useProgram(program);
gl.uniform1i(gl.getUniformLocation(program, "uTexture"), 0);
gl.uniform1i(gl.getUniformLocation(program, "uHeightTex"), 1);
// ---------------- グリッドジオメトリ生成 ----------------
function generateGrid(size, divisions) {
let vertices = [];
let indices = [];
for (let i = 0; i <= divisions; i++) {
for (let j = 0; j <= divisions; j++) {
let x = -size / 2 + (size * j) / divisions;
let z = -size / 2 + (size * i) / divisions;
let u = j / divisions;
let v = i / divisions;
vertices.push(x, 0, z, u, v);
}
}
for (let i = 0; i < divisions; i++) {
for (let j = 0; j < divisions; j++) {
let row1 = i * (divisions + 1);
let row2 = (i + 1) * (divisions + 1);
indices.push(row1 + j, row2 + j, row1 + j + 1);
indices.push(row1 + j + 1, row2 + j, row2 + j + 1);
}
}
return { vertices: new Float32Array(vertices), indices: new Uint16Array(indices) };
}
const gridSize = 100;
const gridDivisions = 256;
const grid = generateGrid(gridSize, gridDivisions);
const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, grid.vertices, gl.STATIC_DRAW);
const aPositionLoc = gl.getAttribLocation(program, "aPosition");
gl.enableVertexAttribArray(aPositionLoc);
gl.vertexAttribPointer(aPositionLoc, 3, gl.FLOAT, false, 5 * 4, 0);
const aTexCoordLoc = gl.getAttribLocation(program, "aTexCoord");
gl.enableVertexAttribArray(aTexCoordLoc);
gl.vertexAttribPointer(aTexCoordLoc, 2, gl.FLOAT, false, 5 * 4, 3 * 4);
const indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, grid.indices, gl.STATIC_DRAW);
gl.bindVertexArray(null);
// --------------- 行列計算用関数(ライブラリ不要) ---------------
function identity() {
return [
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
];
}
function perspective(fovy, aspect, near, far) {
const f = 1.0 / Math.tan(fovy / 2);
const nf = 1 / (near - far);
return [
f / aspect, 0, 0, 0,
0, f, 0, 0,
0, 0, (far + near) * nf, -1,
0, 0, (2 * far * near) * nf, 0
];
}
function multiply(a, b) {
let out = new Array(16);
for (let j = 0; j < 4; j++) {
for (let i = 0; i < 4; i++) {
out[j * 4 + i] = 0;
for (let k = 0; k < 4; k++) {
out[j * 4 + i] += a[k * 4 + i] * b[j * 4 + k];
}
}
}
return out;
}
function translate(m, tx, ty, tz) {
let t = identity();
t[12] = tx;
t[13] = ty;
t[14] = tz;
return multiply(m, t);
}
function rotateX(m, angle) {
const c = Math.cos(angle), s = Math.sin(angle);
let r = identity();
r[5] = c; r[6] = s;
r[9] = -s; r[10] = c;
return multiply(m, r);
}
// ---------------- カメラ更新処理 ----------------
let startTime = performance.now();
let brakeStartTime = null; // ブレーキ全開状態の開始時刻
function updateCamera() {
// ジョイスティックとキーボード入力を集計
// accelInput:右側ジョイスティックの値(アクセル/ブレーキ)
// steerValue:左側ジョイスティックの値(ステアリング)
let effectiveAccel = accelInput;
if (keyboardAccel > 0) effectiveAccel = Math.max(effectiveAccel, 1);
if (keyboardAccel < 0) effectiveAccel = Math.min(effectiveAccel, -1);
if (effectiveAccel > 0) {
// アクセル時
moveSpeed += accel * (effectiveAccel / 50);
let currentMaxSpeed = minMaxSpeed + (maxSpeed - minMaxSpeed) * effectiveAccel;
if (moveSpeed > currentMaxSpeed) moveSpeed = currentMaxSpeed;
// ブレーキ全開タイマーはリセット
brakeStartTime = null;
} else if (effectiveAccel < 0) {
// ブレーキ時:全開か否かで減速係数を切り替え
let brakeDecelEffective;
if (effectiveAccel <= -0.99) {
// 全開の場合
if (brakeStartTime === null) {
brakeStartTime = performance.now();
}
let elapsed = performance.now() - brakeStartTime;
if (elapsed < 1000) {
brakeDecelEffective = 0.5;
} else {
brakeDecelEffective = 0.2;
}
} else {
// 全開以外はデフォルト値
brakeDecelEffective = 0.5;
brakeStartTime = null;
}
moveSpeed -= brakeDecelEffective * ((-effectiveAccel) / 50);
let currentMinSpeed = - (minMaxSpeed + (maxSpeed - minMaxSpeed) * (-effectiveAccel));
if (moveSpeed < currentMinSpeed) moveSpeed = currentMinSpeed;
} else {
// 何も入力がないときは徐々に減速
if (moveSpeed > 0) {
moveSpeed -= 0.005;
if (moveSpeed < 0) moveSpeed = 0;
}
if (moveSpeed < 0) {
moveSpeed += 0.005;
if (moveSpeed > 0) moveSpeed = 0;
}
// 入力がないときはブレーキタイマーリセット
brakeStartTime = null;
}
// 【ステアリング】ジョイスティックとキーボードは個別にスケール
let effectiveSteer = -(steerValue) / 0.5 - (keyboardSteer) / 50;
camRotY += 0.03 * effectiveSteer;
camX -= Math.sin(camRotY) * moveSpeed;
camZ -= Math.cos(camRotY) * moveSpeed;
// カメラシェイク
const shakeSliderValue = parseFloat(sliderShake.value);
const shakeFrequency = 0.5 * shakeSliderValue;
const shakeAmplitudeAdjusted = 0.05 * shakeSliderValue;
const shakeX = Math.sin(performance.now() * shakeFrequency) * shakeAmplitudeAdjusted;
const shakeZ = Math.cos(performance.now() * shakeFrequency) * shakeAmplitudeAdjusted;
camY += shakeX;
camZ += shakeZ;
}
function render() {
let elapsed = performance.now() - startTime;
if (elapsed < 10000) {
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, baseTexture);
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, heightTexture);
}
updateCamera();
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
const aspect = canvas.width / canvas.height;
const proj = perspective(fov, aspect, 0.1, 1000);
let view = identity();
// カメラのY軸回転
const cosY = Math.cos(camRotY);
const sinY = Math.sin(camRotY);
view[0] = cosY;
view[2] = sinY;
view[8] = -sinY;
view[10] = cosY;
view = rotateX(view, camPitch);
view = translate(view, -camX, -camY, -camZ);
const mvp = multiply(proj, view);
const uMVPLoc = gl.getUniformLocation(program, "uMVP");
gl.useProgram(program);
gl.uniformMatrix4fv(uMVPLoc, false, new Float32Array(mvp));
gl.uniform1f(gl.getUniformLocation(program, "uHeightScale"), uHeightScale);
gl.uniform1f(gl.getUniformLocation(program, "uGridSize"), gridSize);
gl.bindVertexArray(vao);
gl.drawElements(gl.TRIANGLES, grid.indices.length, gl.UNSIGNED_SHORT, 0);
gl.bindVertexArray(null);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
// ---------------- キーボード操作 ----------------
document.addEventListener("keydown", e => {
if (["input", "textarea"].includes(e.target.tagName.toLowerCase())) return;
if (e.key === "ArrowUp") { keyboardAccel = 50; }
if (e.key === "ArrowDown") { keyboardAccel = -50; }
if (e.key === "ArrowLeft") { keyboardSteer = -50; }
if (e.key === "ArrowRight") { keyboardSteer = 50; }
});
document.addEventListener("keyup", e => {
if (["input", "textarea"].includes(e.target.tagName.toLowerCase())) return;
if (e.key === "ArrowUp" || e.key === "ArrowDown") { keyboardAccel = 0; }
if (e.key === "ArrowLeft" || e.key === "ArrowRight") { keyboardSteer = 0; }
});
})();
</script>
</body>
</html>
ジミニー Code Editor
下記のエディタ内で、上記のコードの実行や編集が可能です。ぜひお試しください!
※今回のこの画面でコードを実行するときはモードを3、か4にして実行してください
ジミニー Code Editor は こちらのリンク からアクセスできます。