Make 3D canvas multiplayer

This commit is contained in:
HF 2020-01-28 00:24:25 +01:00
parent 29755b7ff1
commit 1b185f4e5b
31 changed files with 932 additions and 259 deletions

View File

@ -213,13 +213,14 @@ export function requestPlacePixel(
color: ColorIndex,
token: ?string = null,
): ThunkAction {
const [x, y] = coordinates;
const [x, y, z] = coordinates;
return async (dispatch) => {
const body = JSON.stringify({
cn: canvasId,
x,
y,
z,
clr: color,
token,
});
@ -283,7 +284,9 @@ export function tryPlacePixel(
): ThunkAction {
return (dispatch, getState) => {
const state = getState();
const { canvasId } = state.canvas;
const {
canvasId,
} = state.canvas;
const selectedColor = (color === undefined || color === null)
? state.gui.selectedColor
: color;

View File

@ -134,7 +134,7 @@
"bcd": 2000,
"pcd" : 2000,
"cds": 60000,
"req": 0,
"req": -1,
"sd": "2020-01-08",
"desc": "Test 3D canvas. Changes are not saved."
}

View File

@ -8,6 +8,8 @@ import { connect } from 'react-redux';
import { MdFileDownload } from 'react-icons/md';
import fileDownload from 'react-file-download';
import { getRenderer } from '../ui/renderer';
import type { State } from '../reducers';
@ -15,21 +17,25 @@ import type { State } from '../reducers';
* https://jsfiddle.net/AbdiasSoftware/7PRNN/
*/
function download(view) {
// TODO id shouldnt be hardcoded
const $viewport = document.getElementById('gameWindow');
if (!$viewport) return;
// TODO change name
const renderer = getRenderer();
const viewport = renderer.getViewport();
if (!viewport) return;
const [x, y] = view.map(Math.round);
const filename = `pixelplanet-${x}-${y}.png`;
$viewport.toBlob((blob) => fileDownload(blob, filename));
viewport.toBlob((blob) => fileDownload(blob, filename));
}
const DownloadButton = ({ view }) => (
<div id="downloadbutton" className="actionbuttons" onClick={() => download(view)}>
<div
id="downloadbutton"
className="actionbuttons"
role="button"
tabIndex={0}
onClick={() => download(view)}
>
<MdFileDownload />
</div>
);

View File

@ -9,8 +9,6 @@ import { MdPerson } from 'react-icons/md';
import { showUserAreaModal } from '../actions';
import type { State } from '../reducers';
const LogInButton = ({ open }) => (
<div id="loginbutton" className="actionbuttons" onClick={open}>

View File

@ -24,6 +24,7 @@ const MdToggleButtonHover = ({ value, onToggle }) => (
height: 32,
}}
animateThumbStyleHover={(n) => ({
// eslint-disable-next-line max-len
boxShadow: `0 0 ${2 + (4 * n)}px rgba(0,0,0,.16),0 ${2 + (3 * n)}px ${4 + (8 * n)}px rgba(0,0,0,.32)`,
})}
/>

View File

@ -24,6 +24,10 @@ import {
Vector2,
Vector3,
} from 'three';
import {
onViewFinishChange,
setViewCoordinates,
} from '../actions';
// This set of controls performs orbiting, dollying (zooming),
// and panning and smooth moving by keys.
@ -34,7 +38,7 @@ import {
// or arrow keys / touch: two-finger move
class VoxelPainterControls extends EventDispatcher {
constructor(object, domElement) {
constructor(object, domElement, store) {
super();
// eslint-disable-next-line max-len
if (domElement === undefined) console.warn('THREE.VoxelPainterControls: The second parameter "domElement" is now mandatory.');
@ -43,6 +47,7 @@ class VoxelPainterControls extends EventDispatcher {
this.object = object;
this.domElement = domElement;
this.store = store;
// Set to false to disable this control
this.enabled = true;
@ -298,8 +303,8 @@ class VoxelPainterControls extends EventDispatcher {
.subVectors(rotateEnd, rotateStart)
.multiplyScalar(scope.rotateSpeed);
const element = scope.domElement;
rotateLeft(2 * Math.PI * rotateDelta.x / element.clientHeight); // yes, height
rotateUp(2 * Math.PI * rotateDelta.y / element.clientHeight);
rotateLeft(Math.PI * rotateDelta.x / element.clientHeight); // yes, height
rotateUp(Math.PI * rotateDelta.y / element.clientHeight);
rotateStart.copy(rotateEnd);
scope.update();
}
@ -806,6 +811,9 @@ class VoxelPainterControls extends EventDispatcher {
const lastPosition = new Vector3();
const lastQuaternion = new Quaternion();
// const rotationFinishThreshold = Math.PI / 180 / 4;
let updateTime = Date.now();
const direction = new Vector3();
const velocity = new Vector3();
let prevTime = Date.now();
@ -822,9 +830,9 @@ class VoxelPainterControls extends EventDispatcher {
}
const delta = (time - prevTime) / 1000.0;
velocity.x -= velocity.x * 50.0 * delta;
velocity.y -= velocity.y * 50.0 * delta;
velocity.z -= velocity.z * 50.0 * delta;
velocity.x -= velocity.x * 40.0 * delta;
velocity.y -= velocity.y * 40.0 * delta;
velocity.z -= velocity.z * 40.0 * delta;
const length = velocity.length();
if (length < 1 || length > 10) {
velocity.set(0, 0, 0);
@ -836,13 +844,13 @@ class VoxelPainterControls extends EventDispatcher {
direction.normalize();
if (moveLeft || moveRight) {
velocity.x -= direction.x * 5000.0 * delta;
velocity.x -= direction.x * 500.0 * delta;
}
if (moveUp || moveDown) {
velocity.y -= direction.y * 5000.0 * delta;
velocity.y -= direction.y * 500.0 * delta;
}
if (moveForward || moveBackward) {
velocity.z -= direction.z * 2500.0 * delta;
velocity.z -= direction.z * 250.0 * delta;
}
// controls.moveRight( -velocity.x * delta);
@ -908,7 +916,7 @@ class VoxelPainterControls extends EventDispatcher {
// move target to panned location
if (panOffset.length() > 10000) {
if (panOffset.length() > 1000) {
panOffset.set(0, 0, 0);
}
if (scope.enableDamping === true) {
@ -928,16 +936,37 @@ class VoxelPainterControls extends EventDispatcher {
position.copy(scope.target).add(offset);
scope.object.lookAt(scope.target);
if (scope.enableDamping === true) {
sphericalDelta.theta *= (1 - scope.dampingFactor);
sphericalDelta.phi *= (1 - scope.dampingFactor);
panOffset.multiplyScalar(1 - scope.dampingFactor);
if (panOffset.length() < 0.2 && panOffset.length() !== 0.0) {
panOffset.set(0, 0, 0);
scope.store.dispatch(setViewCoordinates(scope.target.toArray()));
scope.store.dispatch(onViewFinishChange());
} else if (panOffset.length() !== 0.0) {
const curTime = Date.now();
if (curTime > updateTime + 500) {
updateTime = curTime;
scope.store.dispatch(setViewCoordinates(scope.target.toArray()));
scope.store.dispatch(onViewFinishChange());
}
}
/*
if (Math.abs(sphericalDelta.theta) < rotationFinishThreshold
&& sphericalDelta.theta != 0.0
&& Math.abs(sphericalDelta.phi) < rotationFinishThreshold
&& sphericalDelta.phi != 0.0) {
sphericalDelta.set(0, 0, 0);
console.log(`rotation finished`);
}
*/
} else {
sphericalDelta.set(0, 0, 0);
panOffset.set(0, 0, 0);
}

View File

@ -15,7 +15,7 @@ import Palette from './Palette';
/*
* Load iamge from ABGR buffer onto canvas
* (be aware that tis function does no validation of arguments)
* @param canvadIs numerical ID of canvas
* @param canvasId numerical ID of canvas
* @param x X coordinate on canvas
* @param y Y coordinate on canvas
* @param data buffer of image in ABGR format
@ -47,7 +47,7 @@ export async function imageABGR2Canvas(
let chunk;
for (let cx = ucx; cx <= lcx; cx += 1) {
for (let cy = ucy; cy <= lcy; cy += 1) {
chunk = await RedisCanvas.getChunk(cx, cy, canvasId);
chunk = await RedisCanvas.getChunk(canvasId, cx, cy);
chunk = (chunk)
? new Uint8Array(chunk)
: new Uint8Array(TILE_SIZE * TILE_SIZE);
@ -90,7 +90,7 @@ export async function imageABGR2Canvas(
/*
* Load iamgemask from ABGR buffer and execute function for each black pixel
* (be aware that tis function does no validation of arguments)
* @param canvadIs numerical ID of canvas
* @param canvasId numerical ID of canvas
* @param x X coordinate on canvas
* @param y Y coordinate on canvas
* @param data buffer of image in ABGR format
@ -124,7 +124,7 @@ export async function imagemask2Canvas(
let chunk;
for (let cx = ucx; cx <= lcx; cx += 1) {
for (let cy = ucy; cy <= lcy; cy += 1) {
chunk = await RedisCanvas.getChunk(cx, cy, canvasId);
chunk = await RedisCanvas.getChunk(canvasId, cx, cy);
chunk = (chunk)
? new Uint8Array(chunk)
: new Uint8Array(TILE_SIZE * TILE_SIZE);

View File

@ -136,7 +136,7 @@ export async function createZoomTileFromChunk(
let chunk = null;
for (let dy = 0; dy < TILE_ZOOM_LEVEL; dy += 1) {
for (let dx = 0; dx < TILE_ZOOM_LEVEL; dx += 1) {
chunk = await redisCanvas.getChunk(xabs + dx, yabs + dy, canvasId);
chunk = await redisCanvas.getChunk(canvasId, xabs + dx, yabs + dy);
if (!chunk) {
na.push([dx, dy]);
continue;
@ -303,7 +303,7 @@ export async function createTexture(
} else {
for (let dy = 0; dy < amount; dy += 1) {
for (let dx = 0; dx < amount; dx += 1) {
chunk = await redisCanvas.getChunk(dx, dy, canvasId);
chunk = await redisCanvas.getChunk(canvasId, dx, dy);
if (!chunk) {
na.push([dx, dy]);
continue;

View File

@ -4,7 +4,9 @@
import path from 'path';
if (process.env.BROWSER) {
throw new Error('Do not import `config.js` from inside the client-side code.');
throw new Error(
'Do not import `config.js` from inside the client-side code.'
);
}
export const PORT = process.env.PORT || 80;
@ -30,7 +32,8 @@ export const MYSQL_USER = process.env.MYSQL_USER || 'pixelplanet';
export const MYSQL_PW = process.env.MYSQL_PW || 'password';
// Social
export const DISCORD_INVITE = process.env.DISCORD_INVITE || 'https://discordapp.com/';
export const DISCORD_INVITE = process.env.DISCORD_INVITE
|| 'https://discordapp.com/';
// Logging
export const LOG_MYSQL = parseInt(process.env.LOG_MYSQL, 10) || false;

View File

@ -64,8 +64,8 @@ export const TILE_LOADING_IMAGE = './loading.png';
// constants for 3D voxel canvas
export const THREE_CANVAS_HEIGHT = 128;
export const THREE_TILE_SIZE = 64;
// one bigchunk has 16x16 smallchunks, one smallchunk has 64x64 pixel, so one bigchunk is 1024x1024 pixels
export const THREE_TILE_SIZE = 32;
// 2D tile size
export const TILE_SIZE = 256;
// how much to scale for a new tiled zoomlevel
export const TILE_ZOOM_LEVEL = 4;

View File

@ -61,31 +61,58 @@ async function draw(
const canvasMaxXY = canvas.size / 2;
const canvasMinXY = -canvasMaxXY;
if (x < canvasMinXY || y < canvasMinXY
|| x >= canvasMaxXY || y >= canvasMaxXY) {
if (x < canvasMinXY || x >= canvasMaxXY) {
return {
error: 'Coordinates not within canvas',
error: 'x Coordinate not within canvas',
success: false,
};
}
if (z !== null) {
if (z >= THREE_CANVAS_HEIGHT) {
if (canvas.v) {
if (z < canvasMinXY || z >= canvasMaxXY) {
return {
error: 'z Coordinate not within canvas',
success: false,
};
}
if (y >= THREE_CANVAS_HEIGHT) {
return {
error: 'You reached build limit. Can\'t place higher than 128 blocks.',
success: false,
};
}
if (!canvas.v) {
if (y < 0) {
return {
error: 'This is not a 3D canvas',
error: 'Can\'t place on y < 0',
success: false,
};
}
} else if (canvas.v) {
return {
error: 'This is a 3D canvas. z is required.',
success: false,
};
if (z === null) {
return {
error: 'This is a 3D canvas. z is required.',
success: false,
};
}
} else {
if (y < canvasMinXY || y >= canvasMaxXY) {
return {
error: 'y Coordinate not within canvas',
success: false,
};
}
if (color < 2) {
return {
error: 'Invalid color selected',
success: false,
};
}
if (z !== null) {
if (!canvas.v) {
return {
error: 'This is not a 3D canvas',
success: false,
};
}
}
}
if (canvas.req !== -1) {
@ -139,7 +166,7 @@ async function draw(
};
}
setPixel(canvasId, color, x, y, null);
setPixel(canvasId, color, x, y, z);
user.setWait(waitLeft, canvasId);
user.incrementPixelcount();

View File

@ -39,14 +39,15 @@ export function clamp(n: number, min: number, max: number): number {
}
export function getChunkOfPixel(
canvasSize: number = null,
canvasSize: number,
x: number,
y: number,
z: number = null,
): Cell {
const tileSize = (z === null) ? TILE_SIZE : THREE_TILE_SIZE;
const width = (z == null) ? y : z;
const cx = Math.floor((x + (canvasSize / 2)) / tileSize);
const cy = Math.floor((y + (canvasSize / 2)) / tileSize);
const cy = Math.floor((width + (canvasSize / 2)) / tileSize);
return [cx, cy];
}
@ -72,6 +73,8 @@ export function getCanvasBoundaries(canvasSize: number): number {
return [canvasMinXY, canvasMaxXY];
}
// z is assumed to be height here
// in ui and rendeer, y is height
export function getOffsetOfPixel(
canvasSize: number = null,
x: number,
@ -79,10 +82,11 @@ export function getOffsetOfPixel(
z: number = null,
): number {
const tileSize = (z === null) ? TILE_SIZE : THREE_TILE_SIZE;
let offset = (z === null) ? 0 : (z * tileSize * tileSize);
const width = (z == null) ? y : z;
let offset = (z === null) ? 0 : (y * tileSize * tileSize);
const modOffset = mod((canvasSize / 2), tileSize);
const cx = mod(x + modOffset, tileSize);
const cy = mod(y + modOffset, tileSize);
const cy = mod(width + modOffset, tileSize);
offset += (cy * tileSize) + cx;
return offset;
}
@ -198,11 +202,11 @@ export function numberToStringFull(num: number): string {
} if (num < 1000) {
return num;
} if (num < 1000000) {
return `${Math.floor(num / 1000)}.${(`00${num % 1000}`).slice(-3)}`;
return `${Math.floor(num / 1000)}.${(`00${String(num % 1000)}`).slice(-3)}`;
}
// eslint-disable-next-line max-len
return `${Math.floor(num / 1000000)}.${(`00${Math.floor(num / 1000)}`).slice(-3)}.${(`00${num % 1000}`).slice(-3)}`;
return `${Math.floor(num / 1000000)}.${(`00${String(Math.floor(num / 1000))}`).slice(-3)}.${(`00${String(num % 1000)}`).slice(-3)}`;
}
export function colorFromText(str: string) {

View File

@ -1,7 +1,7 @@
/* @flow */
import { getChunkOfPixel, getOffsetOfPixel } from '../../core/utils';
import { TILE_SIZE } from '../../core/constants';
import { TILE_SIZE, THREE_CANVAS_HEIGHT } from '../../core/constants';
import canvases from '../../canvases.json';
import logger from '../../core/logger';
@ -12,6 +12,10 @@ const UINT_SIZE = 'u8';
const EMPTY_CACA = new Uint8Array(TILE_SIZE * TILE_SIZE);
const EMPTY_CHUNK_BUFFER = Buffer.from(EMPTY_CACA.buffer);
const THREE_EMPTY_CACA = new Uint8Array(
TILE_SIZE * TILE_SIZE * THREE_CANVAS_HEIGHT,
);
const THREE_EMPTY_CHUNK_BUFFER = Buffer.from(THREE_EMPTY_CACA.buffer);
// cache existence of chunks
const chunks: Set<string> = new Set();
@ -24,9 +28,14 @@ class RedisCanvas {
RedisCanvas.registerChunkChange = cb;
}
static getChunk(i: number, j: number, canvasId: number): Promise<Buffer> {
static getChunk(
canvasId: number,
i: number,
j: number,
): Promise<Buffer> {
// this key is also hardcoded into core/tilesBackup.js
return redis.getAsync(`ch:${canvasId}:${i}:${j}`);
const key = `ch:${canvasId}:${i}:${j}`;
return redis.getAsync(key);
}
static async setChunk(i: number, j: number, chunk: Uint8Array,
@ -64,7 +73,12 @@ class RedisCanvas {
const key = `ch:${canvasId}:${i}:${j}`;
if (!chunks.has(key)) {
await redis.setAsync(key, EMPTY_CHUNK_BUFFER, 'NX');
const is3D = canvases[canvasId].v;
if (is3D) {
await redis.setAsync(key, THREE_EMPTY_CHUNK_BUFFER, 'NX');
} else {
await redis.setAsync(key, EMPTY_CHUNK_BUFFER, 'NX');
}
chunks.add(key);
}

View File

@ -28,7 +28,7 @@ export default function modal(
// fixes a bug with iPad
case 'SHOW_MODAL': {
const { modalType, modalProps } = action;
const chatOpen = (modalType == 'CHAT') ? false : state.chatOpen;
const chatOpen = (modalType === 'CHAT') ? false : state.chatOpen;
return {
...state,
modalType,

View File

@ -104,7 +104,6 @@ export default function user(
case 'RECEIVE_CHAT_MESSAGE': {
const { name, text } = action;
let { chatMessages } = state;
console.log('received chat message');
if (chatMessages.length > 50) {
chatMessages = chatMessages.slice(-50);
}
@ -124,7 +123,9 @@ export default function user(
case 'RECEIVE_COOLDOWN': {
const { waitSeconds } = action;
const wait = waitSeconds ? new Date(Date.now() + waitSeconds * 1000) : null;
const wait = waitSeconds
? new Date(Date.now() + waitSeconds * 1000)
: null;
return {
...state,
wait,

View File

@ -28,7 +28,10 @@ export default async (req: Request, res: Response) => {
const x = parseInt(req.body.x, 10);
const y = parseInt(req.body.y, 10);
if (x < CANVAS_MIN_XY || y < CANVAS_MIN_XY || x >= CANVAS_MAX_XY || y >= CANVAS_MAX_XY) {
if (x < CANVAS_MIN_XY
|| y < CANVAS_MIN_XY
|| x >= CANVAS_MAX_XY
|| y >= CANVAS_MAX_XY) {
res.status(400);
res.json({
success: false,

View File

@ -27,7 +27,7 @@ async function validate(req: Request, res: Response, next) {
const x = parseInt(req.body.x, 10);
const y = parseInt(req.body.y, 10);
let z = null;
if (req.body.z) {
if (typeof req.body.z !== 'undefined') {
z = parseInt(req.body.z, 10);
}
const clr = parseInt(req.body.clr, 10);
@ -40,13 +40,13 @@ async function validate(req: Request, res: Response, next) {
error = 'y is not a valid number';
} else if (Number.isNaN(clr)) {
error = 'No color selected';
} else if (clr < 2 || clr > 31) {
} else if (clr < 0 || clr > 31) {
error = 'Invalid color selected';
} else if (z !== null && Number.isNaN(z)) {
error = 'z is not a valid number';
}
if (error !== null) {
res.status(400).json({ errors: [error] });
res.status(400).json({ errors: [{ msg: error }] });
return;
}
@ -70,7 +70,7 @@ async function validate(req: Request, res: Response, next) {
user = req.user;
}
if (!user || !user.ip) {
res.status(400).json({ errors: ["Couldn't authenticate"] });
res.status(400).json({ errors: [{ msg: "Couldn't authenticate" }] });
return;
}

View File

@ -15,10 +15,16 @@ import logger from '../core/logger';
* Send binary chunk to the client
*/
export default async (req: Request, res: Response, next) => {
const { c: paramC, x: paramX, y: paramY } = req.params;
const {
c: paramC,
x: paramX,
y: paramY,
z: paramZ,
} = req.params;
const c = parseInt(paramC, 10);
const x = parseInt(paramX, 10);
const y = parseInt(paramY, 10);
const z = (paramZ) ? parseInt(paramZ, 10) : null;
try {
// botters where using cachebreakers to update via chunk API
// lets not allow that for now
@ -27,7 +33,10 @@ export default async (req: Request, res: Response, next) => {
return;
}
const chunk = await RedisCanvas.getChunk(x, y, c);
// z is in preeration for 3d chunks that are also
// divided in height, which is not used yet
// - this is not used and probably won't ever be used
const chunk = await RedisCanvas.getChunk(c, x, y, z);
res.set({
'Cache-Control': `public, s-maxage=${60}, max-age=${50}`, // seconds
@ -41,7 +50,7 @@ export default async (req: Request, res: Response, next) => {
// for temporary logging to see if we have invalid chunks in redis
if (chunk.length !== TILE_SIZE * TILE_SIZE) {
logger.error(`Chunk ${x},${y} has invalid length ${chunk.length}!`);
logger.error(`Chunk ${x},${y},${z} has invalid length ${chunk.length}!`);
}
const curEtag = etag(chunk, { weak: true });

View File

@ -26,7 +26,7 @@ async function basetile(req: Request, res: Response, next) {
const x = parseInt(paramX, 10);
const y = parseInt(paramY, 10);
try {
let tile = await RedisCanvas.getChunk(x, y, c);
let tile = await RedisCanvas.getChunk(c, x, y);
res.set({
'Cache-Control': `public, s-maxage=${5 * 60}, max-age=${3 * 60}`, // seconds

View File

@ -80,6 +80,7 @@ class ProtocolClient extends EventEmitter {
this.emit('open', {});
this.requestChatHistory();
console.log(`Register ${chunks.length} chunks`);
// TODO RegisterMultipleChunks before RegisterCanvas doesn't make sense
this.ws.send(RegisterMultipleChunks.dehydrate(chunks));
if (this.canvasId !== null) {
this.ws.send(RegisterCanvas.dehydrate(this.canvasId));

View File

@ -15,12 +15,12 @@ export default {
OP_CODE,
hydrate(data: DataView): PixelUpdatePacket {
// CLIENT
const i = data.getInt16(1);
const j = data.getInt16(3);
// const offset = (data.getUint8(5) << 16) | data.getUint16(6);
// const color = data.getUint8(8);
const offset = data.getUint16(5);
const color = data.getUint8(7);
const i = data.getUint8(1);
const j = data.getUint8(2);
const offset = (data.getUint8(3) << 16) | data.getUint16(4);
const color = data.getUint8(6);
// const offset = data.getUint16(5);
// const color = data.getUint8(7);
return {
i, j, offset, color,
};
@ -28,16 +28,16 @@ export default {
dehydrate(i, j, offset, color): Buffer {
// SERVER
if (!process.env.BROWSER) {
const buffer = Buffer.allocUnsafe(1 + 2 + 2 + 1 + 2 + 1);
const buffer = Buffer.allocUnsafe(1 + 1 + 1 + 1 + 2 + 1);
buffer.writeUInt8(OP_CODE, 0);
buffer.writeInt16BE(i, 1);
buffer.writeInt16BE(j, 3);
// buffer.writeUInt8(offset >>> 16, 5);
// buffer.writeUInt16BE(offset & 0x00FFFF, 6);
// buffer.writeUInt8(color, 8);
buffer.writeUInt16BE(offset, 5);
buffer.writeUInt8(color, 7);
buffer.writeUInt8(i, 1);
buffer.writeUInt8(j, 2);
buffer.writeUInt8(offset >>> 16, 3);
buffer.writeUInt16BE(offset & 0x00FFFF, 4);
buffer.writeUInt8(color, 6);
// buffer.writeUInt16BE(offset, 5);
// buffer.writeUInt8(color, 7);
return buffer;
}

View File

@ -6,10 +6,13 @@
import ProtocolClient from '../socket/ProtocolClient';
export default store => next => action => {
export default (store) => (next) => (action) => {
switch (action.type) {
case 'RECEIVE_BIG_CHUNK':
case 'RECEIVE_BIG_CHUNK_FAILURE': {
if (!action.center) {
break;
}
const [, cx, cy] = action.center;
ProtocolClient.registerChunk([cx, cy]);
break;

View File

@ -37,12 +37,14 @@ export default (store) => (next) => (action) => {
view,
viewscale,
canvasIdent,
is3D,
} = store.getState().canvas;
let [x, y] = view;
x = Math.round(x);
y = Math.round(y);
const scale = Math.round(Math.log2(viewscale) * 10);
const newhash = `#${canvasIdent},${x},${y},${scale}`;
const coords = view.map((u) => Math.round(u)).join(',');
let newhash = `#${canvasIdent},${coords}`;
if (!is3D) {
const scale = Math.round(Math.log2(viewscale) * 10);
newhash += `,${scale}`;
}
window.history.replaceState(undefined, undefined, newhash);
break;
}

View File

@ -135,7 +135,6 @@ class ChunkLoader {
chunkRGB,
) {
const { canvasId } = this;
const { key } = chunkRGB;
let url = `${window.backupurl}/${historicalDate}/`;
if (historicalTime) {
// incremential tiles
@ -144,13 +143,13 @@ class ChunkLoader {
// full tiles
url += `${canvasId}/tiles/${cx}/${cy}.png`;
}
this.store.dispatch(requestBigChunk(key));
this.store.dispatch(requestBigChunk(null));
try {
const img = await loadImage(url);
chunkRGB.fromImage(img);
this.store.dispatch(receiveBigChunk(key));
this.store.dispatch(receiveBigChunk(null));
} catch (error) {
this.store.dispatch(receiveBigChunkFailure(key, error));
this.store.dispatch(receiveBigChunkFailure(null, error));
if (historicalTime) {
chunkRGB.empty(true);
} else {

View File

@ -4,14 +4,10 @@
* @flow
*/
import * as THREE from 'three';
import {
THREE_CANVAS_HEIGHT,
THREE_TILE_SIZE,
} from '../core/constants';
import {
requestBigChunk,
receiveBigChunk,
receiveBigChunkFailure,
} from '../actions';
import Chunk from './ChunkRGB3D';
@ -24,7 +20,6 @@ class ChunkLoader {
chunks: Map<string, Chunk>;
constructor(store) {
console.log("Created Chunk loader");
this.store = store;
const state = store.getState();
const {
@ -36,6 +31,13 @@ class ChunkLoader {
this.chunks = new Map();
}
destructor() {
this.chunks.forEach((chunk) => {
chunk.destructor();
});
this.chunks = new Map();
}
getVoxelUpdate(
xc: number,
zc: number,
@ -45,20 +47,13 @@ class ChunkLoader {
const key = `${xc}:${zc}`;
const chunk = this.chunks.get(key);
if (chunk) {
/*
const offsetXZ = offset % (THREE_TILE_SIZE ** 2);
const iy = (offset - offsetXZ) / (THREE_TILE_SIZE ** 2);
const ix = offsetXZ % THREE_TILE_SIZE;
const iz = (offsetXZ - ix) / THREE_TILE_SIZE;
*/
chunk.setVoxelByOffset(offset, color);
//this.store.dispatch(receiveBigChunk(key));
}
}
getChunk(xc, zc, fetch: boolean) {
const chunkKey = `${xc}:${zc}`;
console.log(`Get chunk ${chunkKey}`);
// console.log(`Get chunk ${chunkKey}`);
let chunk = this.chunks.get(chunkKey);
if (chunk) {
if (chunk.ready) {
@ -75,12 +70,39 @@ class ChunkLoader {
return null;
}
async fetchChunk(cx: number, cz: number, chunk) {
const center = [0, cx, cz];
this.store.dispatch(requestBigChunk(center));
try {
const url = `/chunks/${this.canvasId}/${cx}/${cz}.bmp`;
const response = await fetch(url);
if (response.ok) {
const arrayBuffer = await response.arrayBuffer();
if (arrayBuffer.byteLength) {
const chunkArray = new Uint8Array(arrayBuffer);
chunk.fromBuffer(chunkArray);
} else {
throw new Error('Chunk response was invalid');
}
this.store.dispatch(receiveBigChunk(center));
} else {
throw new Error('Network response was not ok.');
}
} catch (error) {
chunk.empty();
this.store.dispatch(receiveBigChunkFailure(center, error));
}
}
/*
// sine environment creation for load tests
async fetchChunk(xc: number, zc: number, chunk) {
const { key } = chunk;
console.log(`Fetch chunk ${key}`);
await chunk.generateSin();
this.store.dispatch(receiveBigChunk(key));
}
*/
}
export default ChunkLoader;

View File

@ -4,6 +4,9 @@
* @flow
*/
/* We have to look for performance here not for good looking code */
/* eslint-disable prefer-destructuring */
import * as THREE from 'three';
import {
@ -78,30 +81,38 @@ class Chunk {
buffer: Uint8Array;
mesh: THREE.Mesh = null;
faceCnt: number;
lastPixel: number;
heightMap: Array;
constructor(palette, key) {
this.key = key;
this.palette = palette;
}
destructor() {
if (this.mesh) {
this.mesh.geometry.dispose();
}
}
getVoxel(x: number, y: number, z: number) {
const { buffer } = this;
if (!buffer) return 0;
if (x < 0 || x >= THREE_TILE_SIZE || y >= THREE_CANVAS_HEIGHT
|| z < 0 || z >= THREE_TILE_SIZE)
return 0;
if (y < 0)
return 1;
|| z < 0 || z >= THREE_TILE_SIZE) return 0;
if (y < 0) return 1;
// z and y are swapped in api/pixel for compatibility
// with 2D canvas
const offset = Chunk.getOffsetOfVoxel(x, y, z)
const offset = Chunk.getOffsetOfVoxel(x, y, z);
return this.buffer[offset];
}
/*
// Test Sin encironment creation for load tests
async generateSin() {
let cnt = 0;
this.buffer = new Uint8Array(THREE_TILE_SIZE * THREE_TILE_SIZE * THREE_CANVAS_HEIGHT);
const cellSize = 64;
const cellSize = THREE_TILE_SIZE;
for (let y = 0; y < THREE_CANVAS_HEIGHT; ++y) {
for (let z = 0; z < THREE_TILE_SIZE; ++z) {
for (let x = 0; x < THREE_TILE_SIZE; ++x) {
@ -110,7 +121,8 @@ class Chunk {
const offset = x
+ z * THREE_TILE_SIZE
+ y * THREE_TILE_SIZE * THREE_TILE_SIZE;
const clr = 1 + Math.floor(Math.random() * 31);
// const clr = 1 + Math.floor(Math.random() * 31);
const clr = 14;
this.buffer[offset] = clr;
cnt += 1;
}
@ -118,56 +130,94 @@ class Chunk {
}
}
console.log(`Created buffer with ${cnt} voxels`);
this.faceCnt = Chunk.estimateNeededFaces(this.buffer);
const [faceCnt, lastPixel, heightMap] = Chunk.calculateMetaData(this.buffer);
this.faceCnt = faceCnt;
this.lastPixel = lastPixel;
this.heightMap = heightMap;
this.renderChunk();
this.ready = true;
}
*/
static estimateNeededFaces(buffer: Uint8Array) {
let totalCnt = 0;
static calculateMetaData(buffer: Uint8Array) {
const rowVolume = THREE_TILE_SIZE ** 2;
const heightMap = new Uint8Array(rowVolume);
let u = 0;
for (let y = 0; y < THREE_CANVAS_HEIGHT; ++y) {
for (let z = 0; z < THREE_TILE_SIZE; ++z) {
for (let x = 0; x < THREE_TILE_SIZE; ++x) {
let totalHeight = 0;
let lastPixel = 0;
let faceCnt = 0;
for (let z = THREE_TILE_SIZE - 1; z >= 0; --z) {
for (let x = THREE_TILE_SIZE - 1; x >= 0; --x) {
let heighestPixel = 0;
const startOffset = x + z * THREE_TILE_SIZE;
let u = startOffset;
for (let y = 0; y < THREE_CANVAS_HEIGHT; ++y) {
if (buffer[u] !== 0) {
// heighest pixel fo x,z in heightmap
heighestPixel = y;
// number of faces to render
if (x === 0
|| buffer[u - 1] === 0) {
totalCnt += 1;
faceCnt += 1;
}
if (x === THREE_TILE_SIZE - 1
|| buffer[u + 1] === 0) {
totalCnt += 1;
faceCnt += 1;
}
if (z === 0
|| buffer[u - THREE_TILE_SIZE] === 0) {
totalCnt += 1;
faceCnt += 1;
}
if (z === THREE_TILE_SIZE - 1
|| buffer[u + THREE_TILE_SIZE] === 0) {
totalCnt += 1;
faceCnt += 1;
}
if (y !== 0
&& buffer[u - (THREE_TILE_SIZE ** 2)] === 0) {
totalCnt += 1;
faceCnt += 1;
}
if (y === THREE_CANVAS_HEIGHT - 1
|| buffer[u + (THREE_TILE_SIZE ** 2)] === 0) {
totalCnt += 1;
faceCnt += 1;
}
}
u += 1;
u += rowVolume;
}
heightMap[startOffset] = heighestPixel;
if (heighestPixel > totalHeight) {
// last total pixel
totalHeight = heighestPixel;
lastPixel = Chunk.getOffsetOfVoxel(x, heighestPixel, z);
}
}
}
return totalCnt;
return [faceCnt, lastPixel, heightMap];
}
static getOffsetOfVoxel(x: number, y: number, z: number) {
return x + z * THREE_TILE_SIZE + y * THREE_TILE_SIZE * THREE_TILE_SIZE;
}
static getXZOfVoxel(offset) {
const xzOffset = offset % (THREE_TILE_SIZE * THREE_TILE_SIZE);
const y = (offset - xzOffset) / (THREE_TILE_SIZE * THREE_TILE_SIZE);
const x = xzOffset % THREE_TILE_SIZE;
const z = (xzOffset - x) / THREE_TILE_SIZE;
return [x, y, z];
}
setVoxelByOffset(offset: number, clr: number) {
if (offset > this.lastPixel) {
this.lastPixel = offset;
}
// TODO heightmap if pixel got deleted instead
// of set
const rowVolume = THREE_TILE_SIZE ** 2;
const rowOffset = offset % rowVolume;
const y = (offset - rowOffset) / rowVolume;
if (y > this.heightMap[rowOffset]) {
this.heightMap[rowOffset] = y;
}
this.buffer[offset] = clr;
this.faceCnt += 6;
this.renderChunk();
@ -180,30 +230,51 @@ class Chunk {
async fromBuffer(chunkBuffer: Uint8Array) {
this.buffer = chunkBuffer;
const [faceCnt, lastPixel, heightMap] = Chunk.calculateMetaData(
chunkBuffer,
);
this.faceCnt = faceCnt;
this.lastPixel = lastPixel;
this.heightMap = heightMap;
this.renderChunk();
this.ready = true;
}
renderChunk() {
let time1 = Date.now();
empty() {
const buffer = new Uint8Array(
THREE_TILE_SIZE * THREE_TILE_SIZE * THREE_CANVAS_HEIGHT,
);
const heightMap = new Uint8Array(
THREE_TILE_SIZE * THREE_TILE_SIZE,
);
this.buffer = buffer;
this.heightMap = heightMap;
this.faceCnt = 0;
this.lastPixel = 0;
this.renderChunk();
this.ready = true;
}
calculateGeometryData() {
const rowVolume = THREE_TILE_SIZE ** 2;
let cnt = 0;
let cntv = 0;
let voxel;
const faceCnt = this.faceCnt;
const { faceCnt } = this;
const positions = new Float32Array(faceCnt * 4 * 3);
const normals = new Float32Array(faceCnt * 4 * 3);
const colors = new Uint8Array(faceCnt * 4 * 3);
const indices = new Uint32Array(faceCnt * 6);
const { rgb } = this.palette;
// just render faces that do not have an adjescent voxel
for (let y = 0; y < THREE_CANVAS_HEIGHT; ++y) {
for (let z = 0; z < THREE_TILE_SIZE; ++z) {
for (let x = 0; x < THREE_TILE_SIZE; ++x) {
voxel = this.getVoxel(x, y, z);
for (let z = 0; z < THREE_TILE_SIZE; ++z) {
for (let x = 0; x < THREE_TILE_SIZE; ++x) {
const startOffset = x + z * THREE_TILE_SIZE;
const height = this.heightMap[startOffset];
let u = startOffset;
for (let y = 0; y <= height; ++y) {
voxel = this.buffer[u];
if (voxel !== 0) {
voxel *= 3;
cntv += 1;
for (let i = 0; i < 6; ++i) {
const dir = faceDirs[i];
const corners = faceCorners[i];
@ -242,10 +313,18 @@ class Chunk {
}
}
}
u += rowVolume;
}
}
}
let time2 = Date.now();
this.faceCnt = cnt;
return [positions, normals, colors, indices];
}
renderChunk() {
const time1 = Date.now();
const [positions, normals, colors, indices] = this.calculateGeometryData();
const time2 = Date.now();
const geometry = (this.mesh)
? this.mesh.geometry
@ -275,17 +354,16 @@ class Chunk {
);
geometry.setIndex(new THREE.BufferAttribute(indices, 1));
geometry.computeBoundingSphere();
geometry.setDrawRange(0, cnt * 6);
this.faceCnt = cnt;
geometry.setDrawRange(0, this.faceCnt * 6);
if (!this.mesh) {
this.mesh = new THREE.Mesh(geometry, material);
this.mesh.name = this.key;
}
let time3 = Date.now();
console.log(`Created mesh for ${cntv} voxels with ${cnt} faces in ${time3 - time2}ms webgl and ${time2 - time1}ms data creation`);
const time3 = Date.now();
// eslint-disable-next-line no-console, max-len
console.log(`Created mesh with ${this.faceCnt} faces in ${time3 - time2}ms webgl and ${time2 - time1}ms data creation`);
}
}

View File

@ -0,0 +1,125 @@
/*
* by Fyrestar
* https://github.com/Fyrestar/THREE.InfiniteGridHelper
* MIT License
*
* @flow
*/
/* eslint-disable max-len */
import * as THREE from 'three';
const InfiniteGridHelper = function InfiniteGridHelper(
size1,
size2,
color,
distance,
) {
color = color || new THREE.Color('white');
size1 = size1 || 10;
size2 = size2 || 100;
distance = distance || 8000;
const geometry = new THREE.PlaneBufferGeometry(2, 2, 1, 1);
const material = new THREE.ShaderMaterial({
side: THREE.DoubleSide,
uniforms: {
uSize1: {
value: size1,
},
uSize2: {
value: size2,
},
uColor: {
value: color,
},
uDistance: {
value: distance,
},
},
transparent: true,
vertexShader: `
varying vec3 worldPosition;
uniform float uDistance;
void main() {
vec3 pos = position.xzy * uDistance;
pos.xz += cameraPosition.xz;
worldPosition = pos;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
`,
fragmentShader: `
varying vec3 worldPosition;
uniform float uSize1;
uniform float uSize2;
uniform vec3 uColor;
uniform float uDistance;
float getGrid(float size) {
vec2 r = worldPosition.xz / size;
vec2 grid = abs(fract(r - 0.5) - 0.5) / fwidth(r);
float line = min(grid.x, grid.y);
return 1.0 - min(line, 1.0);
}
void main() {
float d = 1.0 - min(distance(cameraPosition.xz, worldPosition.xz) / uDistance, 1.0);
float g1 = getGrid(uSize1);
float g2 = getGrid(uSize2);
gl_FragColor = vec4(uColor.rgb, mix(g2, g1, g1) * pow(d, 3.0));
gl_FragColor.a = mix(0.5 * gl_FragColor.a, gl_FragColor.a, g2);
if ( gl_FragColor.a <= 0.0 ) discard;
}
`,
extensions: {
derivatives: true,
},
});
THREE.Mesh.call(this, geometry, material);
this.frustumCulled = false;
};
InfiniteGridHelper.prototype = {
...THREE.Mesh.prototype,
...THREE.Object3D.prototype,
...THREE.EventDispatcher.prototype,
};
export default InfiniteGridHelper;

View File

@ -94,6 +94,10 @@ class Renderer {
this.viewport.remove();
}
getViewport() {
return this.viewport;
}
getAllChunks() {
return this.chunkLoader.getAllChunks();
}
@ -317,6 +321,9 @@ class Renderer {
chunk = this.chunkLoader.getChunk(tiledZoom, cx, cy, fetch);
if (chunk) {
context.drawImage(chunk, x, y);
if (fetch) {
chunk.timestamp = curTime;
}
} else {
context.fillRect(x, y, TILE_SIZE, TILE_SIZE);
}
@ -368,7 +375,11 @@ class Renderer {
const [cx, cy] = this.centerChunk;
// if we have to render pixelnotify
const doRenderPixelnotify = (viewscale >= 0.5 && showPixelNotify && pixelNotify.doRender());
const doRenderPixelnotify = (
viewscale >= 0.5
&& showPixelNotify
&& pixelNotify.doRender()
);
// if we have to render placeholder
const doRenderPlaceholder = (
viewscale >= 3
@ -433,12 +444,18 @@ class Renderer {
Math.floor(height / 2 - CANVAS_HEIGHT / 2 + ((cy + 0.5) * TILE_SIZE / this.tiledScale - canvasCenter - y) * viewscale));
}
if (showGrid && viewscale >= 8) renderGrid(state, viewport, viewscale, isLightGrid);
if (showGrid && viewscale >= 8) {
renderGrid(state, viewport, viewscale, isLightGrid);
}
if (doRenderPixelnotify) pixelNotify.render(state, viewport);
if (hover && doRenderPlaceholder) renderPlaceholder(state, viewport, viewscale);
if (hover && doRenderPotatoPlaceholder) renderPotatoPlaceholder(state, viewport, viewscale);
if (hover && doRenderPlaceholder) {
renderPlaceholder(state, viewport, viewscale);
}
if (hover && doRenderPotatoPlaceholder) {
renderPotatoPlaceholder(state, viewport, viewscale);
}
}
@ -521,21 +538,33 @@ class Renderer {
context.fillRect(x, y, TILE_SIZE, TILE_SIZE);
} else {
// full chunks
chunk = this.chunkLoader.getHistoricalChunk(cx, cy, fetch, historicalDate);
chunk = this.chunkLoader
.getHistoricalChunk(cx, cy, fetch, historicalDate);
if (chunk) {
context.drawImage(chunk, x, y);
if (fetch) {
chunk.timestamp = curTime;
}
} else {
context.fillRect(x, y, TILE_SIZE, TILE_SIZE);
}
// incremential chunks
if (historicalTime === '0000') continue;
chunk = this.chunkLoader.getHistoricalChunk(cx, cy, fetch, historicalDate, historicalTime);
chunk = this.chunkLoader
.getHistoricalChunk(cx, cy, fetch, historicalDate, historicalTime);
if (chunk) {
context.drawImage(chunk, x, y);
if (fetch) {
chunk.timestamp = curTime;
}
} else if (oldHistoricalTime) {
chunk = this.chunkLoader.getHistoricalChunk(cx, cy, false, historicalDate, oldHistoricalTime);
chunk = this.chunkLoader
.getHistoricalChunk(cx, cy, false, historicalDate, oldHistoricalTime);
if (chunk) {
context.drawImage(chunk, x, y);
if (fetch) {
chunk.timestamp = curTime;
}
}
}
}

View File

@ -5,18 +5,20 @@
*/
import * as THREE from 'three';
import { Sky } from './Sky';
import InfiniteGridHelper from './InfiniteGridHelper';
import VoxelPainterControls from '../controls/VoxelPainterControls';
import ChunkLoader from './ChunkLoader3D';
import {
getChunkOfPixel,
getOffsetOfPixel,
} from '../core/utils';
import {
THREE_TILE_SIZE,
} from '../core/constants';
import {
setHover,
tryPlacePixel,
} from '../actions';
@ -28,8 +30,6 @@ class Renderer {
scene: Object;
camera: Object;
rollOverMesh: Object;
voxel: Object;
voxelMaterials: Array<Object>;
objects: Array<Object>;
loadedChunks: Array<Object>;
plane: Object;
@ -49,14 +49,14 @@ class Renderer {
const state = store.getState();
this.objects = [];
this.loadedChunks = new Map();
this.chunkLoader = new ChunkLoader(store);
this.chunkLoader = null;
// camera
const camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight,
1,
200,
400,
);
camera.position.set(10, 16, 26);
camera.lookAt(0, 0, 0);
@ -64,10 +64,43 @@ class Renderer {
// scene
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xf0f0f0);
// scene.background = new THREE.Color(0xf0f0f0);
this.scene = scene;
window.scene = scene;
window.THREE = THREE;
// lights
const ambientLight = new THREE.AmbientLight(0x222222);
scene.add(ambientLight);
// const directionalLight = new THREE.DirectionalLight(0xffffff);
// directionalLight.position.set(1, 1.2, 0.8).normalize();
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(80, 80, 75);
const contourLight = new THREE.DirectionalLight(0xffffff, 0.4);
contourLight.position.set(-80, 80, -75);
scene.add(directionalLight);
scene.add(contourLight);
const sky = new Sky();
sky.scale.setScalar(450000);
scene.add(sky);
const effectController = {
turbidity: 10,
rayleigh: 2,
mieCoefficient: 0.005,
mieDirectionalG: 0.8,
luminance: 1,
inclination: 0.49, // elevation / inclination
azimuth: 0.25, // Facing front,
sun: !true,
};
const { uniforms } = sky.material;
uniforms.turbidity.value = effectController.turbidity;
uniforms.rayleigh.value = effectController.rayleigh;
uniforms.luminance.value = effectController.luminance;
uniforms.mieCoefficient.value = effectController.mieCoefficient;
uniforms.mieDirectionalG.value = effectController.mieDirectionalG;
uniforms.sunPosition.value.set(400000, 400000, 400000);
// hover helper
const rollOverGeo = new THREE.BoxBufferGeometry(1, 1, 1);
@ -79,12 +112,9 @@ class Renderer {
this.rollOverMesh = new THREE.Mesh(rollOverGeo, rollOverMaterial);
scene.add(this.rollOverMesh);
// cubes
this.voxel = new THREE.BoxBufferGeometry(1, 1, 1);
this.initCubeMaterials(state);
// grid
const gridHelper = new THREE.GridHelper(100, 10, 0x555555, 0x555555);
// const gridHelper = new THREE.GridHelper(100, 10, 0x555555, 0x555555);
const gridHelper = new InfiniteGridHelper(1, 10);
scene.add(gridHelper);
//
@ -92,7 +122,7 @@ class Renderer {
this.mouse = new THREE.Vector2();
// Plane Floor
const geometry = new THREE.PlaneBufferGeometry(500, 500);
const geometry = new THREE.PlaneBufferGeometry(1024, 1024);
geometry.rotateX(-Math.PI / 2);
const plane = new THREE.Mesh(
geometry,
@ -101,14 +131,7 @@ class Renderer {
scene.add(plane);
this.plane = plane;
this.objects.push(plane);
// lights
const ambientLight = new THREE.AmbientLight(0x606060);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff);
directionalLight.position.set(1, 0.75, 0.5).normalize();
scene.add(directionalLight);
this.plane.position.y = -0.1;
// renderer
const threeRenderer = new THREE.WebGLRenderer({ antialias: true });
@ -116,17 +139,21 @@ class Renderer {
threeRenderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(threeRenderer.domElement);
this.threeRenderer = threeRenderer;
const { domElement } = threeRenderer;
// controls
const controls = new VoxelPainterControls(camera, threeRenderer.domElement);
const controls = new VoxelPainterControls(
camera,
domElement,
store,
);
controls.enableDamping = true;
controls.dampingFactor = 0.75;
controls.dampingFactor = 0.10;
controls.maxPolarAngle = Math.PI / 2;
controls.minDistance = 10.00;
controls.maxDistance = 100.00;
this.controls = controls;
const { domElement } = threeRenderer;
this.onDocumentMouseMove = this.onDocumentMouseMove.bind(this);
this.onDocumentMouseDown = this.onDocumentMouseDown.bind(this);
@ -137,7 +164,7 @@ class Renderer {
domElement.addEventListener('mouseup', this.onDocumentMouseUp, false);
window.addEventListener('resize', this.onWindowResize, false);
this.forceNextRender = true;
this.updateCanvasData(state);
}
destructor() {
@ -148,68 +175,132 @@ class Renderer {
domElement.remove();
}
static getAllChunks() {
updateView() {
this.forceNextRender = true;
}
getViewport() {
return this.threeRenderer.domElement;
}
updateCanvasData(state: State) {
const {
canvasId,
} = state.canvas;
if (canvasId !== this.canvasId) {
this.canvasId = canvasId;
if (canvasId !== null) {
if (this.chunkLoader) {
// destroy old chunks,
// meshes need to get disposed
this.loadedChunks.forEach((chunk) => {
this.scene.remove(chunk);
this.objects = [this.plane];
});
this.chunkLoader.destructor();
}
this.chunkLoader = new ChunkLoader(this.store);
this.forceNextRender = true;
}
}
}
// eslint-disable-next-line class-methods-use-this
updateScale() {
return null;
}
initCubeMaterials(state) {
const { palette } = state.canvas;
const { colors } = palette;
const cubeMaterials = [];
for (let index = 0; index < colors.length; index++) {
const material = new THREE.MeshLambertMaterial({
color: colors[index],
});
cubeMaterials.push(material);
// TODO use GC to dispose unused chunks
// eslint-disable-next-line class-methods-use-this
getAllChunks() {
return null;
}
renderPixel(
i: number,
j: number,
offset: number,
color: number,
) {
const { chunkLoader } = this;
if (chunkLoader) {
chunkLoader.getVoxelUpdate(i, j, offset, color);
}
this.voxelMaterials = cubeMaterials;
}
reloadChunks() {
console.log('Reload Chunks');
const renderDistance = 50;
if (!this.chunkLoader) {
return;
}
const renderDistance = 110;
const state = this.store.getState();
const { canvasSize, view } = state.canvas;
// const [x,, z] = view;
const [x, z] = [0, 0];
const {
canvasSize,
view,
} = state.canvas;
const x = view[0];
const z = view[2] || 0;
const {
scene,
loadedChunks,
chunkLoader,
} = this;
const [xcMin, zcMin] = getChunkOfPixel(canvasSize, x - 50, z - 50, 0);
const [xcMax, zcMax] = getChunkOfPixel(canvasSize, x + 50, z + 50, 0);
console.log(`Get ${xcMin} - ${xcMax} - ${zcMin} - ${zcMax}`);
const [xcMin, zcMin] = getChunkOfPixel(
canvasSize,
x - renderDistance,
0,
z - renderDistance,
);
const [xcMax, zcMax] = getChunkOfPixel(
canvasSize,
x + renderDistance,
0,
z + renderDistance,
);
// console.log(`Get ${xcMin} - ${xcMax} - ${zcMin} - ${zcMax}`);
const curLoadedChunks = [];
for (let zc = zcMin; zc <= zcMax; ++zc) {
for (let xc = xcMin; xc <= xcMax; ++xc) {
const chunkKey = `${xc}:${zc}`;
const chunk = chunkLoader.getChunk(xc, zc, true);
if (chunk) {
console.log(`Got Chunk ${chunkKey}`);
loadedChunks.set(chunkKey, chunk);
this.objects.push(chunk);
chunk.position.fromArray([
xc * THREE_TILE_SIZE - canvasSize / 2,
0,
zc * THREE_TILE_SIZE - canvasSize / 2,
]);
window.chunk = chunk;
scene.add(chunk);
console.log(`added chunk`);
curLoadedChunks.push(chunkKey);
if (!loadedChunks.has(chunkKey)) {
const chunk = chunkLoader.getChunk(xc, zc, true);
if (chunk) {
loadedChunks.set(chunkKey, chunk);
chunk.position.fromArray([
xc * THREE_TILE_SIZE - canvasSize / 2,
0,
zc * THREE_TILE_SIZE - canvasSize / 2,
]);
window.chunk = chunk;
scene.add(chunk);
}
}
}
}
const newObjects = [this.plane];
loadedChunks.forEach((chunk, chunkKey) => {
if (curLoadedChunks.includes(chunkKey)) {
newObjects.push(chunk);
} else {
scene.remove(chunk);
loadedChunks.delete(chunkKey);
}
});
this.plane.position.x = x;
this.plane.position.z = z;
this.objects = newObjects;
}
render() {
if (!this.threeRenderer) {
return;
}
this.controls.update();
if (this.forceNextRender) {
this.reloadChunks();
this.forceNextRender = false;
}
this.controls.update();
this.threeRenderer.render(this.scene, this.camera);
}
@ -235,7 +326,11 @@ class Renderer {
raycaster,
mouse,
rollOverMesh,
store,
} = this;
const {
placeAllowed,
} = store.getState().user;
mouse.set(
(clientX / innerWidth) * 2 - 1,
@ -255,6 +350,9 @@ class Renderer {
}
const hover = rollOverMesh.position
.toArray().map((u) => Math.floor(u));
if (!placeAllowed) {
rollOverMesh.position.y = -10;
}
this.store.dispatch(setHover(hover));
}
@ -266,6 +364,15 @@ class Renderer {
if (Date.now() - this.pressTime > 600) {
return;
}
const state = this.store.getState();
const {
placeAllowed,
} = state.user;
if (!placeAllowed) {
return;
}
event.preventDefault();
const {
clientX,
@ -280,11 +387,7 @@ class Renderer {
objects,
raycaster,
mouse,
plane,
voxel,
voxelMaterials,
store,
scene,
} = this;
mouse.set(
@ -301,56 +404,32 @@ class Renderer {
switch (event.button) {
case 0: {
// left mouse button
const state = store.getState();
const { selectedColor, hover } = state.gui;
const { canvasSize } = state.canvas;
//const pos = new THREE.Vector3();
const [x, y, z] = hover;
/*
const [x, y, z] = pos.copy(intersect.point)
const target = intersect.point.clone()
.add(intersect.face.normal.multiplyScalar(0.5))
.floor()
.addScalar(0.5)
.toArray();
*/
const offset = getOffsetOfPixel(canvasSize, x, z, y);
const [xc, zc] = getChunkOfPixel(canvasSize, x, z, y);
this.chunkLoader.getVoxelUpdate(xc, zc, offset, selectedColor);
/*
const newVoxel = new THREE.Mesh(
voxel,
voxelMaterials[selectedColor],
);
newVoxel.position
.copy(intersect.point)
.add(intersect.face.normal.multiplyScalar(0.5));
newVoxel.position
.floor()
.addScalar(0.5);
scene.add(newVoxel);
objects.push(newVoxel);
*/
.floor();
if (target.clone().sub(camera.position).length() < 120) {
const cell = target.toArray();
store.dispatch(tryPlacePixel(cell));
}
}
break;
case 2:
case 2: {
// right mouse button
const state = store.getState();
const { hover } = state.gui;
const { canvasSize } = state.canvas;
const normal = intersect.face.normal;
let [x, y, z] = hover;
x -= normal.x;
y -= normal.y;
z -= normal.z;
const offset = getOffsetOfPixel(canvasSize, x, z, y);
const [xc, zc] = getChunkOfPixel(canvasSize, x, z, y);
this.chunkLoader.getVoxelUpdate(xc, zc, offset, 0);
/*
if (intersect.object !== plane) {
scene.remove(intersect.object);
objects.splice(objects.indexOf(intersect.object), 1);
const target = intersect.point.clone()
.add(intersect.face.normal.multiplyScalar(-0.5))
.floor()
.addScalar(0.5)
.floor();
if (target.y < 0) {
return;
}
*/
if (target.clone().sub(camera.position).length() < 120) {
const cell = target.toArray();
store.dispatch(tryPlacePixel(cell, 0));
}
}
break;
default:
break;

237
src/ui/Sky.js Normal file
View File

@ -0,0 +1,237 @@
/**
* @author zz85 / https://github.com/zz85
*
* Based on "A Practical Analytic Model for Daylight"
* aka The Preetham Model, the de facto standard analytic skydome model
* http://www.cs.utah.edu/~shirley/papers/sunsky/sunsky.pdf
*
* First implemented by Simon Wallner
* http://www.simonwallner.at/projects/atmospheric-scattering
*
* Improved by Martin Upitis
* http://blenderartists.org/forum/showthread.php?245954-preethams-sky-impementation-HDR
*
* Three.js integration by zz85 http://twitter.com/blurspline
*/
/*
* taken from three.js examples
*/
/* eslint-disable */
import {
BackSide,
BoxBufferGeometry,
Mesh,
ShaderMaterial,
UniformsUtils,
Vector3
} from 'three';
var Sky = function () {
var shader = Sky.SkyShader;
var material = new ShaderMaterial( {
fragmentShader: shader.fragmentShader,
vertexShader: shader.vertexShader,
uniforms: UniformsUtils.clone( shader.uniforms ),
side: BackSide
} );
Mesh.call( this, new BoxBufferGeometry( 1, 1, 1 ), material );
};
Sky.prototype = Object.create( Mesh.prototype );
Sky.SkyShader = {
uniforms: {
"luminance": { value: 1 },
"turbidity": { value: 2 },
"rayleigh": { value: 1 },
"mieCoefficient": { value: 0.005 },
"mieDirectionalG": { value: 0.8 },
"sunPosition": { value: new Vector3() },
"up": { value: new Vector3( 0, 1, 0 ) }
},
vertexShader: [
'uniform vec3 sunPosition;',
'uniform float rayleigh;',
'uniform float turbidity;',
'uniform float mieCoefficient;',
'uniform vec3 up;',
'varying vec3 vWorldPosition;',
'varying vec3 vSunDirection;',
'varying float vSunfade;',
'varying vec3 vBetaR;',
'varying vec3 vBetaM;',
'varying float vSunE;',
// constants for atmospheric scattering
'const float e = 2.71828182845904523536028747135266249775724709369995957;',
'const float pi = 3.141592653589793238462643383279502884197169;',
// wavelength of used primaries, according to preetham
'const vec3 lambda = vec3( 680E-9, 550E-9, 450E-9 );',
// this pre-calcuation replaces older TotalRayleigh(vec3 lambda) function:
// (8.0 * pow(pi, 3.0) * pow(pow(n, 2.0) - 1.0, 2.0) * (6.0 + 3.0 * pn)) / (3.0 * N * pow(lambda, vec3(4.0)) * (6.0 - 7.0 * pn))
'const vec3 totalRayleigh = vec3( 5.804542996261093E-6, 1.3562911419845635E-5, 3.0265902468824876E-5 );',
// mie stuff
// K coefficient for the primaries
'const float v = 4.0;',
'const vec3 K = vec3( 0.686, 0.678, 0.666 );',
// MieConst = pi * pow( ( 2.0 * pi ) / lambda, vec3( v - 2.0 ) ) * K
'const vec3 MieConst = vec3( 1.8399918514433978E14, 2.7798023919660528E14, 4.0790479543861094E14 );',
// earth shadow hack
// cutoffAngle = pi / 1.95;
'const float cutoffAngle = 1.6110731556870734;',
'const float steepness = 1.5;',
'const float EE = 1000.0;',
'float sunIntensity( float zenithAngleCos ) {',
' zenithAngleCos = clamp( zenithAngleCos, -1.0, 1.0 );',
' return EE * max( 0.0, 1.0 - pow( e, -( ( cutoffAngle - acos( zenithAngleCos ) ) / steepness ) ) );',
'}',
'vec3 totalMie( float T ) {',
' float c = ( 0.2 * T ) * 10E-18;',
' return 0.434 * c * MieConst;',
'}',
'void main() {',
' vec4 worldPosition = modelMatrix * vec4( position, 1.0 );',
' vWorldPosition = worldPosition.xyz;',
' gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );',
' gl_Position.z = gl_Position.w;', // set z to camera.far
' vSunDirection = normalize( sunPosition );',
' vSunE = sunIntensity( dot( vSunDirection, up ) );',
' vSunfade = 1.0 - clamp( 1.0 - exp( ( sunPosition.y / 450000.0 ) ), 0.0, 1.0 );',
' float rayleighCoefficient = rayleigh - ( 1.0 * ( 1.0 - vSunfade ) );',
// extinction (absorbtion + out scattering)
// rayleigh coefficients
' vBetaR = totalRayleigh * rayleighCoefficient;',
// mie coefficients
' vBetaM = totalMie( turbidity ) * mieCoefficient;',
'}'
].join( '\n' ),
fragmentShader: [
'varying vec3 vWorldPosition;',
'varying vec3 vSunDirection;',
'varying float vSunfade;',
'varying vec3 vBetaR;',
'varying vec3 vBetaM;',
'varying float vSunE;',
'uniform float luminance;',
'uniform float mieDirectionalG;',
'uniform vec3 up;',
'const vec3 cameraPos = vec3( 0.0, 0.0, 0.0 );',
// constants for atmospheric scattering
'const float pi = 3.141592653589793238462643383279502884197169;',
'const float n = 1.0003;', // refractive index of air
'const float N = 2.545E25;', // number of molecules per unit volume for air at 288.15K and 1013mb (sea level -45 celsius)
// optical length at zenith for molecules
'const float rayleighZenithLength = 8.4E3;',
'const float mieZenithLength = 1.25E3;',
// 66 arc seconds -> degrees, and the cosine of that
'const float sunAngularDiameterCos = 0.999956676946448443553574619906976478926848692873900859324;',
// 3.0 / ( 16.0 * pi )
'const float THREE_OVER_SIXTEENPI = 0.05968310365946075;',
// 1.0 / ( 4.0 * pi )
'const float ONE_OVER_FOURPI = 0.07957747154594767;',
'float rayleighPhase( float cosTheta ) {',
' return THREE_OVER_SIXTEENPI * ( 1.0 + pow( cosTheta, 2.0 ) );',
'}',
'float hgPhase( float cosTheta, float g ) {',
' float g2 = pow( g, 2.0 );',
' float inverse = 1.0 / pow( 1.0 - 2.0 * g * cosTheta + g2, 1.5 );',
' return ONE_OVER_FOURPI * ( ( 1.0 - g2 ) * inverse );',
'}',
// Filmic ToneMapping http://filmicgames.com/archives/75
'const float A = 0.15;',
'const float B = 0.50;',
'const float C = 0.10;',
'const float D = 0.20;',
'const float E = 0.02;',
'const float F = 0.30;',
'const float whiteScale = 1.0748724675633854;', // 1.0 / Uncharted2Tonemap(1000.0)
'vec3 Uncharted2Tonemap( vec3 x ) {',
' return ( ( x * ( A * x + C * B ) + D * E ) / ( x * ( A * x + B ) + D * F ) ) - E / F;',
'}',
'void main() {',
// optical length
// cutoff angle at 90 to avoid singularity in next formula.
' float zenithAngle = acos( max( 0.0, dot( up, normalize( vWorldPosition - cameraPos ) ) ) );',
' float inverse = 1.0 / ( cos( zenithAngle ) + 0.15 * pow( 93.885 - ( ( zenithAngle * 180.0 ) / pi ), -1.253 ) );',
' float sR = rayleighZenithLength * inverse;',
' float sM = mieZenithLength * inverse;',
// combined extinction factor
' vec3 Fex = exp( -( vBetaR * sR + vBetaM * sM ) );',
// in scattering
' float cosTheta = dot( normalize( vWorldPosition - cameraPos ), vSunDirection );',
' float rPhase = rayleighPhase( cosTheta * 0.5 + 0.5 );',
' vec3 betaRTheta = vBetaR * rPhase;',
' float mPhase = hgPhase( cosTheta, mieDirectionalG );',
' vec3 betaMTheta = vBetaM * mPhase;',
' vec3 Lin = pow( vSunE * ( ( betaRTheta + betaMTheta ) / ( vBetaR + vBetaM ) ) * ( 1.0 - Fex ), vec3( 1.5 ) );',
' Lin *= mix( vec3( 1.0 ), pow( vSunE * ( ( betaRTheta + betaMTheta ) / ( vBetaR + vBetaM ) ) * Fex, vec3( 1.0 / 2.0 ) ), clamp( pow( 1.0 - dot( up, vSunDirection ), 5.0 ), 0.0, 1.0 ) );',
// nightsky
' vec3 direction = normalize( vWorldPosition - cameraPos );',
' float theta = acos( direction.y ); // elevation --> y-axis, [-pi/2, pi/2]',
' float phi = atan( direction.z, direction.x ); // azimuth --> x-axis [-pi/2, pi/2]',
' vec2 uv = vec2( phi, theta ) / vec2( 2.0 * pi, pi ) + vec2( 0.5, 0.0 );',
' vec3 L0 = vec3( 0.1 ) * Fex;',
// composition + solar disc
' float sundisk = smoothstep( sunAngularDiameterCos, sunAngularDiameterCos + 0.00002, cosTheta );',
' L0 += ( vSunE * 19000.0 * Fex ) * sundisk;',
' vec3 texColor = ( Lin + L0 ) * 0.04 + vec3( 0.0, 0.0003, 0.00075 );',
' vec3 curr = Uncharted2Tonemap( ( log2( 2.0 / pow( luminance, 4.0 ) ) ) * texColor );',
' vec3 color = curr * whiteScale;',
' vec3 retColor = pow( color, vec3( 1.0 / ( 1.2 + ( 1.2 * vSunfade ) ) ) );',
' gl_FragColor = vec4( retColor, 1.0 );',
'}'
].join( '\n' )
};
export { Sky };

View File

@ -120,7 +120,7 @@ app.use('/discord', (req, res) => {
//
// Serving Chunks
// -----------------------------------------------------------------------------
app.get('/chunks/:c([0-9]+)/:x([0-9]+)/:y([0-9]+).bmp', chunks);
app.get('/chunks/:c([0-9]+)/:x([0-9]+)/:y([0-9]+)(/)?:z([0-9]+)?.bmp', chunks);
//