initial rendering test

This commit is contained in:
LabyStudio
2022-01-30 23:48:49 +01:00
commit ddc7480529
12 changed files with 545 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
*.iml
.idea
+16
View File
@@ -0,0 +1,16 @@
<html lang="en">
<head>
<title>Minecraft</title>
<link rel="stylesheet" href="style.css"/>
</head>
<body>
<div id="canvas-container"></div>
<span id="pre-status">Loading page...</span>
</body>
<script src="src/start.js"></script>
</html>
+6
View File
File diff suppressed because one or more lines are too long
+30
View File
@@ -0,0 +1,30 @@
window.GameWindow = class {
constructor(renderer, canvasWrapperId) {
this.renderer = renderer;
this.canvasWrapperId = canvasWrapperId;
// Add web renderer canvas to wrapper
document.getElementById(this.canvasWrapperId).appendChild(renderer.canvasElement);
// Init
this.initialize();
// On resize
let scope = this;
window.addEventListener('resize', _ => scope.initialize(), false);
}
initialize() {
// Get canvas size
let canvasElement = document.getElementById(this.canvasWrapperId);
this.canvasWidth = canvasElement.offsetWidth;
this.canvasHeight = canvasElement.offsetHeight;
// Adjust camera
this.renderer.camera.aspect = this.canvasWidth / this.canvasHeight;
this.renderer.camera.updateProjectionMatrix();
this.renderer.webRenderer.setSize(this.canvasWidth, this.canvasHeight);
}
}
+63
View File
@@ -0,0 +1,63 @@
window.Minecraft = class {
/**
* Create Minecraft instance and render it on a canvas
*/
constructor(canvasWrapperId) {
this.worldRenderer = new WorldRenderer(this);
this.window = new GameWindow(this.worldRenderer, canvasWrapperId);
this.timer = new Timer(20);
this.frames = 0;
this.lastTime = Date.now();
this.world = new World();
this.worldRenderer.scene.add(this.world.group);
this.init();
}
init() {
this.running = true;
this.requestNextFrame();
}
requestNextFrame() {
let scope = this;
requestAnimationFrame(function () {
if (scope.running) {
scope.requestNextFrame();
scope.onRender();
}
});
}
onRender() {
// Update the timer
this.timer.advanceTime();
// Call the tick to reach updates 20 per seconds
for (let i = 0; i < this.timer.ticks; i++) {
this.onTick();
}
// Render the game
this.worldRenderer.render(this.timer.partialTicks);
// Increase rendered frame
this.frames++;
// Loop if a second passed
while (Date.now() >= this.lastTime + 1000) {
console.log(this.frames + " fps");
this.lastTime += 1000;
this.frames = 0;
}
}
onTick() {
}
}
@@ -0,0 +1,65 @@
window.WorldRenderer = class {
constructor(minecraft) {
this.minecraft = minecraft;
this.supportWebGL = !!WebGLRenderingContext
&& (!!document.createElement('canvas').getContext('experimental-webgl')
|| !!document.createElement('canvas').getContext('webgl'));
// Create cameras
this.camera = new THREE.PerspectiveCamera(85, 1, 1, 10000);
this.camera.position.set(0, 3, 0);
this.camera.up = new THREE.Vector3(0, 0, 1);
// Create scene
this.scene = new THREE.Scene();
// Create web renderer
this.canvasElement = document.createElement('canvas')
this.webRenderer = this.supportWebGL ? new THREE.WebGLRenderer({
canvas: this.canvasElement,
antialias: true
}) : new THREE.CanvasRenderer({
canvas: this.canvasElement,
antialias: true
});
this.webRenderer.shadowMap.enabled = true;
this.webRenderer.shadowMap.type = THREE.PCFSoftShadowMap; // default THREE.PCFShadowMap
this.webRenderer.autoClear = false;
this.webRenderer.setClearColor(0x000000, 0);
this.webRenderer.clear();
const nightLight = new THREE.AmbientLight(0x888888, 1.0);
this.scene.add(nightLight);
}
render(partialTicks) {
let world = this.minecraft.world;
const xKeys = Object.keys(world.chunks)
for (let x = 0; x < xKeys.length; x++) {
let zArray = world.chunks[xKeys[x]];
const zKeys = Object.keys(zArray)
for (let z = 0; z < zKeys.length; z++) {
let chunk = zArray[zKeys[z]];
for (let y = 0; y < chunk.sections.length; y++) {
let section = chunk.sections[y];
if (section.dirty) {
section.rebuild();
}
}
}
}
// Render window
this.webRenderer.render(this.scene, this.camera);
}
}
+35
View File
@@ -0,0 +1,35 @@
window.Chunk = class {
constructor(x, z) {
this.group = new THREE.Object3D();
this.x = x;
this.z = z;
// Initialize sections
this.sections = [];
for (let y = 0; y < 16; y++) {
let section = new ChunkSection(x, y, z);
this.sections[y] = section;
this.group.add(section.group);
}
}
getSection(y) {
return this.sections[y];
}
rebuild() {
for (let y = 0; y < this.sections.length; y++) {
this.sections[y].rebuild();
}
}
queueForRebuild() {
for (let y = 0; y < this.sections.length; y++) {
this.sections[y].queueForRebuild();
}
}
}
@@ -0,0 +1,70 @@
window.ChunkSection = class {
static get SIZE() {
return 16;
}
constructor(x, y, z) {
this.x = x;
this.y = y;
this.z = z;
this.group = new THREE.Object3D();
this.dirty = true;
this.blocks = [];
for (let x = 0; x < ChunkSection.SIZE; x++) {
for (let y = 0; y < ChunkSection.SIZE; y++) {
for (let z = 0; z < ChunkSection.SIZE; z++) {
this.setBlockAt(x, y, z, 0);
}
}
}
}
rebuild() {
this.dirty = false;
this.group.clear();
for (let x = 0; x < ChunkSection.SIZE; x++) {
for (let y = 0; y < ChunkSection.SIZE; y++) {
for (let z = 0; z < ChunkSection.SIZE; z++) {
let typeId = this.getBlockAt(x, y, z);
if (typeId !== 0) {
let absoluteX = this.x * ChunkSection.SIZE + x;
let absoluteY = this.y * ChunkSection.SIZE + y;
let absoluteZ = this.z * ChunkSection.SIZE + z;
// Debug stuff
let color = 0x888888 | (Math.random() * 223);
let geometry = new THREE.BoxGeometry(1, 1, 1);
let material = new THREE.MeshBasicMaterial({
color: color
});
let cube = new THREE.Mesh(geometry, material);
cube.position.set(absoluteX - 0.5, absoluteY - 0.5, absoluteZ - 0.5);
this.group.add(cube);
}
}
}
}
}
getBlockAt(x, y, z) {
let index = y << 8 | z << 4 | x;
return this.blocks[index];
}
setBlockAt(x, y, z, typeId) {
let index = y << 8 | z << 4 | x;
this.blocks[index] = typeId;
}
queueForRebuild() {
this.dirty = true;
}
}
+74
View File
@@ -0,0 +1,74 @@
window.World = class {
static get TOTAL_HEIGHT() {
return ChunkSection.SIZE * 16 - 1;
}
constructor() {
this.group = new THREE.Object3D();
this.chunks = [];
for (let x = -16; x < 16; x++) {
for (let z = -16; z < 16; z++) {
this.setBlockAt(x, 0, z, 1);
}
}
}
setBlockAt(x, y, z, type) {
let chunkSection = this.getChunkAtBlock(x, y, z);
if (chunkSection != null) {
chunkSection.setBlockAt(x & 15, y & 15, z & 15, type);
}
this.blockChanged(x, y, z);
}
getChunkAt(x, z) {
let zArray = this.chunks[x];
if (typeof zArray === 'undefined') {
zArray = this.chunks[x] = [];
}
let chunk = zArray[z];
if (typeof chunk === 'undefined') {
chunk = new Chunk(x, z);
this.chunks[x][z] = chunk;
this.group.add(chunk.group);
}
return chunk;
}
blockChanged(x, y, z) {
this.setDirty(x - 1, y - 1, z - 1, x + 1, y + 1, z + 1);
}
setDirty(minX, minY, minZ, maxX, maxY, maxZ) {
// To chunk coordinates
minX = minX >> 4;
maxX = maxX >> 4;
minY = minY >> 4;
maxY = maxY >> 4;
minZ = minZ >> 4;
maxZ = maxZ >> 4;
// Minimum and maximum y
minY = Math.max(0, minY);
maxY = Math.min(15, maxY);
for (let x = minX; x <= maxX; x++) {
for (let y = minY; y <= maxY; y++) {
for (let z = minZ; z <= maxZ; z++) {
this.getChunkAt(x, y, z).queueForRebuild();
}
}
}
}
getChunkAtBlock(x, y, z) {
let chunk = this.getChunkAt(x >> 4, z >> 4);
return y < 0 || y > World.TOTAL_HEIGHT ? null : chunk.getSection(y >> 4);
}
}
+84
View File
@@ -0,0 +1,84 @@
window.Timer = class {
/**
* Timer to control the tick speed independently of the framerate
*
* @param ticksPerSecond Amount of ticks per second
*/
constructor(ticksPerSecond) {
this.MS_PER_SECOND = 1000;
this.MAX_MS_PER_UPDATE = 1000;
this.MAX_TICKS_PER_UPDATE = 100;
/**
* Amount of ticks per second
*/
this.ticksPerSecond = ticksPerSecond;
/**
* Last time updated in nano seconds
*/
this.lastTime = this._nanoTime();
/**
* Scale the tick speed
*/
this.timeScale = 1.0;
/**
* Framerate of the advanceTime update
*/
this.fps = 0.0;
/**
* Passed time since last game update
*/
this.passedTime = 0.0;
/**
* The amount of ticks for the current game update.
* It's the passed time as an integer
*/
this.ticks = 0;
/**
* The overflow of the current tick, caused by casting the passed time to an integer
*/
this.partialTicks = 0;
}
/**
* This function calculates the amount of ticks required to reach the ticksPerSecond.
* Call this function in the main render loop of the game
*/
advanceTime() {
let now = this._nanoTime();
let passedMs = now - this.lastTime;
// Store nano time of this update
this.lastTime = now;
// Maximum and minimum
passedMs = Math.max(0, passedMs);
passedMs = Math.min(this.MAX_MS_PER_UPDATE, passedMs);
// Calculate fps
this.fps = this.MS_PER_SECOND / passedMs;
// Calculate passed time and ticks
this.passedTime += passedMs * this.timeScale * this.ticksPerSecond / this.MS_PER_SECOND;
this.ticks = parseInt(this.passedTime);
// Maximum ticks per update
this.ticks = Math.min(this.MAX_TICKS_PER_UPDATE, this.ticks);
// Calculate the overflow of the current tick
this.passedTime -= this.ticks;
this.partialTicks = this.passedTime;
}
_nanoTime() {
return Date.now();
}
}
+63
View File
@@ -0,0 +1,63 @@
// Browser test function
function isES6() {
try {
Function("() => {};");
return true;
} catch (exception) {
return false;
}
}
// Test for browser support
if (!isES6()) {
alert("Your browser isn't supported! Please use another one.");
}
// Script loader
function loadScripts(scripts) {
let total = scripts.length;
let index = 0;
return scripts.reduce((currentPromise, scriptUrl) => {
return currentPromise.then(() => {
return new Promise((resolve, reject) => {
// Update status message
updatePreStatus("Loading scripts... " + index + "/" + total);
// Load script
let script = document.createElement('script');
script.async = true;
script.src = scriptUrl;
script.onload = () => resolve();
document.getElementsByTagName('head')[0].appendChild(script);
index++;
});
});
}, Promise.resolve());
}
function updatePreStatus(message) {
document.getElementById("pre-status").innerText = message;
}
// Load scripts
loadScripts([
// Dependencies
"libraries/three.min.js",
// Minecraft Source
"src/net/minecraft/util/Timer.js",
"src/net/minecraft/client/GameWindow.js",
"src/net/minecraft/client/world/ChunkSection.js",
"src/net/minecraft/client/world/Chunk.js",
"src/net/minecraft/client/world/World.js",
"src/net/minecraft/client/Minecraft.js",
"src/net/minecraft/client/render/WorldRenderer.js"
]).then(() => {
// Remove pre status
document.getElementById("pre-status").remove();
// Start Minecraft
window.app = new Minecraft("canvas-container");
});
+37
View File
@@ -0,0 +1,37 @@
body {
margin: 0;
background-color: black;
}
#pre-status {
/* Position */
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
/* No select */
-webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none; /* Safari */
-khtml-user-select: none; /* Konqueror HTML */
-moz-user-select: none; /* Old versions of Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */
user-select: none; /* Non-prefixed version, currently
supported by Chrome, Edge, Opera and Firefox */
/* No overflow */
white-space: nowrap;
overflow: hidden;
/* Font */
font-size: 25px;
font-family: "FoundryGridnik", serif;
color: white;
}
#canvas-container {
display: block;
width: 100%;
height: 100%;
white-space: nowrap;
overflow: hidden;
}