Compare commits

...
Sign in to create a new pull request.

126 commits

Author SHA1 Message Date
Vitaly Turovsky
7be450d6d2 a bit bigger oversize 2024-08-02 05:04:45 +03:00
Vitaly Turovsky
d46a475e24 effective updates & fov 2024-08-02 04:55:26 +03:00
Vitaly Turovsky
eeca0e6360 Merge branch 'webgpu-true' into webgpu-tree-fresh 2024-08-02 03:49:21 +03:00
Vitaly Turovsky
c01f8185f6 fix make it work 2024-08-02 03:48:07 +03:00
Илья Белов
aba18dc896 Small fixes for mobile platform 2024-08-02 03:44:13 +03:00
Илья Белов
b34a637952 typo fix 2024-08-02 03:19:28 +03:00
Илья Белов
c23075b33e Working frustrum culling
Co-authored-by: Vitaly <vital2580@icloud.com>
2024-08-02 03:05:21 +03:00
Vitaly Turovsky
7d3c046eeb Merge remote-tracking branch 'origin/next' into webgpu-tree-fresh 2024-08-02 02:19:56 +03:00
Vitaly Turovsky
2dfd17939f Merge remote-tracking branch 'origin/next' into webgpu-true-fresh 2024-08-02 02:15:42 +03:00
Илья Белов
e62b4d8cda Passtrough of cubes 2024-07-31 04:06:33 +03:00
Илья Белов
de8e3dea69 Working compute shader 2024-07-31 02:43:40 +03:00
Vitaly Turovsky
8e7d7b9cb6 wip compute pipeline: indirect draw 2024-07-19 01:40:09 +03:00
Vitaly Turovsky
16d4f5db19 fix build
Co-authored-by: Ilya <sa2urami@users.noreply.github.com>
2024-07-19 01:08:36 +03:00
Vitaly Turovsky
c9b1a9e8c3 refactor webgpu renderer
Co-authored-by: Ilya <sa2urami@users.noreply.github.com>
2024-07-10 23:53:08 +03:00
Vitaly Turovsky
92afdf54fb Merge remote-tracking branch 'origin/next' into webgpu-true 2024-07-10 20:26:04 +03:00
Vitaly Turovsky
2ea730e9d1 huge work by ilya. again. 2024-07-10 02:29:26 +03:00
Vitaly Turovsky
ae4618b1d8 fix basic world opening, wip export tiles
Co-authored-by: Ilya <sa2urami@users.noreply.github.com>
2024-06-25 02:42:34 +03:00
Vitaly Turovsky
5c2be2e147 add textures, colors
Co-authored-by: Ilya <sa2urami@users.noreply.github.com>
2024-05-25 04:22:24 +03:00
Vitaly Turovsky
408df667bb Merge remote-tracking branch 'origin/webgpu-2' into webgpu-true 2024-05-25 03:41:57 +03:00
Vitaly Turovsky
02a84b83ab add textures 2024-05-25 03:41:00 +03:00
Vitaly Turovsky
1c05804398 fix build 2024-05-25 03:40:31 +03:00
Vitaly Turovsky
6d375d83e9 Refactor shader code for cube rendering 2024-05-25 03:17:57 +03:00
Vitaly Turovsky
6f0e238409 reduce size
Co-authored-by: Ilya <sa2urami@users.noreply.github.com>
2024-05-25 03:15:54 +03:00
Vitaly Turovsky
560d5fb5e3 up cube
Co-authored-by: Ilya <sa2urami@users.noreply.github.com>
2024-05-25 02:02:50 +03:00
Vitaly Turovsky
bbe2a6bef1 blocks adding 2024-05-25 01:32:19 +03:00
Vitaly Turovsky
208b026530 instancing 2024-05-25 00:50:53 +03:00
Vitaly Turovsky
c1880b137d Merge branch 'next' into pr/sa2urami/120
Co-authored-by: Ilya <sa2urami@users.noreply.github.com>
2024-05-25 00:05:47 +03:00
Vitaly Turovsky
c33ab7ad6c fix crashes 2024-05-22 05:11:06 +03:00
Vitaly Turovsky
fb6b965f98 use new insane workers api, cleanup webgpu constants 2024-05-22 05:06:46 +03:00
Vitaly Turovsky
c8310945aa initial instance shading
Co-authored-by: Ilya <sa2urami@users.noreply.github.com>
2024-05-22 02:19:31 +03:00
Vitaly Turovsky
4abdead924 working cube! 2024-05-22 00:14:46 +03:00
Vitaly Turovsky
130daac35e triangle 2024-05-21 23:14:03 +03:00
Vitaly Turovsky
137666c65c FIX waitForChunksToRender ! fix a lot of worker issues! implement allChunksLoaded method correctly!! 2024-04-14 18:43:19 +03:00
Vitaly Turovsky
3c4996f1ef impl removal, fix cam 2024-04-14 18:37:12 +03:00
Vitaly Turovsky
6c5fdbdc5e another fixup for cam pos 2024-04-12 19:38:12 +03:00
Vitaly Turovsky
4e1257cb7c fix cam fixtup 2024-04-12 19:33:03 +03:00
Vitaly Turovsky
e9ca5cf3d2 fix playground and slow cam move in worker 2024-04-12 19:28:36 +03:00
Vitaly Turovsky
662c823c23 [pick] fix alias set 2024-04-11 20:19:32 +03:00
Vitaly Turovsky
a12ab1cd16 clean fix for image.width is undefined! 2024-04-11 20:15:28 +03:00
Vitaly Turovsky
830424eec2 [to pick] customize gamemode from menu! 2024-04-11 19:57:55 +03:00
Vitaly Turovsky
4f9efb4c01 [wip] fixture export / loading! 2024-04-11 02:56:41 +03:00
Vitaly Turovsky
a3c414e09e commit stats 2024-04-11 00:55:25 +03:00
Vitaly Turovsky
a313c5ffc1 new stats! 2024-04-11 00:52:50 +03:00
Vitaly Turovsky
bb8a8413ae implement buffer extend 2024-04-10 22:13:44 +03:00
Vitaly Turovsky
890afee8e7 [pick] creative gamemode for ?singleplayer=1 2024-04-10 21:38:10 +03:00
Илья Белов
40cda86f01 Merge branch 'webgl' of https://github.com/sa2urami/prismarine-web-client into webgl 2024-04-10 04:27:19 +03:00
Илья Белов
0cebd53e3d Triangle Strip 2024-04-10 04:27:09 +03:00
Vitaly Turovsky
324f07014b fix playground 2024-04-10 04:21:17 +03:00
Vitaly Turovsky
33d563cf94 fix action 2024-04-10 04:19:22 +03:00
Vitaly Turovsky
50efd76595 workaround and potential fix 2024-04-10 04:18:52 +03:00
Vitaly Turovsky
a5f0af8e6b [deploy] fix singleplayer 2024-04-10 02:33:36 +03:00
Илья Белов
f970397886 Merge branch 'webgl' of https://github.com/sa2urami/prismarine-web-client into webgl 2024-04-10 01:48:58 +03:00
Илья Белов
ff5ca18a22 KAVOfasdfasdf 2024-04-10 01:48:14 +03:00
Vitaly Turovsky
e3205ed9c6 prev commit fixes, fix bg, add reset 2024-04-10 01:42:32 +03:00
Илья Белов
2bc843959e Merge branch 'webgl' of https://github.com/sa2urami/prismarine-web-client into webgl 2024-04-10 00:58:55 +03:00
Илья Белов
ed74485b01 Quad instanced rendreing 2024-04-10 00:58:46 +03:00
Vitaly Turovsky
469ef4982e [pick] allow to set 0 render distance for debug 2024-04-09 22:41:28 +03:00
Vitaly Turovsky
de227f08e4 Merge remote-tracking branch 'origin/next' into webgl 2024-04-09 16:47:35 +03:00
Vitaly Turovsky
0edf51aef3 better workarounds! 2024-04-09 16:43:34 +03:00
Vitaly Turovsky
28d55f9cfc reload on worker change 2024-04-09 16:33:06 +03:00
Vitaly Turovsky
c5c8f27e18 allow to override 2024-04-09 16:29:44 +03:00
Vitaly Turovsky
484844b35d add playground scripts! 2024-04-09 14:41:21 +03:00
Vitaly
5476b391ff Merge branch 'next' into webgl 2024-04-09 03:52:08 +03:00
Vitaly
dc9f15e903 naive transparent 2024-04-09 03:51:11 +03:00
Vitaly Turovsky
029a7175ab fix page loadeding indicator showing sometimes 2024-04-07 04:01:04 +03:00
gguio
48d3bbeef5 feat: effects + system indicators in hud! (#95)
Co-authored-by: gguio <nikvish150@gmail.com>
2024-04-07 04:01:04 +03:00
Vitaly
889987d908 three shake three.js (#94) 2024-04-07 04:00:49 +03:00
Vitaly
c4d65ee9fd fix warps display for greenfield & click action 2024-04-07 04:00:49 +03:00
Vitaly Turovsky
db722dba34 fix vercel preview deploy 2024-04-07 04:00:49 +03:00
Илья Белов
9f89af613f Add biome color support to fragment shader 2024-04-07 02:37:21 +03:00
Vitaly Turovsky
31b419c6ee add tint 2024-04-07 02:03:46 +03:00
Vitaly Turovsky
7777307c4f fix critical performance scene render 2024-04-07 01:48:43 +03:00
Илья Белов
183f8e461e Fix texture buffer data type and usage in webglRendererWorker.ts 2024-04-07 01:34:21 +03:00
Vitaly Turovsky
17cd4bff77 fix critical texture index bug 2024-04-07 01:13:03 +03:00
Vitaly Turovsky
9fe0e340f8 minor render tweaks 2024-04-06 23:55:45 +03:00
Vitaly Turovsky
4c58f4538e copy webgl data! 2024-04-06 19:57:06 +03:00
Vitaly Turovsky
12dfa9f9a8 implement fully working animation in playground 2024-04-06 19:48:39 +03:00
Vitaly Turovsky
8b6e1d210b make worldrenderer extendable and customizable with your own render implementations! 2024-04-06 19:00:13 +03:00
Vitaly Turovsky
97a9e92119 impl basic debug animation 2024-04-06 02:55:44 +03:00
Vitaly Turovsky
16fdd37776 better render debug 2024-04-06 02:45:40 +03:00
Vitaly Turovsky
349edd17ed undo l 2024-04-05 23:35:34 +03:00
Vitaly Turovsky
984a2081a7 almost working texture switch in playground 2024-04-05 23:35:17 +03:00
Vitaly Turovsky
d0501d331d atlas now with animated blocks! 2024-04-05 23:16:21 +03:00
Vitaly Turovsky
5951e14b68 fix stone display 2024-04-05 22:57:42 +03:00
Vitaly Turovsky
86f1b5e569 add animation, impl texture indexes, fix perf 2024-04-04 13:46:13 +03:00
Илья Белов
a75aeb0c92 Addon to last commit 2024-04-04 01:05:52 +03:00
Илья Белов
e9ae58612c Super Performant Multi Textures 2024-04-04 01:05:35 +03:00
Илья Белов
61a8a97dbb Multi textures 2024-04-04 01:02:42 +03:00
Vitaly Turovsky
11c04fb3db implement removal, small impr 2024-04-04 00:14:31 +03:00
Vitaly Turovsky
7b61ff68e8 fix worker build on win 2024-04-03 22:46:10 +03:00
Vitaly Turovsky
dc625b58d7 implement correct cubes update via subdata (faster new chunks add) 2024-03-31 23:33:51 +03:00
Vitaly Turovsky
6275d787a2 compute index in worker: less lags 2024-03-31 22:50:57 +03:00
Vitaly Turovsky
5b78000697 a bit better performance on blocks update - new struct 2024-03-29 19:45:08 +03:00
Vitaly Turovsky
61d0ff46b3 up playground impl 2024-03-29 14:31:19 +03:00
Vitaly Turovsky
f168bdbfa4 up tasks 2024-03-29 14:26:03 +03:00
Vitaly Turovsky
a7313b3a70 insane ilya work by working hard (harder than usual) 2024-03-28 03:44:00 +03:00
Vitaly Turovsky
506706e88b fixed camera, coords, cam, textures, controls 2024-03-28 03:43:42 +03:00
Vitaly Turovsky
7d5f1c504f worker part 2 2024-03-27 11:16:07 +03:00
Vitaly Turovsky
a19c9ad784 fix cam 2024-03-25 14:37:37 +03:00
Vitaly Turovsky
f3295593bf build worker correctly 2024-03-25 13:00:24 +03:00
Vitaly Turovsky
d0a6941031 build workers 2024-03-25 12:40:39 +03:00
Vitaly Turovsky
00b9683e78 fix worker output! 2024-03-25 12:35:33 +03:00
Vitaly Turovsky
ce194599d7 up world 2024-03-25 12:27:03 +03:00
Vitaly Turovsky
a3290063e8 restore singleplayer 2024-03-25 12:26:58 +03:00
Vitaly Turovsky
51ba0eaec5 put renderer into worker! viewerWrapper & perf cypress test 2024-03-25 07:28:03 +03:00
Vitaly Turovsky
b35bc0882f Merge remote-tracking branch 'origin/next' into webgl 2024-03-25 03:46:05 +03:00
Vitaly Turovsky
4133eca3a7 dynamic entrypoint 2024-03-24 10:44:00 +03:00
Vitaly Turovsky
c8e6475954 fix textures 2024-03-24 09:38:46 +03:00
Vitaly Turovsky
9e26f70011 add touch 2024-03-24 09:29:11 +03:00
Vitaly Turovsky
c6c4ca841b almost ok for the app 2024-03-22 04:49:33 +03:00
Vitaly Turovsky
b403ed4606 fix build 2024-03-22 03:49:23 +03:00
Vitaly Turovsky
a682d4b3da Instanced mashing with indexed texturing 2024-03-22 02:33:20 +03:00
Vitaly Turovsky
70c9686f89 insane instace shading 2024-03-22 01:31:22 +03:00
Илья Белов
ad5a906a95 fix things 2024-03-22 00:31:24 +03:00
Илья Белов
a1db77a21f Merge remote-tracking branch 'upstream/next' into webgl 2024-03-22 00:02:17 +03:00
Илья Белов
027f5c6917 up package json 2024-03-22 00:02:11 +03:00
Vitaly
2ee0ef3483 pus render 2024-03-21 23:42:33 +03:00
Vitaly Turovsky
73a5e9d8ba up render 2024-03-21 22:50:44 +03:00
Vitaly Turovsky
024891c6d5 add 2024-03-21 15:47:27 +03:00
Vitaly Turovsky
dee63df232 Merge branch 'next' into webgl 2024-03-21 14:17:00 +03:00
Vitaly Turovsky
253fb8a2b5 move code around, +export 2024-03-21 14:16:18 +03:00
Vitaly Turovsky
6e84db0a7a make cubes
undo viewer changes before merge
2024-03-21 02:56:32 +03:00
Vitaly Turovsky
b28f69c126 nice work by ilya 2024-03-18 21:42:42 +03:00
Илья Белов
06b2f3bfa8 degrees_to_radians 2024-03-06 03:26:03 +03:00
Илья Белов
1175d3fae9 Working texture bindings 2024-03-06 02:36:52 +03:00
Илья Белов
163290d841 Stash
Co-authored-by: Vitaly <vital2580@icloud.com>
2024-03-01 03:03:44 +03:00
42 changed files with 3204 additions and 399 deletions

14
.vscode/tasks.json vendored
View file

@ -25,10 +25,20 @@
},
},
{
"label": "viewer server+esbuild",
"label": "webgl-worker",
"type": "shell",
"command": "node buildWorkers.mjs -w",
"problemMatcher": "$esbuild-watch",
"presentation": {
"reveal": "silent"
},
},
{
"label": "viewer server+esbuild+workers",
"dependsOn": [
"viewer-server",
"viewer-esbuild"
"viewer-esbuild",
"webgl-worker"
],
"dependsOrder": "parallel",
}

