ChatGPTとジミニーCode Editorで実現!ピカニーのWebGL2本格3Dレースゲーム開発記!🚀
.jpg)
ChatGPTとジミニーCode Editorで実現!ピカニーのWebGL2本格3Dレースゲーム開発記!🚀
「まいど!ピカニーやで!🚀今回は、ついに!ついにやで!みんなお待ちかねの、本格的な3Dレースゲームに挑戦したで!🎉」
フル画面は こちらのリンク からアクセスできます。
目次
- はじめに:3Dレースゲームへの挑戦!
- WebGL2との出会いと苦悩
- 相棒ChatGPTの登場!
- トライアンドエラーの日々
- 完成!3Dレースゲームの特徴
- ジミニーCode Editorで手軽にプレイ!
- おわりに:夢への挑戦!
はじめに:3Dレースゲームへの挑戦!
「まいど!ピカニーやで!🚀今回は、ついに!ついにやで!みんなお待ちかねの、本格的な3Dレースゲームに挑戦したで!🎉」
「前回の2Dもどきレースゲームも、スーパーファミコンの『モード7』っていう技術を使って、それなりに3Dっぽく見えてたやろ?😉あれはあれで、懐かしい感じがして良かったんやけど…やっぱりホンモノの3Dには敵わんのよ!🔥」
WebGL2との出会いと苦悩
「昔のゲーム機と違って、今のブラウザはWebGL2っていうすごい技術が使えるんや!✨これを使えば、グラフィックボード(GPU)のパワーをフルに引き出して、めちゃくちゃキレイで滑らかな3Dグラフィックが描けるんや!🤩」
「でもな、WebGL2はちょっと難しいんや…😫シェーダーとか、行列計算とか、聞いたこともない言葉がいっぱい出てくるんやもん…💦最初はホンマに、何が何やらさっぱり分からんかったわ…🤯」
相棒ChatGPTの登場!
「そこで頼りになったのが、相棒のChatGPTやったんや!🤖今回は、なんと!ChatGPTに直接『WebGL2で3Dレースゲームのコードを書いて!』ってお願いしたら、ほぼ完璧なコードを生成してくれたんや!🎁ホンマにびっくりしたで!😲」
トライアンドエラーの日々
「…と、言いたいところやけど、実際はそう簡単にはいかんかったんや!😅もちろん、ChatGPTがベースとなるコードを生成してくれたのは事実やで!でもな、そのまま動くわけないやん?🤔トライアンドエラーの繰り返しやったわ!🐜🐜🐜」
「ChatGPTが出力してくれるコードも、時々おかしなところがあったり、こちらの意図と違う動きをしたりするんや。そこを根気よく修正して、デバッグして、やっと動くようになったんや!🛠️🤖」
完成!3Dレースゲームの特徴
「それでも諦めへんのが、このピカニーや!💪ChatGPTと一緒に、夜も寝ずにコードを書き続けた結果、ついに!ついに!ホンモノの3Dレースゲームが完成したんや!😭🎉」
「今回のゲームは、ただ3Dになっただけちゃうで!😏カメラの動きも、車の動きも、コースの形も、全部自分で細かく調整できるようにしたんや!🔧自分だけの理想のレースゲームを作れるって、最高やと思わへん?😆」
「スマホでもサクサク動くように、色々工夫もしたんやで!📱WebGL2とシェーダーを最適化することで、昔の疑似3Dゲームとは比べもんにならんくらいの、快適なプレイ体験を実現したんや!👍」
ジミニーCode Editorで手軽にプレイ!
「今回の挑戦を通して、ホンマに色々なことを学べたで!🎓3Dグラフィックの奥深さを改めて実感したし、何よりも、諦めずに挑戦することの大切さを再確認できたんや!✨そして、ChatGPTみたいなAIをうまく活用すれば、難しいことでも乗り越えられるってことも学んだな!🤖💡」
「そして今回作った3Dレースゲームを簡単に試せるように、ジミニーCode Editorって言うエディターを使ってるねん!💻このエディターはブラウザ上で動くから、インストールとか面倒な設定は一切不要や!今回の3Dレースゲームのコードも、ジミニーCode Editorに貼り付けるだけで、すぐに遊べるで!🎮」
おわりに:夢への挑戦!
「さあ、みんな!ピカニー渾身の3Dレースゲームを、ジミニーCode Editorでぜひプレイしてみてな!🎮そして、キミもChatGPTを相棒にして、ジミニーCode Editorで自分だけのオリジナルゲーム作りに挑戦してみてや!きっと、今まで見たことのない、新しい世界が広がるはずやで!🌈」
最終版ソースコード
下記のコードをコピーして、自由にお使いください。各パラメータはリアルタイムに変更可能なため、動作や見た目のカスタマイズも容易です。
<!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 は こちらのリンク からアクセスできます。