applied a few things i picked up from http://www.redblobgames.com/maps/terrain-from-noise/
A Pen by not important on CodePen.
applied a few things i picked up from http://www.redblobgames.com/maps/terrain-from-noise/
A Pen by not important on CodePen.
<div class="o-wrapper"> | |
<canvas | |
id="js-canvas" | |
width="410" | |
height="410" | |
></canvas> | |
<h1 id="js-loading" class="c-loading">Generating textures...</h1> | |
</div> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r79/three.min.js"></script> | |
<script src="https://codepen.io/clindsey/pen/egQpvE.js"></script> <!-- triangle-textures-4.0.0, GeoGenTextures --> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/seedrandom/2.4.2/lib/alea.js"></script> | |
<script src="https://cdn.rawgit.com/jwagner/simplex-noise.js/87440528bcf8ec89840e974d8f76cfe3da548c37/simplex-noise.min.js"></script> | |
<script>(function(){var script=document.createElement('script');script.onload=function(){var stats=new Stats();document.body.appendChild(stats.dom);requestAnimationFrame(function loop(){stats.update();requestAnimationFrame(loop)});};script.src='//rawgit.com/mrdoob/stats.js/master/build/stats.min.js';document.head.appendChild(script);})()</script> |
setTimeout(() => { | |
const elevationSeed = +(new Date()); // 5625463739 | |
const elevationSimplex = new SimplexNoise(new alea(elevationSeed)); // 5625463739 | |
const moistureSimplex = new SimplexNoise(new alea(elevationSeed + 0x100)); // 5625463739 | |
const textureSize = 64; // must be 2's compliment | |
const segmentIterations = 3; // tesselates a faces into domains, increasing variation | |
const textureIterations = 2; // tesselates domains, increasing number of steps between colors | |
const geometryIterations = 2; // icosahedron face count, try to keep this as low as possible | |
const noiseFactor = 1; // smaller values are boring, bigger are happening | |
const geometryRadius = 100; | |
const scene = new THREE.Scene(); | |
buildLights(scene); | |
const camera = new THREE.PerspectiveCamera(75, 1, 1, 10000); // refactor, magic numbers | |
camera.position.z = 240; | |
const mesh = buildMesh(segmentIterations, textureIterations, geometryIterations, textureSize, geometryRadius, noiseFactor, elevationSimplex, moistureSimplex); | |
scene.add(mesh); | |
const renderer = new THREE.WebGLRenderer({canvas: document.getElementById('js-canvas'), alpha: true}); | |
animate(renderer, scene, camera, mesh); | |
document.getElementById('js-loading').style.display = 'none'; | |
// document.body.appendChild(generateTextureOutput(mesh.material.materials.map(m => m.map), textureSize, textureSize)); | |
}, 0); | |
function animate (renderer, scene, camera, mesh) { | |
mesh.rotation.x += 0.004; | |
mesh.rotation.y += 0.008; | |
renderer.render(scene, camera); | |
requestAnimationFrame(() => { | |
animate(renderer, scene, camera, mesh); | |
}); | |
} | |
function buildLights (scene) { | |
const ambientLight = new THREE.AmbientLight(0x000000); | |
scene.add(ambientLight); | |
const lights = [ | |
new THREE.PointLight(0xffffff, 1, 0), | |
new THREE.PointLight(0xffffff, 1, 0), | |
new THREE.PointLight(0xffffff, 1, 0) | |
]; | |
lights[0].position.set(0, 2000, 0); | |
lights[1].position.set(0, 100, 400); | |
lights[2].position.set(-1000, -2000, -1000); | |
scene.add(lights[0]); | |
scene.add(lights[1]); | |
scene.add(lights[2]); | |
} | |
function generateTextureOutput (textures, sW, sH) { | |
const canvasEl = document.createElement('canvas'); | |
const $ = canvasEl.getContext('2d'); | |
const size = Math.ceil(Math.sqrt(textures.length)); | |
canvasEl.width = sW * size; | |
canvasEl.height = sH * size; | |
textures.forEach(({image}, index) => { | |
const x = index % size; | |
const y = Math.floor(index / size); | |
$.drawImage(image, 0, 0, sW, sH, x * sW, y * sH, sW, sH); | |
}); | |
return canvasEl; | |
} | |
function buildMesh (segmentIterations, textureIterations, geometryIterations, textureSize, radius, factor, elevationSimplex, moistureSimplex) { | |
const geometry = new THREE.IcosahedronGeometry(radius, geometryIterations); | |
const materials = []; | |
const pointsUp = GeoGenTextures.buildPoints(3, Math.PI * 1.5); | |
const vec0 = pointsUp[0].map(i => 0.5 - i / 2); | |
const vec1 = pointsUp[1].map(i => 0.5 - i / 2); | |
const vec2 = pointsUp[2].map(i => 0.5 - i / 2); | |
geometry.faceVertexUvs[0] = geometry.faces.map((face, index) => { | |
geometry.faces[index].materialIndex = index; | |
const {a, b, c} = geometry.faces[index]; | |
const material = createMaterial(textureIterations, segmentIterations, a, b, c, geometry.vertices, radius * 2, textureSize, factor, elevationSimplex, moistureSimplex); | |
materials.push(material); | |
return [ | |
new THREE.Vector2(vec0[0], vec0[1]), | |
new THREE.Vector2(vec1[0], vec1[1]), | |
new THREE.Vector2(vec2[0], vec2[1]) | |
]; | |
}); | |
geometry.computeFaceNormals(); | |
geometry.dynamic = true; | |
geometry.uvsNeedUpdate = true; | |
return new THREE.Mesh(geometry, new THREE.MeshFaceMaterial(materials)); | |
} | |
function colorPicker (vertices, order, colorLookup, radius, elevationSimplex, moistureSimplex, f) { | |
return factors => { | |
const [x, y, z] = factors.map((factor, i) => ([ | |
factor * vertices[order[i]].x, | |
factor * vertices[order[i]].y, | |
factor * vertices[order[i]].z | |
])).reduce(([pX, pY, pZ], [cX, cY, cZ]) => ([pX + cX, pY + cY, pZ + cZ]), [0, 0, 0]); | |
const fX = Math.round((x / radius) * 1000000) / 1000000; | |
const fY = Math.round((y / radius) * 1000000) / 1000000; | |
const fZ = Math.round((z / radius) * 1000000) / 1000000; | |
let e = 1 * elevationSimplex.noise3D((1 * f) * fX, (1 * f) * fY, (1 * f) * fZ); | |
e += 0.5 * elevationSimplex.noise3D((2 * f) * fX, (2 * f) * fY, (2 * f) * fZ); | |
e += 0.25 * elevationSimplex.noise3D((4 * f) * fX, (4 * f) * fY, (4 * f) * fZ); | |
e += 0.13 * elevationSimplex.noise3D((8 * f) * fX, (8 * f) * fY, (8 * f) * fZ); | |
e += 0.06 * elevationSimplex.noise3D((16 * f) * fX, (16 * f) * fY, (16 * f) * fZ); | |
e += 0.03 * elevationSimplex.noise3D((32 * f) * fX, (32 * f) * fY, (32 * f) * fZ); | |
e = (e + (1.97 / 2)) / 1.97; | |
e = Math.pow(e, 3); | |
let m = 1 * moistureSimplex.noise3D((1 * f) * fX, (1 * f) * fY, (1 * f) * fZ); | |
m += 0.75 * moistureSimplex.noise3D((2 * f) * fX, (2 * f) * fY, (2 * f) * fZ); | |
m += 0.33 * moistureSimplex.noise3D((4 * f) * fX, (4 * f) * fY, (4 * f) * fZ); | |
m += 0.33 * moistureSimplex.noise3D((8 * f) * fX, (8 * f) * fY, (8 * f) * fZ); | |
m += 0.33 * moistureSimplex.noise3D((16 * f) * fX, (16 * f) * fY, (16 * f) * fZ); | |
m += 0.5 * moistureSimplex.noise3D((32 * f) * fX, (32 * f) * fY, (32 * f) * fZ); | |
m = (m + (3.24 / 2)) / 3.24; | |
m = Math.pow(m, 1); | |
return colorLookup(e, m); | |
}; | |
} | |
function createMaterial (textureIterations, segmentIterations, a, b, c, vertices, radius, textureSize, factor, elevationSimplex, moistureSimplex) { // refactor, use better variable names than `a, b, c` | |
const order = [b, a, c]; // refactor, bad variable name | |
const mapColorPicker = colorPicker(vertices, order, mapColorLookup, radius, elevationSimplex, moistureSimplex, factor); | |
const specularColorPicker = colorPicker(vertices, order, specularColorLookup, radius, elevationSimplex, moistureSimplex, factor); | |
const bumpColorPicker = colorPicker(vertices, order, bumpColorLookup, radius, elevationSimplex, moistureSimplex, factor); | |
const material = new THREE.MeshPhongMaterial({ | |
map: new THREE.Texture(GeoGenTextures.createNestedTile(textureSize, textureSize, segmentIterations, textureIterations, mapColorPicker, true)), | |
specularMap: new THREE.Texture(GeoGenTextures.createNestedTile(textureSize, textureSize, segmentIterations, textureIterations, specularColorPicker, true)), | |
specular: new THREE.Color(0x222222), | |
side: THREE.DoubleSide, | |
bumpMap: new THREE.Texture(GeoGenTextures.createNestedTile(textureSize, textureSize, segmentIterations, textureIterations, bumpColorPicker, true)), | |
bumpScale: 1.5, | |
wireframe: false | |
}); | |
material.map.needsUpdate = true; | |
material.bumpMap.needsUpdate = true; | |
material.specularMap.needsUpdate = true; | |
return material; | |
} | |
const OCEAN = '44447a'; | |
const BEACH = '9e8e77'; | |
const TROPICAL_RAIN_FOREST = '337755'; | |
const TROPICAL_SEASONAL_FOREST = '559944'; | |
const GRASSLAND = '88aa55'; | |
const SUBTROPICAL_DESERT = 'd2b98b'; | |
const TEMPERATE_RAIN_FOREST = '448855'; | |
const TEMPERATE_DECIDUOUS_FOREST = '679459'; | |
const TEMPERATE_DESERT = 'c9d29b'; | |
const TAIGA = '99aa77'; | |
const SHRUBLAND = '889977'; | |
const SNOW = 'dddde4'; | |
const TUNDRA = 'bbbbaa'; | |
const BARE = '888888'; | |
const SCORCHED = '555555'; | |
function mapColorLookup (e, m) { // taken from http://www.redblobgames.com/maps/terrain-from-noise/ | |
if (e < 0.1) return OCEAN; | |
if (e < 0.12) return BEACH; | |
if (e > 0.8) { | |
if (m < 0.1) return SCORCHED; | |
if (m < 0.2) return BARE; | |
if (m < 0.5) return TUNDRA; | |
return SNOW; | |
} | |
if (e > 0.6) { | |
if (m < 0.33) return TEMPERATE_DESERT; | |
if (m < 0.66) return SHRUBLAND; | |
return TAIGA; | |
} | |
if (e > 0.3) { | |
if (m < 0.16) return TEMPERATE_DESERT; | |
if (m < 0.50) return GRASSLAND; | |
if (m < 0.83) return TEMPERATE_DECIDUOUS_FOREST; | |
return TEMPERATE_RAIN_FOREST; | |
} | |
if (m < 0.16) return SUBTROPICAL_DESERT; | |
if (m < 0.33) return GRASSLAND; | |
if (m < 0.66) return TROPICAL_SEASONAL_FOREST; | |
return TROPICAL_RAIN_FOREST; | |
} | |
function bumpColorLookup (e, m) { | |
if (e < 0.1) { | |
return '333333'; | |
} | |
if (e < 0.12) { | |
return '666666'; | |
} | |
if (e > 0.8) { | |
return 'ffffff'; | |
} | |
if (e > 0.3) { | |
return 'cccccc'; | |
} | |
return '999999'; | |
} | |
function specularColorLookup (e, m) { | |
if (e < 0.1) { | |
return 'ffffff'; | |
} | |
return '000000'; | |
} |
body { | |
background-color: #3b3251; | |
font-family: sans-serif; | |
color: white; | |
} | |
.o-wrapper { | |
position: relative; | |
} | |
canvas { | |
display: block; | |
margin: 0 auto; | |
} | |
.c-loading { | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
} |