59
buildWorkers.mjs Normal file
View file

@ -0,0 +1,59 @@
// main worker file intended for computing world geometry is built using prismarine-viewer/buildWorker.mjs
import { build, context } from 'esbuild'
import fs from 'fs'
import path from 'path'
const watch = process.argv.includes('-w')
const result = await (watch ? context : build)({
bundle: true,
platform: 'browser',
entryPoints: ['prismarine-viewer/examples/webgpuRendererWorker.ts'],
outdir: 'prismarine-viewer/public/',
sourcemap: watch ? 'inline' : 'external',
minify: !watch,
treeShaking: true,
logLevel: 'info',
alias: {
'three': './node_modules/three/src/Three.js',
events: 'events', // make explicit
buffer: 'buffer',
'fs': 'browserfs/dist/shims/fs.js',
http: 'http-browserify',
perf_hooks: './src/perf_hooks_replacement.js',
crypto: './src/crypto.js',
stream: 'stream-browserify',
net: 'net-browserify',
assert: 'assert',
dns: './src/dns.js'
},
plugins: [
{
name: 'writeOutput',
setup (build) {
build.onEnd(({ outputFiles }) => {
for (const file of outputFiles) {
for (const dir of ['prismarine-viewer/public', 'dist']) {
const baseName = path.basename(file.path)
fs.writeFileSync(path.join(dir, baseName), file.contents)
}
}
})
}
}
],
loader: {
'.vert': 'text',
'.frag': 'text',
'.wgsl': 'text',
},
mainFields: [
'browser', 'module', 'main'
],
keepNames: true,
write: false,
})
if (watch) {
await result.watch()
}

View file

@ -0,0 +1,25 @@
import { cleanVisit, setOptions } from './shared'
it('Loads & renders singleplayer', () => {
cleanVisit('/?singleplayer=1')
setOptions({
renderDistance: 2
})
// wait for .initial-loader to disappear
cy.get('.initial-loader', { timeout: 20_000 }).should('not.exist')
cy.window()
.its('performance')
.invoke('mark', 'worldLoad')
cy.document().then({ timeout: 20_000 }, doc => {
return new Cypress.Promise(resolve => {
doc.addEventListener('cypress-world-ready', resolve)
})
}).then(() => {
const duration = cy.window()
.its('performance')
.invoke('measure', 'modalOpen')
.its('duration')
cy.log('Duration', duration)
})
})

View file

