Skip to content

Commit

Permalink
initial release
Browse files Browse the repository at this point in the history
  • Loading branch information
shama committed Jun 22, 2013
0 parents commit 08236db
Show file tree
Hide file tree
Showing 4 changed files with 420 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
@@ -0,0 +1 @@
/node_modules/
73 changes: 73 additions & 0 deletions README.md
@@ -0,0 +1,73 @@
# voxel-tile

**WORK IN PROGRESS**

Load google map or open street map tiles into [voxel.js](http://voxeljs.com).

## example

```js
var tile = require('voxel-tile')({
game: game,

// Path to tile images
tilepath: 'textures/heightmap/tiles/',

// [optional] Load a separate tile for coloring
// (otherwise it will just use the colors from the heightmap)
colortilepath: 'textures/colormap/tiles/',

// [optional] Size of chunks
size: 32,

// [optional] Size of tiles
tilesize: 256,

// [optional] URL format to tiles
tileformat: '%z/%x/%y.png',

// [optional] Maximum zoom level
zoomax: 6,
// [optional] Minimum zoom level
zoomin: 3,

// [optional] Voxel mesher to mesh with
mesher: voxel.meshers.greedy,

// [optional] Boolean whether the tiles should infinitely repeat
repeat: true,

// [optional] Boolean whether to render the world flat
flat: false,

// [optional] Function to determine height based on color
heightmap: function(r, g, b, a) {
return Math.floor(((r * 255 * 255 + g * 255 + b) / 16646655) * 32)
},
})

// On missing chunk, load up tile for chunk position
game.voxels.on('missingChunk', tile.missingChunk.bind(tile))

// Set zoom level based on the current player height distance
game.on('tick', function() {
tile.setZoom(player.yaw.position.y)
})
```

## install

With [npm](https://npmjs.org) do:

```
npm install voxel-tile
```

Use [browserify](http://browserify.org) to `require('voxel-tile')`.

## release history
* 0.1.0 - initial release

## license
Copyright (c) 2013 Kyle Robinson Young<br/>
Licensed under the MIT license.
304 changes: 304 additions & 0 deletions index.js
@@ -0,0 +1,304 @@
var EE = require('events').EventEmitter
var inherits = require('inherits')
var voxel = require('voxel')
var voxelMesh = require('voxel-mesh')

function Tile(opts) {
EE.call(this)
var self = this
if (opts.THREE) opts = {game:opts}
this.game = opts.game
this.tilepath = opts.tilepath || ''
this.colortilepath = opts.colortilepath || false
this.size = opts.size || this.game.chunkSize || 32
this.scale = opts.scale || this.size
this.tilesize = opts.tilesize || 256
this.tileformat = opts.tileformat || '%z/%x/%y.png'
this.zoomax = opts.zoomax || 6
this.zoomin = opts.zoomin || 3
this.mesher = opts.mesher || voxel.meshers.greedy
this.actualSize = opts.actualSize === true
this.repeat = opts.repeat !== false
this.flat = opts.flat === true
this.zoom = this.zoomax

// how the tile is heightmap'd
this.heightmap = opts.heightmap || function(r, g, b, a) {
return Math.floor(((r * 255 * 255 + g * 255 + b) / 16646655) * self.size)
}

this.queue = Object.create(null)
this.images = Object.create(null)

// basic color material
this.material = new self.game.THREE.MeshBasicMaterial({
vertexColors: self.game.THREE.FaceColors
})
}
inherits(EE, Tile)

module.exports = function(opts) {
return new Tile(opts)
}

// call when a chunk is missing
Tile.prototype.missingChunk = function missingChunk(pos) {
var uri = this.whichImage(pos)
if (uri) this.queuePos(pos, uri)
}

// set the zoom level
Tile.prototype.setZoom = function setZoom(zoom) {
var self = this
zoom = this.zoomax + 1 - Math.floor(Math.abs(zoom) / this.size)
if (zoom > this.zoomax) zoom = this.zoomax
if (zoom < this.zoomin) zoom = this.zoomin

if (zoom === this.zoom) return
this.zoom = zoom
console.log('zoom', zoom)

// Regen chunks if zoom changed
var player = this.game.playerPosition()
var nearbyChunks = this.game.voxels.nearbyChunks(player, this.game.removeDistance).filter(function(pos) {
if (pos[1] < 0) return false
return (Math.abs(pos[1]) <= 2)
}).forEach(function(pos) {
self.missingChunk(pos)
})
}

// determine which image
Tile.prototype.whichImage = function whichImage(pos) {
// TODO: Only 0 now but should handle higher elevations later
if (pos[1] !== 0) return false
var totaltiles = Math.pow(2, this.zoom)
var x = pos[0], z = pos[2]
if (this.actualSize) {
// TODO: for loading in actual size, doesnt work right yet
x = Math.floor(x * this.size / this.tilesize)
z = Math.floor(z * this.size / this.tilesize)
}
if (this.repeat) {
x = Math.abs(mod(x, totaltiles))
z = Math.abs(mod(z, totaltiles))
}
var uri = this.tileformat
return uri.replace('%x', x).replace('%y', z).replace('%z', this.zoom)
}

// queue up the pos for on image load
Tile.prototype.queuePos = function queuePos(pos, uri) {
if (!Array.isArray(this.queue[uri])) this.queue[uri] = []
this.queue[uri].push(pos)
this.loadImage(uri)
}

// after image has loaded, generate and show it
Tile.prototype.processQueue = function processQueue(uri) {
var self = this
if (Array.isArray(this.queue[uri]) && this.queue[uri].length > 0) {
var img = this.images[uri]
if (typeof img === 'object') {
for (var i = 0; i < this.queue[uri].length; i++) {
var pos = this.queue[uri][i]
this.showChunk(this.generateChunk(img, pos))
}
this.queue[uri] = []
} else if (typeof img === 'integer' && img < 5) {
// its lagging, try again
this.images[uri]++
setTimeout(function() {
self.processQueue(uri)
}, 100)
}
}
}

// load the image and process queue when done
Tile.prototype.loadImage = function loadImage(uri) {
var self = this
if (!this.images[uri]) {
var loaded = 0
this.images[uri] = 1
var img = new Image()
img.crossOrigin = ''

function postload(img) {
// rotate 90
var canvas = rotate(img, -90)
// flip horiz
canvas = flip(canvas, 'h')
// TODO: actual size needs to have its orientation fixed
if (self.actualSize !== true) {
// shrink down
canvas = shrink(canvas, self.size)
}
return canvas
}

function onload() {
loaded++
if (loaded > 1) {
//window.open(canvas.toDataURL())
self.images[uri] = [img, imgcolor]
self.processQueue(uri)
}
}

// load color tile
if (this.colortilepath !== false) {
var imgcolor = new Image()
imgcolor.crossOrigin = ''
imgcolor.onload = function() {
imgcolor = postload(imgcolor)
onload()
}
imgcolor.src = self.colortilepath + uri
} else {
loaded++
}

// load height tile
img.onload = function() {
img = postload(img)
onload()
}
img.src = self.tilepath + uri
} else {
this.processQueue(uri)
}
}

// get pixels from region of image
Tile.prototype.getPixels = function getPixels(canvas, pos) {
var x = pos[0], y = pos[1], z = pos[2]
var context = canvas.getContext('2d')
if (this.actualSize === true) {
x = Math.abs(mod(x * this.size, this.tilesize))
z = Math.abs(mod(z * this.size, this.tilesize))
return context.getImageData(x, z, this.size, this.size)
} else {
return context.getImageData(0, 0, this.size, this.size)
}
}

// for generating a chunk
Tile.prototype.generate = function generate(pos, fn) {
var size = this.size
var chunk = {
position: pos,
dims: [size, size, size],
voxels: new Int32Array(size * size * size)
}
for (var idx = 0, x = 0; x < size; x++)
for (var y = 0; y < size; y++)
for (var z = 0; z < size; z++, idx++)
chunk.voxels[idx] = fn(x, y, z)
return chunk
}

// actually generate the chunk
Tile.prototype.generateChunk = function generateChunk(img, pos) {
var self = this
var imgcolor = img[1], img = img[0]
var size = this.size
var pixels = this.getPixels(img, pos)
var pixelData = pixels.data

// if using color map
var pixelsColor = false
var pixelColorData = false
if (imgcolor) {
pixelsColor = this.getPixels(imgcolor, pos)
pixelColorData = pixelsColor.data
}

var chunk = this.generate(pos, function(x, y, z) {
var p = (x + z * self.size) * 4
//var p = x * 4 + z * 4 * self.size
var r = pixelData[p], g = pixelData[p+1], b = pixelData[p+2], a = pixelData[p+3]
var h = 1
if (self.flat !== true) {
h = self.heightmap(r, g, b, a)
}
if (pixelsColor !== false) {
r = pixelColorData[p], g = pixelColorData[p+1], b = pixelColorData[p+2], a = pixelColorData[p+3]
}
var color = rgbtohex(r, g, b)
return y < h ? color : 0
})
//pixels.data = pixelData
/*delete canvas
delete context
delete pixelData
delete pixels*/
return chunk
}

// our own show chunk as it's handled a little different
Tile.prototype.showChunk = function showChunk(chunk) {
var game = this.game
//game.showChunk(chunk)
var chunkIndex = chunk.position.join('|')
var chunkBounds = game.voxels.getBounds.apply(game.voxels, chunk.position)
var scale = new game.THREE.Vector3(1, 1, 1)
var mesh = voxelMesh(chunk, this.mesher, scale, game.THREE)
game.voxels.chunks[chunkIndex] = chunk
if (game.voxels.meshes[chunkIndex]) game.scene.remove(game.voxels.meshes[chunkIndex][game.meshType])
game.voxels.meshes[chunkIndex] = mesh
mesh.createSurfaceMesh(this.material)
mesh.setPosition(chunkBounds[0][0], chunkBounds[0][1], chunkBounds[0][2])
mesh.addToScene(game.scene)
game.emit('renderChunk', chunk)
return mesh
}


function mod(num, n) { return ((num % n) + n) % n }

function rgbtohex(r, g, b) {
return parseInt('0x' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1))
}

function rotate(img, deg) {
var canvas = document.createElement('canvas')
canvas.width = img.width
canvas.height = img.height
var context = canvas.getContext('2d')
context.save()
context.translate(img.width/2, img.height/2)
context.rotate(deg * Math.PI / 180)
context.drawImage(img, -(img.width/2), -(img.height/2), img.width, img.height)
context.restore()
return canvas
}

function shrink(img, size) {
var canvas = document.createElement('canvas')
canvas.width = canvas.height = size
var context = canvas.getContext('2d')
context.save()
context.drawImage(img, 0, 0, img.width, img.height, 0, 0, size, size)
context.restore()
return canvas
}

function flip(img, way) {
way = way || 'h'
var canvas = document.createElement('canvas')
canvas.width = img.width
canvas.height = img.height
var context = canvas.getContext('2d')
context.save()
if (way !== 'h') {
context.translate(img.width, 0)
context.scale(-1, 1)
} else {
context.translate(0, img.height)
context.scale(1, -1)
}
context.drawImage(img, 0, 0)
context.restore()
return canvas
}

0 comments on commit 08236db

Please sign in to comment.