Make 3D canvas multiplayer
This commit is contained in:
parent
29755b7ff1
commit
1b185f4e5b
|
@ -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;
|
||||
|
|
|
@ -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."
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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)`,
|
||||
})}
|
||||
/>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
125
src/ui/InfiniteGridHelper.js
Normal file
125
src/ui/InfiniteGridHelper.js
Normal 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;
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
237
src/ui/Sky.js
Normal 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 };
|
|
@ -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);
|
||||
|
||||
|
||||
//
|
||||
|
|
Loading…
Reference in New Issue
Block a user