@ -6,11 +6,11 @@
"dev-rsbuild": "rsbuild dev",
"dev-proxy": "node server.js",
"start": "run-p dev-rsbuild dev-proxy",
"start-watch-script": "nodemon -w rsbuild.config.ts --watch",
"build": "rsbuild build",
"build-analyze": "BUNDLE_ANALYZE=true rsbuild build",
"build": "node buildWorkers.mjs && rsbuild build",
"build-analyze": "BUNDLE_ANALYZE=true rsbuild build && node buildWorkers.mjs",
"check-build": "tsx scripts/genShims.ts && tsc && pnpm build",
"test:cypress": "cypress run",
"test:cypress:perf": "cypress run --spec cypress/e2e/perf.spec.ts --browser edge",
"test-unit": "vitest",
"test:e2e": "start-test http-get://localhost:8080 test:cypress",
"prod-start": "node server.js --prod",
@ -20,12 +20,13 @@
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build && node scripts/build.js moveStorybookFiles",
"start-experiments": "vite --config experiments/vite.config.ts --host",
"watch-other-workers": "echo NOT IMPLEMENTED",
"watch-other-workers": "node buildWorkers.mjs -w",
"build-mesher": "node prismarine-viewer/buildMesherWorker.mjs",
"watch-mesher": "pnpm build-mesher -w",
"run-playground": "run-p watch-mesher watch-other-workers playground-server watch-playground",
"run-all": "run-p start run-playground",
"playground-server": "live-server --port=9090 prismarine-viewer/public",
"start-watch-script": "nodemon -w rsbuild.config.ts --watch",
"watch-playground": "node prismarine-viewer/esbuild.mjs -w"
},
"keywords": [
@ -51,6 +52,7 @@
"@types/react": "^18.2.20",
"@types/react-dom": "^18.2.7",
"@types/wicg-file-system-access": "^2023.10.2",
"@webgpu/types": "^0.1.44",
"@xmcl/text-component": "^2.1.3",
"@zardoy/react-util": "^0.2.4",
"@zardoy/utils": "^0.0.11",
@ -72,6 +74,7 @@
"google-drive-browserfs": "github:zardoy/browserfs#google-drive",
"jszip": "^3.10.1",
"lodash-es": "^4.17.21",
"math.gl": "^4.0.0",
"minecraft-data": "3.65.0",
"minecraft-protocol": "github:PrismarineJS/node-minecraft-protocol#master",
"mineflayer-item-map-downloader": "github:zardoy/mineflayer-item-map-downloader",
@ -99,6 +102,7 @@
"stats.js": "^0.17.0",
"tabbable": "^6.2.0",
"title-case": "3.x",
"twgl.js": "^5.5.4",
"ua-parser-js": "^1.0.37",
"use-typed-event-listener": "^4.0.2",
"valtio": "^1.11.1",

1384
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -38,7 +38,7 @@ const buildOptions = {
...mesherSharedPlugins,
{
name: 'external-json',
setup (build) {
setup(build) {
build.onResolve({ filter: /\.json$/ }, args => {
const fileName = args.path.split('/').pop().replace('.json', '')
if (args.resolveDir.includes('minecraft-data')) {

View file

@ -46,18 +46,20 @@ const buildOptions = {
http: 'http-browserify',
stream: 'stream-browserify',
net: 'net-browserify',
// 'mc-assets': '/Users/vitaly/Documents/mc-assets',
'stats.js': 'stats.js/src/Stats.js',
},
inject: [],
metafile: true,
loader: {
'.png': 'dataurl',
'.vert': 'text',
'.frag': 'text',
'.obj': 'text',
},
plugins: [
{
name: 'minecraft-data',
setup(build) {
setup (build) {
build.onLoad({
filter: /minecraft-data[\/\\]data.js$/,
}, () => {

View file

@ -0,0 +1,45 @@
struct Cube {
position: vec3f,
textureIndex: f32,
colorBlend: vec3f,
//tt: f32
}
struct Uniforms {
ViewProjectionMatrix: mat4x4<f32>,
}
struct IndirectDrawParams {
vertexCount: u32,
instanceCount: atomic<u32>,
firstVertex: u32,
firstInstance: u32,
}
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
@group(0) @binding(1) var<storage, read> cubes: array<Cube>;
@group(0) @binding(2) var<storage, read_write> visibleCubes: array<Cube>; // Changed to @binding(4)
@group(0) @binding(3) var<storage, read_write> drawParams: IndirectDrawParams;
@compute @workgroup_size(256)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
let index = global_id.x;
if (index >= arrayLength(&cubes)) {
return;
}
let cube = cubes[index];
// Transform cube position to clip space
let clipPos = uniforms.ViewProjectionMatrix * (vec4<f32>(cube.position, 1.0) + vec4<f32>(0.5, 0.0, 0.5, 0.0));
let clipDepth = clipPos.z / clipPos.w; // Obtain depth in clip space
let clipX = clipPos.x / clipPos.w;
let clipY = clipPos.y / clipPos.w;
// Check if cube is within the view frustum z-range (depth within near and far planes)
let Oversize = 1.2;
if (clipDepth >= 0.0 && clipDepth <= 1.0 && clipX > -1.0 * Oversize && clipX < 1.0 * Oversize && clipY > -1.0 * Oversize && clipY < 1.0 * Oversize) { //Small Oversize because binding size
let visibleIndex = atomicAdd(&drawParams.instanceCount, 1);
visibleCubes[visibleIndex] = cube;
}
}

View file

@ -0,0 +1,14 @@
@group(0) @binding(1) var mySampler: sampler;
@group(0) @binding(2) var myTexture: texture_2d<f32>;
@fragment
fn main(
@location(0) fragUV: vec2f,
@location(1) @interpolate(flat) TextureIndex: f32,
@location(2) @interpolate(flat) ColorBlend: vec3f
) -> @location(0) vec4f {
let textureSize: vec2<f32> = vec2<f32>(textureDimensions(myTexture));
let tileSize: vec2<f32> = vec2<f32>(16.0f,16.0f);
let tilesPerTexture: vec2<f32> = vec2<f32>(textureSize)/tileSize;
return textureSample(myTexture, mySampler, fragUV/tilesPerTexture + vec2f(trunc(TextureIndex%tilesPerTexture.y),trunc(TextureIndex/tilesPerTexture.x) )/tilesPerTexture) * vec4f(ColorBlend,1.0);
}

View file

@ -0,0 +1,36 @@
struct Uniforms {
ViewProjectionMatrix: mat4x4<f32>,
}
struct Cube {
position: vec3f,
textureIndex: f32,
colorBlend: vec3f,
//tt: f32
}
struct VertexOutput {
@builtin(position) Position: vec4f,
@location(0) fragUV: vec2f,
@location(1) @interpolate(flat) TextureIndex: f32,
@location(2) @interpolate(flat) ColorBlend: vec3f
}
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
@group(0) @binding(3) var<storage, read> visibleCubes: array<Cube>;
@vertex
fn main(
@builtin(instance_index) instanceIndex: u32,
@location(0) position: vec4<f32>,
@location(1) uv: vec2<f32>
) -> VertexOutput {
let cube = visibleCubes[instanceIndex];
//cube.position.x = instance_index * 2;
var output: VertexOutput;
output.Position = uniforms.ViewProjectionMatrix * (position + vec4<f32>(cube.position, 0.0) + vec4<f32>(0.5, 0.0, 0.5, 0.0));
output.fragUV = uv;
output.TextureIndex = cube.textureIndex;
output.ColorBlend = cube.colorBlend;
return output;
}

View file

@ -0,0 +1,95 @@
export const cubeVertexSize = 4 * 5 // Byte size of one cube vertex.
export const cubePositionOffset = 0
//export const cubeColorOffset = 4 * 3 // Byte offset of cube vertex color attribute.
export const cubeUVOffset = 4 * 3
export const cubeVertexCount = 36
//@ts-format-ignore-region
export const cubeVertexArray = new Float32Array([
-0.5, -0.5, -0.5, 0.0, 0.0, // Bottom-let
0.5, -0.5, -0.5, 1.0, 0.0, // bottom-right
0.5, 0.5, -0.5, 1.0, 1.0, // top-right
0.5, 0.5, -0.5, 1.0, 1.0, // top-right
-0.5, 0.5, -0.5, 0.0, 1.0, // top-let
-0.5, -0.5, -0.5, 0.0, 0.0, // bottom-let
// ront ace
-0.5, -0.5, 0.5, 0.0, 0.0, // bottom-let
0.5, 0.5, 0.5, 1.0, 1.0, // top-right
0.5, -0.5, 0.5, 1.0, 0.0, // bottom-right
0.5, 0.5, 0.5, 1.0, 1.0, // top-right
-0.5, -0.5, 0.5, 0.0, 0.0, // bottom-let
-0.5, 0.5, 0.5, 0.0, 1.0, // top-let
// Let ace
-0.5, 0.5, 0.5, 1.0, 0.0, // top-right
-0.5, -0.5, -0.5, 0.0, 1.0, // bottom-let
-0.5, 0.5, -0.5, 1.0, 1.0, // top-let
-0.5, -0.5, -0.5, 0.0, 1.0, // bottom-let
-0.5, 0.5, 0.5, 1.0, 0.0, // top-right
-0.5, -0.5, 0.5, 0.0, 0.0, // bottom-right
// Right ace
0.5, 0.5, 0.5, 1.0, 0.0, // top-let
0.5, 0.5, -0.5, 1.0, 1.0, // top-right
0.5, -0.5, -0.5, 0.0, 1.0, // bottom-right
0.5, -0.5, -0.5, 0.0, 1.0, // bottom-right
0.5, -0.5, 0.5, 0.0, 0.0, // bottom-let
0.5, 0.5, 0.5, 1.0, 0.0, // top-let
// Bottom ace
-0.5, -0.5, -0.5, 0.0, 1.0, // top-right
0.5, -0.5, 0.5, 1.0, 0.0, // bottom-let
0.5, -0.5, -0.5, 1.0, 1.0, // top-let
0.5, -0.5, 0.5, 1.0, 0.0, // bottom-let
-0.5, -0.5, -0.5, 0.0, 1.0, // top-right
-0.5, -0.5, 0.5, 0.0, 0.0, // bottom-right
// Top ace
-0.5, 0.5, -0.5, 0.0, 1.0, // top-let
0.5, 0.5, -0.5, 1.0, 1.0, // top-right
0.5, 0.5, 0.5, 1.0, 0.0, // bottom-right
0.5, 0.5, 0.5, 1.0, 0.0, // bottom-right
-0.5, 0.5, 0.5, 0.0, 0.0, // bottom-let
-0.5, 0.5, -0.5, 0.0, 1.0// top-let˚
]);
export const cubeVertexArraySub = new Float32Array([
-0.5, -0.5, -0.5, 0.0, 0.0, // Bottom-let
0.5, -0.5, -0.5, 1.0, 0.0, // bottom-right
0.5, 0.5, -0.5, 1.0, 1.0, // top-right
0.5, 0.5, -0.5, 1.0, 1.0, // top-right
-0.5, 0.5, -0.5, 0.0, 1.0, // top-let
-0.5, -0.5, -0.5, 0.0, 0.0, // bottom-let
// ront ace
-0.5, -0.5, 0.5, 0.0, 0.0, // bottom-let
0.5, 0.5, 0.5, 1.0, 1.0, // top-right
0.5, -0.5, 0.5, 1.0, 0.0, // bottom-right
0.5, 0.5, 0.5, 1.0, 1.0, // top-right
-0.5, -0.5, 0.5, 0.0, 0.0, // bottom-let
-0.5, 0.5, 0.5, 0.0, 1.0, // top-let
// Let ace
-0.5, 0.5, 0.5, 1.0, 0.0, // top-right
-0.5, -0.5, -0.5, 0.0, 1.0, // bottom-let
-0.5, 0.5, -0.5, 1.0, 1.0, // top-let
-0.5, -0.5, -0.5, 0.0, 1.0, // bottom-let
-0.5, 0.5, 0.5, 1.0, 0.0, // top-right
-0.5, -0.5, 0.5, 0.0, 0.0, // bottom-right
// Right ace
0.5, 0.5, 0.5, 1.0, 0.0, // top-let
0.5, 0.5, -0.5, 1.0, 1.0, // top-right
0.5, -0.5, -0.5, 0.0, 1.0, // bottom-right
0.5, -0.5, -0.5, 0.0, 1.0, // bottom-right
0.5, -0.5, 0.5, 0.0, 0.0, // bottom-let
0.5, 0.5, 0.5, 1.0, 0.0, // top-let
// Bottom ace
-0.5, -0.5, -0.5, 0.0, 1.0, // top-right
0.5, -0.5, 0.5, 1.0, 0.0, // bottom-let
0.5, -0.5, -0.5, 1.0, 1.0, // top-let
0.5, -0.5, 0.5, 1.0, 0.0, // bottom-let
-0.5, -0.5, -0.5, 0.0, 1.0, // top-right
-0.5, -0.5, 0.5, 0.0, 0.0, // bottom-right
// Top ace
-0.5, 0.5, -0.5, 0.0, 1.0, // top-let
0.5, 0.5, -0.5, 1.0, 1.0, // top-right
0.5, 0.5, 0.5, 1.0, 0.0, // bottom-right
0.5, 0.5, 0.5, 1.0, 0.0, // bottom-right
-0.5, 0.5, 0.5, 0.0, 0.0, // bottom-let
-0.5, 0.5, -0.5, 0.0, 1.0, 5.0// top-let˚
]);

View file

@ -0,0 +1,69 @@
export type AnimationControlSwitches = {
tick: number
interpolationTick: number // next one
}
type Data = {
interpolate: boolean;
frametime: number;
frames: ({
index: number;
time: number;
} | number)[] | undefined;
};
export class TextureAnimation {
data: Data;
frameImages: number;
frameDelta: number;
frameTime: number;
framesToSwitch: number;
frameIndex: number;
constructor(public animationControl: AnimationControlSwitches, data: Data, public framesImages: number) {
this.data = {
interpolate: false,
frametime: 1,
...data
};
this.frameImages = 1;
this.frameDelta = 0;
this.frameTime = this.data.frametime * 50;
this.frameIndex = 0;
this.framesToSwitch = this.frameImages;
if (this.data.frames) {
this.framesToSwitch = this.data.frames.length;
}
}
step (deltaMs: number) {
this.frameDelta += deltaMs;
if (this.frameDelta > this.frameTime) {
this.frameDelta -= this.frameTime;
this.frameDelta %= this.frameTime;
this.frameIndex++;
this.frameIndex %= this.framesToSwitch;
const frames = this.data.frames.map(frame => typeof frame === 'number' ? { index: frame, time: this.data.frametime } : frame);
if (frames) {
let frame = frames[this.frameIndex]
let nextFrame = frames[(this.frameIndex + 1) % this.framesToSwitch];
this.animationControl.tick = frame.index;
this.animationControl.interpolationTick = nextFrame.index;
this.frameTime = frame.time * 50;
} else {
this.animationControl.tick = this.frameIndex;
this.animationControl.interpolationTick = (this.frameIndex + 1) % this.framesToSwitch;
}
}
if (this.data.interpolate) {
this.animationControl.interpolationTick = this.frameDelta / this.frameTime;
}
}
}

View file

@ -0,0 +1,63 @@
import React, { useEffect } from 'react'
import { LeftTouchArea, RightTouchArea, useInterfaceState } from '@dimaka/interface'
import { css } from '@emotion/css'
import { Viewer } from '../viewer/lib/viewer'
import { renderToDom } from '@zardoy/react-util'
import { Vec3 } from 'vec3'
import * as THREE from 'three'
declare const viewer: Viewer
const Controls = () => {
// todo setting
const usingTouch = navigator.maxTouchPoints > 0
useEffect(() => {
let vec3 = new Vec3(0, 0, 0)
setInterval(() => {
viewer.camera.position.add(new THREE.Vector3(vec3.x, vec3.y, vec3.z))
}, 1000 / 30)
useInterfaceState.setState({
isFlying: false,
uiCustomization: {
touchButtonSize: 40,
},
updateCoord ([coord, state]) {
vec3 = new Vec3(0, 0, 0)
vec3[coord] = state
}
})
}, [])
if (!usingTouch) return null
return (
<div
style={{ zIndex: 8 }}
className={css`
position: fixed;
inset: 0;
height: 100%;
display: flex;
width: 100%;
justify-content: space-between;
align-items: flex-end;
pointer-events: none;
touch-action: none;
& > div {
pointer-events: auto;
}
`}
>
<LeftTouchArea />
<div />
<RightTouchArea />
</div>
)
}
export const renderPlayground = () => {
renderToDom(<Controls />, {
// selector: 'body',
})
}

View file

@ -0,0 +1,35 @@
let rightOffset = 0
const stats = {}
export const addNewStat = (id: string, width = 80, x = rightOffset, y = 0) => {
const pane = document.createElement('div')
pane.id = 'fps-counter'
pane.style.position = 'fixed'
pane.style.top = `${y}px`
pane.style.right = `${x}px`
// gray bg
pane.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'
pane.style.color = 'white'
pane.style.padding = '2px'
pane.style.fontFamily = 'monospace'
pane.style.fontSize = '12px'
pane.style.zIndex = '10000'
pane.style.pointerEvents = 'none'
document.body.appendChild(pane)
stats[id] = pane
if (y === 0) { // otherwise it's a custom position
rightOffset += width
}
return {
updateText: (text: string) => {
pane.innerText = text
}
}
}
export const updateStatText = (id, text) => {
if (!stats[id]) return
stats[id].innerText = text
}

View file

@ -11,13 +11,22 @@ import JSZip from 'jszip'
import { TWEEN_DURATION } from '../viewer/lib/entities'
import { EntityMesh } from '../viewer/lib/entity/EntityMesh'
import blockstatesModels from 'mc-assets/dist/blockStatesModels.json'
// import * as Mathgl from 'math.gl'
import { initWebgpuRenderer, loadFixtureSides, setAnimationTick, webgpuChannel } from './webgpuRendererMain'
import { renderToDom } from '@zardoy/react-util'
globalThis.THREE = THREE
//@ts-ignore
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { toMajorVersion } from '../../src/utils'
import { renderPlayground } from './TouchControls2'
import { WorldRendererWebgpu } from '../viewer/lib/worldrendererWebgpu'
import { TextureAnimation } from './TextureAnimation'
import { BlockType } from './shared'
import { addNewStat } from './newStats'
const gui = new GUI()
const { updateText: updateTextEvent } = addNewStat('events', 90, 0, 40)
// initial values
const params = {
@ -40,7 +49,8 @@ const params = {
camera: '',
playSound () { },
blockIsomorphicRenderBundle () { },
modelVariant: 0
modelVariant: 0,
animationTick: 0
}
const qs = new URLSearchParams(window.location.search)
@ -60,10 +70,20 @@ const setQs = () => {
let ignoreResize = false
const enableControls = new URLSearchParams(window.location.search).get('controls') === 'true'
async function main () {
let continuousRender = false
const { version } = params
// const { version } = params
let fixtureUrl = qs.get('fixture')
let fixture: undefined | Record<string, any>
if (fixtureUrl) {
console.log('Loading fixture')
fixture = await fetch(fixtureUrl).then(r => r.json())
console.log('Loaded fixture')
}
const version = fixture?.version ?? '1.20.2'
// temporary solution until web worker is here, cache data for faster reloads
const globalMcData = window['mcData']
if (!globalMcData['version']) {
@ -94,6 +114,7 @@ async function main () {
gui.add(params, 'skipQs')
gui.add(params, 'playSound')
gui.add(params, 'blockIsomorphicRenderBundle')
const animationController = gui.add(params, 'animationTick', -1, 20, 1).listen()
gui.open(false)
let metadataFolder = gui.addFolder('metadata')
// let entityRotationFolder = gui.addFolder('entity metadata')
@ -125,25 +146,133 @@ async function main () {
return chunk
})
let stopUpdate = false
// let stopUpdate = true
// await schem.paste(world, new Vec3(0, 60, 0))
const worldView = new WorldDataEmitter(world, viewDistance, targetPos)
// Create three.js context, add to page
const renderer = new THREE.WebGLRenderer({ alpha: true, ...localStorage['renderer'] })
renderer.setPixelRatio(window.devicePixelRatio || 1)
renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(renderer.domElement)
// Create viewer
const viewer = new Viewer(renderer, { numWorkers: 1, showChunkBorders: false, })
const nullRenderer = new THREE.WebGLRenderer({ antialias: true })
const viewer = new Viewer(nullRenderer, { numWorkers: 1, showChunkBorders: false })
viewer.world.blockstatesModels = blockstatesModels
viewer.entities.setDebugMode('basic')
viewer.world.stopBlockUpdate = stopUpdate
viewer.setVersion(version)
viewer.entities.onSkinUpdate = () => {
viewer.render()
globalThis.viewer = viewer
await initWebgpuRenderer(() => { }, !enableControls && !fixture, true)
const simpleControls = () => {
let pressedKeys = new Set<string>()
const loop = () => {
// Create a vector that points in the direction the camera is looking
let direction = new THREE.Vector3(0, 0, 0)
if (pressedKeys.has('KeyW')) {
direction.z = -0.5
}
if (pressedKeys.has('KeyS')) {
direction.z += 0.5
}
if (pressedKeys.has('KeyA')) {
direction.x -= 0.5
}
if (pressedKeys.has('KeyD')) {
direction.x += 0.5
}
if (pressedKeys.has('ShiftLeft')) {
viewer.camera.position.y -= 0.5
}
if (pressedKeys.has('Space')) {
viewer.camera.position.y += 0.5
}
direction.applyQuaternion(viewer.camera.quaternion)
direction.y = 0
if (pressedKeys.has('ShiftLeft')) {
direction.y *= 2
direction.x *= 2
direction.z *= 2
}
// Add the vector to the camera's position to move the camera
viewer.camera.position.add(direction)
}
setInterval(loop, 1000 / 30)
const keys = (e) => {
const code = e.code
const pressed = e.type === 'keydown'
if (pressed) {
pressedKeys.add(code)
} else {
pressedKeys.delete(code)
}
}
window.addEventListener('keydown', keys)
window.addEventListener('keyup', keys)
window.addEventListener('blur', (e) => {
for (const key of pressedKeys) {
keys(new KeyboardEvent('keyup', { code: key }))
}
})
// mouse
const mouse = { x: 0, y: 0 }
let mouseMoveCounter = 0
const mouseMove = (e: PointerEvent) => {
if ((e.target as HTMLElement).closest('.lil-gui')) return
if (e.buttons === 1 || e.pointerType === 'touch') {
mouseMoveCounter++
viewer.camera.rotation.x -= e.movementY / 100
//viewer.camera.
viewer.camera.rotation.y -= e.movementX / 100
if (viewer.camera.rotation.x < -Math.PI / 2) viewer.camera.rotation.x = -Math.PI / 2
if (viewer.camera.rotation.x > Math.PI / 2) viewer.camera.rotation.x = Math.PI / 2
// yaw += e.movementY / 20;
// pitch += e.movementX / 20;
}
if (e.buttons === 2) {
viewer.camera.position.set(0, 0, 0)
}
}
setInterval(() => {
updateTextEvent(`Mouse Events: ${mouseMoveCounter}`)
mouseMoveCounter = 0
}, 1000)
window.addEventListener('pointermove', mouseMove)
}
viewer.world.mesherConfig.enableLighting = false
viewer.camera.position.set(0, 0, 8)
simpleControls()
renderPlayground()
const writeToIndexedDb = async (name, data) => {
const db = await window.indexedDB.open(name, 1)
db.onupgradeneeded = (e) => {
const db = (e.target as any).result
db.createObjectStore(name)
}
db.onsuccess = (e) => {
const db = (e.target as any).result
const tx = db.transaction(name, 'readwrite')
const store = tx.objectStore(name)
store.add(data, name)
}
}
if (fixture) {
loadFixtureSides(fixture.sides)
const pos = fixture.camera[0]
viewer.camera.position.set(pos[0], pos[1], pos[2])
}
let blocks: Record<string, BlockType> = {}
let i = 0
console.log('generating random data')
webgpuChannel.generateRandom(490_000)
return
// Create viewer
viewer.listen(worldView)
// Load chunks
@ -151,127 +280,17 @@ async function main () {
window['worldView'] = worldView
window['viewer'] = viewer
params.blockIsomorphicRenderBundle = () => {
const canvas = renderer.domElement
const onlyCurrent = !confirm('Ok - render all blocks, Cancel - render only current one')
const sizeRaw = prompt('Size', '512')
if (!sizeRaw) return
const size = parseInt(sizeRaw)
// const size = 512
ignoreResize = true
canvas.width = size
canvas.height = size
renderer.setSize(size, size)
//@ts-ignore
viewer.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 10)
viewer.scene.background = null
const rad = THREE.MathUtils.degToRad(-120)
viewer.directionalLight.position.set(
Math.cos(rad),
Math.sin(rad),
0.2
).normalize()
viewer.directionalLight.intensity = 1
const cameraPos = targetPos.offset(2, 2, 2)
const pitch = THREE.MathUtils.degToRad(-30)
const yaw = THREE.MathUtils.degToRad(45)
viewer.camera.rotation.set(pitch, yaw, 0, 'ZYX')
// viewer.camera.lookAt(center.x + 0.5, center.y + 0.5, center.z + 0.5)
viewer.camera.position.set(cameraPos.x + 1, cameraPos.y + 0.5, cameraPos.z + 1)
const allBlocks = mcData.blocksArray.map(b => b.name)
// const allBlocks = ['stone', 'warped_slab']
let blockCount = 1
let blockName = allBlocks[0]
const updateBlock = () => {
//@ts-ignore
// viewer.setBlockStateId(targetPos, mcData.blocksByName[blockName].minStateId)
params.block = blockName
// todo cleanup (introduce getDefaultState)
onUpdate.block()
applyChanges(false, true)
}
viewer.waitForChunksToRender().then(async () => {
// wait for next macro task
await new Promise(resolve => {
setTimeout(resolve, 0)
})
if (onlyCurrent) {
viewer.render()
onWorldUpdate()
} else {
// will be called on every render update
viewer.world.renderUpdateEmitter.addListener('update', onWorldUpdate)
updateBlock()
}
})
const zip = new JSZip()
zip.file('description.txt', 'Generated with prismarine-viewer')
const end = async () => {
// download zip file
const a = document.createElement('a')
const blob = await zip.generateAsync({ type: 'blob' })
const dataUrlZip = URL.createObjectURL(blob)
a.href = dataUrlZip
a.download = 'blocks_render.zip'
a.click()
URL.revokeObjectURL(dataUrlZip)
console.log('end')
viewer.world.renderUpdateEmitter.removeListener('update', onWorldUpdate)
}
async function onWorldUpdate () {
// await new Promise(resolve => {
// setTimeout(resolve, 50)
// })
const dataUrl = canvas.toDataURL('image/png')
zip.file(`${blockName}.png`, dataUrl.split(',')[1], { base64: true })
if (onlyCurrent) {
end()
} else {
nextBlock()
}
}
const nextBlock = async () => {
blockName = allBlocks[blockCount++]
console.log(allBlocks.length, '/', blockCount, blockName)
if (blockCount % 5 === 0) {
await new Promise(resolve => {
setTimeout(resolve, 100)
})
}
if (blockName) {
updateBlock()
} else {
end()
}
}
}
//@ts-ignore
const controls = new OrbitControls(viewer.camera, renderer.domElement)
controls.target.set(targetPos.x + 0.5, targetPos.y + 0.5, targetPos.z + 0.5)
// const controls = new OrbitControls(viewer.camera, nullRenderer.domElement)
// controls.target.set(targetPos.x + 0.5, targetPos.y + 0.5, targetPos.z + 0.5)
const cameraPos = targetPos.offset(2, 2, 2)
const pitch = THREE.MathUtils.degToRad(-45)
const yaw = THREE.MathUtils.degToRad(45)
viewer.camera.rotation.set(pitch, yaw, 0, 'ZYX')
viewer.camera.lookAt(targetPos.x + 0.5, targetPos.y + 0.5, targetPos.z + 0.5)
viewer.camera.position.set(cameraPos.x + 0.5, cameraPos.y + 0.5, cameraPos.z + 0.5)
controls.update()
// viewer.camera.lookAt(targetPos.x + 0.5, targetPos.y + 0.5, targetPos.z + 0.5)
viewer.camera.position.set(cameraPos.x, cameraPos.y, cameraPos.z)
// controls.update()
let blockProps = {}
let entityOverrides = {}
@ -302,6 +321,9 @@ async function main () {
}, TWEEN_DURATION)
}
params.block ||= 'stone'
let textureAnimation: TextureAnimation | undefined
const onUpdate = {
version (initialUpdate) {
if (initialUpdate) return
@ -381,6 +403,27 @@ async function main () {
},
modelVariant () {
viewer.world.mesherConfig.debugModelVariant = params.modelVariant === 0 ? undefined : [params.modelVariant]
},
animationTick () {
// TODO
const webgl = (viewer.world as WorldRendererWebgpu).playgroundGetWebglData() as unknown as { animation: any }
if (!webgl?.animation) {
setAnimationTick(0)
return
}
if (params.animationTick === -1) {
textureAnimation = new TextureAnimation(new Proxy({} as any, {
set (t, p, v) {
if (p === 'tick') {
setAnimationTick(v)
}
return true
}
}), webgl.animation, webgl.animation.framesCount)
} else {
setAnimationTick(params.animationTick)
textureAnimation = undefined
}
}
}
@ -418,7 +461,9 @@ async function main () {
if (object === params) {
if (property === 'camera') return
onUpdate[property]?.()
applyChanges(property === 'metadata')
if (property !== 'animationTick') {
applyChanges(property === 'metadata')
}
} else {
applyChanges()
}
@ -432,25 +477,35 @@ async function main () {
update(true)
}
applyChanges()
gui.openAnimated()
// gui.openAnimated()
})
const animate = () => {
const animate = () => { }
const animate2 = () => {
// if (controls) controls.update()
// worldView.updatePosition(controls.target)
viewer.render()
// window.requestAnimationFrame(animate)
window.requestAnimationFrame(animate2)
}
viewer.world.renderUpdateEmitter.addListener('update', () => {
animate()
// const frames = viewer.world.hasWithFrames ? viewer.world.hasWithFrames - 1 : 0;
const webgl = (viewer.world as WorldRendererWebgpu).playgroundGetWebglData()
// if (webgl?.animation) {
// params.animationTick = -1
// animationController.show()
// animationController.max(webgl.animation.framesCount)
// } else {
// animationController.hide()
// }
onUpdate.animationTick()
})
animate()
animate2()
// #region camera rotation param
if (params.camera) {
const [x, y] = params.camera.split(',')
viewer.camera.rotation.set(parseFloat(x), parseFloat(y), 0, 'ZYX')
controls.update()
// controls.update()
console.log(viewer.camera.rotation.x, parseFloat(x))
}
const throttledCamQsUpdate = _.throttle(() => {
@ -458,16 +513,16 @@ async function main () {
// params.camera = `${camera.rotation.x.toFixed(2)},${camera.rotation.y.toFixed(2)}`
setQs()
}, 200)
controls.addEventListener('change', () => {
throttledCamQsUpdate()
animate()
})
// controls.addEventListener('change', () => {
// throttledCamQsUpdate()
// animate()
// })
// #endregion
let time = performance.now()
const continuousUpdate = () => {
if (continuousRender) {
animate()
}
textureAnimation?.step(performance.now() - time)
time = performance.now()
requestAnimationFrame(continuousUpdate)
}
continuousUpdate()
@ -482,7 +537,7 @@ async function main () {
const { camera } = viewer
viewer.camera.aspect = window.innerWidth / window.innerHeight
viewer.camera.updateProjectionMatrix()
renderer.setSize(window.innerWidth, window.innerHeight)
nullRenderer.setSize(window.innerWidth, window.innerHeight)
animate()
}

View file

@ -0,0 +1,11 @@
export type BlockFaceType = {
side: number
textureIndex: number
textureName?: string
tint?: [number, number, number]
isTransparent?: boolean
}
export type BlockType = {
faces: BlockFaceType[]
}

View file

@ -0,0 +1,511 @@
import * as THREE from 'three';
import { BlockFaceType } from './shared';
import * as tweenJs from '@tweenjs/tween.js';
import { cubePositionOffset, cubeUVOffset, cubeVertexArray, cubeVertexCount, cubeVertexSize } from './CubeDef';
import VertShader from './Cube.vert.wgsl';
import FragShader from './Cube.frag.wgsl';
import ComputeShader from './Cube.comp.wgsl';
import { updateSize, allSides } from './webgpuRendererWorker';
export class WebgpuRenderer {
rendering = true
NUMBER_OF_CUBES = 490_000;
renderedFrames = 0
localStorage: any = {}
ready = false;
device: GPUDevice;
renderPassDescriptor: GPURenderPassDescriptor;
uniformBindGroup: GPUBindGroup;
UniformBuffer: GPUBuffer;
ViewUniformBuffer: GPUBuffer;
ProjectionUniformBuffer: GPUBuffer;
ctx: GPUCanvasContext;
verticesBuffer: GPUBuffer;
InstancedModelBuffer: GPUBuffer;
pipeline: GPURenderPipeline;
InstancedTextureIndexBuffer: GPUBuffer;
InstancedColorBuffer: GPUBuffer;
notRenderedAdditions = 0;
// Add these properties to the WebgpuRenderer class
computePipeline: GPUComputePipeline;
indirectDrawBuffer: GPUBuffer;
cubesBuffer: GPUBuffer;
visibleCubesBuffer: GPUBuffer;
computeBindGroup: GPUBindGroup;
computeBindGroupLayout: GPUBindGroupLayout;
indirectDrawParams: Uint32Array;
maxBufferSize: number
constructor(public canvas: HTMLCanvasElement, public imageBlob: ImageBitmapSource, public isPlayground: boolean, public camera: THREE.PerspectiveCamera) {
this.init();
}
async init () {
const { canvas, imageBlob, isPlayground, localStorage } = this;
updateSize(canvas.width, canvas.height);
const textureBitmap = await createImageBitmap(imageBlob);
const textureWidth = textureBitmap.width;
const textureHeight = textureBitmap.height;
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) throw new Error('WebGPU not supported');
this.device = await adapter.requestDevice();
const { device } = this;
this.maxBufferSize = device.limits.maxStorageBufferBindingSize;
this.renderedFrames = device.limits.maxComputeWorkgroupSizeX;
console.log('max buffer size', this.maxBufferSize / 1024 / 1024, 'MB')
const ctx = this.ctx = canvas.getContext('webgpu')!;
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
ctx.configure({
device,
format: presentationFormat,
alphaMode: 'premultiplied',
});
const verticesBuffer = device.createBuffer({
size: cubeVertexArray.byteLength,
usage: GPUBufferUsage.VERTEX,
mappedAtCreation: true,
});
this.verticesBuffer = verticesBuffer;
new Float32Array(verticesBuffer.getMappedRange()).set(cubeVertexArray);
verticesBuffer.unmap();
const pipeline = device.createRenderPipeline({
label: 'mainPipeline',
layout: 'auto',
vertex: {
module: device.createShaderModule({
code: localStorage.vertShader || VertShader,
}),
buffers: [
{
arrayStride: cubeVertexSize,
attributes: [
{
shaderLocation: 0,
offset: cubePositionOffset,
format: 'float32x3',
},
{
shaderLocation: 1,
offset: cubeUVOffset,
format: 'float32x2',
},
],
},
// {
// arrayStride: 3 * 4,
// attributes: [
// {
// shaderLocation: 2,
// offset: 0,
// format: 'float32x3',
// },
// ],
// stepMode: 'instance',
// },
// {
// arrayStride: 1 * 4,
// attributes: [
// {
// shaderLocation: 3,
// offset: 0,
// format: 'float32',
// },
// ],
// stepMode: 'instance',
// },
// {
// arrayStride: 3 * 4,
// attributes: [
// {
// shaderLocation: 4,
// offset: 0,
// format: 'float32x3',
// },
// ],
// stepMode: 'instance',
// },
],
},
fragment: {
module: device.createShaderModule({
code: localStorage.fragShader || FragShader,
}),
targets: [
{
format: presentationFormat,
blend: {
color: {
srcFactor: 'src-alpha',
dstFactor: 'one-minus-src-alpha',
operation: 'add',
},
alpha: {
srcFactor: 'src-alpha',
dstFactor: 'one-minus-src-alpha',
operation: 'add',
},
},
},
],
},
primitive: {
topology: 'triangle-list',
cullMode: 'front',
},
depthStencil: {
depthWriteEnabled: true,
depthCompare: 'less',
format: 'depth24plus',
},
});
this.pipeline = pipeline;
const depthTexture = device.createTexture({
size: [canvas.width, canvas.height],
format: 'depth24plus',
usage: GPUTextureUsage.RENDER_ATTACHMENT,
});
const uniformBufferSize = 4 * (4 * 4); // 4x4 matrix
this.UniformBuffer = device.createBuffer({
size: uniformBufferSize,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
// Fetch the image and upload it into a GPUTexture.
let cubeTexture: GPUTexture;
{
cubeTexture = device.createTexture({
size: [textureBitmap.width, textureBitmap.height, 1],
format: 'rgb10a2unorm',
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
});
device.queue.copyExternalImageToTexture(
{ source: textureBitmap },
{ texture: cubeTexture },
[textureBitmap.width, textureBitmap.height]
);
}
const sampler = device.createSampler({
magFilter: 'nearest',
minFilter: 'nearest',
});
this.renderPassDescriptor = {
label: 'MainRenderPassDescriptor',
colorAttachments: [
{
view: undefined as any, // Assigned later
clearValue: [0.6784313725490196, 0.8470588235294118, 0.9019607843137255, 1],
loadOp: 'clear',
storeOp: 'store',
},
],
depthStencilAttachment: {
view: depthTexture.createView(),
depthClearValue: 1,
depthLoadOp: 'clear',
depthStoreOp: 'store',
},
};
// Create compute pipeline
const computeShaderModule = device.createShaderModule({
code: localStorage.computeShader || ComputeShader,
label: 'Culled Instance',
});
const computeBindGroupLayout = device.createBindGroupLayout({
label: 'computeBindGroupLayout',
entries: [
{ binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'uniform' } },
{ binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } },
{ binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } },
{ binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } },
],
});
const computePipelineLayout = device.createPipelineLayout({
label: 'computePipelineLayout',
bindGroupLayouts: [computeBindGroupLayout],
})
this.computePipeline = device.createComputePipeline({
label: 'Culled Instance',
layout: computePipelineLayout,
// layout: 'auto',
compute: {
module: computeShaderModule,
entryPoint: 'main',
},
});
// Create buffers for compute shader and indirect drawing
this.cubesBuffer = device.createBuffer({
label: 'cubesBuffer',
size: this.NUMBER_OF_CUBES * 32, // 8 floats per cube - minimum buffer size
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
});
this.visibleCubesBuffer = device.createBuffer({
label: 'visibleCubesBuffer',
size: this.NUMBER_OF_CUBES * 32,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.VERTEX,
});
this.indirectDrawBuffer = device.createBuffer({
label: 'indirectDrawBuffer',
size: 16, // 4 uint32 values
usage: GPUBufferUsage.INDIRECT | GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
});
// Initialize indirect draw parameters
const indirectDrawParams = new Uint32Array([cubeVertexCount, 0, 0, 0]);
device.queue.writeBuffer(this.indirectDrawBuffer, 0, indirectDrawParams);
// const vertexBindGroupLayout = device.createBindGroupLayout({
// label: 'vertexBindGroupLayout',
// entries: [
// { binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: 'uniform' } },
// { binding: 2, visibility: GPUShaderStage.VERTEX, buffer: { type: 'read-only-storage' } } // Read-only storage
// ]
// });
// Create bind group for render pass
this.uniformBindGroup = device.createBindGroup({
label: 'uniformBindGroups',
//layout: vertexBindGroupLayout,
layout: pipeline.getBindGroupLayout(0),
entries: [
{
binding: 0,
resource: {
buffer: this.UniformBuffer,
},
},
{
binding: 1,
resource: sampler,
},
{
binding: 2,
resource: cubeTexture.createView(),
},
{
binding: 3,
resource: {
buffer: this.visibleCubesBuffer
}
}
],
});
// // Create bind group for compute shader
// this.computeBindGroupLayout = device.createBindGroupLayout({
// label: 'computeBindGroupLayout',
// entries: [
// {
// binding: 0,
// visibility: GPUShaderStage.COMPUTE,
// buffer: {
// type: 'uniform',
// },
// },
// {
// binding: 1,
// visibility: GPUShaderStage.COMPUTE,
// buffer: {
// type: 'storage',
// },
// },
// {
// binding: 2,
// visibility: GPUShaderStage.COMPUTE,
// buffer: {
// type: '',
// },
// },
// {
// binding: 3,
// visibility: GPUShaderStage.COMPUTE,
// buffer: {
// type: 'storage',
// },
// },
// ],
// });
this.computeBindGroup = device.createBindGroup({
//layout: this.computeBindGroupLayout,
layout: this.computePipeline.getBindGroupLayout(0),
label: 'computeBindGroup',
entries: [
{
binding: 0,
resource: { buffer: this.UniformBuffer },
},
{
binding: 1,
resource: { buffer: this.cubesBuffer },
},
{
binding: 2,
resource: { buffer: this.visibleCubesBuffer },
},
{
binding: 3,
resource: { buffer: this.indirectDrawBuffer },
},
],
})
this.indirectDrawParams = new Uint32Array([cubeVertexCount, 0, 0, 0]);
// always last!
this.rendering = false;
console.log('init finish')
this.updateSides()
this.loop();
this.ready = true;
return canvas;
}
removeOne () { }
realNumberOfCubes = 0;
updateSides (startOffset = 0) {
console.time('updateSides')
this.rendering = true;
const positions = [] as number[];
let textureIndexes = [] as number[];
let colors = [] as number[];
const blocksPerFace = {} as Record<string, BlockFaceType>;
for (const side of allSides.slice(startOffset)) {
if (!side) continue;
const [x, y, z] = side.slice(0, 3);
const key = `${x},${y},${z}`;
if (blocksPerFace[key]) continue;
blocksPerFace[key] = side[3];
}
for (const key in blocksPerFace) {
const side = key.split(',').map(Number);
positions.push(...[side[0], side[1], side[2]]);
const face = blocksPerFace[key];
textureIndexes.push(face.textureIndex);
colors.push(1, 1, 1);
}
const NUMBER_OF_CUBES_NEEDED = Math.ceil(positions.length / 3);
this.realNumberOfCubes = NUMBER_OF_CUBES_NEEDED;
if (NUMBER_OF_CUBES_NEEDED > this.NUMBER_OF_CUBES) {
console.warn('extending number of cubes', NUMBER_OF_CUBES_NEEDED, this.NUMBER_OF_CUBES)
this.NUMBER_OF_CUBES = NUMBER_OF_CUBES_NEEDED + 5000;
}
const BYTES_PER_ELEMENT = 8;
const cubeData = new Float32Array(this.NUMBER_OF_CUBES * BYTES_PER_ELEMENT);
for (let i = 0; i < this.NUMBER_OF_CUBES; i++) {
const offset = i * BYTES_PER_ELEMENT;
cubeData[offset] = positions[i * 3];
cubeData[offset + 1] = positions[i * 3 + 1];
cubeData[offset + 2] = positions[i * 3 + 2];
cubeData[offset + 3] = textureIndexes[i];
cubeData[offset + 4] = colors[i * 3];
cubeData[offset + 5] = colors[i * 3 + 1];
cubeData[offset + 6] = colors[i * 3 + 2];
//cubeData[offset + 7] = 0.5; // Sphere radius
}
console.time('writeCubes buffer')
this.device.queue.writeBuffer(this.cubesBuffer, 0, cubeData);
console.timeEnd('writeCubes buffer')
// Reset indirect draw parameters
// this.indirectDrawParams = new Uint32Array([cubeVertexCount, 0, 0, 0]);
//this.device.queue.writeBuffer(this.indirectDrawBuffer, 0, this.indirectDrawParams);
this.notRenderedAdditions++;
console.timeEnd('updateSides')
}
lastCall = performance.now();
logged = false;
loop () {
if (!this.rendering) {
requestAnimationFrame(() => this.loop());
return;
}
const { device, UniformBuffer: uniformBuffer, renderPassDescriptor, uniformBindGroup, pipeline, ctx, verticesBuffer } = this;
const now = Date.now();
tweenJs.update();
const ViewProjectionMat4 = new THREE.Matrix4();
this.camera.updateMatrix();
const projectionMatrix = this.camera.projectionMatrix;
ViewProjectionMat4.multiplyMatrices(projectionMatrix, this.camera.matrix.invert());
const ViewProjection = new Float32Array(ViewProjectionMat4.elements);
device.queue.writeBuffer(
uniformBuffer,
0,
ViewProjection
);
// const EmptyVisibleCubes = new Float32Array([36, 0, 0, 0]) ;
device.queue.writeBuffer(
this.indirectDrawBuffer, 0, this.indirectDrawParams);
renderPassDescriptor.colorAttachments[0].view = ctx
.getCurrentTexture()
.createView();
let commandEncoder = device.createCommandEncoder();
// Compute pass for occlusion culling
commandEncoder.label = "Main Comand Encoder"
const computePass = commandEncoder.beginComputePass();
computePass.label = "ComputePass"
computePass.setPipeline(this.computePipeline);
//computePass.setBindGroup(0, this.uniformBindGroup);
computePass.setBindGroup(0, this.computeBindGroup);
computePass.dispatchWorkgroups(Math.ceil(this.NUMBER_OF_CUBES / 256));
computePass.end();
device.queue.submit([commandEncoder.finish()]);
commandEncoder = device.createCommandEncoder();
//device.queue.submit([commandEncoder.finish()]);
// Render pass
//console.log(this.indirectDrawBuffer.getMappedRange());
const renderPass = commandEncoder.beginRenderPass(this.renderPassDescriptor);
renderPass.label = "RenderPass"
renderPass.setPipeline(pipeline);
renderPass.setBindGroup(0, this.uniformBindGroup);
renderPass.setVertexBuffer(0, verticesBuffer);
// Use indirect drawing
renderPass.drawIndirect(this.indirectDrawBuffer, 0);
renderPass.end();
device.queue.submit([commandEncoder.finish()]);
this.renderedFrames++;
requestAnimationFrame(() => this.loop());
this.notRenderedAdditions = 0;
}
}

View file

@ -0,0 +1,204 @@
import { generateSpiralMatrix } from 'flying-squid/dist/utils'
import { Viewer } from '../viewer/lib/viewer'
import { addNewStat } from './newStats'
import type { workerProxyType } from './webgpuRendererWorker'
import { useWorkerProxy } from './workerProxy'
import { MesherGeometryOutput } from '../viewer/lib/mesher/shared'
import { pickObj } from '@zardoy/utils'
let worker: Worker
export let webgpuChannel: typeof workerProxyType['__workerProxy'] = new Proxy({}, {
get: () => () => { }
}) as any // placeholder to avoid crashes
declare const viewer: Viewer
let allReceived = false
declare const customEvents
declare const bot
if (typeof customEvents !== 'undefined') {
customEvents.on('gameLoaded', () => {
const chunksExpected = generateSpiralMatrix(globalThis.options.renderDistance)
let received = 0
bot.on('chunkColumnLoad', (data) => {
received++
if (received === chunksExpected.length) {
allReceived = true
// addBlocksSection('all', viewer.world.newChunks)
}
})
})
}
let isWaitingToUpload = false
globalThis.tiles = {}
export const addBlocksSection = (key, data: MesherGeometryOutput) => {
if (globalThis.tiles[key]) return
globalThis.tiles[key] = data.tiles
webgpuChannel.addBlocksSection(data.tiles, key, false)
if (playground && !isWaitingToUpload) {
isWaitingToUpload = true
// viewer.waitForChunksToRender().then(() => {
// isWaitingToUpload = false
// sendWorkerMessage({
// type: 'addBlocksSectionDone'
// })
// })
}
}
export const loadFixtureSides = (json) => {
webgpuChannel.loadFixture(json)
}
export const sendCameraToWorker = () => {
const cameraVectors = ['rotation', 'position'].reduce((acc, key) => {
acc[key] = ['x', 'y', 'z'].reduce((acc2, key2) => {
acc2[key2] = viewer.camera[key][key2]
return acc2
}, {})
return acc
}, {}) as any
webgpuChannel.camera({
...cameraVectors,
fov: viewer.camera.fov
})
}
export const removeBlocksSection = (key) => {
webgpuChannel.removeBlocksSection(key)
}
let playground = false
export const initWebgpuRenderer = async (postRender = () => { }, playgroundModeInWorker = false, actuallyPlayground = false) => {
playground = actuallyPlayground
await new Promise(resolve => {
// console.log('viewer.world.material.map!.image', viewer.world.material.map!.image)
// viewer.world.material.map!.image.onload = () => {
// console.log(this.material.map!.image)
// resolve()
// }
viewer.world.renderUpdateEmitter.once('textureDownloaded', resolve)
})
const image = viewer.world.material.map!.image
const imageBlob = await fetch(image.src).then((res) => res.blob())
const canvas = document.createElement('canvas')
canvas.width = window.innerWidth * window.devicePixelRatio
canvas.height = window.innerHeight * window.devicePixelRatio
document.body.appendChild(canvas)
canvas.id = 'viewer-canvas'
console.log('starting offscreen')
const offscreen = canvas.transferControlToOffscreen()
// replacable by initWebglRenderer
worker = new Worker('./webgpuRendererWorker.js')
addFpsCounters()
webgpuChannel = useWorkerProxy<typeof workerProxyType>(worker, true)
webgpuChannel.canvas(offscreen, imageBlob, playgroundModeInWorker, pickObj(localStorage, 'vertShader', 'fragShader', 'computeShader'))
let oldWidth = window.innerWidth
let oldHeight = window.innerHeight
let oldCamera = {
position: { x: 0, y: 0, z: 0 },
rotation: { x: 0, y: 0, z: 0 }
}
let focused = true
window.addEventListener('focus', () => {
focused = true
webgpuChannel.startRender()
})
window.addEventListener('blur', () => {
focused = false
webgpuChannel.stopRender()
})
const mainLoop = () => {
requestAnimationFrame(mainLoop)
//@ts-ignore
if (!focused || window.stopRender) return
if (oldWidth !== window.innerWidth || oldHeight !== window.innerHeight) {
oldWidth = window.innerWidth
oldHeight = window.innerHeight
webgpuChannel.resize(window.innerWidth * window.devicePixelRatio, window.innerHeight * window.devicePixelRatio)
}
postRender()
// TODO! do it in viewer to avoid possible delays
if (actuallyPlayground && ['rotation', 'position'].some((key) => oldCamera[key] !== viewer.camera[key])) {
// TODO fix
for (const [key, val] of Object.entries(oldCamera)) {
for (const key2 of Object.keys(val)) {
oldCamera[key][key2] = viewer.camera[key][key2]
}
}
sendCameraToWorker()
}
}
requestAnimationFrame(mainLoop)
}
export const setAnimationTick = (tick: number, frames?: number) => {
webgpuChannel.animationTick(tick, frames)
}
export const exportLoadedTiles = () => {
webgpuChannel.exportData()
const controller = new AbortController()
worker.addEventListener('message', async (e) => {
const receivedData = e.data.data
console.log('received fixture')
// await new Promise(resolve => {
// setTimeout(resolve, 0)
// })
try {
const a = document.createElement('a')
type Vec3 = [number, number, number]
type PlayTimeline = [pos: Vec3, rot: Vec3, time: number]
const vec3ToArr = (vec3: { x, y, z }) => [vec3.x, vec3.y, vec3.z] as Vec3
// const dataObj = {
// ...receivedData,
// version: viewer.version,
// camera: [vec3ToArr(viewer.camera.position), vec3ToArr(viewer.camera.rotation)],
// playTimeline: [] as PlayTimeline[]
// }
// split into two chunks
const objectURL = URL.createObjectURL(new Blob([receivedData.sides.buffer], { type: 'application/octet-stream' }))
a.href = objectURL
a.download = 'dumped-chunks-tiles.bin'
a.click()
URL.revokeObjectURL(objectURL)
} finally {
controller.abort()
}
}, { signal: controller.signal })
}
const addFpsCounters = () => {
const { updateText } = addNewStat('fps')
let prevTimeout
worker.addEventListener('message', (e) => {
if (e.data.type === 'fps') {
updateText(`FPS: ${e.data.fps}`)
if (prevTimeout) clearTimeout(prevTimeout)
prevTimeout = setTimeout(() => {
updateText('<hanging>')
}, 1002)
}
})
const { updateText: updateText2 } = addNewStat('fps-main', 90, 0, 20)
let updates = 0
const mainLoop = () => {
requestAnimationFrame(mainLoop)
updates++
}
mainLoop()
setInterval(() => {
updateText2(`Main Loop: ${updates}`)
updates = 0
}, 1000)
}

View file

@ -0,0 +1,241 @@
/// <reference types="@webgpu/types" />
import * as THREE from 'three'
import { BlockFaceType, BlockType } from './shared'
import * as tweenJs from '@tweenjs/tween.js'
//@ts-ignore
//@ts-ignore
import { createWorkerProxy } from './workerProxy'
import { WebgpuRenderer } from './webgpuRenderer'
export let allSides = [] as ([number, number, number, BlockFaceType] | undefined)[]
globalThis.allSides = allSides
let allSidesAdded = 0
let needsSidesUpdate = false
let chunksArrIndexes = {}
let freeArrayIndexes = [] as [number, number][]
let sidePositions
let lastNotUpdatedIndex
let lastNotUpdatedArrSize
let animationTick = 0
const camera = new THREE.PerspectiveCamera(75, 1 / 1, 0.1, 10_000)
globalThis.camera = camera
let webgpuRenderer: WebgpuRenderer | undefined
setInterval(() => {
if (!webgpuRenderer) return
// console.log('FPS:', renderedFrames)
postMessage({ type: 'fps', fps: webgpuRenderer.renderedFrames })
webgpuRenderer.renderedFrames = 0
}, 1000)
export const updateSize = (width, height) => {
camera.aspect = width / height
camera.updateProjectionMatrix()
}
let fullReset
const updateCubesWhenAvailable = (pos) => {
if (webgpuRenderer?.ready) {
webgpuRenderer.updateSides(pos)
} else {
setTimeout(updateCubesWhenAvailable, 100)
}
}
let started = false
let newWidth: number | undefined
let newHeight: number | undefined
let autoTickUpdate = undefined as number | undefined
export const workerProxyType = createWorkerProxy({
canvas (canvas, imageBlob, isPlayground, localStorage) {
started = true
webgpuRenderer = new WebgpuRenderer(canvas, imageBlob, isPlayground, camera)
webgpuRenderer.localStorage
globalThis.webgpuRenderer = webgpuRenderer
},
startRender () {
if (!webgpuRenderer) return
webgpuRenderer.rendering = true
},
stopRender () {
if (!webgpuRenderer) return
webgpuRenderer.rendering = false
},
resize (newWidth, newHeight) {
newWidth = newWidth
newHeight = newHeight
updateSize(newWidth, newHeight)
},
generateRandom (count: number) {
const square = Math.sqrt(count)
if (square % 1 !== 0) throw new Error('square must be a whole number')
const blocks = {}
const getFace = (face: number) => {
return {
side: face,
textureIndex: Math.floor(Math.random() * 512)
}
}
for (let x = 0; x < square; x++) {
for (let z = 0; z < square; z++) {
blocks[`${x},${0},${z}`] = {
faces: [
getFace(0),
getFace(1),
getFace(2),
getFace(3),
getFace(4),
getFace(5)
],
}
}
}
console.log('data ready')
this.addBlocksSection(blocks, `0,0,0`)
},
addBlocksSection (tiles: Record<string, BlockType>, key: string, update = true) {
const currentLength = allSides.length
// in: object - name, out: [x, y, z, name]
const newData = Object.entries(tiles).flatMap(([key, value]) => {
const [x, y, z] = key.split(',').map(Number)
const block = value as BlockType
return block.faces.map((side) => {
return [x, y, z, side] as [number, number, number, BlockFaceType]
})
})
// find freeIndexes if possible
const freeArea = freeArrayIndexes.find(([startIndex, endIndex]) => endIndex - startIndex >= newData.length)
if (freeArea) {
const [startIndex, endIndex] = freeArea
allSides.splice(startIndex, newData.length, ...newData)
lastNotUpdatedIndex ??= startIndex
const freeAreaIndex = freeArrayIndexes.indexOf(freeArea)
freeArrayIndexes[freeAreaIndex] = [startIndex + newData.length, endIndex]
if (freeArrayIndexes[freeAreaIndex][0] >= freeArrayIndexes[freeAreaIndex][1]) {
freeArrayIndexes.splice(freeAreaIndex, 1)
// todo merge
}
lastNotUpdatedArrSize = newData.length
console.log('using free area', freeArea)
}
chunksArrIndexes[key] = [currentLength, currentLength + newData.length]
let i = 0;
while (i < newData.length) {
allSides.splice(currentLength + i, 0, ...newData.slice(i, i + 1024));
i += 1024;
}
lastNotUpdatedIndex ??= currentLength
// if (webglRendererWorker && webglRendererWorker.notRenderedAdditions < 5) {
if (update) {
updateCubesWhenAvailable(currentLength)
}
},
addBlocksSectionDone () {
updateCubesWhenAvailable(lastNotUpdatedIndex)
lastNotUpdatedIndex = undefined
lastNotUpdatedArrSize = undefined
},
removeBlocksSection (key) {
return
// fill data with 0
const [startIndex, endIndex] = chunksArrIndexes[key]
for (let i = startIndex; i < endIndex; i++) {
allSides[i] = undefined
}
lastNotUpdatedArrSize = endIndex - startIndex
updateCubesWhenAvailable(startIndex)
// freeArrayIndexes.push([startIndex, endIndex])
// // merge freeArrayIndexes TODO
// if (freeArrayIndexes.at(-1)[0] === freeArrayIndexes.at(-2)?.[1]) {
// const [startIndex, endIndex] = freeArrayIndexes.pop()!
// const [startIndex2, endIndex2] = freeArrayIndexes.pop()!
// freeArrayIndexes.push([startIndex2, endIndex])
// }
},
camera (newCam: { rotation: { x: number, y: number, z: number }, position: { x: number, y: number, z: number }, fov: number }) {
// if (webgpuRenderer?.isPlayground) {
// camera.rotation.order = 'ZYX'
// new tweenJs.Tween(camera.rotation).to({ x: newCam.rotation.x, y: newCam.rotation.y, z: newCam.rotation.z }, 50).start()
// } else {
camera.rotation.set(newCam.rotation.x, newCam.rotation.y, newCam.rotation.z, 'ZYX')
// }
if (newCam.position.x === 0 && newCam.position.y === 0 && newCam.position.z === 0) {
// initial camera position
camera.position.set(newCam.position.x, newCam.position.y, newCam.position.z)
} else {
new tweenJs.Tween(camera.position).to({ x: newCam.position.x, y: newCam.position.y, z: newCam.position.z }, 50).start()
}
if (newCam.fov !== camera.fov) {
camera.fov = newCam.fov
camera.updateProjectionMatrix()
}
},
animationTick (frames, tick) {
if (frames <= 0) {
autoTickUpdate = undefined
animationTick = 0
return
}
if (tick === -1) {
autoTickUpdate = frames
} else {
autoTickUpdate = undefined
animationTick = tick % 20 // todo update automatically in worker
}
},
fullReset () {
fullReset()
},
exportData () {
const exported = exportData()
postMessage({ type: 'exportData', data: exported }, undefined as any, [exported.sides.buffer])
},
loadFixture (json) {
// allSides = json.map(([x, y, z, face, textureIndex]) => {
// return [x, y, z, { face, textureIndex }] as [number, number, number, BlockFaceType]
// })
const dataSize = json.length / 5
for (let i = 0; i < json.length; i += 5) {
allSides.push([json[i], json[i + 1], json[i + 2], { side: json[i + 3], textureIndex: json[i + 4] }])
}
updateCubesWhenAvailable(0)
},
})
globalThis.testDuplicates = () => {
const duplicates = allSides.filter((value, index, self) => self.indexOf(value) !== index)
console.log('duplicates', duplicates)
}
const exportData = () => {
// Calculate the total length of the final array
const totalLength = allSides.length * 5
// Create a new Int16Array with the total length
const flatData = new Int16Array(totalLength)
// Fill the flatData array
for (let i = 0; i < allSides.length; i++) {
const sideData = allSides[i]
if (!sideData) continue
const [x, y, z, side] = sideData
flatData.set([x, y, z, side.side, side.textureIndex], i * 5)
}
return { sides: flatData }
}
setInterval(() => {
if (autoTickUpdate) {
animationTick = (animationTick + 1) % autoTickUpdate
}
}, 1000 / 20)

View file

@ -0,0 +1,58 @@
export function createWorkerProxy<T extends Record<string, (...args: any[]) => void>> (handlers: T): { __workerProxy: T } {
addEventListener('message', (event) => {
const { type, args } = event.data
if (handlers[type]) {
handlers[type](...args)
}
})
return null as any
}
/**
* in main thread
* ```ts
* // either:
* import type { importedTypeWorkerProxy } from './worker'
* // or:
* type importedTypeWorkerProxy = import('./worker').importedTypeWorkerProxy
*
* const workerChannel = useWorkerProxy<typeof importedTypeWorkerProxy>(worker)
* ```
*/
export const useWorkerProxy = <T extends { __workerProxy: Record<string, (...args: any[]) => void> }> (worker: Worker, autoTransfer = true): T['__workerProxy'] & {
transfer: (...args: Transferable[]) => T['__workerProxy']
} => {
// in main thread
return new Proxy({} as any, {
get: (target, prop) => {
if (prop === 'transfer') return (...transferable: Transferable[]) => {
return new Proxy({}, {
get: (target, prop) => {
return (...args: any[]) => {
worker.postMessage({
type: prop,
args,
}, transferable)
}
}
})
}
return (...args: any[]) => {
const transfer = autoTransfer ? args.filter(arg => arg instanceof ArrayBuffer || arg instanceof MessagePort || arg instanceof ImageBitmap || arg instanceof OffscreenCanvas || arg instanceof ImageData) : []
worker.postMessage({
type: prop,
args,
}, transfer)
}
}
})
}
// const workerProxy = createWorkerProxy({
// startRender (canvas: HTMLCanvasElement) {
// },
// })
// const worker = useWorkerProxy(null, workerProxy)
// worker.

9
prismarine-viewer/globals.d.ts vendored Normal file
View file

@ -0,0 +1,9 @@
type StringKeys<T extends object> = Extract<keyof T, string>
interface ObjectConstructor {
keys<T extends object> (obj: T): Array<StringKeys<T>>
entries<T extends object> (obj: T): Array<[StringKeys<T>, T[keyof T]]>
// todo review https://stackoverflow.com/questions/57390305/trying-to-get-fromentries-type-right
fromEntries<T extends Array<[string, any]>> (obj: T): Record<T[number][0], T[number][1]>
assign<T extends Record<string, any>, K extends Record<string, any>> (target: T, source: K): asserts target is T & K
}

View file

@ -16,6 +16,7 @@
},
"dependencies": {
"@tweenjs/tween.js": "^20.0.3",
"live-server": "^1.2.2",
"assert": "^2.0.0",
"buffer": "^6.0.3",
"filesize": "^10.0.12",

View file

@ -7,6 +7,8 @@
html {
overflow: hidden;
background: black;
user-select: none;
touch-action: none;
}
html, body {
@ -31,6 +33,7 @@
</style>
</head>
<body>
<div id="root"></div>
<script type="text/javascript" src="playground.js"></script>
</body>
</html>

View file

@ -0,0 +1,3 @@
export const sharedPlaygroundMainOptions = {
alias: {}
}

View file

@ -84,7 +84,7 @@ const handleMessage = data => {
}
if (data.type === 'mesherData') {
setMesherData(data.blockstatesModels, data.blocksAtlas)
setMesherData(data.blockstatesModels, data.blocksAtlas, data.config.outputFormat === 'webgpu')
allDataReady = true
} else if (data.type === 'dirty') {
const loc = new Vec3(data.x, data.y, data.z)
@ -128,7 +128,7 @@ setInterval(() => {
const chunk = world.getColumn(x, z)
if (chunk?.getSection(new Vec3(x, y, z))) {
const geometry = getSectionGeometry(x, y, z, world)
const transferable = [geometry.positions.buffer, geometry.normals.buffer, geometry.colors.buffer, geometry.uvs.buffer]
const transferable = [geometry.positions?.buffer, geometry.normals?.buffer, geometry.colors?.buffer, geometry.uvs?.buffer].filter(Boolean)
//@ts-ignore
postMessage({ type: 'geometry', key, geometry }, transferable)
} else {

View file

@ -6,6 +6,8 @@ import worldBlockProvider, { WorldBlockProvider } from 'mc-assets/dist/worldBloc
import { BlockElement, buildRotationMatrix, elemFaces, matmul3, matmulmat3, vecadd3, vecsub3 } from './modelsGeometryCommon'
let blockProvider: WorldBlockProvider
import { BlockType } from '../../../examples/shared'
import { MesherGeometryOutput } from './shared'
const tints: any = {}
let needTiles = false
@ -20,6 +22,19 @@ for (const key of Object.keys(tintsData)) {
tints[key] = prepareTints(tintsData[key])
}
type TestTileData = {
block: string
faces: {
face: string
neighbor: string
light?: number
}[]
}
type Tiles = {
[blockPos: string]: BlockType & TestTileData
}
function prepareTints (tints) {
const map = new Map()
const defaultValue = tintToGl(tints.default)
@ -104,18 +119,23 @@ function getLiquidRenderHeight (world, block, type, pos) {
return ((block.metadata >= 8 ? 8 : 7 - block.metadata) + 1) / 9
}
const everyArray = (array, callback) => {
if (!array?.length) return false
return array.every(callback)
}
const isCube = (block: Block) => {
const isCube = (block) => {
if (!block || block.transparent) return false
if (block.isCube) return true
if (!block.models?.length || block.models.length !== 1) return false
// all variants
return block.models[0].every(v => v.elements!.every(e => {
// TODO!
// if (!block.variant) block.variant = getModelVariants(block)
if (!block.variant?.length) return false
return block.variant.every(v => everyArray(v?.model?.elements, e => {
return e.from[0] === 0 && e.from[1] === 0 && e.from[2] === 0 && e.to[0] === 16 && e.to[1] === 16 && e.to[2] === 16
}))
}
function renderLiquid (world: World, cursor: Vec3, texture: any | undefined, type: number, biome: string, water: boolean, attr: Record<string, any>) {
function renderLiquid (world, cursor, texture, type, biome, water, attr) {
const heights: number[] = []
for (let z = -1; z <= 1; z++) {
for (let x = -1; x <= 1; x++) {
@ -134,12 +154,12 @@ function renderLiquid (world: World, cursor: Vec3, texture: any | undefined, typ
const { dir, corners } = elemFaces[face]
const isUp = dir[1] === 1
const neighborPos = cursor.offset(...dir as [number, number, number])
const neighborPos = cursor.offset(...dir)
const neighbor = world.getBlock(neighborPos)
if (!neighbor) continue
if (neighbor.type === type) continue
const isGlass = neighbor.name.includes('glass')
if ((isCube(neighbor) && !isUp) || neighbor.getProperties().waterlogged) continue
if ((isCube(neighbor) && !isUp) || neighbor.material === 'plant' || neighbor.getProperties().waterlogged) continue
let tint = [1, 1, 1]
if (water) {
@ -151,11 +171,12 @@ function renderLiquid (world: World, cursor: Vec3, texture: any | undefined, typ
}
if (needTiles) {
attr.tiles[`${cursor.x},${cursor.y},${cursor.z}`] ??= {
const tiles = attr.tiles as Tiles
tiles[`${cursor.x},${cursor.y},${cursor.z}`] ??= {
block: 'water',
faces: [],
}
attr.tiles[`${cursor.x},${cursor.y},${cursor.z}`].faces.push({
tiles[`${cursor.x},${cursor.y},${cursor.z}`].faces.push({
face,
neighbor: `${neighborPos.x},${neighborPos.y},${neighborPos.z}`,
// texture: eFace.texture.name,
@ -182,7 +203,7 @@ function renderLiquid (world: World, cursor: Vec3, texture: any | undefined, typ
let needRecompute = false
function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO: boolean, attr: Record<string, any>, globalMatrix: any, globalShift: any, block: Block, biome: string) {
function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO: boolean, attr: MesherGeometryOutput, globalMatrix: any, globalShift: any, block: Block, biome: string) {
const position = cursor
// const key = `${position.x},${position.y},${position.z}`
// if (!globalThis.allowedBlocks.includes(key)) return
@ -190,7 +211,7 @@ function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO:
for (const face in element.faces) {
const eFace = element.faces[face]
const { corners, mask1, mask2 } = elemFaces[face]
const { corners, mask1, mask2, side } = elemFaces[face]
const dir = matmul3(globalMatrix, elemFaces[face].dir)
if (eFace.cullface) {
@ -322,17 +343,24 @@ function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO:
attr.colors.push(baseLight * tint[0] * light, baseLight * tint[1] * light, baseLight * tint[2] * light)
}
const lightWithColor = [baseLight * tint[0], baseLight * tint[1], baseLight * tint[2]]
if (needTiles) {
attr.tiles[`${cursor.x},${cursor.y},${cursor.z}`] ??= {
const tiles = attr.tiles as Tiles
tiles[`${cursor.x},${cursor.y},${cursor.z}`] ??= {
block: block.name,
faces: [],
}
attr.tiles[`${cursor.x},${cursor.y},${cursor.z}`].faces.push({
tiles[`${cursor.x},${cursor.y},${cursor.z}`].faces.push({
face,
side,
textureIndex: eFace.texture.tileIndex,
neighbor: `${neighborPos.x},${neighborPos.y},${neighborPos.z}`,
light: baseLight
// texture: eFace.texture.name,
})
light: baseLight,
// color: lightWithColor,
//@ts-ignore debug prop
texture: eFace.texture.debugName || block.name,
} satisfies BlockType['faces'][number] & TestTileData['faces'][number] as any)
}
if (doAO && aos[0] + aos[3] >= aos[1] + aos[2]) {
@ -357,7 +385,7 @@ let unknownBlockModel: BlockModelPartsResolved
export function getSectionGeometry (sx, sy, sz, world: World) {
let delayedRender = [] as (() => void)[]
const attr = {
const attr: MesherGeometryOutput = {
sx: sx + 8,
sy: sy + 8,
sz: sz + 8,
@ -373,7 +401,7 @@ export function getSectionGeometry (sx, sy, sz, world: World) {
tiles: {},
// todo this can be removed here
signs: {}
} as Record<string, any>
}
const cursor = new Vec3(0, 0, 0)
for (cursor.y = sy; cursor.y < sy + 16; cursor.y++) {
@ -492,7 +520,7 @@ export function getSectionGeometry (sx, sy, sz, world: World) {
delayedRender = []
let ndx = attr.positions.length / 3
for (let i = 0; i < attr.t_positions.length / 12; i++) {
for (let i = 0; i < attr.t_positions!.length / 12; i++) {
attr.indices.push(
ndx, ndx + 1, ndx + 2,
ndx + 2, ndx + 1, ndx + 3,
@ -503,10 +531,10 @@ export function getSectionGeometry (sx, sy, sz, world: World) {
ndx += 4
}
attr.positions.push(...attr.t_positions)
attr.normals.push(...attr.t_normals)
attr.colors.push(...attr.t_colors)
attr.uvs.push(...attr.t_uvs)
attr.positions.push(...attr.t_positions!)
attr.normals.push(...attr.t_normals!)
attr.colors.push(...attr.t_colors!)
attr.uvs.push(...attr.t_uvs!)
delete attr.t_positions
delete attr.t_normals
@ -518,6 +546,13 @@ export function getSectionGeometry (sx, sy, sz, world: World) {
attr.colors = new Float32Array(attr.colors) as any
attr.uvs = new Float32Array(attr.uvs) as any
if (needTiles) {
delete attr.positions
delete attr.normals
delete attr.colors
delete attr.uvs
}
return attr
}

View file

@ -74,6 +74,7 @@ export function matmulmat3 (a, b) {
export const elemFaces = {
up: {
side: 0,
dir: [0, 1, 0],
mask1: [1, 1, 0],
mask2: [0, 1, 1],
@ -85,6 +86,7 @@ export const elemFaces = {
]
},
down: {
side: 1,
dir: [0, -1, 0],
mask1: [1, 1, 0],
mask2: [0, 1, 1],
@ -96,6 +98,7 @@ export const elemFaces = {
]
},
east: {
side: 2,
dir: [1, 0, 0],
mask1: [1, 1, 0],
mask2: [1, 0, 1],
@ -107,6 +110,7 @@ export const elemFaces = {
]
},
west: {
side: 3,
dir: [-1, 0, 0],
mask1: [1, 1, 0],
mask2: [1, 0, 1],
@ -118,6 +122,7 @@ export const elemFaces = {
]
},
north: {
side: 4,
dir: [0, 0, -1],
mask1: [1, 0, 1],
mask2: [0, 1, 1],
@ -129,6 +134,7 @@ export const elemFaces = {
]
},
south: {
side: 5,
dir: [0, 0, 1],
mask1: [1, 0, 1],
mask2: [0, 1, 1],

View file

@ -1,11 +1,32 @@
import { BlockType } from '../../../examples/shared'
export const defaultMesherConfig = {
version: '',
enableLighting: true,
skyLight: 15,
smoothLighting: true,
outputFormat: 'threeJs' as 'threeJs' | 'webgl',
outputFormat: 'threeJs' as 'threeJs' | 'webgpu',
textureSize: 1024, // for testing
debugModelVariant: undefined as undefined | number[]
}
export type MesherConfig = typeof defaultMesherConfig
export type MesherGeometryOutput = {
sx: number,
sy: number,
sz: number,
// resulting: float32array
positions: any,
normals: any,
colors: any,
uvs: any,
t_positions?: number[],
t_normals?: number[],
t_colors?: number[],
t_uvs?: number[],
indices: number[],
tiles: Record<string, BlockType>,
signs: Record<string, any>,
}

View file

@ -2,7 +2,7 @@
const THREE = require('three')
const textureCache = {}
function loadTexture (texture, cb, onLoad) {
function loadTexture(texture, cb, onLoad) {
const cached = textureCache[texture]
if (!cached) {
textureCache[texture] = new THREE.TextureLoader().load(texture, onLoad)
@ -11,7 +11,7 @@ function loadTexture (texture, cb, onLoad) {
if (cached) onLoad?.()
}
function loadJSON (url, callback) {
function loadJSON(url, callback) {
const xhr = new XMLHttpRequest()
xhr.open('GET', url, true)
xhr.responseType = 'json'

View file

@ -1,11 +1,13 @@
import * as THREE from 'three'
import { Vec3 } from 'vec3'
import { WorldRendererWebgpu } from './worldrendererWebgpu'
import { Entities } from './entities'
import { Primitives } from './primitives'
import EventEmitter from 'events'
import { WorldRendererThree } from './worldrendererThree'
import { generateSpiralMatrix } from 'flying-squid/dist/utils'
import { WorldRendererCommon, WorldRendererConfig, defaultWorldRendererConfig } from './worldrendererCommon'
import { defaultWorldRendererConfig } from './worldrendererCommon'
import { sendCameraToWorker } from '../../examples/webgpuRendererMain'
import { WorldRendererThree } from './worldrendererThree'
import { versionToNumber } from '../prepare/utils'
import worldBlockProvider from 'mc-assets/dist/worldBlockProvider'
import { renderBlockThree } from './mesher/standaloneRenderer'
@ -14,33 +16,36 @@ export class Viewer {
scene: THREE.Scene
ambientLight: THREE.AmbientLight
directionalLight: THREE.DirectionalLight
world: WorldRendererCommon
camera: THREE.PerspectiveCamera
world: WorldRendererWebgpu/* | WorldRendererThree */
entities: Entities
// primitives: Primitives
domElement: HTMLCanvasElement
playerHeight = 1.62
isSneaking = false
threeJsWorld: WorldRendererThree
// threeJsWorld: WorldRendererThree
cameraObjectOverride?: THREE.Object3D // for xr
audioListener: THREE.AudioListener
renderingUntilNoUpdates = false
processEntityOverrides = (e, overrides) => overrides
webgpuWorld: WorldRendererWebgpu
get camera () {
return this.world.camera
}
set camera (camera) {
this.world.camera = camera
}
// get camera () {
// return this.world.camera
// }
// set camera (camera) {
// this.world.camera = camera
// }
constructor (public renderer: THREE.WebGLRenderer, worldConfig = defaultWorldRendererConfig) {
constructor(public renderer: THREE.WebGLRenderer, worldConfig = defaultWorldRendererConfig) {
// https://discourse.threejs.org/t/updates-to-color-management-in-three-js-r152/50791
THREE.ColorManagement.enabled = false
renderer.outputColorSpace = THREE.LinearSRGBColorSpace
this.scene = new THREE.Scene()
this.scene.matrixAutoUpdate = false // for perf
this.threeJsWorld = new WorldRendererThree(this.scene, this.renderer, worldConfig)
// this.threeJsWorld = new WorldRendererThree(this.scene, this.renderer, worldConfig)
this.webgpuWorld = new WorldRendererWebgpu(worldConfig)
this.setWorld()
this.resetScene()
this.entities = new Entities(this.scene)
@ -50,7 +55,7 @@ export class Viewer {
}
setWorld () {
this.world = this.threeJsWorld
this.world = this.webgpuWorld
}
resetScene () {
@ -132,11 +137,14 @@ export class Viewer {
setFirstPersonCamera (pos: Vec3 | null, yaw: number, pitch: number, roll = 0) {
const cam = this.cameraObjectOverride || this.camera
let yOffset = this.playerHeight
if (this.isSneaking) yOffset -= 0.3
if (this.world instanceof WorldRendererThree) this.world.camera = cam as THREE.PerspectiveCamera
this.world.updateCamera(pos?.offset(0, yOffset, 0) ?? null, yaw, pitch)
if (pos) {
let y = pos.y + this.playerHeight
if (this.isSneaking) y -= 0.3
// new tweenJs.Tween(cam.position).to({ x: pos.x, y, z: pos.z }, 50).start()
cam.position.set(pos.x, y, pos.z)
}
cam.rotation.set(pitch, yaw, roll, 'ZYX')
sendCameraToWorker()
}
playSound (position: Vec3, path: string, volume = 1, pitch = 1) {
@ -184,7 +192,7 @@ export class Viewer {
})
// todo remove and use other architecture instead so data flow is clear
emitter.on('blockEntities', (blockEntities) => {
if (this.world instanceof WorldRendererThree) this.world.blockEntities = blockEntities
if (this.world instanceof WorldRendererThree) (this.world as WorldRendererThree).blockEntities = blockEntities
})
emitter.on('unloadChunk', ({ x, z }) => {
@ -199,6 +207,16 @@ export class Viewer {
this.world.updateViewerPosition(pos)
})
emitter.on('renderDistance', (d) => {
this.world.viewDistance = d
this.world.chunksLength = d === 0 ? 1 : generateSpiralMatrix(d).length
})
emitter.on('renderDistance', (d) => {
this.world.viewDistance = d
this.world.chunksLength = d === 0 ? 1 : generateSpiralMatrix(d).length
})
emitter.on('renderDistance', (d) => {
this.world.viewDistance = d
this.world.chunksLength = d === 0 ? 1 : generateSpiralMatrix(d).length
@ -206,7 +224,7 @@ export class Viewer {
})
emitter.on('updateLight', ({ pos }) => {
if (this.world instanceof WorldRendererThree) this.world.updateLight(pos.x, pos.z)
if (this.world instanceof WorldRendererThree) (this.world as WorldRendererThree).updateLight(pos.x, pos.z)
})
emitter.on('time', (timeOfDay) => {
@ -227,15 +245,24 @@ export class Viewer {
if (this.world.mesherConfig.skyLight === skyLight) return
this.world.mesherConfig.skyLight = skyLight
; (this.world as WorldRendererThree).rerenderAllChunks?.()
if (this.world instanceof WorldRendererThree) {
(this.world as WorldRendererThree).rerenderAllChunks?.()
}
})
emitter.emit('listening')
}
loadChunksFixture () { }
render () {
this.world.render()
this.entities.render()
// if (this.composer) {
// this.renderPass.camera = this.camera
// this.composer.render()
// } else {
// this.renderer.render(this.scene, this.camera)
// }
// this.entities.render()
}
async waitForChunksToRender () {

View file

@ -80,7 +80,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
blocks?: CustomTexturesData
} = {}
abstract outputFormat: 'threeJs' | 'webgl'
abstract outputFormat: 'threeJs' | 'webgpu'
constructor (public config: WorldRendererConfig) {
// this.initWorkers(1) // preload script on page load

View file

@ -4,6 +4,10 @@ import nbt from 'prismarine-nbt'
import PrismarineChatLoader from 'prismarine-chat'
import { renderSign } from '../sign-renderer/'
import { chunkPos, sectionPos } from './simpleUtils'
function mod (x, n) {
return ((x % n) + n) % n
}
import { WorldRendererCommon, WorldRendererConfig } from './worldrendererCommon'
import * as tweenJs from '@tweenjs/tween.js'
import { BloomPass, RenderPass, UnrealBloomPass, EffectComposer, WaterPass, GlitchPass } from 'three-stdlib'

View file

@ -0,0 +1,103 @@
import { Vec3 } from 'vec3'
import { updateStatText } from '../../examples/newStats'
import { addBlocksSection, removeBlocksSection, webgpuChannel } from '../../examples/webgpuRendererMain'
import type { WebglData } from '../prepare/webglData'
import { loadJSON } from './utils.web'
import { WorldRendererCommon } from './worldrendererCommon'
import { MesherGeometryOutput } from './mesher/shared'
export class WorldRendererWebgpu extends WorldRendererCommon {
outputFormat = 'webgpu' as const
newChunks = {} as Record<string, any>
// webglData: WebglData
stopBlockUpdate = false
lastChunkDistance = 0
constructor(config) {
super(config)
this.renderUpdateEmitter.on('update', () => {
const loadedChunks = Object.keys(this.finishedChunks).length
updateStatText('loaded-chunks', `${loadedChunks}/${this.chunksLength} chunks (${this.lastChunkDistance})`)
})
}
playgroundGetWebglData () {
const playgroundChunk = Object.values(this.newChunks).filter((x: any) => Object.keys(x?.blocks ?? {}).length > 0)?.[0] as any
if (!playgroundChunk) return
const block = Object.values(playgroundChunk.blocks)?.[0] as any
if (!block) return
const { textureName } = block
if (!textureName) return
// return this.webglData[textureName]
}
setBlockStateId (pos: any, stateId: any): void {
if (this.stopBlockUpdate) return
super.setBlockStateId(pos, stateId)
}
isWaitingForChunksToRender = false
allChunksLoaded (): void {
console.log('allChunksLoaded')
webgpuChannel.addBlocksSectionDone()
}
handleWorkerMessage (data: { geometry: MesherGeometryOutput, type, key }): void {
if (data.type === 'geometry' && Object.keys(data.geometry.tiles).length) {
const chunkCoords = data.key.split(',').map(Number) as [number, number, number]
if (/* !this.loadedChunks[chunkCoords[0] + ',' + chunkCoords[2]] || */ !this.active) return
addBlocksSection(data.key, data.geometry)
this.lastChunkDistance = Math.max(...this.getDistance(new Vec3(chunkCoords[0], 0, chunkCoords[2])))
// todo
this.newChunks[data.key] = data.geometry
}
}
updateCamera (pos: Vec3 | null, yaw: number, pitch: number): void { }
render (): void { }
chunksReset () {
webgpuChannel.fullReset()
}
updatePosDataChunk (key: string) {
}
async updateTexturesData (): Promise<void> {
await super.updateTexturesData()
}
updateShowChunksBorder (value: boolean) {
// todo
}
removeColumn (x, z) {
console.log('removeColumn', x, z)
super.removeColumn(x, z)
for (const key of Object.keys(this.newChunks)) {
const [xSec, _ySec, zSec] = key.split(',').map(Number)
// if (Math.floor(x / 16) === x && Math.floor(z / 16) === z) {
if (x === xSec && z === zSec) {
// foundSections.push(key)
removeBlocksSection(key)
}
}
// for (let y = this.worldConfig.minY; y < this.worldConfig.worldHeight; y += 16) {
// this.setSectionDirty(new Vec3(x, y, z), false)
// const key = `${x},${y},${z}`
// const mesh = this.sectionObjects[key]
// if (mesh) {
// this.scene.remove(mesh)
// dispose3(mesh)
// }
// delete this.sectionObjects[key]
// }
}
}

View file

@ -0,0 +1,28 @@
import { JsonAtlas } from './atlas';
import { join } from 'path'
import fs from 'fs'
export type WebglData = ReturnType<typeof prepareWebglData>
export const prepareWebglData = (blockTexturesDir: string, atlas: JsonAtlas) => {
// todo
return Object.fromEntries(Object.entries(atlas.textures).map(([texture, { animatedFrames }]) => {
if (!animatedFrames) return null!
const mcMeta = JSON.parse(fs.readFileSync(join(blockTexturesDir, texture + '.png.mcmeta'), 'utf8')) as {
animation: {
interpolate: boolean,
frametime: number,
frames: ({
index: number,
time: number
} | number)[]
}
}
return [texture, {
animation: {
...mcMeta.animation,
framesCount: animatedFrames
}
}] as const
}).filter(Boolean))
}

View file

@ -121,6 +121,12 @@ export default defineConfig({
} else {
await execAsync('pnpm run build-mesher')
}
if (fs.existsSync('./prismarine-viewer/public/webgpuRendererWorker.js')) {
// copy worker
fs.copyFileSync('./prismarine-viewer/public/webgpuRendererWorker.js', './dist/webgpuRendererWorker.js')
} else {
await execAsync('pnpm run build-other-workers')
}
fs.writeFileSync('./dist/version.txt', buildingVersion, 'utf-8')
console.timeEnd('total-prep')
}
@ -166,6 +172,10 @@ export default defineConfig({
test: /\.obj$/,
type: 'asset/source',
},
{
test: /\.wgsl$/,
type: 'asset/source',
},
{
test: /\.mp3$/,
type: 'asset/source',

View file

@ -438,6 +438,10 @@ export const f3Keybinds = [
console.warn('forcefully removed chunk from scene')
}
}
viewer.world.chunksReset() // todo
viewer.world.newChunks = {}
if (localServer) {
//@ts-expect-error not sure why it is private... maybe revisit api?
localServer.players[0].world.columns = {}

13
src/globals.d.ts vendored
View file

@ -32,4 +32,17 @@ declare interface Document {
exitPointerLock?(): void
}
declare module '*.frag' {
const png: string
export default png
}
declare module '*.vert' {
const png: string
export default png
}
declare module '*.wgsl' {
const png: string
export default png
}
declare interface Window extends Record<string, any> { }

View file

@ -99,6 +99,9 @@ import { signInMessageState } from './react/SignInMessageProvider'
import { updateAuthenticatedAccountData, updateLoadedServerData } from './react/ServersListProvider'
import { versionToNumber } from 'prismarine-viewer/viewer/prepare/utils'
import packetsPatcher from './packetsPatcher'
import { initWebgpuRenderer } from 'prismarine-viewer/examples/webgpuRendererMain'
import { addNewStat } from 'prismarine-viewer/examples/newStats'
// import { ViewerBase } from 'prismarine-viewer/viewer/lib/viewerWrapper'
import blockstatesModels from 'mc-assets/dist/blockStatesModels.json'
import { mainMenuState } from './react/MainMenuRenderApp'
import { ItemsRenderer } from 'mc-assets/dist/itemsRenderer'
@ -414,10 +417,15 @@ async function connect (connectOptions: ConnectOptions) {
viewer.setVersion(version, options.useVersionsTextures === 'latest' ? version : options.useVersionsTextures)
}
// serverOptions.version = '1.18.1'
const downloadVersion = connectOptions.botVersion || (singleplayer ? serverOptions.version : undefined)
if (downloadVersion) {
await downloadMcData(downloadVersion)
}
await initWebgpuRenderer(() => {
renderWrapper.postRender()
})
addNewStat('loaded-chunks')
if (singleplayer) {
// SINGLEPLAYER EXPLAINER:

View file

@ -3,6 +3,7 @@ import { useEffect } from 'react'
import { useSnapshot } from 'valtio'
import { usedServerPathsV1 } from 'flying-squid/dist/lib/modules/world'
import { openURL } from 'prismarine-viewer/viewer/lib/simpleUtils'
import { exportLoadedTiles } from 'prismarine-viewer/examples/webgpuRendererMain'
import {
activeModalStack,
showModal,
@ -98,10 +99,16 @@ export default () => {
if (fsStateSnap.inMemorySave || !singleplayer) {
return showOptionsModal('World actions...', [])
}
const action = await showOptionsModal('World actions...', ['Save to browser memory'])
const action = await showOptionsModal('World actions...', [
...!fsStateSnap.inMemorySave && singleplayer ? ['Save to browser memory'] : [],
'Dump loaded chunks'
])
if (action === 'Save to browser memory') {
await saveToBrowserMemory()
}
if (action === 'Dump loaded chunks') {
exportLoadedTiles()
}
}
if (!isModalActive) return null

View file

@ -39,7 +39,7 @@ if (hasRamPanel) {
addStat(stats2.dom)
}
const hideStats = localStorage.hideStats || isCypress()
const hideStats = localStorage.hideStats || isCypress() || true
if (hideStats) {
stats.dom.style.display = 'none'
stats2.dom.style.display = 'none'

2
src/worldSaveWorker.ts Normal file
View file

@ -0,0 +1,2 @@
import './workerWorkaround'
import './browserfs'

View file

@ -15,8 +15,8 @@
"forceConsistentCasingInFileNames": true,
"useUnknownInCatchVariables": false,
"skipLibCheck": true,
"experimentalDecorators": true,
"strictBindCallApply": true,
"experimentalDecorators": true,
// this the only options that allows smooth transition from js to ts (by not dropping types from js files)
// however might need to consider includeing *only needed libraries* instead of using this
"maxNodeModuleJsDepth": 1,