Skip to content

Instantly share code, notes, and snippets.

@palaniraja
Created May 9, 2026 04:41
Show Gist options
  • Select an option

  • Save palaniraja/4d3542030925413436d0950b03a1f6e5 to your computer and use it in GitHub Desktop.

Select an option

Save palaniraja/4d3542030925413436d0950b03a1f6e5 to your computer and use it in GitHub Desktop.
Threejs RC Trainer
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Threejs RC Trainer</title>
<style>
body {
margin: 0;
overflow: hidden;
background: #c2c9d1;
}
#hud {
position: absolute;
top: 20px;
left: 20px;
color: white;
font-family: sans-serif;
background: rgba(0, 0, 0, 0.7);
padding: 15px;
border-radius: 8px;
pointer-events: none;
z-index: 100;
border: 1px solid #00bcd4;
}
.key {
color: #00bcd4;
font-weight: bold;
}
</style>
</head>
<body>
<div id="hud">
<span class="key">W / S</span> : Pitch | <span class="key">A / D</span> : Roll<br>
<span class="key">Arrows L / R</span> : Yaw | <span class="key">Up / Dn</span> : Throttle
</div>
<script type="importmap">
{ "imports": { "three": "https://unpkg.com/three@0.160.0/build/three.module.js" } }
</script>
<script type="module">
import * as THREE from 'three';
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xc2c9d1);
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(5, 10, 7);
scene.add(light, new THREE.AmbientLight(0xffffff, 0.6));
const matMain = new THREE.MeshStandardMaterial({ color: 0x00bcd4, flatShading: true });
const matNose = new THREE.MeshStandardMaterial({ color: 0xffffff, flatShading: true });
const matControl = new THREE.MeshStandardMaterial({ color: 0xff0000, flatShading: true });
const matTire = new THREE.MeshStandardMaterial({ color: 0x333333, flatShading: true });
const airplane = new THREE.Group();
scene.add(airplane);
// 1. FUSELAGE (Cylinder)
const fuse = new THREE.Mesh(new THREE.CylinderGeometry(0.15, 0.5, 3.5, 8), matMain);
fuse.rotation.x = -Math.PI / 2;
airplane.add(fuse);
// 2. NOSE & PROP
const nose = new THREE.Mesh(new THREE.ConeGeometry(0.5, 0.6, 8), matNose);
nose.rotation.x = Math.PI / 2;
nose.position.z = 2.05;
airplane.add(nose);
const propGroup = new THREE.Group();
propGroup.position.z = 2.4;
airplane.add(propGroup);
const propBlade = new THREE.Mesh(new THREE.BoxGeometry(0.1, 1.8, 0.05), matTire);
propGroup.add(propBlade);
// 3. WINGS (Swept and Inset to remove gaps)
function createSweptWing(isRight) {
const group = new THREE.Group();
const shape = new THREE.Shape();
shape.moveTo(0, 0);
shape.lineTo(2.5, -0.4); // Swept leading edge
shape.lineTo(2.5, -1.2); // Tip trailing edge
shape.lineTo(0, -1.2); // Root trailing edge
shape.lineTo(0, 0);
const wingGeom = new THREE.ExtrudeGeometry(shape, { depth: 0.05, bevelEnabled: false });
const wingMesh = new THREE.Mesh(wingGeom, matMain);
wingMesh.rotation.x = Math.PI / 2;
group.add(wingMesh);
const aileron = new THREE.Mesh(new THREE.BoxGeometry(1.5, 0.1, 0.3), matControl);
aileron.position.set(1.6, 0, -1.05);
group.add(aileron);
if (isRight) group.scale.x = -1;
return { group, aileron };
}
// MOVED INWARD (x=0.25) to intersect the cylinder and hide gaps
const leftWing = createSweptWing(false);
leftWing.group.position.set(0.25, 0.2, 0.8);
airplane.add(leftWing.group);
const rightWing = createSweptWing(true);
rightWing.group.position.set(-0.25, 0.2, 0.8);
airplane.add(rightWing.group);
// 4. TAIL UNIT
const fin = new THREE.Mesh(new THREE.BoxGeometry(0.05, 0.8, 0.6), matMain);
fin.position.set(0, 0.6, -1.4);
airplane.add(fin);
const rudder = new THREE.Mesh(new THREE.BoxGeometry(0.08, 0.8, 0.3), matControl);
rudder.position.set(0, 0, -0.45);
fin.add(rudder);
const stab = new THREE.Mesh(new THREE.BoxGeometry(2, 0.05, 0.6), matMain);
stab.position.set(0, 0, -1.4);
airplane.add(stab);
const elevator = new THREE.Mesh(new THREE.BoxGeometry(1.8, 0.08, 0.3), matControl);
elevator.position.set(0, 0, -0.45);
stab.add(elevator);
camera.position.set(6, 4, 6);
camera.lookAt(0, 0, 0);
// --- CONTROLS & ANIMATION ---
const keys = {};
let thrust = 0.1;
window.addEventListener('keydown', (e) => keys[e.code] = true);
window.addEventListener('keyup', (e) => keys[e.code] = false);
function animate() {
requestAnimationFrame(animate);
leftWing.aileron.rotation.x = 0;
rightWing.aileron.rotation.x = 0;
elevator.rotation.x = 0;
rudder.rotation.y = 0;
if (keys['KeyS']) { elevator.rotation.x = -0.5; airplane.rotation.x -= 0.02; }
if (keys['KeyW']) { elevator.rotation.x = 0.5; airplane.rotation.x += 0.02; }
if (keys['KeyA']) {
leftWing.aileron.rotation.x = 0.5;
rightWing.aileron.rotation.x = -0.5;
airplane.rotation.z -= 0.03;
}
if (keys['KeyD']) {
leftWing.aileron.rotation.x = -0.5;
rightWing.aileron.rotation.x = 0.5;
airplane.rotation.z += 0.03;
}
if (keys['ArrowLeft']) { rudder.rotation.y = -0.5; airplane.rotation.y += 0.02; }
if (keys['ArrowRight']) { rudder.rotation.y = 0.5; airplane.rotation.y -= 0.02; }
if (keys['ArrowUp']) thrust += 0.01;
if (keys['ArrowDown']) thrust = Math.max(0.02, thrust - 0.01);
propGroup.rotation.z += thrust;
renderer.render(scene, camera);
}
animate();
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment