|
window.addEventListener("DOMContentLoaded",app); |
|
|
|
function app() { |
|
let asideBtn = document.getElementById("aside-btn"), |
|
resetBtn = document.getElementById("reset-btn"), |
|
imgUpload = document.getElementsByName("img_upload")[0], |
|
imgName = document.getElementsByName("img_name")[0], |
|
canvas = document.createElement("canvas"), |
|
c = canvas.getContext("2d"), |
|
img = null, |
|
scene, |
|
camera, |
|
camControls, |
|
renderer, |
|
textureLoader = new THREE.TextureLoader(), |
|
city, |
|
dust, |
|
// adjustable |
|
skyColor = 0x8fa6af, |
|
terrainColor = 0xe8bfa9, |
|
chunkSize = 64, |
|
gridSize = 7, |
|
roadWidth = 8, |
|
minBldgHt = 16, |
|
maxBldgHt = 48, |
|
bldgSize = 12, |
|
bldgFragHt = 2, |
|
bldgsPerChunkSide = 3, |
|
bldgDisplaceFactor = 0.25, |
|
dustParticleSpeed = 0.2, |
|
dustParticlesPerChunk = 12, |
|
sunAngle = 30, |
|
worldHeight = 64, |
|
worldSize = 1600, |
|
// technical |
|
chunkSizeHalf = chunkSize / 2, |
|
gridSizeEven = gridSize % 2 == 0, |
|
gridSizeHalf = gridSize / 2, |
|
bldgCellsPerSide = bldgsPerChunkSide * gridSize, |
|
roadsSide = chunkSize * gridSize + roadWidth, |
|
roadsSideHalf = roadsSide / 2, |
|
dustParticles = dustParticlesPerChunk * gridSize ** 2, |
|
// functions |
|
adjustWindow = () => { |
|
camera.aspect = window.innerWidth / window.innerHeight; |
|
camera.updateProjectionMatrix(); |
|
renderer.setSize(window.innerWidth,window.innerHeight); |
|
}, |
|
clearCity = () => { |
|
img = null; |
|
|
|
if (scene) { |
|
let children = city.children; |
|
|
|
while (children.length) { |
|
let child = children[0]; |
|
// kill windows of a building |
|
if (child.name == "Civilian Structure") { |
|
let gchild = child.children[0]; |
|
gchild.geometry.dispose(); |
|
gchild.material.dispose(); |
|
child.remove(gchild); |
|
} |
|
child.geometry.dispose(); |
|
child.material.dispose(); |
|
city.remove(child); |
|
} |
|
} |
|
}, |
|
getDistance = (x1,y1,x2,y2) => { |
|
let raw = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2), |
|
rounded = Math.round(raw * 1e3) / 1e3; |
|
|
|
return rounded; |
|
}, |
|
getLightness = (R,G,B) => { |
|
let r = R / 255, |
|
g = G / 255, |
|
b = B / 255, |
|
cmin = Math.min(r,g,b), |
|
cmax = Math.max(r,g,b), |
|
light = (cmax + cmin) / 2; |
|
|
|
return light; |
|
}, |
|
generateCity = imgData => { |
|
let bldgSizeHalf = bldgSize / 2, |
|
bldgNegEdge = -bldgSizeHalf - 0.01, |
|
bldgPosEdge = bldgSizeHalf + 0.01, |
|
chunkDiv = chunkSize / bldgsPerChunkSide, |
|
chunkDivHalf = chunkDiv / 2, |
|
bldgTrans = -(chunkSize * gridSizeHalf) + chunkDivHalf, |
|
pixelIndex = 0, |
|
sidewalks = scene.children.filter(child => child.name == "Sidewalk"); |
|
|
|
for (let z = 0; z < bldgCellsPerSide; ++z) { |
|
for (let x = 0; x < bldgCellsPerSide; ++x) { |
|
if ( |
|
bldgsPerChunkSide % 2 == 0 || |
|
z % bldgsPerChunkSide != Math.floor(bldgsPerChunkSide / 2) || |
|
x % bldgsPerChunkSide != Math.floor(bldgsPerChunkSide / 2) |
|
) { |
|
// building |
|
let bldgHeight, |
|
bldgHeightHalf, |
|
bldgGeo, |
|
bldgMat, |
|
bldg, |
|
alpha = imgData ? imgData[pixelIndex + 3] : 0; |
|
|
|
if (imgData && alpha > 0.1) { |
|
let red = imgData[pixelIndex], |
|
green = imgData[pixelIndex + 1], |
|
blue = imgData[pixelIndex + 2], |
|
lightness = getLightness(red,green,blue); |
|
|
|
bldgHeight = minBldgHt + Math.round(lightness * (maxBldgHt - minBldgHt) / bldgFragHt) * bldgFragHt; |
|
bldgHeightHalf = bldgHeight / 2; |
|
bldgGeo = new THREE.BoxBufferGeometry(bldgSize,bldgHeight,bldgSize); |
|
bldgMat = new THREE.MeshPhongMaterial({ |
|
color: `rgb(${red},${green},${blue})` |
|
}); |
|
bldg = new THREE.Mesh(bldgGeo,bldgMat); |
|
|
|
} else { |
|
let randHue = randomHue(); |
|
bldgHeight = minBldgHt + Math.round(Math.random() * (maxBldgHt - minBldgHt) / bldgFragHt) * bldgFragHt; |
|
bldgHeightHalf = bldgHeight / 2; |
|
bldgGeo = new THREE.BoxBufferGeometry(bldgSize,bldgHeight,bldgSize); |
|
bldgMat = new THREE.MeshPhongMaterial({ |
|
color: `hsl(${randHue},50%,45%)` |
|
}); |
|
bldg = new THREE.Mesh(bldgGeo,bldgMat); |
|
} |
|
|
|
bldgMat.shininess = 90; |
|
bldg.name = "Civilian Structure"; |
|
bldg.castShadow = true; |
|
bldg.receiveShadow = true; |
|
bldg.position.set( |
|
x * chunkDiv + bldgTrans, |
|
bldgHeight / 2 + 1, |
|
z * chunkDiv + bldgTrans |
|
); |
|
// displacement towards the center of the sidewalk |
|
let sidewalkIndex = Math.floor(x / bldgsPerChunkSide) + (gridSize * Math.floor(z / bldgsPerChunkSide)), |
|
sidewalk = sidewalks[sidewalkIndex]; |
|
if (sidewalk) { |
|
let sidewalkCoords = sidewalks[sidewalkIndex].position, |
|
sX = sidewalkCoords.x, |
|
bX = bldg.position.x, |
|
sZ = sidewalkCoords.z, |
|
bZ = bldg.position.z, |
|
distFromSWCenter = getDistance(sX,sZ,bX,bZ), |
|
newDist = distFromSWCenter * (1 - bldgDisplaceFactor), |
|
distX = Math.abs(sX - bX), |
|
distZ = Math.abs(sZ - bZ), |
|
distAngle = Math.atan(distZ / distX); |
|
|
|
if (bX > sX && bZ <= sZ) |
|
distAngle += Math.PI / 2; |
|
else if (bX <= sX && bZ <= sZ) |
|
distAngle += Math.PI; |
|
else if (bX <= sX && bZ > sZ) |
|
distAngle += Math.PI * 1.5; |
|
|
|
bldg.position.x = sX + (newDist * Math.sin(distAngle)); |
|
bldg.position.z = sZ + (newDist * Math.cos(distAngle)); |
|
} |
|
|
|
city.add(bldg); |
|
// windows |
|
let windowGeo = new THREE.BufferGeometry(), |
|
windowVertArr = []; |
|
|
|
for (let wy = 0; wy < bldgHeight; wy += 2) { |
|
for (let wx = 0; wx < 12; wx += 2) { |
|
let leftWinEdge = (-bldgSizeHalf + 0.5) + wx, |
|
rightWinEdge = (-bldgSizeHalf + 1.5) + wx, |
|
bottomWinEdge = (-bldgHeightHalf + 0.5) + wy, |
|
topWinEdge = (-bldgHeightHalf + 1.5) + wy; |
|
|
|
windowVertArr.push( |
|
// north |
|
rightWinEdge,bottomWinEdge,bldgNegEdge, |
|
leftWinEdge,bottomWinEdge,bldgNegEdge, |
|
leftWinEdge,topWinEdge,bldgNegEdge, |
|
leftWinEdge,topWinEdge,bldgNegEdge, |
|
rightWinEdge,topWinEdge,bldgNegEdge, |
|
rightWinEdge,bottomWinEdge,bldgNegEdge, |
|
// east |
|
bldgPosEdge,bottomWinEdge,rightWinEdge, |
|
bldgPosEdge,bottomWinEdge,leftWinEdge, |
|
bldgPosEdge,topWinEdge,leftWinEdge, |
|
bldgPosEdge,topWinEdge,leftWinEdge, |
|
bldgPosEdge,topWinEdge,rightWinEdge, |
|
bldgPosEdge,bottomWinEdge,rightWinEdge, |
|
// south |
|
leftWinEdge,bottomWinEdge,bldgPosEdge, |
|
rightWinEdge,bottomWinEdge,bldgPosEdge, |
|
rightWinEdge,topWinEdge,bldgPosEdge, |
|
rightWinEdge,topWinEdge,bldgPosEdge, |
|
leftWinEdge,topWinEdge,bldgPosEdge, |
|
leftWinEdge,bottomWinEdge,bldgPosEdge, |
|
// west |
|
bldgNegEdge,bottomWinEdge,leftWinEdge, |
|
bldgNegEdge,bottomWinEdge,rightWinEdge, |
|
bldgNegEdge,topWinEdge,rightWinEdge, |
|
bldgNegEdge,topWinEdge,rightWinEdge, |
|
bldgNegEdge,topWinEdge,leftWinEdge, |
|
bldgNegEdge,bottomWinEdge,leftWinEdge |
|
); |
|
} |
|
} |
|
|
|
let windowVerts = new Float32Array(windowVertArr), |
|
windowMat = new THREE.MeshBasicMaterial({ |
|
color: 0x17181c |
|
}); |
|
windows = new THREE.Mesh(windowGeo,windowMat); |
|
|
|
windowGeo.setAttribute("position",new THREE.BufferAttribute(windowVerts,3)); |
|
bldg.add(windows); |
|
} |
|
pixelIndex += 4; |
|
} |
|
} |
|
}, |
|
handleImgUpload = e => { |
|
return new Promise((resolve,reject) => { |
|
if (imgUpload) { |
|
let target = !e ? imgUpload : e.target; |
|
if (target.files.length) { |
|
let reader = new FileReader(); |
|
reader.onload = e2 => { |
|
img = new Image(); |
|
img.src = e2.target.result; |
|
img.onload = () => { |
|
resolve(); |
|
}; |
|
img.onerror = () => { |
|
img = null; |
|
reject("The image was nullified due to corruption or a non-image upload."); |
|
}; |
|
|
|
if (imgName) |
|
imgName.placeholder = target.files[0].name; |
|
}; |
|
reader.readAsDataURL(target.files[0]); |
|
} |
|
|
|
} else { |
|
reject("The file input is missing."); |
|
} |
|
}); |
|
}, |
|
imgUploadValid = () => { |
|
if (imgUpload) { |
|
let files = imgUpload.files, |
|
fileIsThere = files.length > 0, |
|
isImage = files[0].type.match("image.*"), |
|
valid = fileIsThere && isImage; |
|
|
|
return valid; |
|
|
|
} else { |
|
return false; |
|
} |
|
}, |
|
init = () => { |
|
// setup |
|
scene = new THREE.Scene(); |
|
scene.fog = new THREE.Fog(skyColor,512,640); |
|
// renderer |
|
renderer = new THREE.WebGLRenderer({ |
|
logarithmicDepthBuffer: true |
|
}); |
|
renderer.setClearColor(skyColor); |
|
renderer.setSize(window.innerWidth,window.innerHeight); |
|
renderer.shadowMap.enabled = true; |
|
// camera |
|
camera = new THREE.PerspectiveCamera(60,window.innerWidth / window.innerHeight,0.1,1000); |
|
camera.position.set(160,160,160); |
|
camera.lookAt(scene.position); |
|
camControls = new THREE.OrbitControls(camera,renderer.domElement); |
|
camControls.enablePan = false; |
|
camControls.minDistance = 8; |
|
camControls.maxDistance = 512; |
|
camControls.minPolarAngle = -Math.PI / 2; |
|
camControls.maxPolarAngle = Math.PI / 2; |
|
// lighting |
|
let daylight = new THREE.AmbientLight(0xfbfbb6,1); |
|
daylight.name = "Daylight"; |
|
scene.add(daylight); |
|
|
|
let sun = new THREE.PointLight(0xffffff,2,worldSize,2); |
|
sun.name = "Sun"; |
|
sun.position.set( |
|
worldSize / 2 * Math.sin(sunAngle * Math.PI / 180), |
|
worldSize / 2 * Math.cos(sunAngle * Math.PI / 180), |
|
0 |
|
); |
|
sun.castShadow = true; |
|
scene.add(sun); |
|
// terrain |
|
let terrainGeo = new THREE.PlaneBufferGeometry(worldSize,worldSize), |
|
terrainMat = new THREE.MeshStandardMaterial({ |
|
color: terrainColor |
|
}), |
|
terrain = new THREE.Mesh(terrainGeo,terrainMat); |
|
terrain.name = "Terrain"; |
|
terrain.rotation.x = -0.5 * Math.PI; |
|
terrain.position.y = -0.01; |
|
terrain.receiveShadow = true; |
|
scene.add(terrain); |
|
// roads |
|
let roadGeo = new THREE.PlaneBufferGeometry(roadsSide,roadsSide), |
|
roadMat = new THREE.MeshPhongMaterial({ |
|
color: 0x2e3138 |
|
}), |
|
road = new THREE.Mesh(roadGeo,roadMat); |
|
roadMat.shininess = 35; |
|
road.name = "Road"; |
|
road.rotation.x = -0.5 * Math.PI; |
|
road.receiveShadow = true; |
|
scene.add(road); |
|
// sidewalks |
|
let sidewalkGeo = new THREE.BoxBufferGeometry( |
|
chunkSize - roadWidth, |
|
1, |
|
chunkSize - roadWidth |
|
), |
|
sidewalkMat = new THREE.MeshPhongMaterial({ |
|
color: 0x5c6270 |
|
}), |
|
sidewalk = new THREE.Mesh(sidewalkGeo,sidewalkMat); |
|
|
|
sidewalk.name = "Sidewalk"; |
|
sidewalk.receiveShadow = true; |
|
|
|
let zStart = -Math.floor(gridSizeHalf), |
|
zEnd = -zStart - (gridSizeEven ? 1 : 0), |
|
xStart = zStart, |
|
xEnd = zEnd; |
|
|
|
for (let z = zStart; z <= zEnd; ++z) { |
|
for (let x = xStart; x <= xEnd; ++x) { |
|
let sidewalkUnit = sidewalk.clone(); |
|
sidewalkUnit.position.set( |
|
chunkSize * x, |
|
0.5, |
|
chunkSize * z |
|
); |
|
if (gridSizeEven) { |
|
sidewalkUnit.position.x += chunkSizeHalf; |
|
sidewalkUnit.position.z += chunkSizeHalf; |
|
} |
|
scene.add(sidewalkUnit); |
|
} |
|
} |
|
// build the city |
|
city = new THREE.Object3D(); |
|
city.name = "City"; |
|
scene.add(city); |
|
generateCity(); |
|
// dust particles |
|
let dustGeo = new THREE.BufferGeometry(), |
|
dustVertArr = []; |
|
|
|
for (let p = 0; p < dustParticles; ++p) { |
|
dustVertArr.push( |
|
Math.round(roadsSide * Math.random() - roadsSide / 2), |
|
Math.round(Math.random() * worldHeight), |
|
Math.round(roadsSide * Math.random() - roadsSide / 2) |
|
); |
|
} |
|
let dustVerts = new Float32Array(dustVertArr), |
|
dustMat = new THREE.PointsMaterial({ |
|
map: textureLoader.load("https://i.ibb.co/mqQrvZ1/dust.png"), |
|
color: 0xffff00, |
|
size: 2, |
|
transparent: true |
|
}); |
|
|
|
dustGeo.setAttribute("position",new THREE.BufferAttribute(dustVerts,3)); |
|
dust = new THREE.Points(dustGeo,dustMat); |
|
dust.name = "Dust Particles"; |
|
scene.add(dust); |
|
// render |
|
let body = document.body; |
|
body.insertBefore(renderer.domElement,body.firstChild); |
|
renderScene(); |
|
// deal with preserved input |
|
if (imgUpload && imgUpload.value != "") |
|
renderPromise(); |
|
}, |
|
moveDust = () => { |
|
let posArr = dust.geometry.attributes.position.array, |
|
dirs = 8, |
|
newPosArr = posArr.map((a,i) => { |
|
let dim = i % 3, |
|
dir = i % dirs, |
|
angle = 360 * (dir / dirs) * Math.PI / 180; |
|
|
|
if (dim == 0) |
|
a += dustParticleSpeed * Math.sin(angle); |
|
else if (dim == 2) |
|
a += dustParticleSpeed * Math.cos(angle); |
|
|
|
if (dim == 0 || dim == 2) { |
|
a += dustParticleSpeed; |
|
if (a > roadsSideHalf) |
|
a -= roadsSide; |
|
else if (a < -roadsSideHalf) |
|
a += roadsSide; |
|
|
|
} else if (dim == 1) { |
|
a += dustParticleSpeed; |
|
if (a >= worldHeight) |
|
a = 0; |
|
} |
|
|
|
return a; |
|
}); |
|
|
|
let newDustVerts = new Float32Array(newPosArr); |
|
dust.geometry.setAttribute("position",new THREE.BufferAttribute(newDustVerts,3)); |
|
dust.geometry.verticesNeedUpdate = true; |
|
}, |
|
randomHue = () => { |
|
let roundHueTo = 30, |
|
r = Math.floor(Math.random() * 360 / roundHueTo) * roundHueTo; |
|
return r; |
|
}, |
|
renderPromise = e => { |
|
handleImgUpload(e).then(() => { |
|
if (imgUploadValid()) { |
|
updateCanvas(); |
|
updateImg(); |
|
} |
|
|
|
}).catch(msg => { |
|
console.log(msg); |
|
}); |
|
}, |
|
renderScene = () => { |
|
moveDust(); |
|
renderer.render(scene,camera); |
|
requestAnimationFrame(renderScene); |
|
}, |
|
resetCity = () => { |
|
if (imgName) |
|
imgName.placeholder = "No file selected"; |
|
|
|
clearCity(); |
|
generateCity(); |
|
}, |
|
toggleAside = e => { |
|
let aside = document.querySelector("aside"); |
|
if (aside) { |
|
let openClass = "aside-open"; |
|
if (e.keyCode == 27) |
|
aside.classList.remove(openClass); |
|
else if (!e.keyCode) |
|
aside.classList.toggle(openClass); |
|
} |
|
}, |
|
updateCanvas = () => { |
|
// restrict image size, keep it proportional |
|
let imgWidth = img.width, |
|
imgHeight = img.height; |
|
|
|
if (imgWidth >= imgHeight) { |
|
if (imgWidth >= bldgCellsPerSide) { |
|
imgWidth = bldgCellsPerSide; |
|
imgHeight = imgWidth * (img.height / img.width); |
|
} |
|
} else { |
|
if (imgHeight >= bldgCellsPerSide) { |
|
imgHeight = bldgCellsPerSide; |
|
imgWidth = imgHeight * (img.width / img.height); |
|
} |
|
} |
|
// update canvas |
|
c.clearRect(0,0,bldgCellsPerSide,bldgCellsPerSide); |
|
|
|
let imgX = bldgCellsPerSide / 2 - imgWidth / 2, |
|
imgY = bldgCellsPerSide / 2 - imgHeight / 2; |
|
|
|
c.drawImage(img,imgX,imgY,imgWidth,imgHeight); |
|
}, |
|
updateImg = () => { |
|
let imgData = c.getImageData(0,0,bldgCellsPerSide,bldgCellsPerSide), |
|
data = imgData.data; |
|
|
|
clearCity(); |
|
generateCity(data); |
|
}; |
|
|
|
init(); |
|
|
|
if (asideBtn) { |
|
asideBtn.addEventListener("click",toggleAside); |
|
window.addEventListener("keydown",toggleAside); |
|
} |
|
if (resetBtn) |
|
resetBtn.addEventListener("click",resetCity); |
|
if (imgUpload) |
|
imgUpload.addEventListener("change",renderPromise); |
|
|
|
window.addEventListener("resize",adjustWindow); |
|
} |