html5 game dev with three.js - hexgl
DESCRIPTION
These are the slides of my talk about HexGL at the Adobe User Group meetup in the Netherlands. More info: http://bkcore.com/blog/general/adobe-user-group-nl-talk-video-hexgl.htmlTRANSCRIPT
The making of HexGL
• Thibaut Despoulain (@bkcore – bkcore.com)
• 22 year-old student in Computer Engineering
• University of Technology of Belfort-Montbéliard (France)
• Web dev and 3D enthousiast
• The guy behind the HexGL project
• Fast-paced, futuristic racing game
• Inspired by the F-Zero and Wipeout series
• HTML5, JavaScript, WebGL (via Three.js)
• Less than 2 months
• Just me.
• JavaScript API
• OpenGL ES 2.0
• Chrome, FireFox, (Opera, Safari)
• <Canvas> (HTML5)
• Rendering engine
• Maintained by Ricardo Cabello (MrDoob) and Altered Qualia
• R50/stable
• + : Active community, stable, updated frequently
• - : Documentation
• First « real » game
• 2 months to learn and code
• Little to no modeling and texturing skills
• Physics? Controls? Gameplay?
• Last time I could have 2 months free
• Visibility to get an internship
• Huge learning opportunity
• Explore Three.js for good
• Third-party physics engine (rejected)
– Slow learning curve
– Not really meant for racing games
• Ray casting (rejected)
– Heavy perfomance-wise
– Needs Octree-like structure
– > too much time to learn and implement
• Home-made 2D approximation
– Little to no learning curve
– Easy to implement with 2D maps
– Pretty fast
– > But with some limitations
• Home-made 2D approximation
– No track overlap
– Limited track twist and gradient
– Accuracy depends on map resolution
– > Enough for what I had in mind
• No pixel getter on JS Image object/tag
• Canvas2D to the rescue
Load
ing Load data
texture with JS Image object
Dra
win
g Draw it on a Canvas using 2D context
Get
tin
g Get canvas pixels using getImageData()
• ImageData (BKcore package)
– Github.com/Bkcore/bkcore-js
var a = new bkcore.ImageData(path, callback);
//…
a.getPixel(x, y);
a.getPixelBilinear(xf, yf);
// -> {r, g, b, a};
Game loop:
Convert world position to pixel indexes
Get current pixel intensity (red)
If pixel is not white:
Collision
Test pixels relatively (front, left, right)
:end
Front
Left Right
Track Void
Front
Left Right
Height map
Gradient
Front
Current
Tilt
Left
Right
Three.js JSON model
Python converter
Model.OBJ
Materials.MTL
$ python convert_obj_three.py -i mesh.obj -o mesh.js
var scene = new THREE.Scene();var loader = new THREE.JSONLoader();
var createMesh = function(geometry){
var mesh = new THREE.Mesh(geometry, new THREE.MeshFaceMaterial());mesh.position.set(0, 0, 0);mesh.scale.set(3, 3, 3);scene.add(mesh);
};
loader.load("mesh.js", createMesh);
var renderer = new THREE.WebGLRenderer({
antialias: false,
clearColor: 0x000000
});
renderer.autoClear = false;
renderer.sortObjects = false;
renderer.setSize(width, height);
document.body.appendChild(renderer.domElement);
renderer.gammaInput = true;
renderer.gammaOutput = true;
renderer.shadowMapEnabled = true;
renderer.shadowMapSoft = true;
• Blinn-phong
– Diffuse + Specular + Normal + Environment maps
• THREE.ShaderLib.normal
– > Per vertex point lights
• Custom shader with per-pixel point lights for the road
– > Booster light
var boosterSprite = new THREE.Sprite(
{
map: spriteTexture,
blending: THREE.AdditiveBlending,
useScreenCoordinates: false,
color: 0xffffff
});
boosterSprite.mergeWith3D = false;
boosterMesh.add(boosterSprite);
var material = new THREE.ParticleBasicMaterial({
color: 0xffffff,
map: THREE.ImageUtils.loadTexture(“tex.png”),
size: 4,
blending: THREE.AdditiveBlending,
depthTest: false,
transparent: true,
vertexColors: true,sizeAttenuation: true
});
var pool = [];
var geometry = new THREE.Geometry();
geometry.dynamic = true;
for(var i = 0; i < 1000; ++i)
{
var p = new bkcore.Particle();
pool.push(p);
geometry.vertices.push(p.position);
geometry.colors.push(p.color);
}
bkcore.Particle = function()
{
this.position = new THREE.Vector3();
this.velocity = new THREE.Vector3();
this.force = new THREE.Vector3();
this.color = new THREE.Color(0x000000);
this.basecolor = new THREE.Color(0x000000);
this.life = 0.0;
this.available = true;
}
var system = new THREE.ParticleSystem(
geometry,
material
);
system.sort = false;
system.position.set(x, y, z);
system.rotation.set(a, b, c);
scene.add(system);
// Particle physics
var p = pool[i];
p.position.addSelf(p.velocity);
//…
geometry.verticesNeedUpdate = true;
geometry.colorsNeedUpdate = true;
• Particles (BKcore package)
– Github.com/BKcore/Three-extensions
var clouds = new bkcore.Particles({
opacity: 0.8,
tint: 0xffffff, color: 0x666666, color2: 0xa4f1ff,
texture: THREE.ImageUtils.loadTexture(“cloud.png”),
blending: THREE.NormalBlending,
size: 6, life: 60, max: 500,
spawn: new THREE.Vector3(3, 3, 0),
spawnRadius: new THREE.Vector3(1, 1, 2),
velocity: new THREE.Vector3(0, 0, 4),
randomness: new THREE.Vector3(5, 5, 1)
});
scene.add(clouds);
// Game loop
clouds.emit(10);
clouds.update(dt);
• Built-in support for off-screen passes
• Already has some pre-made post effects
– Bloom
– FXAA
• Easy to use and Extend
• Custom shaders
var renderTargetParameters = {
minFilter: THREE.LinearFilter,
magFilter: THREE.LinearFilter,
format: THREE.RGBFormat,
stencilBuffer: false
};
var renderTarget = new THREE.WebGLRenderTarget(
width, height,
renderTargetParameters
);
var composer = new THREE.EffectComposer(
renderer,
renderTarget
);
composer.addPass( … );
composer.render();
• Generic passes– RenderPass
– ShaderPass
– SavePass
– MaskPass
• Pre-made passes– BloomPass
– FilmPass
– Etc.
var renderModel = new THREE.RenderPass(
scene,
camera
);
renderModel.clear = false;
composer.addPass(renderModel);
var effectBloom = new THREE.BloomPass(
0.8, // Strengh
25, // Kernel size
4, // Sigma
256 // Resolution
);
composer.addPass(effectBloom);
var hexvignette: {uniforms: {
tDiffuse: { type: "t", value: 0, texture: null },tHex: { type: "t", value: 1, texture: null},size: { type: "f", value: 512.0}, color: { type: "c", value: new THREE.Color(0x458ab1) }
},fragmentShader: [
"uniform float size;","uniform vec3 color;","uniform sampler2D tDiffuse;","uniform sampler2D tHex;",
"varying vec2 vUv;",
"void main() { ... }"
].join("\n")};
var effectHex = new THREE.ShaderPass(hexvignette);
effectHex.uniforms['size'].value = 512.0;
effectHex.uniforms['tHex'].texture = hexTexture;
composer.addPass(effectHex);
//…
effectHex.renderToScreen = true;