elation-engine
Version:
WebGL/WebVR engine written in Javascript
640 lines (596 loc) • 25.2 kB
JavaScript
elation.require('engine.things.generic', function() {
/**
* terrain_clipmap
*/
elation.component.add("engine.things.terrain_clipmap", function() {
this.postinit = function() {
this.defineProperties({
'color': { type: 'color', default: 0x998877 },
'simple': { type: 'boolean', default: true },
'size': { type: 'float', default: 1000.0 },
'resolution': { type: 'int', default: 64 },
'textures.map': { type: 'texture', default: null},
'textures.mapRepeat': { type: 'vector2', default: [1, 1]},
'textures.normalMap': { type: 'texture', default: null},
'textures.normalMapRepeat': { type: 'vector2', default: [1, 1]},
'textures.displacementMap': { type: 'texture', default: null},
'textures.displacementMapRepeat': { type: 'vector2', default: [1, 1]},
});
this.view = [64, 64];
this.res = [819200, 819200];
this.pos = [.5, .5];
this.tiles = [];
this.tiles_free = [];
this.levels = [];
this.visiblelevels = [null, null];
this.currentaltitude = 1;
this.deformableOffset = new THREE.Vector2();
this.maxlevel = Math.ceil(Math.log(this.res[0]/4) / Math.log(2));
//this.objects['3d'].scale.set(this.properties.terrain.scale, this.properties.terrain.scale, this.properties.terrain.scale);
/*
this.addControlContext('terrain', {
'zoom_out': ['mouse_wheel_up', function() { this.setViewingAltitude(this.currentaltitude * (1+1/6)); }],
'zoom_in': ['mouse_wheel_down', function() { this.setViewingAltitude(this.currentaltitude * (1-1/6)); }],
});
this.engine.systems.get('controls').activateContext('terrain', this);
*/
}
this.createObject3D = function() {
this.objects['3d'] = new THREE.Object3D();
this.root = new THREE.Object3D();
this.objects['3d'].add(this.root);
this.createPlane();
this.initTextures();
//this.showLevel(this.maxlevel, this.maxlevel);
this.setViewingAltitude(100);
/*
var cubes = 20;
var cubesize = 200000;
var cubebounds = 10000000;
for (var i = 0; i < cubes; i++) {
//var box = new THREE.Mesh(new THREE.CubeGeometry(Math.random() * cubesize, Math.random() * cubesize, Math.random() * cubesize), new THREE.MeshPhongMaterial({color: 0xff0000}));
//box.position.set((Math.random() * cubebounds) - cubebounds/2, Math.random() * cubebounds, Math.random() * -cubebounds);
//this.objects['3d'].add(box);
var pos = [0,0,0];
pos[2] = Math.round(Math.random() * -cubebounds);
pos[0] = Math.round(pos[2] * (Math.random() * 2 - 1));
pos[1] = pos[2] / -10;
//var size = Math.round(Math.random() * cubesize);
var size = pos[2] / ((Math.random() * -10) - 10);
var box = elation.engine.things.generic('shape-'+i, elation.html.create(), {
properties: {
physical: {
position: pos,
},
generic: {
geometry: {
type: (Math.random() < .5 ? 'sphere' : 'cube'),
size: size
},
material: {
type: 'phong',
color: Math.floor(0xffffff * Math.random()),
//normalMap: '/media/space/textures/asphalt-normal.jpg' //elation.engine.materials.getTexture('/media/space/textures/asphalt-normal.jpg', [1, 1])
}
}
}
});
this.add(box);
}
*/
return this.objects['3d'] ;
}
this.createObjectDOM = function() {
/*
// FIXME - temporary interface for development
var foo = elation.html.create({className: "terrain_debug", append: document.body});
foo.innerHTML = '';
var slider = elation.ui.slider(null, elation.html.create({append: foo}), {minpos: 0.1, maxpos: 25});
slider.setposition(1);
elation.events.add(slider, "ui_slider_change", elation.bind(this, function(ev) { this.setViewingAltitude(Math.pow(2, ev.data ) - 1); }));
setTimeout(function() { slider.setposition(1); }, 1000);
return null;
*/
}
/**
* Create the plane geometry, which is shared between all terrain chunks
* - Each terrain level is made of 16 chunks (4x4)
* - Each terrain level is exactly 2x the resolution of the previous
* - Number of terrain levels is determined by heightmap resolution
*/
this.createPlane = function() {
this.plane = new THREE.PlaneGeometry(1, 1, this.view[0] / 4, this.view[1] / 4);
var mat = new THREE.Matrix4().makeRotationFromEuler(new THREE.Euler(-Math.PI/2, 0, 0));
this.plane.applyMatrix(mat);
}
/**
* Create the texture objects which will be used for the various detail levsl
*/
this.initTextures = function() {
this.textures = {};
/*
var textures = elation.utils.arrayget(this.properties, "terrain.textures");
for (var k in textures) {
this.textures[k] = elation.engine.materials.getTexture(textures[k], true);
}
*/
if (this.properties.textures.map) {
this.textures.map = this.properties.textures.map;
if (this.properties.textures.mapRepeat) {
elation.engine.materials.setTextureRepeat(this.textures.map, this.properties.textures.mapRepeat);
}
}
if (this.properties.textures.normalMap) {
this.textures.normalMap = this.properties.textures.normalMap;
if (this.properties.textures.normalMapRepeat) {
elation.engine.materials.setTextureRepeat(this.textures.normalMap, this.properties.textures.normalMapRepeat);
}
if (this.properties.textures.normalScale) {
//matargs.normalScale = this.properties.textures.normalScale;
}
}
console.log('TEXTURES', this.textures);
}
/**
* Return a tile object for the requested level/offset
* Re-use tiles which may have been freed, whenever possible
*/
this.getTile = function(level, x, y) {
if (this.tiles_free.length > 0) {
var tile = this.tiles_free.pop();
} else {
var tile = new elation.engine.things.terrain.tile(this);
}
tile.setLevel(level, [x, y], this.position);
//tile.show();
return tile;
}
/**
* Free a tile so it can be reused
* We don't want to hide it here, we just mark it as available for reuse
* If the tile is not reused in the same frame, cleanupTiles hides it
*/
this.removeTile = function(tile) {
this.tiles_free.push(tile);
}
/**
* Hide any tiles which are left in the tiles_free array
*/
this.cleanupTiles = function() {
for (var i = 0; i < this.tiles_free.length; i++) {
if (this.tiles_free[i].tilevisible) {
this.tiles_free[i].hide();
}
}
}
/**
* Show a specific range of levels
*/
this.showLevel = function(levelstart, levelend) {
var start = Math.min(levelstart, this.visiblelevels[0]);
var end = Math.max(levelend, this.visiblelevels[1]);
for (var level = start; level <= end; level++) {
if (level >= levelstart && level <= levelend) {
if (!this.levels[level]) {
this.levels[level] = new elation.engine.things.terrain.level(this, level);
}
this.levels[level].show();
this.levels[level].setFill(false);
} else {
if (this.levels[level]) {
this.levels[level].hide();
}
}
}
this.levels[levelstart].setFill(true);
this.cleanupTiles();
this.visiblelevels = [levelstart, levelend];
}
this.setViewingAltitude = function(altitude) {
altitude = elation.utils.math.clamp(altitude, .000000001, 10000000);
this.currentaltitude = altitude;
var minlevel = Math.min(this.maxlevel, Math.floor(Math.log(altitude) / Math.LN2));
if (minlevel != this.visiblelevels[0]) {
this.showLevel(minlevel, this.maxlevel);
}
var view = this.engine.systems.get('render').views['main'];
/*
if (view) {
view.camera.position.y = altitude;
if (altitude <= .1) {
view.camera.near = .00000001;
} else if (altitude <= 5) {
view.camera.near = .01;
//view.camera.far = 1000;
} else if (altitude > 5 && altitude < 10000) {
view.camera.near = 5;
} else {
view.camera.near = 10000;
//view.camera.far = 100000;
}
view.camera.updateProjectionMatrix();
}
*/
}
this.updateLOD = function(camera) {
/*
this.root.position.x = camera.position.x;
this.root.position.z = camera.position.z;
this.deformableOffset.x = -camera.position.x / this.res[0];
this.deformableOffset.y = -camera.position.z / this.res[1];
this.setViewingAltitude(camera.position.y);
*/
}
this.getMaterialArgs = function(level, tileoffset, tileposition) {
var terrainargs = {
color: new THREE.Color((0xff0000 * tileoffset[0] / 4) + (0x00ff00 * tileoffset[1] / 4) + 0x000099),
//diffuse: new THREE.Color((0xff0000 * tileoffset[0] / 4) + (0x00ff00 * tileoffset[1] / 4) + 0x000099),
level: level,
radius: 1000,
textureOffset: new THREE.Vector2(tileoffset[0], tileoffset[1]),
deformableOffset: this.deformableOffset
};
for (var k in this.textures) {
terrainargs[k] = this.textures[k];
}
return terrainargs;
}
}, elation.engine.things.generic);
elation.extend("engine.things.terrain.level", function(terrain, level) {
this.terrain = terrain;
this.level = level;
this.tiles = false;
/**
* Creates the 4x4 tile grid that represents each level
*/
this.create = function() {
this.tiles = [];
for (var y = 0; y < 4; y++) {
this.tiles[y] = [];
for (var x = 0; x < 4; x++) {
// Initially, we just create a 4x4 ring of 12 tiles with a 2x2 hole in the middle
// The hole will be filled in with 4 more tiles only for the highest detail level
if ((x == 0 || x == 3) || (y == 0 || y == 3)) {
this.tiles[y][x] = this.terrain.getTile(this.level, x, y);
}
}
}
}
this.show = function() {
if (!this.levelvisible) {
//if (!this.tiles) {
this.create();
//}
for (var y = 0; y < 4; y++) {
for (var x = 0; x < 4; x++) {
if (this.tiles[y][x]) this.tiles[y][x].show();
}
}
this.levelvisible = true;
console.log('show level', this);
}
}
this.hide = function() {
if (this.levelvisible) {
//console.log('hide level', this);
for (var y = 0; y < 4; y++) {
for (var x = 0; x < 4; x++) {
if (this.tiles[y][x] instanceof elation.engine.things.terrain.tile) {
this.terrain.removeTile(this.tiles[y][x]);
}
}
}
this.levelvisible = false;
}
}
this.setFill = function(fill) {
if (this.tiles == false) this.create();
for (var y = 1; y < 3; y++) {
for (var x = 1; x < 3; x++) {
if (this.tiles[y][x] instanceof elation.engine.things.terrain.tile) {
if (!fill) {
this.terrain.removeTile(this.tiles[y][x]);
this.tiles[y][x] = false;
} else {
this.tiles[y][x].show();
}
} else {
if (fill) {
this.tiles[y][x] = this.terrain.getTile(this.level, x, y);
this.tiles[y][x].show();
}
}
}
}
}
});
elation.extend("engine.things.terrain.tile", function(terrain, args) {
this.terrain = terrain;
this.init = function() {
THREE.Mesh.call(this, terrain.plane, this.getMaterial());
this.tilevisible = false;
}
this.show = function() {
if (!this.tilevisible) {
if (this.parent && this.parent == this.terrain.objects['3d']) {
console.log('SHOW TILE ERROR: already in scene', this);
this.tilevisible = true;
} else {
//console.log('SHOW TILE', this);
this.terrain.root.add(this);
this.tilevisible = true;
}
}
}
this.hide = function() {
if (this.tilevisible) {
if (this.terrain && this.parent == this.terrain.root) {
//console.log('HIDE TILE', this);
this.parent.remove(this);
this.tilevisible = false;
} else {
console.log('HIDE TILE ERROR: not in scene', this);
}
}
}
this.setLevel = function(level, offset, position) {
this.name = "tile_" + level + "_" + offset[0] + "_" + offset[1];
var scale = Math.pow(2, level);
this.level = level;
this.tileoffset = offset;
this.tileposition = position;
this.position.set((offset[0] - 1.5) * scale, 0, (offset[1] - 1.5) * scale);
this.scale.set(scale, scale, scale);
//this.material.color.setHex((0xff0000 * offset[0] / 4) + (0x00ff00 * offset[1] / 4) + 0x000099);
var materialargs = this.terrain.getMaterialArgs(this.level, this.tileoffset, this.tileposition);
//console.log(materialargs);
if (this.material instanceof THREE.ShaderMaterial) {
//console.log('set uniforms', materialargs, this.material.uniforms);
for (var k in materialargs) {
if (this.material.uniforms[k]) {
this.material.uniforms[k].value = materialargs[k];
}
}
} else {
for (var k in materialargs) {
this.material[k] = materialargs[k];
}
}
}
this.getMaterial = function() {
/*
var materialargs = {
map: elation.engine.material.getTexture(this.properties.terrain.textures.map)
};
*/
//return new THREE.MeshBasicMaterial({color: 0xff0000, wireframe: true, opacity: .8, transparent: true});
return elation.engine.materials.getShaderMaterial("planet_surface", {}, {"USE_MAP": "", "USE_NORMALMAP": "", "DEFORM_SPHERE": ""});
//return new THREE.MeshBasicMaterial();
}
this.init();
});
elation.engine.things.terrain.tile.prototype = Object.create(THREE.Mesh.prototype);
/*** SHADER STUFF ***/
// geometry_clipmap
// Render a planet using a series of tiles arranged as a half-sphere.
// This half-sphere is oriented to always face towards the camera.
// The source mesh is one 64x64 grid of vertices. Each LOD level is made of 4x4 (16)
// instances of these tiles. We map each of these tiles to form a portion of our
// spherical shape, and apply our heightmap onto each tile. The map and displacementMap
// images for each level are managed on the CPU in the form of canvases composited from
// DeepZoom image tiles (tileable map images + spherical geometry clipmaps)
elation.engine.materials.addChunk("geometry_clipmap", {
uniforms: {
"radius" : { type: "f", value: 1.0 },
"level" : { type: "f", value: 1.0 },
"latlon" : { type: "v2", value: new THREE.Vector2() },
"subdivisions" : { type: "f", value: 1.0 },
"scale" : { type: "f", value: 1.0 },
"tangentMatrix" : { type: "m4", value: new THREE.Matrix4() },
"deformableOffset" : { type: "v2", value: new THREE.Vector2() },
"textureOffset" : { type: "v2", value: new THREE.Vector2() },
"displacementMap" : { type: "t", value: null },
"displacementScale" : { type: "f", value: 10.075 },
},
vertex_pars: [
"#define USE_MAP",
"#define PI 3.141592",
"varying vec3 vViewPosition;",
"uniform float radius;",
"uniform float scale;",
"uniform float level;",
"uniform vec2 latlon;",
"uniform float subdivisions;",
"uniform vec2 deformableOffset;",
"uniform vec2 textureOffset;",
"uniform mat4 tangentMatrix;",
"uniform sampler2D displacementMap;",
"uniform float displacementScale;",
//"varying vec2 vUv;",
//"uniform vec4 offsetRepeat;"
].join('\n'),
vertex: [
// Map vertex position x/z to [0..1] based on which tile this is
"vec2 abspos = vec2((position.x + deformableOffset.x) / subdivisions, (position.z + deformableOffset.y) / subdivisions);",
"float tphi = (abspos.x - .5) * PI * scale;", // longitude is between -PI/2 and PI/2
"float ttheta = (abspos.y - .5) * PI * scale + PI/2.0;", // latitude is between 0 and PI
"float buf = 2.0;",
//"vUv = vec2((abspos.x + (1.0 - 1.0/buf)) / buf + textureOffset.x, (abspos.y + (1.0 - 1.0 / buf)) / buf + textureOffset.y) ;",
"float theta = (PI / 2.0 - latlon[0]);",
"vec3 plocal = vec3(sin(tphi) * sin(ttheta), cos(ttheta), cos(tphi) * sin(ttheta));",
//"vec3 pglobal = vec3(cos(theta) * plocal.x - sin(theta) * plocal.z, plocal.y, -sin(theta) * plocal.x + cos(theta) * plocal.z);",
"vec3 pglobal = vec3(plocal.x, -sin(theta) * plocal.z + cos(theta) * plocal.y, cos(theta) * plocal.z - sin(theta) * plocal.y * -1.0);",
"vec2 globaluv = vec2(atan(pglobal.y, pglobal.x) / PI + 1.0, (acos(pglobal.z) - theta) / PI + .5);",
//"vec2 localuv = vec2((abspos.x / 2.0) + textureOffset.x), abspos.y + textureOffset.y);",
"vec2 localuv = vec2(clamp((abspos.x + (1.0 - 1.0/buf)) / buf + textureOffset.x, 0.0, 1.0), clamp((abspos.y + (1.0 - 1.0 / buf)) / buf + textureOffset.y, 0.0, 1.0));",
"",
//"vUv = localuv;",
"vUv *= pow(2.0, level);",
// Look up displacement for this vertex
"vec4 dispvec = texture2D(displacementMap, vUv);",
// Unpack 16-bit heightmap value
//"float displacement = (dispvec.r * 256.0 + dispvec.g) * displacementScale;",
"float displacement = 0.0;",
"float tradius = radius + displacement;",
"#ifdef DEFORM_SPHERE",
"vec4 tPosition = vec4(plocal * tradius, 1.0);", //vec4(tradius * sin(a) * sin(b), tradius * cos(b), tradius * cos(a) * sin(b), 1.0 );",
"#else",
"vec4 tPosition = vec4(tradius * (abspos.x - .5) * scale, tradius * (-abspos.y + .5) * scale, tradius, 1.0 );",
"#endif",
//"vec4 mvPosition = modelViewMatrix * tangentMatrix * vec4(tPosition.xyz, 1.0);",
//"vec4 mvPosition = tPosition;",
"mvPosition = tPosition;",
//"vec4 mPosition = objectMatrix * tangentMatrix * vec4(tPosition.xyz, 1.0);",
//"vec4 mvPosition = modelViewMatrix * vec4(position + vec3(0,-1,0), 1.0);",
//"gl_Position = projectionMatrix * mvPosition;",
].join('\n'),
fragment_pars: [
"#define USE_MAP",
"uniform vec3 diffuse;",
"uniform float opacity;",
].join('\n'),
fragment: [
//"gl_FragColor = vec4( 1.0, 1.0, 1.0, opacity );",
"gl_FragColor = vec4( diffuse, opacity );",
].join('\n')
});
elation.engine.materials.addChunk("atmosphere", {
uniforms: {
"v3LightPosition": { type: "v3", value: new THREE.Vector3(1e8, 0, 1e8).normalize() },
"v3InvWavelength": { type: "v3", value: new THREE.Vector3(5.6, 9.47, 19.64) },
"fCameraHeight": { type: "f", value: 0},
"fCameraHeight2": { type: "f", value: 0},
//"fInnerRadius": { type: "f", value: 6350},
"outerradius": { type: "f", value: 6667},
//"fOuterRadius2": { type: "f", value: 44448889},
"fKrESun": { type: "f", value: 0.05},
"fKmESun": { type: "f", value: 0.02},
"fKr4PI": { type: "f", value: .0314152},
"fKm4PI": { type: "f", value: .0125664},
"fScale": { type: "f", value: .00315457413249211356},
"fScaleDepth": { type: "f", value: .25},
"fScaleOverScaleDepth": { type: "f", value: .01261829652996845424},
},
vertex_pars: [
"uniform vec3 v3LightPosition;", // The direction vector to the light source
"uniform vec3 v3InvWavelength;", // 1 / pow(wavelength, 4) for the red, green, and blue channels
"uniform float fCameraHeight;", // The camera's current height
"uniform float fCameraHeight2;", // fCameraHeight^2
"uniform float outerradius;", // The outer (atmosphere) radius
//"uniform float fOuterRadius2;", // fOuterRadius^2
//"uniform float fInnerRadius;", // The inner (planetary) radius
"uniform float fKrESun;", // Kr * ESun
"uniform float fKmESun;", // Km * ESun
"uniform float fKr4PI;", // Kr * 4 * PI
"uniform float fKm4PI;", // Km * 4 * PI
"uniform float fScale;", // 1 / (outerradius - fInnerRadius)
"uniform float fScaleDepth;", // The scale depth (i.e. the altitude at which the atmosphere's average density is found)
"uniform float fScaleOverScaleDepth;", // fScale / fScaleDepth
"varying vec3 cFront;",
"varying vec3 cSecondary;",
"const int nSamples = 3;",
"const float fSamples = 3.0;",
"float atmosphere_scale(float fCos) {",
"float x = 1.0 - fCos;",
"return fScaleDepth * exp(-0.00287 + x*(0.459 + x*(3.83 + x*(-6.80 + x*5.25))));",
"}",
].join('\n'),
vertex: [
// Get the ray from the camera to the vertex, and its length (which is the
// far point of the ray passing through the atmosphere)
"vec3 v3Ray = mPosition.xyz - cameraPosition;",
"float fFar = length(v3Ray);",
"v3Ray /= fFar;",
// Calculate the ray's starting position
"#ifdef ATMOSCAT_FROM_SPACE",
// We're outside the atmosphere, so calculate the closest intersection
// of the ray with the outer atmosphere (which is the near point of the
// ray passing through the atmosphere)
"float B = 2.0 * dot(cameraPosition, v3Ray);",
"float C = fCameraHeight2 - (outerradius * outerradius);",
"float fDet = max(0.0, B*B - 4.0 * C);",
"float fNear = 0.5 * (-B - sqrt(fDet));",
"vec3 v3Start = cameraPosition + v3Ray * fNear;",
"fFar -= fNear;",
"float fDepth = exp((radius - outerradius) / fScaleDepth);",
"#else",
"#ifdef ATMOSCAT_FROM_ATMOSPHERE",
"vec3 v3Start = cameraPosition;",
"float fDepth = exp((radius - fCameraHeight) / fScaleDepth);",
"#endif",
"#endif", // for #else
// Calculate the scattering offset
"float fCameraAngle = dot(-v3Ray, mPosition.xyz) / length(mPosition.xyz);",
"float fLightAngle = dot(v3LightPosition, mPosition.xyz) / length(mPosition.xyz);",
"float fCameraScale = atmosphere_scale(fCameraAngle);",
"float fLightScale = atmosphere_scale(fLightAngle);",
"float fCameraOffset = fDepth*fCameraScale;",
"float fTemp = (fLightScale + fCameraScale);",
// Initialize the scattering loop variables
"float fSampleLength = fFar / fSamples;",
"float fScaledLength = fSampleLength * fScale;",
"vec3 v3SampleRay = v3Ray * fSampleLength;",
"vec3 v3SamplePoint = v3Start + v3SampleRay * 0.5;",
// Now loop through the sample rays
"vec3 v3FrontColor = vec3(0.0, 0.0, 0.0);",
"vec3 v3Attenuate;",
"for(int i=0; i<nSamples; i++) {",
"float fHeight = length(v3SamplePoint);",
"float fDepth = exp(fScaleOverScaleDepth * (radius - fHeight));",
"float fScatter = fDepth*fTemp - fCameraOffset;",
"v3Attenuate = exp(-fScatter * (v3InvWavelength * fKr4PI + fKm4PI));",
"v3FrontColor += v3Attenuate * (fDepth * fScaledLength);",
"v3SamplePoint += v3SampleRay;",
"}",
// Calculate the attenuation factor for the ground
"cFront = v3FrontColor * (v3InvWavelength * fKrESun + fKmESun);",
//"cFront = (vec3(length(v3Start) / 10000.0, 0, 0));",
//"cFront = (vec3(exp(fScaleOverScaleDepth * (radius - length(v3Start)))));",
//"cFront = (vec3(1,0,0));",
"cSecondary = v3Attenuate;",
].join('\n'),
fragment_pars: [
"uniform vec3 v3LightPosition;", // The direction vector to the light source
"varying vec3 cFront;",
"varying vec3 cSecondary;",
].join('\n'),
fragment: [
"float phong = 1.0;",//dot(normalize(vNormal), normalize(v3LightPosition));",
"gl_FragColor = vec4((cFront + gl_FragColor.rgb) * phong, opacity);" // * vec4(cSecondary, 0);",
].join('\n')
});
elation.engine.materials.buildShader("planet_surface", {
uniforms: [
'common',
'normalmap',
'lights',
'lights_phong',
'geometry_clipmap',
'atmosphere'
],
chunks_vertex: [
'normal',
//'atmosphere',
'map',
'lightmap',
'envmap',
'color',
'default',
'geometry_clipmap',
'shadowmap'
],
chunks_fragment: [
'geometry_clipmap',
'normal',
'map',
//'specularmap',
'alphatest',
'lightmap',
'color',
'envmap',
'shadowmap',
//'atmosphere',
//'linear_to_gamma'
]
});
});