diff --git a/.eslintrc.json b/.eslintrc.json index 8b2225b56..9add49ea9 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -10,6 +10,7 @@ "@stylistic" ], "rules": { + "unicorn/no-typeof-undefined": "off", // style "@stylistic/space-infix-ops": "error", "@stylistic/no-multi-spaces": "error", diff --git a/.vscode/launch.json b/.vscode/launch.json index dec881630..e15432385 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -35,7 +35,25 @@ "outFiles": [ "${workspaceFolder}/dist/**/*.js", // "!${workspaceFolder}/dist/**/*vendors*", - "!${workspaceFolder}/dist/**/*mc-data*", + "!**/node_modules/**" + ], + "skipFiles": [ + // "/**/*vendors*" + "/**/*mc-data*" + ], + }, + { + // not recommended as in most cases it will slower as it launches from extension host so it slows down extension host, not sure why + "type": "chrome", + "name": "Launch Chrome (playground)", + "request": "launch", + "url": "http://localhost:9090/", + "pathMapping": { + "/": "${workspaceFolder}/prismarine-viewer/dist" + }, + "outFiles": [ + "${workspaceFolder}/prismarine-viewer/dist/**/*.js", + // "!${workspaceFolder}/dist/**/*vendors*", "!**/node_modules/**" ], "skipFiles": [ diff --git a/.vscode/tasks.json b/.vscode/tasks.json index ca74a57d3..6c66309eb 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -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", } diff --git a/buildWorkers.mjs b/buildWorkers.mjs new file mode 100644 index 000000000..5139d58f5 --- /dev/null +++ b/buildWorkers.mjs @@ -0,0 +1,109 @@ +//@ts-check +// 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, { join } from 'path' +import { polyfillNode } from 'esbuild-plugin-polyfill-node' + +const watch = process.argv.includes('-w') + +const result = await (watch ? context : build)({ + bundle: true, + platform: 'browser', + // entryPoints: ['prismarine-viewer/examples/webgpuRendererWorker.ts', 'src/worldSaveWorker.ts'], + entryPoints: ['prismarine-viewer/examples/webgpuRendererWorker.ts'], + outdir: 'prismarine-viewer/dist/', + 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 }) => { + fs.mkdirSync('prismarine-viewer/public', { recursive: true }) + fs.mkdirSync('dist', { recursive: true }) + for (const file of outputFiles) { + for (const dir of ['prismarine-viewer/dist', 'dist']) { + const baseName = path.basename(file.path) + fs.mkdirSync(dir, { recursive: true }) + fs.writeFileSync(path.join(dir, baseName), file.contents) + } + } + }) + } + }, + { + name: 'fix-dynamic-require', + setup (build) { + build.onResolve({ + filter: /1\.14\/chunk/, + }, async ({ resolveDir, path }) => { + if (!resolveDir.includes('prismarine-provider-anvil')) return + return { + namespace: 'fix-dynamic-require', + path, + pluginData: { + resolvedPath: `${join(resolveDir, path)}.js`, + resolveDir + }, + } + }) + build.onLoad({ + filter: /.+/, + namespace: 'fix-dynamic-require', + }, async ({ pluginData: { resolvedPath, resolveDir } }) => { + const resolvedFile = await fs.promises.readFile(resolvedPath, 'utf8') + return { + contents: resolvedFile.replace("require(`prismarine-chunk/src/pc/common/BitArray${noSpan ? 'NoSpan' : ''}`)", "noSpan ? require(`prismarine-chunk/src/pc/common/BitArray`) : require(`prismarine-chunk/src/pc/common/BitArrayNoSpan`)"), + resolveDir, + loader: 'js', + } + }) + } + }, + polyfillNode({ + polyfills: { + fs: false, + dns: false, + crypto: false, + events: false, + http: false, + stream: false, + buffer: false, + perf_hooks: false, + net: false, + assert: false, + }, + }) + ], + loader: { + '.vert': 'text', + '.frag': 'text', + '.wgsl': 'text', + }, + mainFields: [ + 'browser', 'module', 'main' + ], + keepNames: true, + write: false, +}) + +if (watch) { + //@ts-ignore + await result.watch() +} diff --git a/config.json b/config.json index 7813b5911..c96a707f5 100644 --- a/config.json +++ b/config.json @@ -2,7 +2,7 @@ "version": 1, "defaultHost": "", "defaultProxy": "proxy.mcraft.fun", - "mapsProvider": "https://maps.mcraft.fun/", + "mapsProvider": "https://maps.mcraft.fun/?label=webgpu", "peerJsServer": "", "peerJsServerFallback": "https://p2p.mcraft.fun", "promoteServers": [ diff --git a/cypress/e2e/performance.spec.ts b/cypress/e2e/performance.spec.ts new file mode 100644 index 000000000..f2fc4d46e --- /dev/null +++ b/cypress/e2e/performance.spec.ts @@ -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) + }) +}) diff --git a/package.json b/package.json index 4ed5a4494..6afdbfa08 100644 --- a/package.json +++ b/package.json @@ -5,12 +5,13 @@ "scripts": { "dev-rsbuild": "rsbuild dev", "dev-proxy": "node server.js", - "start": "run-p dev-proxy dev-rsbuild watch-mesher", - "start2": "run-p dev-rsbuild watch-mesher", + "start": "run-p dev-rsbuild dev-proxy watch-mesher watch-other-workers", + "start2": "run-p dev-rsbuild watch-mesher watch-other-workers", "build": "pnpm build-other-workers && rsbuild build", "build-analyze": "BUNDLE_ANALYZE=true rsbuild build && pnpm build-other-workers", "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", @@ -19,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", - "build-other-workers": "echo NOT IMPLEMENTED", + "watch-other-workers": "node buildWorkers.mjs -w", + "build-other-workers": "node buildWorkers.mjs", "build-mesher": "node prismarine-viewer/buildMesherWorker.mjs", "watch-mesher": "pnpm build-mesher -w", "run-playground": "run-p watch-mesher watch-other-workers watch-playground", "run-all": "run-p start run-playground", + "run-all2": "run-p start2 run-playground", "build-playground": "rsbuild build --config prismarine-viewer/rsbuild.config.ts", "watch-playground": "rsbuild dev --config prismarine-viewer/rsbuild.config.ts" }, @@ -47,11 +49,13 @@ "@nxg-org/mineflayer-auto-jump": "^0.7.12", "@nxg-org/mineflayer-tracker": "1.2.1", "@react-oauth/google": "^0.12.1", + "@rsbuild/plugin-basic-ssl": "^1.1.1", "@stylistic/eslint-plugin": "^2.6.1", "@types/gapi": "^0.0.47", "@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", @@ -102,6 +106,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", @@ -142,7 +147,7 @@ "http-browserify": "^1.7.0", "http-server": "^14.1.1", "https-browserify": "^1.0.0", - "mc-assets": "^0.2.23", + "mc-assets": "^0.2.25", "minecraft-inventory-gui": "github:zardoy/minecraft-inventory-gui#next", "mineflayer": "github:zardoy/mineflayer", "mineflayer-pathfinder": "^2.4.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7a89eb6df..27893e9af 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -55,6 +55,9 @@ importers: '@react-oauth/google': specifier: ^0.12.1 version: 0.12.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@rsbuild/plugin-basic-ssl': + specifier: ^1.1.1 + version: 1.1.1(@rsbuild/core@1.0.1-beta.9) '@stylistic/eslint-plugin': specifier: ^2.6.1 version: 2.6.1(eslint@8.50.0)(typescript@5.5.4) @@ -70,6 +73,9 @@ importers: '@types/wicg-file-system-access': specifier: ^2023.10.2 version: 2023.10.2 + '@webgpu/types': + specifier: ^0.1.44 + version: 0.1.49 '@xmcl/text-component': specifier: ^2.1.3 version: 2.1.3 @@ -220,6 +226,9 @@ importers: title-case: specifier: 3.x version: 3.0.3 + twgl.js: + specifier: ^5.5.4 + version: 5.5.4 ua-parser-js: specifier: ^1.0.37 version: 1.0.37 @@ -346,8 +355,8 @@ importers: specifier: ^1.0.0 version: 1.0.0 mc-assets: - specifier: ^0.2.23 - version: 0.2.23 + specifier: ^0.2.25 + version: 0.2.25 minecraft-inventory-gui: specifier: github:zardoy/minecraft-inventory-gui#next version: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/75e940a4cd50d89e0ba03db3733d5d704917a3c8(@types/react@18.2.20)(react@18.2.0) @@ -420,6 +429,9 @@ importers: lil-gui: specifier: ^0.18.2 version: 0.18.2 + live-server: + specifier: ^1.2.2 + version: 1.2.2 minecraft-wrap: specifier: ^1.3.0 version: 1.5.1(encoding@0.1.13) @@ -466,10 +478,6 @@ importers: node-canvas-webgl: specifier: ^0.3.0 version: 0.3.0(encoding@0.1.13) - devDependencies: - live-server: - specifier: ^1.2.2 - version: 1.2.2 prismarine-viewer/viewer/sign-renderer: dependencies: @@ -2493,6 +2501,14 @@ packages: engines: {node: '>=16.7.0'} hasBin: true + '@rsbuild/plugin-basic-ssl@1.1.1': + resolution: {integrity: sha512-q4u7H8yh/S/DHwxG85bWbGXFiVV9RMDJDupOBHJVPtevU9mLCB4n5Qbrxu/l8CCdmZcBlvfWGjkDA/YoY61dig==} + peerDependencies: + '@rsbuild/core': 0.x || 1.x || ^1.0.1-beta.0 + peerDependenciesMeta: + '@rsbuild/core': + optional: true + '@rsbuild/plugin-node-polyfill@1.0.3': resolution: {integrity: sha512-AoPIOV1pyInIz08K1ECwUjFemLLSa5OUq8sfJN1ShXrGR2qc14b1wzwZKwF4vgKnBromqfMLagVbk6KT/nLIvQ==} peerDependencies: @@ -3044,6 +3060,9 @@ packages: '@types/node-fetch@2.6.6': resolution: {integrity: sha512-95X8guJYhfqiuVVhRFxVQcf4hW/2bCuoPwDasMf/531STFoNoWTT7YDnWdXHEZKqAGUigmpG31r2FE70LwnzJw==} + '@types/node-forge@1.3.11': + resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==} + '@types/node-rsa@1.1.4': resolution: {integrity: sha512-dB0ECel6JpMnq5ULvpUTunx3yNm8e/dIkv8Zu9p2c8me70xIRUUG3q+qXRwcSf9rN3oqamv4116iHy90dJGRpA==} @@ -3339,6 +3358,9 @@ packages: '@webassemblyjs/wast-printer@1.12.1': resolution: {integrity: sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==} + '@webgpu/types@0.1.49': + resolution: {integrity: sha512-NMmS8/DofhH/IFeW+876XrHVWel+J/vdcFCHLDqeJgkH9x0DeiwjVd8LcBdaxdG/T7Rf8VUAYsA8X1efMzLjRQ==} + '@xboxreplay/errors@0.1.0': resolution: {integrity: sha512-Tgz1d/OIPDWPeyOvuL5+aai5VCcqObhPnlI3skQuf80GVF3k1I0lPCnGC+8Cm5PV9aLBT5m8qPcJoIUQ2U4y9g==} @@ -6582,8 +6604,8 @@ packages: peerDependencies: react: ^18.2.0 - mc-assets@0.2.23: - resolution: {integrity: sha512-sLbPhsSOYdW8nYllIyPZbVPnLu7V3bZTgIO4mI4nlG525q17NIbUNEjItHKtdi60u0vI6qLgHKjf0CoNRqa/Nw==} + mc-assets@0.2.25: + resolution: {integrity: sha512-MdtncPBC6kwIkYXsBsSEJGP+q2e+7Q4Wnb4j3FjS7gmafz50Vjp4E/S3MsM7H8R3FoDrjVIx6qR24l/rneW/Lw==} engines: {node: '>=18.0.0'} md5-file@4.0.0: @@ -7010,6 +7032,10 @@ packages: encoding: optional: true + node-forge@1.3.1: + resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} + engines: {node: '>= 6.13.0'} + node-gyp-build-optional-packages@5.1.1: resolution: {integrity: sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==} hasBin: true @@ -8245,6 +8271,10 @@ packages: secure-compare@3.0.1: resolution: {integrity: sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==} + selfsigned@2.4.1: + resolution: {integrity: sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==} + engines: {node: '>=10'} + semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true @@ -8951,6 +8981,9 @@ packages: tweetnacl@0.14.5: resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} + twgl.js@5.5.4: + resolution: {integrity: sha512-6kFOmijOpmblTN9CCwOTCxK4lPg7rCyQjLuub6EMOlEp89Ex6yUcsMjsmH7andNPL2NE3XmHdqHeP5gVKKPhxw==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -11960,6 +11993,12 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + '@rsbuild/plugin-basic-ssl@1.1.1(@rsbuild/core@1.0.1-beta.9)': + dependencies: + selfsigned: 2.4.1 + optionalDependencies: + '@rsbuild/core': 1.0.1-beta.9 + '@rsbuild/plugin-node-polyfill@1.0.3(@rsbuild/core@1.0.1-beta.9)': dependencies: assert: 2.1.0 @@ -12966,6 +13005,10 @@ snapshots: '@types/node': 22.8.1 form-data: 4.0.0 + '@types/node-forge@1.3.11': + dependencies: + '@types/node': 22.8.1 + '@types/node-rsa@1.1.4': dependencies: '@types/node': 22.8.1 @@ -13019,7 +13062,7 @@ snapshots: '@types/readable-stream@4.0.12': dependencies: - '@types/node': 20.12.8 + '@types/node': 22.8.1 safe-buffer: 5.1.2 '@types/resolve@1.17.1': @@ -13363,6 +13406,8 @@ snapshots: '@webassemblyjs/ast': 1.12.1 '@xtuc/long': 4.2.2 + '@webgpu/types@0.1.49': {} + '@xboxreplay/errors@0.1.0': {} '@xboxreplay/xboxlive-auth@3.3.3(debug@4.3.4)': @@ -17453,7 +17498,7 @@ snapshots: dependencies: react: 18.2.0 - mc-assets@0.2.23: {} + mc-assets@0.2.25: {} md5-file@4.0.0: {} @@ -17754,7 +17799,7 @@ snapshots: '@types/readable-stream': 4.0.12 aes-js: 3.1.2 buffer-equal: 1.0.1 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.7 endian-toggle: 0.0.0 lodash.get: 4.4.2 lodash.merge: 4.6.2 @@ -18086,6 +18131,8 @@ snapshots: optionalDependencies: encoding: 0.1.13 + node-forge@1.3.1: {} + node-gyp-build-optional-packages@5.1.1: dependencies: detect-libc: 2.0.2 @@ -19571,6 +19618,11 @@ snapshots: secure-compare@3.0.1: {} + selfsigned@2.4.1: + dependencies: + '@types/node-forge': 1.3.11 + node-forge: 1.3.1 + semver@5.7.2: {} semver@6.3.1: {} @@ -20478,6 +20530,8 @@ snapshots: tweetnacl@0.14.5: optional: true + twgl.js@5.5.4: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 diff --git a/prismarine-viewer/buildMesherWorker.mjs b/prismarine-viewer/buildMesherWorker.mjs index 03b952b4f..57753a07a 100644 --- a/prismarine-viewer/buildMesherWorker.mjs +++ b/prismarine-viewer/buildMesherWorker.mjs @@ -22,7 +22,7 @@ const buildOptions = { }, platform: 'browser', entryPoints: [path.join(__dirname, './viewer/lib/mesher/mesher.ts')], - minify: true, + minify: !watch, logLevel: 'info', drop: !watch ? [ 'debugger' @@ -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')) { diff --git a/prismarine-viewer/examples/Cube.comp.wgsl b/prismarine-viewer/examples/Cube.comp.wgsl new file mode 100644 index 000000000..9a7101c83 --- /dev/null +++ b/prismarine-viewer/examples/Cube.comp.wgsl @@ -0,0 +1,101 @@ +struct Cube { + cube: array +} + +struct Chunk { + x: i32, + z: i32, + opacity: i32, +} + + +struct Depth { + locks: array, 4096>, 4096> +} + +struct Uniforms { + textureSize: vec2 +} + +struct CameraPosition { + position: vec3, +} + +@group(1) @binding(3) var ViewProjectionMatrix: mat4x4; +@group(1) @binding(0) var chunks: array; +@group(0) @binding(1) var cubes: array; +@group(1) @binding(1) var occlusion : Depth; +@group(1) @binding(2) var depthAtomic : Depth; +@group(2) @binding(0) var uniforms: Uniforms; +@group(1) @binding(4) var cameraPosition: CameraPosition; +@group(0) @binding(5) var depthTexture: texture_depth_2d; +@group(0) @binding(6) var rejectZ: u32; + +fn linearize_depth_ndc(ndc_z: f32, z_near: f32, z_far: f32) -> f32 { + return z_near * z_far / (z_far - ndc_z * (z_far - z_near)); +} + +@compute @workgroup_size(256) +fn main(@builtin(global_invocation_id) global_id: vec3) { + let index = global_id.x; + if (index >= arrayLength(&cubes)) { + return; + } + + let cube = cubes[index]; + + let i = cube.cube[2]; + let chunk = chunks[i]; + + var positionX: f32 = f32(i32(cube.cube[0] & 15) + chunk.x * 16); //4 bytes + let positionY: f32 = f32((cube.cube[0] >> 4) & 1023); //10 bytes + var positionZ: f32 = f32(i32((cube.cube[0] >> 14) & 15) + chunk.z * 16); + positionX += 0.5; + positionZ += 0.5; + let position = vec4f(positionX, positionY, positionZ, 1.0); + let transopesPos = position.xyz - cameraPosition.position; + let nearby : bool = abs(transopesPos.x) <= 8 && abs(transopesPos.y) <= 8 && abs(transopesPos.z) <= 8; + // Transform cube position to clip space + let clipPos = ViewProjectionMatrix * position; + let clipDepth = clipPos.z / clipPos.w; // Obtain depth in clip space + var clipX = clipPos.x / clipPos.w; + var clipY = clipPos.y / clipPos.w; + let textureSize = uniforms.textureSize; + // Check if cube is within the view frustum z-range (depth within near and far planes) + if ( + ((clipDepth > 0 && clipDepth <= 1) && + (clipX >= -1 && clipX <= 1) && + (clipY >= - 1 && clipY <= 1)) || nearby) + { + if (nearby) { + clipY = clamp(clipY, -1, 1); + clipX = clamp(clipX, -1, 1); + } + var pos : vec2u = vec2u(u32((clipX * 0.5 + 0.5) * f32(textureSize.x)),u32((clipY * 0.5 + 0.5) * f32(textureSize.y))); + let k = linearize_depth_ndc(clipDepth, 0.05, 10000) ; + if (rejectZ == 1 && k - 20 > linearize_depth_ndc(textureLoad(depthTexture, vec2u(pos.x, textureSize.y - pos.y), 0), 0.05, 10000)) { + return; + } + if (nearby) { + if (clipX == 1|| clipX == -1) { + + pos.x = textureSize.x + 1; + } + if (clipY == 1|| clipY == -1) { + + pos.y = index % textureSize.y; + } + } + let depth = u32(k + 1.5); + var depthPrev = atomicMin(&depthAtomic.locks[pos.x][pos.y], depth); + //depthPrev = atomicLoad(&depthAtomic.locks[pos.x][pos.y]); + if (depth < depthPrev) { + // let k = atomicCompareExchangeWeak(&depthAtomic.locks[pos.x][pos.y], depth, depth); + // if (k.exchanged == true) { + + atomicStore(&occlusion.locks[pos.x][pos.y], index + 1); + // } + } + + } +} diff --git a/prismarine-viewer/examples/Cube.frag.wgsl b/prismarine-viewer/examples/Cube.frag.wgsl new file mode 100644 index 000000000..d498b00b6 --- /dev/null +++ b/prismarine-viewer/examples/Cube.frag.wgsl @@ -0,0 +1,28 @@ +@group(0) @binding(1) var mySampler: sampler; +@group(0) @binding(2) var myTexture: texture_2d; +@group(0) @binding(5) var tileSize: vec2; + +@fragment +fn main( + @location(0) fragUV: vec2f, + @location(1) @interpolate(flat) TextureIndex: f32, + @location(2) @interpolate(flat) ColorBlend: vec3f, + @location(3) @interpolate(flat) ChunkOpacity: f32 +) -> @location(0) vec4f { + let textureSize: vec2 = vec2(textureDimensions(myTexture)); + let tilesPerTexture: vec2 = textureSize / tileSize; + let pixelColor = textureSample(myTexture, mySampler, fragUV / tilesPerTexture + vec2f(trunc(TextureIndex % tilesPerTexture.y), trunc(TextureIndex / tilesPerTexture.x)) / tilesPerTexture); + // return vec4f(pixelColor.rgb * ColorBlend / 255, pixelColor.a); // Set alpha to 1.0 for full opacity + return vec4f(pixelColor.rgb * ColorBlend / 255, 1.0 * ChunkOpacity); // Set alpha to 1.0 for full opacity +// only gray: +// let t = textureSample(myTexture, mySampler, fragUV / tilesPerTexture + vec2f(trunc(TextureIndex % tilesPerTexture.y), trunc(TextureIndex / tilesPerTexture.x)) / tilesPerTexture); +// // return vec4f(pixelColor.rgb * ColorBlend / 255, pixelColor.a); // Set alpha to 1.0 for full opacity + +// if (abs(t.x-t.y) <=0.03 || abs(t.x-t.z)<=0.03 ||abs(t.y-t.z) <=0.03) +// { +// return vec4f(t.rgb * ColorBlend / 255, 1.0); +// } +// else { +// return vec4f(t.rgb, 1.0); +// } +} diff --git a/prismarine-viewer/examples/Cube.vert.wgsl b/prismarine-viewer/examples/Cube.vert.wgsl new file mode 100644 index 000000000..aab754205 --- /dev/null +++ b/prismarine-viewer/examples/Cube.vert.wgsl @@ -0,0 +1,102 @@ + +struct Cube { + cube : array +} + +struct Chunk{ + x : i32, + z : i32, + opacity: i32 +} + + +struct CubePointer { + ptr: u32 +} + +struct CubeModel { + textureIndex123: u32, + textureIndex456: u32, +} + +struct VertexOutput { + @builtin(position) Position: vec4f, + @location(0) fragUV: vec2f, + @location(1) @interpolate(flat) TextureIndex: f32, + @location(2) @interpolate(flat) ColorBlend: vec3f, + @location(3) @interpolate(flat) ChunkOpacity: f32 +} +@group(1) @binding(0) var cubes: array; +@group(0) @binding(0) var ViewProjectionMatrix: mat4x4; +@group(0) @binding(3) var models: array; +@group(1) @binding(1) var visibleCubes: array; +@group(1) @binding(2) var chunks : array; +@group(0) @binding(4) var rotatations: array, 6>; + +@vertex +fn main( + @builtin(instance_index) instanceIndex: u32, + @location(0) position: vec4, + @location(1) uv: vec2 +) -> VertexOutput { + let normalIndex = visibleCubes[instanceIndex].ptr & 7; + let cube = cubes[visibleCubes[instanceIndex].ptr >> 3]; + //let chunkIndex = (cube.cube[1] >> 24) + ((cube.cube[0] >> 27) << 8); + let chunk = chunks[cube.cube[2]]; + + var positionX : f32 = f32(i32(cube.cube[0] & 15) + chunk.x * 16); //4 bytes + var positionY : f32 = f32((cube.cube[0] >> 4) & 1023); //10 bytes + var positionZ : f32 = f32(i32((cube.cube[0] >> 14) & 15) + chunk.z * 16); // 4 bytes + let modelIndex : u32 = ((cube.cube[0] >> 18) & 16383); ///14 bits + var textureIndex : u32; + + positionX += 0.5; + positionZ += 0.5; + positionY += 0.5; + + let cube_position = vec4f(positionX, positionY, positionZ, 0.0); + + let colorBlendR : f32 = f32(cube.cube[1] & 255); + let colorBlendG : f32 = f32((cube.cube[1] >> 8) & 255); + let colorBlendB : f32 = f32((cube.cube[1] >> 16) & 255); + let colorBlend = vec3f(colorBlendR, colorBlendG, colorBlendB); + + var normal : mat4x4; + var Uv = vec2(uv.x, (1.0 - uv.y)); + normal = rotatations[normalIndex]; + switch (normalIndex) { + case 0: + { + Uv = vec2((1.0f-uv.x), (1.0 - uv.y)); + textureIndex = models[modelIndex].textureIndex123 & 1023; + } + case 1: + { + textureIndex = (models[modelIndex].textureIndex123 >> 10) & 1023; + } + case 2: + { + textureIndex = (models[modelIndex].textureIndex123 >> 20) & 1023; + } + case 3: + { + textureIndex = models[modelIndex].textureIndex456 & 1023; + } + case 4: + { + textureIndex = (models[modelIndex].textureIndex456 >> 10) & 1023; + } + case 5, default: + { + textureIndex = (models[modelIndex].textureIndex456 >> 20) & 1023; + } + } + + var output: VertexOutput; + output.Position = ViewProjectionMatrix * (position * normal + cube_position); + output.fragUV = Uv; + output.ChunkOpacity = f32(chunk.opacity) / 255; + output.TextureIndex = f32(textureIndex); + output.ColorBlend = colorBlend; + return output; +} diff --git a/prismarine-viewer/examples/CubeDef.ts b/prismarine-viewer/examples/CubeDef.ts new file mode 100644 index 000000000..cf1777ad0 --- /dev/null +++ b/prismarine-viewer/examples/CubeDef.ts @@ -0,0 +1,62 @@ +export const cubeVertexSize = 4 * 5 // Byte size of one cube vertex. +export const PositionOffset = 0 +//export const cubeColorOffset = 4 * 3 // Byte offset of cube vertex color attribute. +export const UVOffset = 4 * 3 +export const cubeVertexCount = 36 + +//@ts-format-ignore-region +export const cubeVertexArray = new Float32Array([ + -0.5, -0.5, -0.5, 0, 0, // Bottom-let + 0.5, -0.5, -0.5, 1, 0, // bottom-right + 0.5, 0.5, -0.5, 1, 1, // top-right + 0.5, 0.5, -0.5, 1, 1, // top-right + -0.5, 0.5, -0.5, 0, 1, // top-let + -0.5, -0.5, -0.5, 0, 0, // bottom-let + // ront ace + -0.5, -0.5, 0.5, 0, 0, // bottom-let + 0.5, 0.5, 0.5, 1, 1, // top-right + 0.5, -0.5, 0.5, 1, 0, // bottom-right + 0.5, 0.5, 0.5, 1, 1, // top-right + -0.5, -0.5, 0.5, 0, 0, // bottom-let + -0.5, 0.5, 0.5, 0, 1, // top-let + // Let ace + -0.5, 0.5, 0.5, 1, 0, // top-right + -0.5, -0.5, -0.5, 0, 1, // bottom-let + -0.5, 0.5, -0.5, 1, 1, // top-let + -0.5, -0.5, -0.5, 0, 1, // bottom-let + -0.5, 0.5, 0.5, 1, 0, // top-right + -0.5, -0.5, 0.5, 0, 0, // bottom-right + // Right ace + 0.5, 0.5, 0.5, 1, 0, // top-let + 0.5, 0.5, -0.5, 1, 1, // top-right + 0.5, -0.5, -0.5, 0, 1, // bottom-right + 0.5, -0.5, -0.5, 0, 1, // bottom-right + 0.5, -0.5, 0.5, 0, 0, // bottom-let + 0.5, 0.5, 0.5, 1, 0, // top-let + // Bottom ace + -0.5, -0.5, -0.5, 0, 1, // top-right + 0.5, -0.5, 0.5, 1, 0, // bottom-let + 0.5, -0.5, -0.5, 1, 1, // top-let + 0.5, -0.5, 0.5, 1, 0, // bottom-let + -0.5, -0.5, -0.5, 0, 1, // top-right + -0.5, -0.5, 0.5, 0, 0, // bottom-right + // Top ace + -0.5, 0.5, -0.5, 0, 1, // top-let + 0.5, 0.5, -0.5, 1, 1, // top-right + 0.5, 0.5, 0.5, 1, 0, // bottom-right + 0.5, 0.5, 0.5, 1, 0, // bottom-right + -0.5, 0.5, 0.5, 0, 0, // bottom-let + -0.5, 0.5, -0.5, 0, 1// top-letĖš +]) + +//export const cubeColorOffset = 4 * 3 // Byte offset of cube vertex color attribute. +export const quadVertexCount = 6 + +export const quadVertexArray = new Float32Array([ + -0.5, -0.5, 0.5, 0, 0, // bottom-let + 0.5, 0.5, 0.5, 1, 1, // top-right + 0.5, -0.5, 0.5, 1, 0, // bottom-right + 0.5, 0.5, 0.5, 1, 1, // top-right + -0.5, -0.5, 0.5, 0, 0, // bottom-let + -0.5, 0.5, 0.5, 0, 1, // top-let +]) \ No newline at end of file diff --git a/prismarine-viewer/examples/CubeSort.comp.wgsl b/prismarine-viewer/examples/CubeSort.comp.wgsl new file mode 100644 index 000000000..57a6691c7 --- /dev/null +++ b/prismarine-viewer/examples/CubeSort.comp.wgsl @@ -0,0 +1,98 @@ +struct IndirectDrawParams { + vertexCount: u32, + instanceCount: atomic, + firstVertex: u32, + firstInstance: u32, +} + +struct CubePointer { + ptr: u32, +} + +struct Cube { + cube: array, +} + +struct Chunk { + x: i32, + z: i32, + opacity: i32 +} + +struct Depth { + locks: array, 4096>, +} + +struct Uniforms { + textureSize: vec2, +} + +struct CameraPosition { + position: vec3, +} + +@group(1) @binding(1) var occlusion: Depth; +@group(1) @binding(2) var depthAtomic: Depth; +@group(0) @binding(2) var visibleCubes: array; +@group(0) @binding(3) var drawParams: IndirectDrawParams; +@group(0) @binding(1) var cubes: array; +@group(1) @binding(0) var chunks: array; +@group(2) @binding(0) var uniforms: Uniforms; +@group(1) @binding(4) var cameraPosition: CameraPosition; + +@compute @workgroup_size(16, 16) +fn main(@builtin(global_invocation_id) global_id: vec3) { + let position = global_id.xy; + storageBarrier(); + depthAtomic.locks[position.x][position.y] = 4294967295; + let textureSize = uniforms.textureSize; + if (position.x >= textureSize.x + 2 || position.y >= textureSize.y) { + return; + } + + var occlusionData: u32 = occlusion.locks[position.x][position.y]; + + if (occlusionData != 0) { + var cube = cubes[occlusionData - 1]; + var visibleSides = (cube.cube[1] >> 24) & 63; + + let chunk = chunks[cube.cube[2]]; + var positionX: f32 = f32(i32(cube.cube[0] & 15) + chunk.x * 16); //4 bytes + let positionY: f32 = f32((cube.cube[0] >> 4) & 1023); //10 bytes + var positionZ: f32 = f32(i32((cube.cube[0] >> 14) & 15) + chunk.z * 16); + let isUpper : bool = positionY > cameraPosition.position.y; + let isLeftier : bool = positionX > cameraPosition.position.x; + let isDeeper : bool = positionZ > cameraPosition.position.z; + occlusionData = (occlusionData - 1) << 3; + + if ((visibleSides & 1) != 0 && !isUpper) { + let visibleIndex = atomicAdd(&drawParams.instanceCount, 1); + visibleCubes[visibleIndex].ptr = occlusionData; + } + + if (((visibleSides >> 1) & 1) != 0 && isUpper) { + let visibleIndex = atomicAdd(&drawParams.instanceCount, 1); + visibleCubes[visibleIndex].ptr = occlusionData | 1; + } + + if (((visibleSides >> 2) & 1) != 0 && !isDeeper) { + let visibleIndex = atomicAdd(&drawParams.instanceCount, 1); + visibleCubes[visibleIndex].ptr = occlusionData | 2; + } + + if (((visibleSides >> 3) & 1) != 0&& isDeeper) { + let visibleIndex = atomicAdd(&drawParams.instanceCount, 1); + visibleCubes[visibleIndex].ptr = occlusionData | 3; + } + + if (((visibleSides >> 4) & 1) != 0 && !isLeftier) { + let visibleIndex = atomicAdd(&drawParams.instanceCount, 1); + visibleCubes[visibleIndex].ptr = occlusionData | 4; + } + + if (((visibleSides >> 5) & 1) != 0 && isLeftier) { + let visibleIndex = atomicAdd(&drawParams.instanceCount, 1); + visibleCubes[visibleIndex].ptr = occlusionData | 5; + } + } +} diff --git a/prismarine-viewer/examples/TextureAnimation.ts b/prismarine-viewer/examples/TextureAnimation.ts new file mode 100644 index 000000000..6c5835437 --- /dev/null +++ b/prismarine-viewer/examples/TextureAnimation.ts @@ -0,0 +1,69 @@ +export type AnimationControlSwitches = { + tick: number + interpolationTick: number // next one +} + +type Data = { + interpolate: boolean; + frametime: number; + frames: Array<{ + 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) { + const frame = frames[this.frameIndex] + const 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 + } + } + +} diff --git a/prismarine-viewer/examples/TouchControls2.tsx b/prismarine-viewer/examples/TouchControls2.tsx new file mode 100644 index 000000000..bc5a7ebd0 --- /dev/null +++ b/prismarine-viewer/examples/TouchControls2.tsx @@ -0,0 +1,81 @@ +import React, { useEffect } from 'react' +import { LeftTouchArea, RightTouchArea, useInterfaceState } from '@dimaka/interface' +import { css } from '@emotion/css' +import { renderToDom } from '@zardoy/react-util' +import { Vec3 } from 'vec3' +import * as THREE from 'three' +import { Viewer } from '../viewer/lib/viewer' + +declare const viewer: Viewer +const Controls = () => { + // todo setting + const usingTouch = navigator.maxTouchPoints > 0 + + useEffect(() => { + window.addEventListener('touchstart', (e) => { + e.preventDefault() + }) + + const pressedKeys = new Set() + useInterfaceState.setState({ + isFlying: false, + uiCustomization: { + touchButtonSize: 40, + }, + updateCoord ([coord, state]) { + const vec3 = new Vec3(0, 0, 0) + vec3[coord] = state + let key: string | undefined + if (vec3.z < 0) key = 'KeyW' + if (vec3.z > 0) key = 'KeyS' + if (vec3.y > 0) key = 'Space' + if (vec3.y < 0) key = 'ShiftLeft' + if (vec3.x < 0) key = 'KeyA' + if (vec3.x > 0) key = 'KeyD' + if (key) { + if (!pressedKeys.has(key)) { + pressedKeys.add(key) + window.dispatchEvent(new KeyboardEvent('keydown', { code: key })) + } + } + for (const k of pressedKeys) { + if (k !== key) { + window.dispatchEvent(new KeyboardEvent('keyup', { code: k })) + pressedKeys.delete(k) + } + } + } + }) + }, []) + + if (!usingTouch) return null + return ( +
div { + pointer-events: auto; + } + `} + > + +
+ +
+ ) +} + +export const renderPlayground = () => { + renderToDom(, { + // selector: 'body', + }) +} diff --git a/prismarine-viewer/examples/baseScene.ts b/prismarine-viewer/examples/baseScene.ts index 1db68eb82..bbd89af78 100644 --- a/prismarine-viewer/examples/baseScene.ts +++ b/prismarine-viewer/examples/baseScene.ts @@ -18,15 +18,18 @@ import { Viewer } from '../viewer/lib/viewer' import { BlockNames } from '../../src/mcDataTypes' import { initWithRenderer, statsEnd, statsStart } from '../../src/topRightStats' import { getSyncWorld } from './shared' +import { defaultWebgpuRendererParams, rendererParamsGui } from './webgpuRendererShared' window.THREE = THREE export class BasePlaygroundScene { + webgpuRendererParams = false continuousRender = false guiParams = {} viewDistance = 0 targetPos = new Vec3(2, 90, 2) params = {} as Record + allParamsValuesInit = {} as Record paramOptions = {} as Partial rendererParamsGui[key])), + }) + + Object.assign(this.paramOptions, { + orbit: { + reloadOnChange: true, + }, + webgpuWorker: { + reloadOnChange: true, + }, + // ...Object.fromEntries(Object.entries(rendererParamsGui)) + }) + } + const qs = new URLSearchParams(window.location.search) - for (const key of Object.keys(this.params)) { + for (const key of qs.keys()) { const value = qs.get(key) if (!value) continue const parsed = /^-?\d+$/.test(value) ? Number(value) : value === 'true' ? true : value === 'false' ? false : value - this.params[key] = parsed + this.allParamsValuesInit[key] = parsed + } + for (const key of Object.keys(this.allParamsValuesInit)) { + if (this.params[key] === undefined) continue + this.params[key] = this.allParamsValuesInit[key] } for (const param of Object.keys(this.params)) { @@ -105,6 +129,18 @@ export class BasePlaygroundScene { } this.updateQs() }) + + if (this.webgpuRendererParams) { + for (const key of Object.keys(defaultWebgpuRendererParams)) { + // eslint-disable-next-line @typescript-eslint/no-loop-func + this.onParamUpdate[key] = () => { + viewer.world.updateRendererParams(this.params) + } + } + + this.enableCameraOrbitControl = this.params.orbit + viewer.world.updateRendererParams(this.params) + } } // mainChunk: import('prismarine-chunk/types/index').PCChunk @@ -121,19 +157,36 @@ export class BasePlaygroundScene { this.world.setBlock(this.targetPos.offset(xOffset, yOffset, zOffset), block) } + lockCameraInUrl () { + this.params.camera = this.getCameraStateString() + this.updateQs() + } + resetCamera () { + this.controls?.reset() const { targetPos } = this this.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) + // viewer.camera.rotation.set(pitch, yaw, 0, 'ZYX') + viewer.camera.lookAt(targetPos.x + 0.5, targetPos.y + 0.5, targetPos.z + 0.5) this.controls?.update() } + getCameraStateString () { + const { camera } = viewer + return [ + camera.position.x.toFixed(2), + camera.position.y.toFixed(2), + camera.position.z.toFixed(2), + camera.rotation.x.toFixed(2), + camera.rotation.y.toFixed(2), + ].join(',') + } + async initData () { await window._LOAD_MC_DATA() const mcData: IndexedData = require('minecraft-data')(this.version) @@ -146,9 +199,8 @@ export class BasePlaygroundScene { world.setBlockStateId(this.targetPos, 0) this.world = world - this.initGui() - const worldView = new WorldDataEmitter(world, this.viewDistance, this.targetPos) + worldView.isPlayground = true worldView.addWaitTime = 0 window.worldView = worldView @@ -158,9 +210,18 @@ export class BasePlaygroundScene { renderer.setSize(window.innerWidth, window.innerHeight) // Create viewer - const viewer = new Viewer(renderer, { numWorkers: 6, showChunkBorders: false, }) + const viewer = new Viewer(renderer, { numWorkers: 6, showChunkBorders: false, isPlayground: true }) + viewer.setFirstPersonCamera(null, viewer.camera.rotation.y, viewer.camera.rotation.x) window.viewer = viewer - const isWebgpu = false + viewer.world.blockstatesModels = blockstatesModels + viewer.addChunksBatchWaitTime = 0 + viewer.entities.setDebugMode('basic') + viewer.world.mesherConfig.enableLighting = false + viewer.world.allowUpdates = true + this.initGui() + await viewer.setVersion(this.version) + + const isWebgpu = true const promises = [] as Array> if (isWebgpu) { // promises.push(initWebgpuRenderer(() => { }, true, true)) // todo @@ -169,14 +230,9 @@ export class BasePlaygroundScene { renderer.domElement.id = 'viewer-canvas' document.body.appendChild(renderer.domElement) } - viewer.addChunksBatchWaitTime = 0 - viewer.world.blockstatesModels = blockstatesModels - viewer.entities.setDebugMode('basic') - viewer.setVersion(this.version) viewer.entities.onSkinUpdate = () => { viewer.render() } - viewer.world.mesherConfig.enableLighting = false await Promise.all(promises) this.setupWorld() @@ -193,7 +249,7 @@ export class BasePlaygroundScene { this.resetCamera() // #region camera rotation param - const cameraSet = this.params.camera || localStorage.camera + const cameraSet = this.allParamsValuesInit.camera || localStorage.camera if (cameraSet) { const [x, y, z, rx, ry] = cameraSet.split(',').map(Number) viewer.camera.position.set(x, y, z) @@ -204,13 +260,7 @@ export class BasePlaygroundScene { const { camera } = viewer // params.camera = `${camera.rotation.x.toFixed(2)},${camera.rotation.y.toFixed(2)}` // this.updateQs() - localStorage.camera = [ - camera.position.x.toFixed(2), - camera.position.y.toFixed(2), - camera.position.z.toFixed(2), - camera.rotation.x.toFixed(2), - camera.rotation.y.toFixed(2), - ].join(',') + localStorage.camera = this.getCameraStateString() }, 200) if (this.controls) { this.controls.addEventListener('change', () => { @@ -283,13 +333,15 @@ export class BasePlaygroundScene { document.addEventListener('keydown', (e) => { if (!e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) { if (e.code === 'KeyR') { - this.controls?.reset() this.resetCamera() } if (e.code === 'KeyE') { worldView?.setBlockStateId(this.targetPos, this.world.getBlockStateId(this.targetPos)) } } + if (e.code === 'KeyT') { + viewer.camera.position.y += 100 * (e.shiftKey ? -1 : 1) + } }) document.addEventListener('visibilitychange', () => { this.windowHidden = document.visibilityState === 'hidden' @@ -331,13 +383,9 @@ export class BasePlaygroundScene { direction.applyQuaternion(viewer.camera.quaternion) direction.y = 0 - if (pressedKeys.has('ShiftLeft')) { - direction.y *= 2 - direction.x *= 2 - direction.z *= 2 - } + const scalar = pressedKeys.has('AltLeft') ? 4 : 1 // Add the vector to the camera's position to move the camera - viewer.camera.position.add(direction.normalize()) + viewer.camera.position.add(direction.normalize().multiplyScalar(scalar)) this.controls?.update() this.render() } diff --git a/prismarine-viewer/examples/chunksStorage.test.ts b/prismarine-viewer/examples/chunksStorage.test.ts new file mode 100644 index 000000000..344ad3345 --- /dev/null +++ b/prismarine-viewer/examples/chunksStorage.test.ts @@ -0,0 +1,239 @@ +import { test, expect } from 'vitest' +import { ChunksStorage } from './chunksStorage' + +globalThis.reportError = err => { + throw err +} +test('Free areas', () => { + const storage = new ChunksStorage() + storage.chunkSizeDisplay = 1 + const blocksWith1 = Object.fromEntries(Array.from({ length: 100 }).map((_, i) => { + return [`${i},0,0`, 1 as any] + })) + const blocksWith2 = Object.fromEntries(Array.from({ length: 100 }).map((_, i) => { + return [`${i},0,0`, 2 as any] + })) + const blocksWith3 = Object.fromEntries(Array.from({ length: 10 }).map((_, i) => { + return [`${i},0,0`, 3 as any] + })) + const blocksWith4 = Object.fromEntries(Array.from({ length: 10 }).map((_, i) => { + return [`${i},0,0`, 4 as any] + })) + + const getRangeString = () => { + const ranges = {} + let lastNum = storage.allBlocks[0]?.[3] + let lastNumI = 0 + for (let i = 0; i < storage.allBlocks.length; i++) { + const num = storage.allBlocks[i]?.[3] + if (lastNum !== num || i === storage.allBlocks.length - 1) { + const inclusive = i === storage.allBlocks.length - 1 + ranges[`[${lastNumI}-${i}${inclusive ? ']' : ')'}`] = lastNum + lastNum = num + lastNumI = i + } + } + return ranges + } + + const testRange = (start, end, number) => { + for (let i = start; i < end; i++) { + expect(storage.allBlocks[i]?.[3], `allblocks ${i} (range ${start}-${end})`).toBe(number) + } + } + + storage.addChunk(blocksWith1, '0,0,0') + storage.addChunk(blocksWith2, '1,0,0') + expect(storage.chunksMap).toMatchInlineSnapshot(` + Map { + "0,0,0" => 0, + "1,0,0" => 1, + } + `) + expect(storage.chunks).toMatchInlineSnapshot(` + [ + { + "free": false, + "length": 100, + "x": 0, + "z": 0, + }, + { + "free": false, + "length": 100, + "x": 1, + "z": 0, + }, + ] + `) + expect(storage.findBelongingChunk(100)).toMatchInlineSnapshot(` + { + "chunk": { + "free": false, + "length": 100, + "x": 1, + "z": 0, + }, + "index": 1, + } + `) + expect(getRangeString()).toMatchInlineSnapshot(` + { + "[0-100)": 1, + "[100-199]": 2, + } + `) + + storage.removeChunk('0,0,0') + expect(storage.chunks[0].free).toBe(true) + expect(storage.chunks[0].length).toBe(100) + + expect(getRangeString()).toMatchInlineSnapshot(` + { + "[0-100)": undefined, + "[100-199]": 2, + } + `) + + storage.addChunk(blocksWith3, `0,0,2`) + expect(storage.chunksMap).toMatchInlineSnapshot(` + Map { + "1,0,0" => 1, + "0,0,2" => 0, + } + `) + expect(storage.chunks).toMatchInlineSnapshot(` + [ + { + "free": false, + "length": 100, + "x": 0, + "z": 2, + }, + { + "free": false, + "length": 100, + "x": 1, + "z": 0, + }, + ] + `) + expect(getRangeString()).toMatchInlineSnapshot(` + { + "[0-10)": 3, + "[10-100)": undefined, + "[100-199]": 2, + } + `) + + // update (no map changes) + storage.addChunk(blocksWith4, `0,0,2`) + expect(storage.chunksMap).toMatchInlineSnapshot(` + Map { + "1,0,0" => 1, + "0,0,2" => 0, + } + `) + expect(storage.chunks).toMatchInlineSnapshot(` + [ + { + "free": false, + "length": 100, + "x": 0, + "z": 2, + }, + { + "free": false, + "length": 100, + "x": 1, + "z": 0, + }, + ] + `) + expect(getRangeString()).toMatchInlineSnapshot(` + { + "[0-10)": 4, + "[10-100)": undefined, + "[100-199]": 2, + } + `) + + storage.addChunk(blocksWith3, `0,0,3`) + expect(storage.chunksMap).toMatchInlineSnapshot(` + Map { + "1,0,0" => 1, + "0,0,2" => 0, + "0,0,3" => 2, + } + `) + expect(storage.chunks).toMatchInlineSnapshot(` + [ + { + "free": false, + "length": 100, + "x": 0, + "z": 2, + }, + { + "free": false, + "length": 100, + "x": 1, + "z": 0, + }, + { + "free": false, + "length": 10, + "x": 0, + "z": 3, + }, + ] + `) + expect(getRangeString()).toMatchInlineSnapshot(` + { + "[0-10)": 4, + "[10-100)": undefined, + "[100-200)": 2, + "[200-209]": 3, + } + `) + expect(storage.allBlocks.length).toBe(210) + + // update 0,0,2 + storage.addChunk(blocksWith1, `0,0,2`) + expect(storage.chunksMap).toMatchInlineSnapshot(` + Map { + "1,0,0" => 1, + "0,0,3" => 2, + "0,0,2" => 0, + } + `) + expect(storage.chunks).toMatchInlineSnapshot(` + [ + { + "free": false, + "length": 100, + "x": 0, + "z": 2, + }, + { + "free": false, + "length": 100, + "x": 1, + "z": 0, + }, + { + "free": false, + "length": 10, + "x": 0, + "z": 3, + }, + ] + `) + expect(getRangeString()).toMatchInlineSnapshot(` + { + "[0-100)": 1, + "[100-200)": 2, + "[200-209]": 3, + } + `) +}) diff --git a/prismarine-viewer/examples/chunksStorage.ts b/prismarine-viewer/examples/chunksStorage.ts new file mode 100644 index 000000000..cb74defdf --- /dev/null +++ b/prismarine-viewer/examples/chunksStorage.ts @@ -0,0 +1,236 @@ +import { BlockFaceType, BlockType, makeError } from './shared' + +export type BlockWithWebgpuData = [number, number, number, BlockType] + +export class ChunksStorage { + allBlocks = [] as Array + chunks = [] as Array<{ length: number, free: boolean, x: number, z: number }> + chunksMap = new Map() + // flatBuffer = new Uint32Array() + updateQueue = [] as Array<{ start: number, end: number }> + + maxDataUpdate = 10_000 + // awaitingUpdateStart: number | undefined + // awaitingUpdateEnd: number | undefined + // dataSize = 0 + lastFetchedSize = 0 + chunkSizeDisplay = 16 + + get dataSize () { + return this.allBlocks.length + } + + findBelongingChunk (blockIndex: number) { + let currentStart = 0 + let i = 0 + for (const chunk of this.chunks) { + const { length: chunkLength } = chunk + currentStart += chunkLength + if (blockIndex < currentStart) { + return { + chunk, + index: i + } + } + i++ + } + } + + printSectionData ({ x, y, z }) { + x = Math.floor(x / 16) * 16 + y = Math.floor(y / 16) * 16 + z = Math.floor(z / 16) * 16 + const key = `${x},${y},${z}` + const chunkIndex = this.chunksMap.get(key) + if (chunkIndex === undefined) return + const chunk = this.chunks[chunkIndex] + let start = 0 + for (let i = 0; i < chunkIndex; i++) { + start += this.chunks[i].length + } + const end = start + chunk.length + return { + blocks: this.allBlocks.slice(start, end), + index: chunkIndex, + range: [start, end] + } + } + + printBlock ({ x, y, z }: { x: number, y: number, z: number }) { + const section = this.printSectionData({ x, y, z }) + if (!section) return + x = Math.floor(x / 16) * 16 + z = Math.floor(z / 16) * 16 + const xRel = ((x % 16) + 16) % 16 + const zRel = ((z % 16) + 16) % 16 + for (const block of section.blocks) { + if (block && block[0] === xRel && block[1] === y && block[2] === zRel) { + return block + } + } + return null + } + + getDataForBuffers () { + this.lastFetchedSize = this.dataSize + const task = this.updateQueue.shift() + if (!task) return + const { start: awaitingUpdateStart, end } = task + const awaitingUpdateEnd = end + // if (awaitingUpdateEnd - awaitingUpdateStart > this.maxDataUpdate) { + // this.awaitingUpdateStart = awaitingUpdateStart + this.maxDataUpdate + // awaitingUpdateEnd = awaitingUpdateStart + this.maxDataUpdate + // } else { + // this.awaitingUpdateStart = undefined + // this.awaitingUpdateEnd = undefined + // } + return { + allBlocks: this.allBlocks, + chunks: this.chunks, + awaitingUpdateStart, + awaitingUpdateSize: awaitingUpdateEnd - awaitingUpdateStart, + } + } + + // setAwaitingUpdate ({ awaitingUpdateStart, awaitingUpdateSize }: { awaitingUpdateStart: number, awaitingUpdateSize: number }) { + // this.awaitingUpdateStart = awaitingUpdateStart + // this.awaitingUpdateEnd = awaitingUpdateStart + awaitingUpdateSize + // } + + clearData () { + this.chunks = [] + this.allBlocks = [] + this.updateQueue = [] + } + + replaceBlocksData (start: number, newData: typeof this.allBlocks) { + if (newData.length > 16 * 16 * 16) { + throw new Error(`Chunk cant be that big: ${newData.length}`) + } + this.allBlocks.splice(start, newData.length, ...newData) + } + + getAvailableChunk (size: number) { + let currentStart = 0 + let usingChunk: typeof this.chunks[0] | undefined + for (const chunk of this.chunks) { + const { length: chunkLength, free } = chunk + currentStart += chunkLength + if (!free) continue + if (chunkLength >= size) { + usingChunk = chunk + usingChunk.free = false + currentStart -= chunkLength + break + } + } + + if (!usingChunk) { + const newChunk = { + length: size, + free: false, + x: -1, + z: -1 + } + this.chunks.push(newChunk) + usingChunk = newChunk + } + + return { + chunk: usingChunk, + start: currentStart + } + } + + removeChunk (chunkPosKey: string) { + if (!this.chunksMap.has(chunkPosKey)) return + let currentStart = 0 + const chunkIndex = this.chunksMap.get(chunkPosKey)! + const chunk = this.chunks[chunkIndex] + for (let i = 0; i < chunkIndex; i++) { + const chunk = this.chunks[i]! + currentStart += chunk.length + } + + this.replaceBlocksData(currentStart, Array.from({ length: chunk.length }).map(() => undefined)) // empty data, will be filled with 0 + this.requestRangeUpdate(currentStart, currentStart + chunk.length) + chunk.free = true + this.chunksMap.delete(chunkPosKey) + // try merge backwards + // for (let i = chunkIndex - 1; i >= 0; i--) { + // const chunk = this.chunks[i]! + // if (!chunk.free) break + // chunk.length += this.chunks[i]!.length + // this.chunks.splice(i, 1) + // chunkIndex-- + // } + // // try merge forwards + // for (let i = chunkIndex + 1; i < this.chunks.length; i++) { + // const chunk = this.chunks[i]! + // if (!chunk.free) break + // chunk.length += this.chunks[i]!.length + // this.chunks.splice(i, 1) + // i-- + // } + } + + addChunk (blocks: Record, rawPosKey: string) { + this.removeChunk(rawPosKey) + + const [xSection, ySection, zSection] = rawPosKey.split(',').map(Number) + const chunkPosKey = `${xSection / 16},${ySection / 16},${zSection / 16}` + + // if (xSection === 0 && (zSection === -16) && ySection === 128) { + // // if (xSection >= 0 && (zSection >= 0) && ySection >= 128) { + // // newData = newData.slice + // } else { + // return + // } + + const newData = Object.entries(blocks).map(([key, value]) => { + const [x, y, z] = key.split(',').map(Number) + const block = value + const xRel = ((x % 16) + 16) % 16 + const zRel = ((z % 16) + 16) % 16 + // if (xRel !== 0 || (zRel !== 1 && zRel !== 0)) return + return [xRel, y, zRel, block] satisfies BlockWithWebgpuData + }).filter(Boolean) + + // if (ySection > 100 && (xSection < 0 || xSection > 0)) { + // newData = Array.from({ length: 16 }, (_, i) => 0).flatMap((_, i) => { + // return Array.from({ length: 16 }, (_, j) => 0).map((_, k) => { + // return [i % 16, ySection + k, k, { + // visibleFaces: [0, 1, 2, 3, 4, 5], + // modelId: k === 0 ? 1 : 0, + // block: '' + // } + // ] + // }) + // }) + // } + + const { chunk, start } = this.getAvailableChunk(newData.length) + chunk.x = xSection / this.chunkSizeDisplay + chunk.z = zSection / this.chunkSizeDisplay + const chunkIndex = this.chunks.indexOf(chunk) + this.chunksMap.set(rawPosKey, chunkIndex) + + for (const b of newData) { + if (b[3] && typeof b[3] === 'object') { + b[3].chunk = chunkIndex + } + } + + this.replaceBlocksData(start, newData) + this.requestRangeUpdate(start, start + newData.length) + return chunkIndex + } + + requestRangeUpdate (start: number, end: number) { + this.updateQueue.push({ start, end }) + } + + clearRange (start: number, end: number) { + this.replaceBlocksData(start, Array.from({ length: end - start }).map(() => undefined)) + } +} diff --git a/prismarine-viewer/examples/messageChannel.ts b/prismarine-viewer/examples/messageChannel.ts new file mode 100644 index 000000000..fa99db319 --- /dev/null +++ b/prismarine-viewer/examples/messageChannel.ts @@ -0,0 +1,28 @@ +export class MessageChannelReplacement { + port1Listeners = [] as Array<(e: MessageEvent) => void> + port2Listeners = [] as Array<(e: MessageEvent) => void> + port1 = { + addEventListener: (type, listener) => { + if (type !== 'message') throw new Error('unsupported type') + this.port1Listeners.push(listener) + }, + postMessage: (data) => { + for (const listener of this.port1Listeners) { + listener(new MessageEvent('message', { data })) + } + }, + start() {} + } as any + port2 = { + addEventListener: (type, listener) => { + if (type !== 'message') throw new Error('unsupported type') + this.port2Listeners.push(listener) + }, + postMessage: (data) => { + for (const listener of this.port2Listeners) { + listener(new MessageEvent('message', { data })) + } + }, + start() {} + } as any +} diff --git a/prismarine-viewer/examples/playground.ts b/prismarine-viewer/examples/playground.ts index cd0fa2190..3838f4b68 100644 --- a/prismarine-viewer/examples/playground.ts +++ b/prismarine-viewer/examples/playground.ts @@ -3,9 +3,18 @@ import { playgroundGlobalUiState } from './playgroundUi' import * as scenes from './scenes' const qsScene = new URLSearchParams(window.location.search).get('scene') -const Scene: typeof BasePlaygroundScene = qsScene ? scenes[qsScene] : scenes.main -playgroundGlobalUiState.scenes = ['main', 'railsCobweb', 'floorRandom', 'lightingStarfield', 'transparencyIssue', 'entities', 'frequentUpdates', 'slabsOptimization'] -playgroundGlobalUiState.selected = qsScene ?? 'main' +// eslint-disable-next-line unicorn/no-useless-spread +playgroundGlobalUiState.scenes = [...new Set([...Object.keys(scenes)])] +playgroundGlobalUiState.selected = qsScene ?? 'floorRandom' +playgroundGlobalUiState.actions = { + 'Lock camera in URL' () { + scene.lockCameraInUrl() + }, + 'Reset camera' () { + scene.resetCamera() + } +} +const Scene: typeof BasePlaygroundScene = scenes[playgroundGlobalUiState.selected] const scene = new Scene() globalThis.scene = scene diff --git a/prismarine-viewer/examples/scenes/cubesHouse.ts b/prismarine-viewer/examples/scenes/cubesHouse.ts new file mode 100644 index 000000000..562ec78e8 --- /dev/null +++ b/prismarine-viewer/examples/scenes/cubesHouse.ts @@ -0,0 +1,49 @@ +import { Vec3 } from 'vec3' +import { BasePlaygroundScene } from '../baseScene' + +export default class RailsCobwebScene extends BasePlaygroundScene { + viewDistance = 16 + continuousRender = true + targetPos = new Vec3(0, 0, 0) + webgpuRendererParams = true + + override initGui (): void { + this.params = { + chunkDistance: 4, + } + + super.initGui() // restore user params + } + + setupWorld () { + viewer.world.allowUpdates = false + + const { chunkDistance } = this.params + // const fullBlocks = loadedData.blocksArray.map(x => x.name) + const fullBlocks = loadedData.blocksArray.filter(block => { + const b = this.Block.fromStateId(block.defaultState, 0) + if (b.shapes?.length !== 1) return false + const shape = b.shapes[0] + return shape[0] === 0 && shape[1] === 0 && shape[2] === 0 && shape[3] === 1 && shape[4] === 1 && shape[5] === 1 + }) + + const squareSize = chunkDistance * 16 + // for (let y = 0; y < squareSize; y += 2) { + // for (let x = 0; x < squareSize; x++) { + // for (let z = 0; z < squareSize; z++) { + // const isEven = x === z + // if (y > 400) continue + // worldView!.world.setBlockStateId(this.targetPos.offset(x, y, z), isEven ? 1 : 2) + // } + // } + // } + + for (let x = 0; x < chunkDistance; x++) { + for (let z = 0; z < chunkDistance; z++) { + for (let y = 0; y < 200; y++) { + viewer.world.webgpuChannel.generateRandom(16 ** 2, x * 16, z * 16, y) + } + } + } + } +} diff --git a/prismarine-viewer/examples/scenes/floorRandom.ts b/prismarine-viewer/examples/scenes/floorRandom.ts index c6d2ccf1c..20ba5b289 100644 --- a/prismarine-viewer/examples/scenes/floorRandom.ts +++ b/prismarine-viewer/examples/scenes/floorRandom.ts @@ -1,33 +1,48 @@ +import { Vec3 } from 'vec3' import { BasePlaygroundScene } from '../baseScene' export default class RailsCobwebScene extends BasePlaygroundScene { - viewDistance = 5 + webgpuRendererParams = true + viewDistance = 0 continuousRender = true + targetPos = new Vec3(0, 0, 0) override initGui (): void { this.params = { - squareSize: 50 + chunksDistance: 16, } - super.initGui() + this.paramOptions.chunksDistance = { + reloadOnChange: true, + } + + super.initGui() // restore user params } setupWorld () { - const squareSize = this.params.squareSize ?? 30 - const maxSquareSize = this.viewDistance * 16 * 2 - if (squareSize > maxSquareSize) throw new Error(`Square size too big, max is ${maxSquareSize}`) - // const fullBlocks = loadedData.blocksArray.map(x => x.name) - const fullBlocks = loadedData.blocksArray.filter(block => { - const b = this.Block.fromStateId(block.defaultState, 0) - if (b.shapes?.length !== 1) return false - const shape = b.shapes[0] - return shape[0] === 0 && shape[1] === 0 && shape[2] === 0 && shape[3] === 1 && shape[4] === 1 && shape[5] === 1 - }) - for (let x = -squareSize; x <= squareSize; x++) { - for (let z = -squareSize; z <= squareSize; z++) { - const i = Math.abs(x + z) * squareSize - worldView!.world.setBlock(this.targetPos.offset(x, 0, z), this.Block.fromStateId(fullBlocks[i % fullBlocks.length].defaultState, 0)) + viewer.world.allowUpdates = true + const chunkDistance = this.params.chunksDistance + for (let x = -chunkDistance; x < chunkDistance; x++) { + for (let z = -chunkDistance; z < chunkDistance; z++) { + viewer.world.webgpuChannel.generateRandom(16 ** 2, x * 16, z * 16) } } + + // const squareSize = this.params.squareSize ?? 30 + // const maxSquareSize = this.viewDistance * 16 * 2 + // if (squareSize > maxSquareSize) throw new Error(`Square size too big, max is ${maxSquareSize}`) + // // const fullBlocks = loadedData.blocksArray.map(x => x.name) + // const fullBlocks = loadedData.blocksArray.filter(block => { + // const b = this.Block.fromStateId(block.defaultState, 0) + // if (b.shapes?.length !== 1) return false + // const shape = b.shapes[0] + // return shape[0] === 0 && shape[1] === 0 && shape[2] === 0 && shape[3] === 1 && shape[4] === 1 && shape[5] === 1 + // }) + // for (let x = -squareSize; x <= squareSize; x++) { + // for (let z = -squareSize; z <= squareSize; z++) { + // const i = Math.abs(x + z) * squareSize + // worldView!.world.setBlock(this.targetPos.offset(x, 0, z), this.Block.fromStateId(fullBlocks[i % fullBlocks.length].defaultState, 0)) + // } + // } } } diff --git a/prismarine-viewer/examples/scenes/floorStoneWorld.ts b/prismarine-viewer/examples/scenes/floorStoneWorld.ts new file mode 100644 index 000000000..5db1c9354 --- /dev/null +++ b/prismarine-viewer/examples/scenes/floorStoneWorld.ts @@ -0,0 +1,46 @@ +import { Vec3 } from 'vec3' +import { BasePlaygroundScene } from '../baseScene' + +export default class Scene extends BasePlaygroundScene { + viewDistance = 16 + continuousRender = true + targetPos = new Vec3(0, 0, 0) + webgpuRendererParams = true + + override initGui (): void { + this.params = { + chunksDistance: 2, + } + + super.initGui() // restore user params + } + + async setupWorld () { + // const chunkDistance = this.params.chunksDistance + // for (let x = -chunkDistance; x < chunkDistance; x++) { + // for (let z = -chunkDistance; z < chunkDistance; z++) { + // webgpuChannel.generateRandom(16 ** 2, x * 16, z * 16) + // } + // } + + const squareSize = this.params.chunksDistance * 16 + const maxSquareSize = this.viewDistance * 16 * 2 + if (squareSize > maxSquareSize) throw new Error(`Square size too big, max is ${maxSquareSize}`) + // const fullBlocks = loadedData.blocksArray.map(x => x.name) + const fullBlocks = loadedData.blocksArray.filter(block => { + const b = this.Block.fromStateId(block.defaultState, 0) + if (b.shapes?.length !== 1) return false + const shape = b.shapes[0] + return shape[0] === 0 && shape[1] === 0 && shape[2] === 0 && shape[3] === 1 && shape[4] === 1 && shape[5] === 1 + }) + + for (let x = -squareSize; x <= squareSize; x++) { + for (let z = -squareSize; z <= squareSize; z++) { + const isEven = x === z + worldView!.world.setBlockStateId(this.targetPos.offset(x, 0, z), isEven ? 1 : 2) + } + } + + console.log('setting done') + } +} diff --git a/prismarine-viewer/examples/scenes/index.ts b/prismarine-viewer/examples/scenes/index.ts index 81657a712..151e7b843 100644 --- a/prismarine-viewer/examples/scenes/index.ts +++ b/prismarine-viewer/examples/scenes/index.ts @@ -1,10 +1,12 @@ // export { default as rotation } from './rotation' export { default as main } from './main' -export { default as railsCobweb } from './railsCobweb' +// export { default as railsCobweb } from './railsCobweb' export { default as floorRandom } from './floorRandom' -export { default as lightingStarfield } from './lightingStarfield' -export { default as transparencyIssue } from './transparencyIssue' -export { default as rotationIssue } from './rotationIssue' -export { default as entities } from './entities' -export { default as frequentUpdates } from './frequentUpdates' -export { default as slabsOptimization } from './slabsOptimization' +export { default as floorStoneWorld } from './floorStoneWorld' +export { default as layers } from './layers' +export { default as cubesHouse } from './cubesHouse' +// export { default as lightingStarfield } from './lightingStarfield' +// export { default as transparencyIssue } from './transparencyIssue' +// export { default as rotationIssue } from './rotationIssue' +// export { default as entities } from './entities' +// export { default as frequentUpdates } from './frequentUpdates' diff --git a/prismarine-viewer/examples/scenes/layers.ts b/prismarine-viewer/examples/scenes/layers.ts new file mode 100644 index 000000000..e01495750 --- /dev/null +++ b/prismarine-viewer/examples/scenes/layers.ts @@ -0,0 +1,45 @@ +import { Vec3 } from 'vec3' +import { BasePlaygroundScene } from '../baseScene' + +export default class Scene extends BasePlaygroundScene { + viewDistance = 16 + continuousRender = true + targetPos = new Vec3(0, 0, 0) + webgpuRendererParams = true + + override initGui (): void { + this.params = { + chunksDistance: 2, + } + + super.initGui() // restore user params + } + + async setupWorld () { + const squareSize = this.params.chunksDistance * 16 + const maxSquareSize = this.viewDistance * 16 * 2 + if (squareSize > maxSquareSize) throw new Error(`Square size too big, max is ${maxSquareSize}`) + // const fullBlocks = loadedData.blocksArray.map(x => x.name) + const fullBlocks = loadedData.blocksArray.filter(block => { + const b = this.Block.fromStateId(block.defaultState, 0) + if (b.shapes?.length !== 1) return false + const shape = b.shapes[0] + return shape[0] === 0 && shape[1] === 0 && shape[2] === 0 && shape[3] === 1 && shape[4] === 1 && shape[5] === 1 + }) + + const start = -squareSize + const end = squareSize + + const STEP = 40 + for (let y = 0; y <= 256; y += STEP) { + for (let x = start; x <= end; x++) { + for (let z = start; z <= end; z++) { + const isEven = x === z + worldView!.world.setBlockStateId(this.targetPos.offset(x, y, z), y === 0 ? fullBlocks.find(block => block.name === 'glass')!.defaultState : fullBlocks[y / STEP]!.defaultState) + } + } + } + + console.log('setting done') + } +} diff --git a/prismarine-viewer/examples/shared.ts b/prismarine-viewer/examples/shared.ts index ba58a57fa..cf4c97282 100644 --- a/prismarine-viewer/examples/shared.ts +++ b/prismarine-viewer/examples/shared.ts @@ -3,7 +3,8 @@ import ChunkLoader from 'prismarine-chunk' export type BlockFaceType = { side: number - textureIndex: number + // textureIndex: number + modelId: number tint?: [number, number, number] isTransparent?: boolean @@ -14,9 +15,13 @@ export type BlockFaceType = { } export type BlockType = { - faces: BlockFaceType[] + // faces: BlockFaceType[] + visibleFaces: number[] + modelId: number + tint?: [number, number, number] // for testing + chunk?: number block: string } diff --git a/prismarine-viewer/examples/webgpuBlockModels.ts b/prismarine-viewer/examples/webgpuBlockModels.ts new file mode 100644 index 000000000..13dbdade1 --- /dev/null +++ b/prismarine-viewer/examples/webgpuBlockModels.ts @@ -0,0 +1,158 @@ +import { versionToNumber } from 'flying-squid/dist/utils' +import worldBlockProvider from 'mc-assets/dist/worldBlockProvider' +import PrismarineBlock, { Block } from 'prismarine-block' +import { IndexedBlock } from 'minecraft-data' +import { getPreflatBlock } from '../viewer/lib/mesher/getPreflatBlock' +import { WEBGPU_FULL_TEXTURES_LIMIT } from './webgpuRendererShared' + +export const prepareCreateWebgpuBlocksModelsData = () => { + const blocksMap = { + 'double_stone_slab': 'stone', + 'stone_slab': 'stone', + 'oak_stairs': 'planks', + 'stone_stairs': 'stone', + 'glass_pane': 'stained_glass', + 'brick_stairs': 'brick_block', + 'stone_brick_stairs': 'stonebrick', + 'nether_brick_stairs': 'nether_brick', + 'double_wooden_slab': 'planks', + 'wooden_slab': 'planks', + 'sandstone_stairs': 'sandstone', + 'cobblestone_wall': 'cobblestone', + 'quartz_stairs': 'quartz_block', + 'stained_glass_pane': 'stained_glass', + 'red_sandstone_stairs': 'red_sandstone', + 'stone_slab2': 'stone_slab', + 'purpur_stairs': 'purpur_block', + 'purpur_slab': 'purpur_block' + } + + const isPreflat = versionToNumber(viewer.world.version!) < versionToNumber('1.13') + const provider = worldBlockProvider(viewer.world.blockstatesModels, viewer.world.blocksAtlasParser?.atlasJson ?? viewer.world.blocksAtlases, 'latest') + const PBlockOriginal = PrismarineBlock(viewer.world.version!) + + const interestedTextureTiles = new Set() + const blocksDataModelDebug = {} as AllBlocksDataModels + const blocksDataModel = {} as AllBlocksDataModels + const blocksProccessed = {} as Record + let i = 0 + const allBlocksStateIdToModelIdMap = {} as AllBlocksStateIdToModelIdMap + + const addBlockModel = (state: number, name: string, props: Record, mcBlockData?: IndexedBlock, defaultState = false) => { + const models = provider.getAllResolvedModels0_1({ + name, + properties: props + }, isPreflat) + // skipping composite blocks + if (models.length !== 1 || !models[0]![0].elements) { + return + } + const elements = models[0]![0]?.elements + if (elements.length !== 1 && name !== 'grass_block') { + return + } + const elem = models[0]![0].elements[0] + if (elem.from[0] !== 0 || elem.from[1] !== 0 || elem.from[2] !== 0 || elem.to[0] !== 16 || elem.to[1] !== 16 || elem.to[2] !== 16) { + // not full block + return + } + const facesMapping = [ + ['front', 'south'], + ['bottom', 'down'], + ['top', 'up'], + ['right', 'east'], + ['left', 'west'], + ['back', 'north'], + ] + const blockData: BlocksModelData = { + textures: [0, 0, 0, 0, 0, 0], + rotation: [0, 0, 0, 0, 0, 0] + } + for (const [face, { texture, cullface, rotation = 0 }] of Object.entries(elem.faces)) { + const faceIndex = facesMapping.findIndex(x => x.includes(face)) + if (faceIndex === -1) { + throw new Error(`Unknown face ${face}`) + } + blockData.textures[faceIndex] = texture.tileIndex + blockData.rotation[faceIndex] = rotation / 90 + if (Math.floor(blockData.rotation[faceIndex]) !== blockData.rotation[faceIndex]) { + throw new Error(`Invalid rotation ${rotation} ${name}`) + } + interestedTextureTiles.add(texture.debugName) + } + const k = i++ + allBlocksStateIdToModelIdMap[state] = k + blocksDataModel[k] = blockData + if (defaultState) { + blocksDataModelDebug[name] ??= blockData + } + blocksProccessed[name] = true + if (mcBlockData) { + blockData.transparent = mcBlockData.transparent + blockData.emitLight = mcBlockData.emitLight + blockData.filterLight = mcBlockData.filterLight + } + } + addBlockModel(-1, 'unknown', {}) + const textureOverrideFullBlocks = { + water: 'water_still', + lava: 'lava_still' + } + outer: for (const b of loadedData.blocksArray) { + for (let state = b.minStateId; state <= b.maxStateId; state++) { + if (interestedTextureTiles.size >= WEBGPU_FULL_TEXTURES_LIMIT) { + console.warn(`Limit in ${WEBGPU_FULL_TEXTURES_LIMIT} textures reached for full blocks, skipping others!`) + break outer + } + const mapping = blocksMap[b.name] + const block = PBlockOriginal.fromStateId(mapping && loadedData.blocksByName[mapping] ? loadedData.blocksByName[mapping].defaultState : state, 0) + if (isPreflat) { + getPreflatBlock(block) + } + + const textureOverride = textureOverrideFullBlocks[block.name] as string | undefined + if (textureOverride) { + const k = i++ + const texture = provider.getTextureInfo(textureOverride) + if (!texture) { + console.warn('Missing texture override') + continue + } + const texIndex = texture.tileIndex + allBlocksStateIdToModelIdMap[state] = k + const blockData: BlocksModelData = { + textures: [texIndex, texIndex, texIndex, texIndex, texIndex, texIndex], + rotation: [0, 0, 0, 0, 0, 0], + filterLight: b.filterLight + } + blocksDataModel[k] = blockData + interestedTextureTiles.add(textureOverride) + continue + } + + if (block.shapes.length === 0 || !block.shapes.every(shape => { + return shape[0] === 0 && shape[1] === 0 && shape[2] === 0 && shape[3] === 1 && shape[4] === 1 && shape[5] === 1 + })) { + continue + } + + addBlockModel(state, block.name, block.getProperties(), b, state === b.defaultState) + } + } + return { + blocksDataModel, + allBlocksStateIdToModelIdMap, + interestedTextureTiles, + blocksDataModelDebug + } +} +export type AllBlocksDataModels = Record +export type AllBlocksStateIdToModelIdMap = Record + +export type BlocksModelData = { + textures: number[] + rotation: number[] + transparent?: boolean + emitLight?: number + filterLight?: number +} diff --git a/prismarine-viewer/examples/webgpuRenderer.ts b/prismarine-viewer/examples/webgpuRenderer.ts new file mode 100644 index 000000000..72ef64c41 --- /dev/null +++ b/prismarine-viewer/examples/webgpuRenderer.ts @@ -0,0 +1,1297 @@ +import * as THREE from 'three' +import * as tweenJs from '@tweenjs/tween.js' +import VolumtetricFragShader from '../webgpuShaders/RadialBlur/frag.wgsl' +import VolumtetricVertShader from '../webgpuShaders/RadialBlur/vert.wgsl' +import { BlockFaceType } from './shared' +import { PositionOffset, UVOffset, quadVertexArray, quadVertexCount, cubeVertexSize } from './CubeDef' +import VertShader from './Cube.vert.wgsl' +import FragShader from './Cube.frag.wgsl' +import ComputeShader from './Cube.comp.wgsl' +import ComputeSortShader from './CubeSort.comp.wgsl' +import { chunksStorage, updateSize, postMessage } from './webgpuRendererWorker' +import { defaultWebgpuRendererParams, RendererInitParams, RendererParams } from './webgpuRendererShared' +import type { BlocksModelData } from './webgpuBlockModels' + +const cubeByteLength = 12 +export class WebgpuRenderer { + destroyed = false + rendering = true + renderedFrames = 0 + rendererParams = { ...defaultWebgpuRendererParams } + chunksFadeAnimationController = new IndexedInOutAnimationController(() => {}) + + ready = false + + device: GPUDevice + renderPassDescriptor: GPURenderPassDescriptor + uniformBindGroup: GPUBindGroup + vertexCubeBindGroup: GPUBindGroup + cameraUniform: GPUBuffer + ViewUniformBuffer: GPUBuffer + ProjectionUniformBuffer: GPUBuffer + ctx: GPUCanvasContext + verticesBuffer: GPUBuffer + InstancedModelBuffer: GPUBuffer + pipeline: GPURenderPipeline + InstancedTextureIndexBuffer: GPUBuffer + InstancedColorBuffer: GPUBuffer + notRenderedBlockChanges = 0 + renderingStats: undefined | { instanceCount: number } + renderingStatsRequestTime: number | undefined + + // Add these properties to the WebgpuRenderer class + computePipeline: GPUComputePipeline + indirectDrawBuffer: GPUBuffer + cubesBuffer: GPUBuffer + visibleCubesBuffer: GPUBuffer + computeBindGroup: GPUBindGroup + computeBindGroupLayout: GPUBindGroupLayout + indirectDrawParams: Uint32Array + maxBufferSize: number + commandEncoder: GPUCommandEncoder + AtlasTexture: GPUTexture + secondCameraUniformBindGroup: GPUBindGroup + secondCameraUniform: GPUBuffer + + multisampleTexture: GPUTexture | undefined + chunksBuffer: GPUBuffer + chunkBindGroup: GPUBindGroup + debugBuffer: GPUBuffer + + realNumberOfCubes = 0 + occlusionTexture: GPUBuffer + computeSortPipeline: GPUComputePipeline + depthTextureBuffer: GPUBuffer + textureSizeBuffer: any + textureSizeBindGroup: GPUBindGroup + cameraComputeUniform: GPUBuffer + modelsBuffer: GPUBuffer + indirectDrawBufferMap: GPUBuffer + indirectDrawBufferMapBeingUsed = false + cameraComputePositionUniform: GPUBuffer + NUMBER_OF_CUBES: number + depthTexture: GPUTexture + rendererDeviceString: string + cameraUpdated = true + lastCameraUpdateTime = 0 + noCameraUpdates = 0 + positiveCameraUpdates = false + lastCameraUpdateDiff = undefined as undefined | { + x: number + y: number + z: number + time: number + } + debugCameraMove = { + x: 0, + y: 0, + z: 0 + } + renderMs = 0 + renderMsCount = 0 + volumetricPipeline: GPURenderPipeline + VolumetricBindGroup: GPUBindGroup + depthTextureAnother: GPUTexture + volumetricRenderPassDescriptor: GPURenderPassDescriptor + tempTexture: GPUTexture + rotationsUniform: GPUBuffer + earlyZRejectUniform: GPUBuffer + tileSizeUniform: GPUBuffer + clearColorBuffer: GPUBuffer + + + // eslint-disable-next-line max-params + constructor (public canvas: HTMLCanvasElement, public imageBlob: ImageBitmapSource, public isPlayground: boolean, public camera: THREE.PerspectiveCamera, public localStorage: any, public blocksDataModel: Record, public rendererInitParams: RendererInitParams) { + this.NUMBER_OF_CUBES = 65_536 + void this.init().catch((err) => { + console.error(err) + postMessage({ type: 'rendererProblem', isContextLost: false, message: err.message }) + }) + } + + changeBackgroundColor (color: [number, number, number]) { + const colorRgba = [color[0], color[1], color[2], 1] + this.renderPassDescriptor.colorAttachments[0].clearValue = colorRgba + this.device.queue.writeBuffer( + this.clearColorBuffer, + 0, + new Float32Array(colorRgba) + ) + } + + updateConfig (newParams: RendererParams) { + this.rendererParams = { ...this.rendererParams, ...newParams } + } + + async init () { + const { canvas, imageBlob, isPlayground, localStorage } = this + + updateSize(canvas.width, canvas.height) + + if (!navigator.gpu) throw new Error('WebGPU not supported (probably can be enabled in settings)') + const adapter = await navigator.gpu.requestAdapter({ + ...this.rendererInitParams + }) + if (!adapter) throw new Error('WebGPU not supported') + const adapterInfo = adapter.info ?? {} // todo fix ios + this.rendererDeviceString = `${adapterInfo.vendor} ${adapterInfo.device} (${adapterInfo.architecture}) ${adapterInfo.description}` + + const twoGigs = 2_147_483_644 + try { + this.device = await adapter.requestDevice({ + // https://developer.mozilla.org/en-US/docs/Web/API/GPUDevice/limits + requiredLimits: { + maxStorageBufferBindingSize: twoGigs, + maxBufferSize: twoGigs, + } + }) + } catch (err) { + 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', 'available features', [...device.features.values()]) + + const ctx = this.ctx = canvas.getContext('webgpu')! + + const presentationFormat = navigator.gpu.getPreferredCanvasFormat() + + ctx.configure({ + device, + format: presentationFormat, + alphaMode: 'opaque', + }) + + const verticesBuffer = device.createBuffer({ + size: quadVertexArray.byteLength, + usage: GPUBufferUsage.VERTEX, + mappedAtCreation: true, + }) + + this.verticesBuffer = verticesBuffer + new Float32Array(verticesBuffer.getMappedRange()).set(quadVertexArray) + 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: PositionOffset, + format: 'float32x3', + }, + { + shaderLocation: 1, + offset: UVOffset, + format: 'float32x2', + }, + ], + }, + ], + }, + 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', + }, + }, + }, + ], + }, + // multisample: { + // count: 4, + // }, + primitive: { + topology: 'triangle-list', + cullMode: 'none', + }, + depthStencil: { + depthWriteEnabled: true, + depthCompare: 'less', + format: 'depth32float', + }, + }) + this.pipeline = pipeline + + this.volumetricPipeline = device.createRenderPipeline({ + label: 'volumtetricPipeline', + layout: 'auto', + vertex: { + module: device.createShaderModule({ + code: localStorage.VolumtetricVertShader || VolumtetricVertShader, + }), + buffers: [ + { + arrayStride: cubeVertexSize, + attributes: [ + { + shaderLocation: 0, + offset: PositionOffset, + format: 'float32x3', + }, + { + shaderLocation: 1, + offset: UVOffset, + format: 'float32x2', + }, + ], + }, + ], + }, + fragment: { + module: device.createShaderModule({ + code: localStorage.VolumtetricFragShader || VolumtetricFragShader, + }), + 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: 'none', + }, + depthStencil: { + depthWriteEnabled: false, + depthCompare: 'less', + format: 'depth32float', + }, + }) + + this.depthTexture = device.createTexture({ + size: [canvas.width, canvas.height], + format: 'depth32float', + usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING, + //sampleCount: 4, + }) + + this.depthTextureAnother = device.createTexture({ + size: [canvas.width, canvas.height], + format: 'depth32float', + usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING, + //sampleCount: 4, + }) + + this.tempTexture = device.createTexture({ + size: [canvas.width, canvas.height], + format: 'bgra8unorm', + usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING, + //sampleCount: 4, + }) + + const Mat4x4BufferSize = 4 * (4 * 4) // 4x4 matrix + + this.cameraUniform = device.createBuffer({ + size: Mat4x4BufferSize, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }) + + this.earlyZRejectUniform = device.createBuffer({ + size: 4, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }) + + this.tileSizeUniform = device.createBuffer({ + size: 8, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }) + + this.rotationsUniform = device.createBuffer({ + size: Mat4x4BufferSize * 6, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }) + + const matrixData = new Float32Array([ + ...new THREE.Matrix4().makeRotationX(THREE.MathUtils.degToRad(90)).toArray(), + ...new THREE.Matrix4().makeRotationX(THREE.MathUtils.degToRad(-90)).toArray(), + ...new THREE.Matrix4().makeRotationY(THREE.MathUtils.degToRad(0)).toArray(), + ...new THREE.Matrix4().makeRotationY(THREE.MathUtils.degToRad(180)).toArray(), + ...new THREE.Matrix4().makeRotationY(THREE.MathUtils.degToRad(-90)).toArray(), + ...new THREE.Matrix4().makeRotationY(THREE.MathUtils.degToRad(90)).toArray(), + ]) + + device.queue.writeBuffer( + this.rotationsUniform, + 0, + matrixData + ) + + this.cameraComputeUniform = device.createBuffer({ + size: Mat4x4BufferSize, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }) + + this.clearColorBuffer = device.createBuffer({ + size: 4 * 4, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }) + + this.cameraComputePositionUniform = device.createBuffer({ + size: 4 * 4, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }) + + this.secondCameraUniform = device.createBuffer({ + size: Mat4x4BufferSize, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }) + + const ViewProjectionMat42 = new THREE.Matrix4() + const { projectionMatrix: projectionMatrix2, matrix: matrix2 } = this.camera2 + ViewProjectionMat42.multiplyMatrices(projectionMatrix2, matrix2.invert()) + const ViewProjection2 = new Float32Array(ViewProjectionMat42.elements) + device.queue.writeBuffer( + this.secondCameraUniform, + 0, + ViewProjection2 + ) + + // upload image into a GPUTexture. + await this.updateTexture(imageBlob, true) + + this.volumetricRenderPassDescriptor = { + label: 'VolumteticRenderPassDescriptor', + colorAttachments: [ + { + view: undefined as any, // Assigned later + clearValue: [0.678_431_372_549_019_6, 0.847_058_823_529_411_8, 0.901_960_784_313_725_5, 1], + loadOp: 'clear', + storeOp: 'store', + }, + ], + depthStencilAttachment: { + view: this.depthTextureAnother.createView(), + depthClearValue: 1, + depthLoadOp: 'clear', + depthStoreOp: 'store', + }, + } + + this.renderPassDescriptor = { + label: 'MainRenderPassDescriptor', + colorAttachments: [ + { + view: undefined as any, // Assigned later + clearValue: [0.678_431_372_549_019_6, 0.847_058_823_529_411_8, 0.901_960_784_313_725_5, 1], + loadOp: 'clear', + storeOp: 'store', + }, + ], + depthStencilAttachment: { + view: this.depthTexture.createView(), + depthClearValue: 1, + depthLoadOp: 'clear', + depthStoreOp: 'store', + }, + } + + // Create compute pipeline + const computeShaderModule = device.createShaderModule({ + code: localStorage.computeShader || ComputeShader, + label: 'Occlusion Writing', + }) + + const computeSortShaderModule = device.createShaderModule({ + code: ComputeSortShader, + label: 'Storage Texture Sorting', + }) + + const 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: 'storage' } }, + { binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } }, + { binding: 4, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } }, + { binding: 5, visibility: GPUShaderStage.COMPUTE, texture: { sampleType: 'depth' } }, + { binding: 6, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'uniform' } }, + ], + }) + + const computeChunksLayout = device.createBindGroupLayout({ + label: 'computeChunksLayout', + entries: [ + { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } }, + { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } }, + { binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } }, + { binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'uniform' } }, + { binding: 4, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'uniform' } }, + ], + }) + + const textureSizeBindGroupLayout = device.createBindGroupLayout({ + entries: [ + { + binding: 0, + visibility: GPUShaderStage.COMPUTE, + buffer: { type: 'uniform' }, + }, + ], + }) + + const computePipelineLayout = device.createPipelineLayout({ + label: 'computePipelineLayout', + bindGroupLayouts: [computeBindGroupLayout, computeChunksLayout, textureSizeBindGroupLayout] + }) + + this.textureSizeBuffer = this.device.createBuffer({ + size: 8, // vec2 consists of two 32-bit unsigned integers + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }) + + + this.textureSizeBindGroup = device.createBindGroup({ + layout: textureSizeBindGroupLayout, + entries: [ + { + binding: 0, + resource: { + buffer: this.textureSizeBuffer, + }, + }, + ], + }) + + this.computePipeline = device.createComputePipeline({ + label: 'Culled Instance', + layout: computePipelineLayout, + // layout: 'auto', + compute: { + module: computeShaderModule, + entryPoint: 'main', + }, + }) + + this.computeSortPipeline = device.createComputePipeline({ + label: 'Culled Instance', + layout: computePipelineLayout, + // layout: 'auto', + compute: { + module: computeSortShaderModule, + entryPoint: 'main', + }, + }) + + this.indirectDrawBuffer = device.createBuffer({ + label: 'indirectDrawBuffer', + size: 16, // 4 uint32 values + usage: GPUBufferUsage.INDIRECT | GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC + }) + + this.indirectDrawBufferMap = device.createBuffer({ + label: 'indirectDrawBufferMap', + size: 16, // 4 uint32 values + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, + }) + + this.debugBuffer = device.createBuffer({ + label: 'debugBuffer', + size: 4 * 8192, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC, + }) + + this.chunksBuffer = this.createVertexStorage(65_535 * 12, 'chunksBuffer') + this.occlusionTexture = this.createVertexStorage(4096 * 4096 * 4, 'occlusionTexture') + this.depthTextureBuffer = this.createVertexStorage(4096 * 4096 * 4, 'depthTextureBuffer') + + // Initialize indirect draw parameters + const indirectDrawParams = new Uint32Array([quadVertexCount, 0, 0, 0]) + device.queue.writeBuffer(this.indirectDrawBuffer, 0, indirectDrawParams) + + // initialize texture size + const textureSize = new Uint32Array([this.canvas.width, this.canvas.height]) + device.queue.writeBuffer(this.textureSizeBuffer, 0, textureSize) + + void device.lost.then((info) => { + console.warn('WebGPU context lost:', info) + postMessage({ type: 'rendererProblem', isContextLost: true, message: info.message }) + }) + + this.updateBlocksModelData() + this.createNewDataBuffers() + + this.indirectDrawParams = new Uint32Array([quadVertexCount, 0, 0, 0]) + + // always last! + this.loop(true) // start rendering + this.ready = true + return canvas + } + + async updateTexture (imageBlob: ImageBitmapSource, isInitial = false) { + const textureBitmap = await createImageBitmap(imageBlob) + this.AtlasTexture?.destroy() + this.AtlasTexture = this.device.createTexture({ + size: [textureBitmap.width, textureBitmap.height, 1], + format: 'rgba8unorm', + usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT, + //sampleCount: 4 + }) + this.device.queue.copyExternalImageToTexture( + { source: textureBitmap }, + { texture: this.AtlasTexture }, + [textureBitmap.width, textureBitmap.height] + ) + + this.device.queue.writeBuffer( + this.tileSizeUniform, + 0, + new Float32Array([16, 16]) + ) + + if (!isInitial) { + this.createUniformBindGroup() + } + } + + safeLoop (isFirst: boolean | undefined, time: number | undefined) { + try { + this.loop(isFirst, time) + } catch (err) { + console.error(err) + postMessage({ type: 'rendererProblem', isContextLost: false, message: err.message }) + } + } + + public updateBlocksModelData () { + const keys = Object.keys(this.blocksDataModel) + // const modelsDataLength = keys.length + const modelsDataLength = +keys.at(-1)! + const modelsBuffer = new Uint32Array(modelsDataLength * 2) + for (let i = 0; i < modelsDataLength; i++) { + const blockData = this.blocksDataModel[i]/* ?? { + textures: [0, 0, 0, 0, 0, 0], + rotation: [0, 0, 0, 0], + } */ + if (!blockData) throw new Error(`Block model ${i} not found`) + const tempBuffer1 = (((blockData.textures[0] << 10) | blockData.textures[1]) << 10) | blockData.textures[2] + const tempBuffer2 = (((blockData.textures[3] << 10) | blockData.textures[4]) << 10) | blockData.textures[5] + modelsBuffer[+i * 2] = tempBuffer1 + modelsBuffer[+i * 2 + 1] = tempBuffer2 + } + + this.modelsBuffer?.destroy() + this.modelsBuffer = this.createVertexStorage(modelsDataLength * cubeByteLength, 'modelsBuffer') + this.device.queue.writeBuffer(this.modelsBuffer, 0, modelsBuffer) + } + + private createUniformBindGroup () { + const { device, pipeline } = this + const sampler = device.createSampler({ + magFilter: 'nearest', + minFilter: 'nearest', + }) + + this.uniformBindGroup = device.createBindGroup({ + label: 'uniformBindGroups', + layout: pipeline.getBindGroupLayout(0), + entries: [ + { + binding: 0, + resource: + { + buffer: this.cameraUniform, + }, + }, + { + binding: 1, + resource: sampler, + }, + { + binding: 2, + resource: this.AtlasTexture.createView(), + }, + { + binding: 3, + resource: { + buffer: this.modelsBuffer + }, + }, + { + binding: 4, + resource: { + buffer: this.rotationsUniform + } + }, + { + binding: 5, + resource: { buffer: this.tileSizeUniform }, + } + ], + }) + + this.vertexCubeBindGroup = device.createBindGroup({ + label: 'vertexCubeBindGroup', + layout: pipeline.getBindGroupLayout(1), + entries: [ + { + binding: 0, + resource: { buffer: this.cubesBuffer }, + }, + { + binding: 1, + resource: { buffer: this.visibleCubesBuffer }, + }, + { + binding: 2, + resource: { buffer: this.chunksBuffer }, + } + ], + }) + + this.secondCameraUniformBindGroup = device.createBindGroup({ + label: 'uniformBindGroupsCamera', + layout: pipeline.getBindGroupLayout(0), + entries: [ + { + binding: 0, + resource: { + buffer: this.secondCameraUniform, + }, + }, + { + binding: 1, + resource: sampler, + }, + { + binding: 2, + resource: this.AtlasTexture.createView(), + }, + { + binding: 3, + resource: { + buffer: this.modelsBuffer + }, + }, + { + binding: 4, + resource: { + buffer: this.rotationsUniform + } + }, + { + binding: 5, + resource: { buffer: this.tileSizeUniform }, + } + ], + }) + + this.VolumetricBindGroup = device.createBindGroup({ + layout: this.volumetricPipeline.getBindGroupLayout(0), + label: 'volumtetricBindGroup', + entries: [ + { + binding: 0, + resource: this.depthTexture.createView(), + }, + { + binding: 1, + resource: sampler, + }, + { + binding: 2, + resource: this.tempTexture.createView(), + }, + { + binding: 3, + resource: { buffer: this.clearColorBuffer }, + } + ] + }) + + + + this.computeBindGroup = device.createBindGroup({ + layout: this.computePipeline.getBindGroupLayout(0), + label: 'computeBindGroup', + entries: [ + { + binding: 0, + resource: { buffer: this.cameraUniform }, + }, + { + binding: 1, + resource: { buffer: this.cubesBuffer }, + }, + { + binding: 2, + resource: { buffer: this.visibleCubesBuffer }, + }, + { + binding: 3, + resource: { buffer: this.indirectDrawBuffer }, + }, + { + binding: 4, + resource: { buffer: this.debugBuffer }, + }, + { + binding: 5, + resource: this.depthTexture.createView(), + }, + { + binding: 6, + resource: { buffer: this.earlyZRejectUniform }, + }, + ], + }) + + this.chunkBindGroup = device.createBindGroup({ + layout: this.computePipeline.getBindGroupLayout(1), + label: 'anotherComputeBindGroup', + entries: [ + { + binding: 0, + resource: { buffer: this.chunksBuffer }, + }, + { + binding: 1, + resource: { buffer: this.occlusionTexture }, + }, + { + binding: 2, + resource: { buffer: this.depthTextureBuffer }, + }, + { + binding: 3, + resource: { buffer: this.cameraComputeUniform }, + }, + { + binding: 4, + resource: { buffer: this.cameraComputePositionUniform }, + } + ], + }) + } + + async readDebugBuffer () { + const readBuffer = this.device.createBuffer({ + size: this.debugBuffer.size, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, + }) + + const commandEncoder = this.device.createCommandEncoder() + commandEncoder.copyBufferToBuffer(this.debugBuffer, 0, readBuffer, 0, this.debugBuffer.size) + this.device.queue.submit([commandEncoder.finish()]) + + await readBuffer.mapAsync(GPUMapMode.READ) + const arrayBuffer = readBuffer.getMappedRange() + const debugData = new Uint32Array(arrayBuffer.slice(0, this.debugBuffer.size)) + readBuffer.unmap() + readBuffer.destroy() + return debugData + } + + createNewDataBuffers () { + const oldCubesBuffer = this.cubesBuffer + const oldVisibleCubesBuffer = this.visibleCubesBuffer + this.commandEncoder = this.device.createCommandEncoder() + + this.cubesBuffer = this.createVertexStorage(this.NUMBER_OF_CUBES * cubeByteLength, 'cubesBuffer') + + this.visibleCubesBuffer = this.createVertexStorage(this.NUMBER_OF_CUBES * cubeByteLength, 'visibleCubesBuffer') + + if (oldCubesBuffer) { + this.commandEncoder.copyBufferToBuffer(oldCubesBuffer, 0, this.cubesBuffer, 0, oldCubesBuffer.size) + this.commandEncoder.copyBufferToBuffer(oldVisibleCubesBuffer, 0, this.visibleCubesBuffer, 0, oldVisibleCubesBuffer.size) + this.device.queue.submit([this.commandEncoder.finish()]) + oldCubesBuffer.destroy() + oldVisibleCubesBuffer.destroy() + + } + + this.createUniformBindGroup() + } + + private createVertexStorage (size: number, label: string) { + return this.device.createBuffer({ + label, + size, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC, + }) + } + + updateSides () { + } + + updateCubesBuffersDataFromLoop () { + const DEBUG_DATA = false + + const dataForBuffers = chunksStorage.getDataForBuffers() + if (!dataForBuffers) return + const { allBlocks, chunks, awaitingUpdateSize: updateSize, awaitingUpdateStart: updateOffset } = dataForBuffers + // console.log('updating', updateOffset, updateSize) + + const NUMBER_OF_CUBES_NEEDED = allBlocks.length + if (NUMBER_OF_CUBES_NEEDED > this.NUMBER_OF_CUBES) { + const NUMBER_OF_CUBES_OLD = this.NUMBER_OF_CUBES + while (NUMBER_OF_CUBES_NEEDED > this.NUMBER_OF_CUBES) this.NUMBER_OF_CUBES += 1_000_000 + + console.warn('extending number of cubes', NUMBER_OF_CUBES_OLD, '->', this.NUMBER_OF_CUBES, `(needed ${NUMBER_OF_CUBES_NEEDED})`) + console.time('recreate buffers') + this.createNewDataBuffers() + console.timeEnd('recreate buffers') + } + this.realNumberOfCubes = NUMBER_OF_CUBES_NEEDED + + const unique = new Set() + const debugCheckDuplicate = (first, second, third) => { + const key = `${first},${third}` + if (unique.has(key)) { + throw new Error(`Duplicate: ${key}`) + } + unique.add(key) + } + + const cubeFlatData = new Uint32Array(updateSize * 3) + const blocksToUpdate = allBlocks.slice(updateOffset, updateOffset + updateSize) + + // eslint-disable-next-line unicorn/no-for-loop + for (let i = 0; i < blocksToUpdate.length; i++) { + let first = 0 + let second = 0 + let third = 0 + const chunkBlock = blocksToUpdate[i] + + if (chunkBlock) { + const [x, y, z, block] = chunkBlock + // if (chunk.index !== block.chunk) { + // throw new Error(`Block chunk mismatch ${block.chunk} !== ${chunk.index}`) + // } + const positions = [x, y + this.rendererParams.cameraOffset[1], z] + const visibility = Array.from({ length: 6 }, (_, i) => (block.visibleFaces.includes(i) ? 1 : 0)) + + const tint = block.tint ?? [1, 1, 1] + const colors = tint.map(x => x * 255) + + first = ((block.modelId << 4 | positions[2]) << 10 | positions[1]) << 4 | positions[0] + const visibilityCombined = (visibility[0]) | + (visibility[1] << 1) | + (visibility[2] << 2) | + (visibility[3] << 3) | + (visibility[4] << 4) | + (visibility[5] << 5) + second = ((visibilityCombined << 8 | colors[2]) << 8 | colors[1]) << 8 | colors[0] + third = block.chunk! + } + + cubeFlatData[i * 3] = first + cubeFlatData[i * 3 + 1] = second + cubeFlatData[i * 3 + 2] = third + if (DEBUG_DATA && chunkBlock) { + debugCheckDuplicate(first, second, third) + } + } + + const { totalFromChunks } = this.updateChunks(chunks) + + if (DEBUG_DATA) { + const actualCount = allBlocks.length + if (totalFromChunks !== actualCount) { + reportError?.(new Error(`Buffers length mismatch: from chunks: ${totalFromChunks}, flat data: ${actualCount}`)) + } + } + + this.device.queue.writeBuffer(this.cubesBuffer, updateOffset * cubeByteLength, cubeFlatData) + + this.notRenderedBlockChanges++ + this.realNumberOfCubes = allBlocks.length + } + + updateChunks (chunks: Array<{ x: number, z: number, length: number }>, offset = 0) { + const chunksCount = chunks.length + const chunksBuffer = new Int32Array(chunksCount * 3) + let totalFromChunks = 0 + for (let i = 0; i < chunksCount; i++) { + const offset = i * 3 + const { x, z, length } = chunks[i]! + const chunkProgress = this.chunksFadeAnimationController.indexes[i]?.progress ?? 1 + chunksBuffer[offset] = x + chunksBuffer[offset + 1] = z + chunksBuffer[offset + 2] = chunkProgress * 255 + const cubesCount = length + totalFromChunks += cubesCount + } + this.device.queue.writeBuffer(this.chunksBuffer, offset, chunksBuffer) + return { totalFromChunks } + } + + lastCall = performance.now() + logged = false + camera2 = (() => { + const camera = new THREE.PerspectiveCamera() + camera.lookAt(0, -1, 0) + camera.position.set(150, 500, 150) + camera.fov = 100 + camera.updateMatrix() + return camera + })() + + lastLoopTime = performance.now() + + loop (forceFrame = false, time = performance.now()) { + if (this.destroyed) return + const nextFrame = () => { + requestAnimationFrame((time) => { + this.safeLoop(undefined, time) + }) + } + + if (!this.rendering) { + nextFrame() + if (!forceFrame) { + return + } + } + const start = performance.now() + const timeDiff = time - this.lastLoopTime + this.loopPre(timeDiff) + + const { device, cameraUniform: uniformBuffer, cameraComputeUniform: computeUniformBuffer, renderPassDescriptor, uniformBindGroup, pipeline, ctx, verticesBuffer } = this + + this.chunksFadeAnimationController.update(time) + // #region update camera + tweenJs.update() + this.camera.near = 0.05 + const oldPos = this.camera.position.clone() + this.camera.position.x += this.rendererParams.cameraOffset[0] + this.camera.position.y += this.rendererParams.cameraOffset[1] + this.camera.position.z += this.rendererParams.cameraOffset[2] + const oversize = 1.1 + + this.camera.updateProjectionMatrix() + this.camera.updateMatrix() + + const { projectionMatrix, matrix } = this.camera + const ViewProjectionMat4 = new THREE.Matrix4() + ViewProjectionMat4.multiplyMatrices(projectionMatrix, matrix.invert()) + let ViewProjection = new Float32Array(ViewProjectionMat4.elements) + device.queue.writeBuffer( + uniformBuffer, + 0, + ViewProjection + ) + + device.queue.writeBuffer( + this.earlyZRejectUniform, + 0, + new Uint32Array([this.rendererParams.earlyZRejection ? 1 : 0]) + ) + + const origFov = this.camera.fov + if (!this.rendererParams.earlyZRejection) this.camera.fov *= oversize + this.camera.updateProjectionMatrix() + + const ViewProjectionMatCompute = new THREE.Matrix4() + ViewProjectionMatCompute.multiplyMatrices(this.camera.projectionMatrix, this.camera.matrix) + ViewProjection = new Float32Array(ViewProjectionMatCompute.elements) + device.queue.writeBuffer( + this.cameraComputeUniform, + 0, + ViewProjection + ) + + const cameraPosition = new Float32Array([this.camera.position.x, this.camera.position.y, this.camera.position.z]) + device.queue.writeBuffer( + this.cameraComputePositionUniform, + 0, + cameraPosition + ) + + this.camera.position.set(oldPos.x, oldPos.y, oldPos.z) + this.camera.fov = origFov + // #endregion + + // let { multisampleTexture } = this; + // // If the multisample texture doesn't exist or + // // is the wrong size then make a new one. + // if (multisampleTexture === undefined || + // multisampleTexture.width !== canvasTexture.width || + // multisampleTexture.height !== canvasTexture.height) { + + // // If we have an existing multisample texture destroy it. + // if (multisampleTexture) { + // multisampleTexture.destroy() + // } + + // // Create a new multisample texture that matches our + // // canvas's size + // multisampleTexture = device.createTexture({ + // format: canvasTexture.format, + // usage: GPUTextureUsage.RENDER_ATTACHMENT, + // size: [canvasTexture.width, canvasTexture.height], + // sampleCount: 4, + // }) + // this.multisampleTexture = multisampleTexture + // } + + + + // TODO! + if (this.rendererParams.godRays) { + renderPassDescriptor.colorAttachments[0].view = this.tempTexture.createView() + this.volumetricRenderPassDescriptor.colorAttachments[0].view = ctx + .getCurrentTexture() + .createView() + } else { + renderPassDescriptor.colorAttachments[0].view = ctx + .getCurrentTexture() + .createView() + } + + + // renderPassDescriptor.colorAttachments[0].view = + // multisampleTexture.createView(); + // // Set the canvas texture as the texture to "resolve" + // // the multisample texture to. + // renderPassDescriptor.colorAttachments[0].resolveTarget = + // canvasTexture.createView(); + + + this.commandEncoder = device.createCommandEncoder() + //this.commandEncoder.clearBuffer(this.occlusionTexture) + + //this.commandEncoder.clearBuffer(this.DepthTextureBuffer); + if (this.rendererParams.occlusionActive) { + this.commandEncoder.clearBuffer(this.occlusionTexture) + this.commandEncoder.clearBuffer(this.visibleCubesBuffer) + device.queue.writeBuffer(this.indirectDrawBuffer, 0, this.indirectDrawParams) + } + // Compute pass for occlusion culling + const textureSize = new Uint32Array([this.canvas.width, this.canvas.height]) + device.queue.writeBuffer(this.textureSizeBuffer, 0, textureSize) + + if (this.realNumberOfCubes) { + if (this.rendererParams.occlusionActive) { + { + const computePass = this.commandEncoder.beginComputePass() + computePass.label = 'Frustrum/Occluision Culling' + computePass.setPipeline(this.computePipeline) + computePass.setBindGroup(0, this.computeBindGroup) + computePass.setBindGroup(1, this.chunkBindGroup) + computePass.setBindGroup(2, this.textureSizeBindGroup) + computePass.dispatchWorkgroups(Math.max(Math.ceil(this.realNumberOfCubes / 256), 65_535)) + computePass.end() + device.queue.submit([this.commandEncoder.finish()]) + } + { + this.commandEncoder = device.createCommandEncoder() + const computePass = this.commandEncoder.beginComputePass() + computePass.label = 'Texture Index Sorting' + computePass.setPipeline(this.computeSortPipeline) + computePass.setBindGroup(0, this.computeBindGroup) + computePass.setBindGroup(1, this.chunkBindGroup) + computePass.setBindGroup(2, this.textureSizeBindGroup) + computePass.dispatchWorkgroups(Math.ceil(this.canvas.width / 16), Math.ceil(this.canvas.height / 16)) + computePass.end() + if (!this.indirectDrawBufferMapBeingUsed) { + this.commandEncoder.copyBufferToBuffer(this.indirectDrawBuffer, 0, this.indirectDrawBufferMap, 0, 16) + } + device.queue.submit([this.commandEncoder.finish()]) + } + } + { + this.commandEncoder = device.createCommandEncoder() + const renderPass = this.commandEncoder.beginRenderPass(this.renderPassDescriptor) + renderPass.label = 'Voxel Main Pass' + renderPass.setPipeline(pipeline) + renderPass.setBindGroup(0, this.uniformBindGroup) + renderPass.setVertexBuffer(0, verticesBuffer) + renderPass.setBindGroup(1, this.vertexCubeBindGroup) + // Use indirect drawing + renderPass.drawIndirect(this.indirectDrawBuffer, 0) + if (this.rendererParams.secondCamera) { + renderPass.setBindGroup(0, this.secondCameraUniformBindGroup) + renderPass.setViewport(this.canvas.width / 2, this.canvas.height / 2, this.canvas.width / 2, this.canvas.height / 2, 0, 0) + renderPass.drawIndirect(this.indirectDrawBuffer, 0) + } + renderPass.end() + + + device.queue.submit([this.commandEncoder.finish()]) + } + // Volumetric lighting pass + if (this.rendererParams.godRays) { + this.commandEncoder = device.createCommandEncoder() + const volumtetricRenderPass = this.commandEncoder.beginRenderPass(this.volumetricRenderPassDescriptor) + volumtetricRenderPass.label = 'Volumetric Render Pass' + volumtetricRenderPass.setPipeline(this.volumetricPipeline) + volumtetricRenderPass.setVertexBuffer(0, verticesBuffer) + volumtetricRenderPass.setBindGroup(0, this.VolumetricBindGroup) + volumtetricRenderPass.draw(6) + volumtetricRenderPass.end() + device.queue.submit([this.commandEncoder.finish()]) + } + } + if (chunksStorage.updateQueue.length) { + // console.time('updateBlocks') + const queue = [...chunksStorage.updateQueue] + while (chunksStorage.updateQueue.length) { + this.updateCubesBuffersDataFromLoop() + } + for (const { start, end } of queue) { + chunksStorage.clearRange(start, end) + } + // console.timeEnd('updateBlocks') + } else if (Object.keys(this.chunksFadeAnimationController.indexes).length) { + this.updateChunks(chunksStorage.chunks) + } + + if (!this.indirectDrawBufferMapBeingUsed && (!this.renderingStatsRequestTime || time - this.renderingStatsRequestTime > 500)) { + this.renderingStatsRequestTime = time + void this.getRenderingTilesCount().then((result) => { + this.renderingStats = result + }) + } + + this.loopPost() + + this.renderedFrames++ + nextFrame() + this.notRenderedBlockChanges = 0 + const took = performance.now() - start + this.renderMs += took + this.renderMsCount++ + if (took > 100) { + console.log('One frame render loop took', took) + } + } + + loopPre (timeDiff: number) { + if (!this.cameraUpdated) { + this.noCameraUpdates++ + if (this.lastCameraUpdateDiff && this.positiveCameraUpdates) { + const pos = {} as { x: number, y: number, z: number } + for (const key of ['x', 'y', 'z']) { + const msDiff = this.lastCameraUpdateDiff[key] / this.lastCameraUpdateDiff.time + pos[key] = this.camera.position[key] + msDiff * timeDiff + } + this.updateCameraPos(pos) + } + } + + // this.updateCameraPos({ + // x: this.camera.position.x + this.debugCameraMove.x, + // y: this.camera.position.y + this.debugCameraMove.y, + // z: this.camera.position.z + this.debugCameraMove.z + // }) + } + + loopPost () { + this.cameraUpdated = false + } + + updateCameraPos (newPos: { x: number, y: number, z: number }) { + //this.camera.position.set(newPos.x, newPos.y, newPos.z) + new tweenJs.Tween(this.camera.position).to({ x: newPos.x, y: newPos.y, z: newPos.z }, 50).start() + } + + async getRenderingTilesCount () { + this.indirectDrawBufferMapBeingUsed = true + await this.indirectDrawBufferMap.mapAsync(GPUMapMode.READ) + const arrayBuffer = this.indirectDrawBufferMap.getMappedRange() + const data = new Uint32Array(arrayBuffer) + // Read the indirect draw parameters + const vertexCount = data[0] + const instanceCount = data[1] + const firstVertex = data[2] + const firstInstance = data[3] + this.indirectDrawBufferMap.unmap() + this.indirectDrawBufferMapBeingUsed = false + return { vertexCount, instanceCount, firstVertex, firstInstance } + } + + destroy () { + this.rendering = false + this.device.destroy() + } +} + +const debugCheckDuplicates = (arr: any[]) => { + const seen = new Set() + for (const item of arr) { + if (seen.has(item)) throw new Error(`Duplicate: ${item}`) + seen.add(item) + } +} + +class IndexedInOutAnimationController { + lastUpdateTime?: number + indexes: Record void }> = {} + + constructor (public updateIndex: (key: string, progress: number, removed: boolean) => void, public DURATION = 500) { } + + update (time: number) { + this.lastUpdateTime ??= time + // eslint-disable-next-line guard-for-in + for (const key in this.indexes) { + const data = this.indexes[key] + const timeDelta = (time - this.lastUpdateTime) / this.DURATION + let removed = false + if (data.isAdding) { + data.progress += timeDelta + if (data.progress >= 1) { + delete this.indexes[key] + } + } else { + data.progress -= timeDelta + if (data.progress <= 0) { + delete this.indexes[key] + removed = true + data.onRemoved?.() + } + } + this.updateIndex(key, data.progress, removed) + } + this.lastUpdateTime = time + } + + addIndex (key: string) { + this.indexes[key] = { progress: 0, isAdding: true } + } + + removeIndex (key: string, onRemoved?: () => void) { + if (this.indexes[key]) { + this.indexes[key].isAdding = false + this.indexes[key].onRemoved = onRemoved + } else { + this.indexes[key] = { progress: 1, isAdding: false, onRemoved } + } + } +} diff --git a/prismarine-viewer/examples/webgpuRendererShared.ts b/prismarine-viewer/examples/webgpuRendererShared.ts new file mode 100644 index 000000000..58b408569 --- /dev/null +++ b/prismarine-viewer/examples/webgpuRendererShared.ts @@ -0,0 +1,32 @@ +const workerParam = new URLSearchParams(typeof window === 'undefined' ? '?' : window.location.search).get('webgpuWorker') +const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent) + +export const defaultWebgpuRendererParams = { + secondCamera: false, + MSAA: false, + cameraOffset: [0, 0, 0] as [number, number, number], + webgpuWorker: workerParam ? workerParam === 'true' : !isSafari, + godRays: true, + occlusionActive: true, + earlyZRejection: false, + allowChunksViewUpdate: false +} + +export const rendererParamsGui = { + secondCamera: true, + MSAA: true, + webgpuWorker: { + qsReload: true + }, + godRays: true, + occlusionActive: true, + earlyZRejection: true, + allowChunksViewUpdate: true +} + +export const WEBGPU_FULL_TEXTURES_LIMIT = 1024 +export const WEBGPU_HEIGHT_LIMIT = 1024 + +export type RendererInitParams = GPURequestAdapterOptions & {} + +export type RendererParams = typeof defaultWebgpuRendererParams diff --git a/prismarine-viewer/examples/webgpuRendererWorker.ts b/prismarine-viewer/examples/webgpuRendererWorker.ts new file mode 100644 index 000000000..229555106 --- /dev/null +++ b/prismarine-viewer/examples/webgpuRendererWorker.ts @@ -0,0 +1,310 @@ +/// +import * as THREE from 'three' +import * as tweenJs from '@tweenjs/tween.js' +import { BlockFaceType, BlockType, makeError } from './shared' +import { createWorkerProxy } from './workerProxy' +import { WebgpuRenderer } from './webgpuRenderer' +import { RendererInitParams, RendererParams } from './webgpuRendererShared' +import { ChunksStorage } from './chunksStorage' + +export const chunksStorage = new ChunksStorage() +globalThis.chunksStorage = chunksStorage + +let animationTick = 0 +let maxFps = 0 + +const camera = new THREE.PerspectiveCamera(75, 1 / 1, 0.1, 10_000) +globalThis.camera = camera + +let webgpuRenderer: WebgpuRenderer | undefined + +export const postMessage = (data, ...args) => { + if (globalThis.webgpuRendererChannel) { + globalThis.webgpuRendererChannel.port2.postMessage(data, ...args) + } else { + globalThis.postMessage(data, ...args) + } +} + +setInterval(() => { + if (!webgpuRenderer) return + // console.log('FPS:', renderedFrames) + const renderMsAvg = (webgpuRenderer.renderMs / webgpuRenderer.renderMsCount).toFixed(0) + postMessage({ type: 'fps', fps: `${webgpuRenderer.renderedFrames} (${new Intl.NumberFormat().format(chunksStorage.lastFetchedSize)} blocks,${renderMsAvg}ms)` }) + webgpuRenderer.noCameraUpdates = 0 + webgpuRenderer.renderedFrames = 0 + webgpuRenderer.renderMs = 0 + webgpuRenderer.renderMsCount = 0 +}, 1000) + +setInterval(() => { + postMessage({ + type: 'stats', + stats: `Rendering Tiles: ${formatLargeNumber(webgpuRenderer?.renderingStats?.instanceCount ?? -1, false)} Buffer: ${formatLargeNumber(webgpuRenderer?.NUMBER_OF_CUBES ?? -1)}`, + device: webgpuRenderer?.rendererDeviceString, + }) +}, 300) + +const formatLargeNumber = (number: number, compact = true) => { + return new Intl.NumberFormat(undefined, { notation: compact ? 'compact' : 'standard', compactDisplay: 'short' }).format(number) +} + +export const updateSize = (width, height) => { + camera.aspect = width / height + camera.updateProjectionMatrix() +} + + +// const updateCubesWhenAvailable = () => { +// onceRendererAvailable((renderer) => { +// renderer.updateSides() +// }) +// } + +let requests = [] as Array<{ resolve: () => void }> +let requestsNamed = {} as Record void> +const onceRendererAvailable = (request: (renderer: WebgpuRenderer) => any, name?: string) => { + if (webgpuRenderer?.ready) { + request(webgpuRenderer) + } else { + requests.push({ resolve: () => request(webgpuRenderer!) }) + if (name) { + requestsNamed[name] = () => request(webgpuRenderer!) + } + } +} + +const availableUpCheck = setInterval(() => { + const { ready } = webgpuRenderer ?? {} + if (ready) { + clearInterval(availableUpCheck) + for (const request of requests) { + request.resolve() + } + requests = [] + for (const request of Object.values(requestsNamed)) { + request() + } + requestsNamed = {} + } +}, 100) + +let started = false +let autoTickUpdate = undefined as number | undefined + +export const workerProxyType = createWorkerProxy({ + // eslint-disable-next-line max-params + canvas (canvas, imageBlob, isPlayground, localStorage, blocksDataModel, initConfig: RendererInitParams) { + if (globalThis.webgpuRendererChannel) { + // HACK! IOS safari bug: no support for transferControlToOffscreen in the same context! so we create a new canvas here! + const newCanvas = document.createElement('canvas') + newCanvas.width = canvas.width + newCanvas.height = canvas.height + canvas = newCanvas + // remove existing canvas + document.querySelector('#viewer-canvas')!.remove() + canvas.id = 'viewer-canvas' + document.body.appendChild(canvas) + } + started = true + webgpuRenderer = new WebgpuRenderer(canvas, imageBlob, isPlayground, camera, localStorage, blocksDataModel, initConfig) + globalThis.webgpuRenderer = webgpuRenderer + postMessage({ type: 'webgpuRendererReady' }) + }, + startRender () { + if (!webgpuRenderer) return + webgpuRenderer.rendering = true + }, + stopRender () { + if (!webgpuRenderer) return + webgpuRenderer.rendering = false + }, + resize (newWidth, newHeight) { + updateSize(newWidth, newHeight) + }, + updateConfig (params: RendererParams) { + // when available + onceRendererAvailable(() => { + webgpuRenderer!.updateConfig(params) + }) + }, + getFaces () { + const faces = [] as any[] + const getFace = (face: number) => { + // if (offsetZ / 16) debugger + return { + side: face, + textureIndex: Math.floor(Math.random() * 512) + // textureIndex: offsetZ / 16 === 31 ? 2 : 1 + } + } + for (let i = 0; i < 6; i++) { + faces.push(getFace(i)) + } + return faces + }, + generateRandom (count: number, offsetX = 0, offsetZ = 0, yOffset = 0, model = 0) { + const square = Math.sqrt(count) + if (square % 1 !== 0) throw new Error('square must be a whole number') + const blocks = {} as Record + for (let x = offsetX; x < square + offsetX; x++) { + for (let z = offsetZ; z < square + offsetZ; z++) { + blocks[`${x},${yOffset},${z}`] = { + visibleFaces: [0, 1, 2, 3, 4, 5], + modelId: model || Math.floor(Math.random() * 3000), + block: '', + } satisfies BlockType + } + } + // console.log('generated random data:', count) + this.addBlocksSection(blocks, `${offsetX},${yOffset},${offsetZ}`) + }, + updateMaxFps (fps) { + maxFps = fps + }, + updateModels (blocksDataModel: WebgpuRenderer['blocksDataModel']) { + webgpuRenderer!.blocksDataModel = blocksDataModel + webgpuRenderer!.updateBlocksModelData() + }, + addAddBlocksFlat (positions: number[]) { + const chunks = new Map() + for (let i = 0; i < positions.length; i += 3) { + const x = positions[i] + const y = positions[i + 1] + const z = positions[i + 2] + + const xChunk = Math.floor(x / 16) * 16 + const zChunk = Math.floor(z / 16) * 16 + const key = `${xChunk},${0},${zChunk}` + if (!chunks.has(key)) chunks.set(key, {}) + chunks.get(key)![`${x},${y},${z}`] = { + faces: this.getFaces() + } + } + for (const [key, value] of chunks) { + this.addBlocksSection(value, key) + } + }, + addBlocksSection (tiles: Record, key: string, animate = true) { + const index = chunksStorage.addChunk(tiles, key) + if (animate && webgpuRenderer) { + webgpuRenderer.chunksFadeAnimationController.addIndex(`${index}`) + } + }, + addBlocksSectionDone () { + }, + updateTexture (imageBlob: Blob) { + if (!webgpuRenderer) return + void webgpuRenderer.updateTexture(imageBlob) + }, + removeBlocksSection (key) { + if (webgpuRenderer) { + webgpuRenderer.chunksFadeAnimationController.removeIndex(key, () => { + chunksStorage.removeChunk(key) + }) + } + }, + debugCameraMove ({ x = 0, y = 0, z = 0 }) { + webgpuRenderer!.debugCameraMove = { x, y, z } + }, + camera (newCam: { rotation: { x: number, y: number, z: number }, position: { x: number, y: number, z: number }, fov: number }) { + const oldPos = camera.position.clone() + camera.rotation.set(newCam.rotation.x, newCam.rotation.y, newCam.rotation.z, 'ZYX') + if (!webgpuRenderer || (camera.position.x === 0 && camera.position.y === 0 && camera.position.z === 0)) { + // initial camera position + camera.position.set(newCam.position.x, newCam.position.y, newCam.position.z) + } else { + webgpuRenderer?.updateCameraPos(newCam.position) + } + + if (newCam.fov !== camera.fov) { + camera.fov = newCam.fov + camera.updateProjectionMatrix() + } + if (webgpuRenderer) { + webgpuRenderer.cameraUpdated = true + if (webgpuRenderer.lastCameraUpdateTime) { + webgpuRenderer.lastCameraUpdateDiff = { + x: oldPos.x - camera.position.x, + y: oldPos.y - camera.position.y, + z: oldPos.z - camera.position.z, + time: performance.now() - webgpuRenderer.lastCameraUpdateTime + } + } + webgpuRenderer.lastCameraUpdateTime = performance.now() + } + }, + 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 + } + }, + fullDataReset () { + if (chunksStorage.chunksMap.size) { + console.warn('fullReset: chunksMap not empty', chunksStorage.chunksMap) + } + // todo clear existing ranges with limit + chunksStorage.clearData() + }, + 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) { + // chunksStorage.allSides.push([json[i], json[i + 1], json[i + 2], { side: json[i + 3], textureIndex: json[i + 4] }]) + // } + // updateCubesWhenAvailable(0) + }, + updateBackground (color) { + onceRendererAvailable((renderer) => { + renderer.changeBackgroundColor(color) + }, 'updateBackground') + }, + destroy () { + chunksStorage.clearData() + webgpuRenderer?.destroy() + } +}, globalThis.webgpuRendererChannel?.port2) + +// globalThis.testDuplicates = () => { +// const duplicates = [...chunksStorage.getDataForBuffers().allSides].flat().filter((value, index, self) => self.indexOf(value) !== index) +// console.log('duplicates', duplicates) +// } + +const exportData = () => { + // const allSides = [...chunksStorage.getDataForBuffers().allSides].flat() + + // // 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 (const [i, sideData] of allSides.entries()) { + // 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) diff --git a/prismarine-viewer/viewer/lib/workerProxy.ts b/prismarine-viewer/examples/workerProxy.ts similarity index 78% rename from prismarine-viewer/viewer/lib/workerProxy.ts rename to prismarine-viewer/examples/workerProxy.ts index a27c817d9..9d8e7fcc0 100644 --- a/prismarine-viewer/viewer/lib/workerProxy.ts +++ b/prismarine-viewer/examples/workerProxy.ts @@ -1,5 +1,6 @@ -export function createWorkerProxy void>> (handlers: T): { __workerProxy: T } { - addEventListener('message', (event) => { +export function createWorkerProxy void>> (handlers: T, channel?: MessagePort): { __workerProxy: T } { + const target = channel ?? globalThis + target.addEventListener('message', (event: any) => { const { type, args } = event.data if (handlers[type]) { handlers[type](...args) @@ -19,7 +20,7 @@ export function createWorkerProxy v * const workerChannel = useWorkerProxy(worker) * ``` */ -export const useWorkerProxy = void> }> (worker: Worker, autoTransfer = true): T['__workerProxy'] & { +export const useWorkerProxy = void> }> (worker: Worker | MessagePort, autoTransfer = true): T['__workerProxy'] & { transfer: (...args: Transferable[]) => T['__workerProxy'] } => { // in main thread @@ -40,11 +41,11 @@ export const useWorkerProxy = { - const transfer = autoTransfer ? args.filter(arg => arg instanceof ArrayBuffer || arg instanceof MessagePort || arg instanceof ImageBitmap || arg instanceof OffscreenCanvas) : [] + 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) + }, transfer as any[]) } } }) diff --git a/prismarine-viewer/package.json b/prismarine-viewer/package.json index 02b0a304d..c29871b81 100644 --- a/prismarine-viewer/package.json +++ b/prismarine-viewer/package.json @@ -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", diff --git a/prismarine-viewer/playground.html b/prismarine-viewer/playground.html index ec4c0f33c..258426feb 100644 --- a/prismarine-viewer/playground.html +++ b/prismarine-viewer/playground.html @@ -11,11 +11,17 @@ html, body { height: 100%; + touch-action: none; margin: 0; padding: 0; } + * { + user-select: none; + -webkit-user-select: none; + } + canvas { height: 100%; width: 100%; diff --git a/prismarine-viewer/rsbuildSharedConfig.ts b/prismarine-viewer/rsbuildSharedConfig.ts index 24a29a26d..71dd6d93c 100644 --- a/prismarine-viewer/rsbuildSharedConfig.ts +++ b/prismarine-viewer/rsbuildSharedConfig.ts @@ -1,6 +1,7 @@ import { defineConfig, ModifyRspackConfigUtils } from '@rsbuild/core'; import { pluginNodePolyfill } from '@rsbuild/plugin-node-polyfill'; import { pluginReact } from '@rsbuild/plugin-react'; +import { pluginBasicSsl } from '@rsbuild/plugin-basic-ssl' import path from 'path' export const appAndRendererSharedConfig = () => defineConfig({ @@ -56,7 +57,8 @@ export const appAndRendererSharedConfig = () => defineConfig({ }, plugins: [ pluginReact(), - pluginNodePolyfill() + pluginNodePolyfill(), + ...process.env.ENABLE_HTTPS ? [pluginBasicSsl()] : [] ], tools: { rspack (config, helpers) { diff --git a/prismarine-viewer/sharedBuildOptions.mjs b/prismarine-viewer/sharedBuildOptions.mjs new file mode 100644 index 000000000..52b2fb8fc --- /dev/null +++ b/prismarine-viewer/sharedBuildOptions.mjs @@ -0,0 +1,3 @@ +export const sharedPlaygroundMainOptions = { + alias: {} +} \ No newline at end of file diff --git a/prismarine-viewer/viewer/lib/mesher/getPreflatBlock.ts b/prismarine-viewer/viewer/lib/mesher/getPreflatBlock.ts new file mode 100644 index 000000000..6c9ebfd5a --- /dev/null +++ b/prismarine-viewer/viewer/lib/mesher/getPreflatBlock.ts @@ -0,0 +1,30 @@ +import legacyJson from '../../../../src/preflatMap.json' + +export const getPreflatBlock = (block, reportIssue?: () => void) => { + const b = block + b._properties = {} + + const namePropsStr = legacyJson.blocks[b.type + ':' + b.metadata] || findClosestLegacyBlockFallback(b.type, b.metadata, reportIssue) + if (namePropsStr) { + b.name = namePropsStr.split('[')[0] + const propsStr = namePropsStr.split('[')?.[1]?.split(']') + if (propsStr) { + const newProperties = Object.fromEntries(propsStr.join('').split(',').map(x => { + let [key, val] = x.split('=') + if (!isNaN(val)) val = parseInt(val, 10) + return [key, val] + })) + b._properties = newProperties + } + } + return b +} + +const findClosestLegacyBlockFallback = (id, metadata, reportIssue) => { + reportIssue?.() + for (const [key, value] of Object.entries(legacyJson.blocks)) { + const [idKey, meta] = key.split(':') + if (idKey === id) return value + } + return null +} diff --git a/prismarine-viewer/viewer/lib/mesher/mesher.ts b/prismarine-viewer/viewer/lib/mesher/mesher.ts index 7c120abc1..bb6bb2672 100644 --- a/prismarine-viewer/viewer/lib/mesher/mesher.ts +++ b/prismarine-viewer/viewer/lib/mesher/mesher.ts @@ -1,6 +1,6 @@ import { Vec3 } from 'vec3' import { World } from './world' -import { getSectionGeometry, setBlockStatesData as setMesherData } from './models' +import { getSectionGeometry, setBlockStatesData as setMesherData, setSpecialBlockState, setWorld, world } from './models' if (module.require) { // If we are in a node environement, we need to fake some env variables @@ -12,7 +12,6 @@ if (module.require) { } let workerIndex = 0 -let world: World let dirtySections = new Map() let allDataReady = false @@ -20,7 +19,7 @@ function sectionKey (x, y, z) { return `${x},${y},${z}` } -const batchMessagesLimit = 100 +const batchMessagesLimit = 1 let queuedMessages = [] as any[] let queueWaiting = false @@ -62,12 +61,6 @@ function setSectionDirty (pos, value = true) { } } -const softCleanup = () => { - // clean block cache and loaded chunks - world = new World(world.config.version) - globalThis.world = world -} - const handleMessage = data => { const globalVar: any = globalThis @@ -82,7 +75,9 @@ const handleMessage = data => { world.erroredBlockModel = undefined } - world ??= new World(data.config.version) + if (!world) { + setWorld(new World(data.config.version)) + } world.config = { ...world.config, ...data.config } globalThis.world = world } @@ -95,6 +90,10 @@ const handleMessage = data => { break } + case 'webgpuData': { + world.setDataForWebgpuRenderer(data.data) + break + } case 'dirty': { const loc = new Vec3(data.x, data.y, data.z) setSectionDirty(loc, data.value) @@ -108,7 +107,6 @@ const handleMessage = data => { } case 'unloadChunk': { world.removeColumn(data.x, data.z) - if (Object.keys(world.columns).length === 0) softCleanup() break } @@ -118,8 +116,13 @@ const handleMessage = data => { break } + case 'specialBlockState': { + setSpecialBlockState(data.data) + + break + } case 'reset': { - world = undefined as any + setWorld(undefined) // blocksStates = null dirtySections = new Map() // todo also remove cached @@ -128,7 +131,7 @@ const handleMessage = data => { break } - // No default + // No default } } diff --git a/prismarine-viewer/viewer/lib/mesher/models.ts b/prismarine-viewer/viewer/lib/mesher/models.ts index 51a00a448..2fabb07d5 100644 --- a/prismarine-viewer/viewer/lib/mesher/models.ts +++ b/prismarine-viewer/viewer/lib/mesher/models.ts @@ -23,6 +23,16 @@ for (const key of Object.keys(tintsData)) { tints[key] = prepareTints(tintsData[key]) } +let specialBlockState: undefined | Record +export const setSpecialBlockState = (blockState) => { + specialBlockState = blockState +} +// eslint-disable-next-line import/no-mutable-exports +export let world: World +export const setWorld = (_world) => { + world = _world +} + type Tiles = { [blockPos: string]: BlockType } @@ -74,13 +84,13 @@ export function preflatBlockCalculation (block: Block, world: World, position: V } // case 'gate_in_wall': {} case 'block_snowy': { - const aboveIsSnow = world.getBlock(position.offset(0, 1, 0))?.name === 'snow' - if (aboveIsSnow) { + const aboveIsSnow = `${world.getBlock(position.offset(0, 1, 0))?.name === 'snow'}` + if (aboveIsSnow === block.getProperties().snowy) { + return + } else { return { - snowy: `${aboveIsSnow}` + snowy: aboveIsSnow } - } else { - return } } case 'door': { @@ -121,14 +131,18 @@ function getLiquidRenderHeight (world, block, type, pos) { const isCube = (block: Block) => { if (!block || block.transparent) return false if (block.isCube) return true - if (!block.models?.length || block.models.length !== 1) return false + if (!block.models?.length || block.models.length !== 1 || !block.models[0]) return false // all variants return block.models[0].every(v => v.elements!.every(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) { +const addPossibleTileCheck = (attr, pos, side) => { + +} + +function renderLiquid (world, cursor, texture, type, biome, water, attr, stateId) { const heights: number[] = [] for (let z = -1; z <= 1; z++) { for (let x = -1; x <= 1; x++) { @@ -145,17 +159,17 @@ function renderLiquid (world: World, cursor: Vec3, texture: any | undefined, typ // eslint-disable-next-line guard-for-in for (const face in elemFaces) { - const { dir, corners } = elemFaces[face] + const { dir, corners, webgpuSide } = 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.material === 'plant' || neighbor.getProperties().waterlogged) continue - let tint = [1, 1, 1] + let tint = [1, 1, 1] as [number, number, number] if (water) { let m = 1 // Fake lighting to improve lisibility if (Math.abs(dir[0]) > 0) m = 0.6 @@ -166,17 +180,24 @@ function renderLiquid (world: World, cursor: Vec3, texture: any | undefined, typ if (needTiles) { const tiles = attr.tiles as Tiles - tiles[`${cursor.x},${cursor.y},${cursor.z}`] ??= { - block: 'water', - faces: [], + const model = world.webgpuModelsMapping[stateId] + // TODO height + if (model) { + tiles[`${cursor.x},${cursor.y},${cursor.z}`] ??= { + block: 'water', + visibleFaces: [], + modelId: model, + tint, + } + tiles[`${cursor.x},${cursor.y},${cursor.z}`].visibleFaces.push(webgpuSide) } - tiles[`${cursor.x},${cursor.y},${cursor.z}`].faces.push({ - face, - neighbor: `${neighborPos.x},${neighborPos.y},${neighborPos.z}`, - side: 0, // todo - textureIndex: 0, - // texture: eFace.texture.name, - }) + // tiles[`${cursor.x},${cursor.y},${cursor.z}`].faces.push({ + // face, + // neighbor: `${neighborPos.x},${neighborPos.y},${neighborPos.z}`, + // side: 0, // todo + // textureIndex: 0, + // // texture: eFace.texture.name, + // }) } const { u } = texture @@ -184,16 +205,18 @@ function renderLiquid (world: World, cursor: Vec3, texture: any | undefined, typ const { su } = texture const { sv } = texture - for (const pos of corners) { - const height = cornerHeights[pos[2] * 2 + pos[0]] - attr.t_positions.push( - (pos[0] ? 0.999 : 0.001) + (cursor.x & 15) - 8, - (pos[1] ? height - 0.001 : 0.001) + (cursor.y & 15) - 8, - (pos[2] ? 0.999 : 0.001) + (cursor.z & 15) - 8 - ) - attr.t_normals.push(...dir) - attr.t_uvs.push(pos[3] * su + u, pos[4] * sv * (pos[1] ? 1 : height) + v) - attr.t_colors.push(tint[0], tint[1], tint[2]) + if (!needTiles) { + for (const pos of corners) { + const height = cornerHeights[pos[2] * 2 + pos[0]] + attr.t_positions.push( + (pos[0] ? 0.999 : 0.001) + (cursor.x & 15) - 8, + (pos[1] ? height - 0.001 : 0.001) + (cursor.y & 15) - 8, + (pos[2] ? 0.999 : 0.001) + (cursor.z & 15) - 8 + ) + attr.t_normals.push(...dir) + attr.t_uvs.push(pos[3] * su + u, pos[4] * sv * (pos[1] ? 1 : height) + v) + attr.t_colors.push(tint[0], tint[1], tint[2]) + } } } } @@ -238,6 +261,11 @@ const identicalCull = (currentElement: BlockElement, neighbor: Block, direction: let needSectionRecomputeOnChange = false +// todo remove +const hasModelForNeighbor = (world: World, block: Block) => { + return !!world.webgpuModelsMapping[block.stateId!]/* ?? world.webgpuModelsMapping[-1] */ +} + 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}` @@ -247,16 +275,19 @@ function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO: // eslint-disable-next-line guard-for-in for (const face in element.faces) { const eFace = element.faces[face] - const { corners, mask1, mask2, side } = elemFaces[face] + const { corners, mask1, mask2, webgpuSide } = elemFaces[face] const dir = matmul3(globalMatrix, elemFaces[face].dir) if (eFace.cullface) { const neighbor = world.getBlock(cursor.plus(new Vec3(...dir)), blockProvider, {}) if (neighbor) { - if (cullIfIdentical && neighbor.stateId === block.stateId) continue - if (!neighbor.transparent && (isCube(neighbor) || identicalCull(element, neighbor, new Vec3(...dir)))) continue + if (hasModelForNeighbor(world, block)) { + if (cullIfIdentical && neighbor.stateId === block.stateId) continue + if (!neighbor.transparent && (isCube(neighbor) || identicalCull(element, neighbor, new Vec3(...dir)))) continue + } } else { needSectionRecomputeOnChange = true + // TODO support sync worlds continue } } @@ -298,8 +329,6 @@ function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO: if (face === 'down') { r += 180 } - const uvcs = Math.cos(r * Math.PI / 180) - const uvsn = -Math.sin(r * Math.PI / 180) let localMatrix = null as any let localShift = null as any @@ -332,6 +361,8 @@ function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO: ] if (!needTiles) { // 10% + const uvcs = Math.cos(r * Math.PI / 180) + const uvsn = -Math.sin(r * Math.PI / 180) vertex = vecadd3(matmul3(localMatrix, vertex), localShift) vertex = vecadd3(matmul3(globalMatrix, vertex), globalShift) vertex = vertex.map(v => v / 16) @@ -350,8 +381,9 @@ function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO: } let light = 1 - const { smoothLighting } = world.config - // const smoothLighting = true + // const { smoothLighting } = world.config + const smoothLighting = false + doAO = false if (doAO) { const dx = pos[0] * 2 - 1 const dy = pos[1] * 2 - 1 @@ -405,24 +437,30 @@ function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO: if (needTiles) { const tiles = attr.tiles as Tiles - tiles[`${cursor.x},${cursor.y},${cursor.z}`] ??= { - block: block.name, - faces: [], - } - const needsOnlyOneFace = false - const isTilesEmpty = tiles[`${cursor.x},${cursor.y},${cursor.z}`].faces.length < 1 - if (isTilesEmpty || !needsOnlyOneFace) { - tiles[`${cursor.x},${cursor.y},${cursor.z}`].faces.push({ - face, - side, - textureIndex: eFace.texture.tileIndex, - neighbor: `${neighborPos.x},${neighborPos.y},${neighborPos.z}`, - light: baseLight, - tint: lightWithColor, - //@ts-expect-error debug prop - texture: eFace.texture.debugName || block.name, - } satisfies BlockType['faces'][number]) + const model = world.webgpuModelsMapping[block.stateId!]/* ?? world.webgpuModelsMapping[-1] */ + if (model !== undefined) { + if (specialBlockState?.value === 'highlight' && specialBlockState.position.x === cursor.x && specialBlockState.position.y === cursor.y && specialBlockState.position.z === cursor.z) { + lightWithColor[0] *= 0.5 + lightWithColor[1] *= 0.5 + lightWithColor[2] *= 0.5 + } + tiles[`${cursor.x},${cursor.y},${cursor.z}`] ??= { + block: block.name, + visibleFaces: [], + modelId: model, + tint: lightWithColor[0] === 1 && lightWithColor[1] === 1 && lightWithColor[2] === 1 ? undefined : lightWithColor, + } + tiles[`${cursor.x},${cursor.y},${cursor.z}`].visibleFaces.push(webgpuSide) } + // tiles[`${cursor.x},${cursor.y},${cursor.z}`].faces.push({ + // face, + // side, + // modelId: eFace, + // // textureIndex: eFace.texture.tileIndex, + // neighbor: `${neighborPos.x},${neighborPos.y},${neighborPos.z}`, + // //@ts-expect-error debug prop + // texture: eFace.texture.debugName || block.name, + // } satisfies BlockType['faces'][number]) } if (!needTiles) { @@ -472,13 +510,13 @@ export function getSectionGeometry (sx, sy, sz, world: World) { for (cursor.z = sz; cursor.z < sz + 16; cursor.z++) { for (cursor.x = sx; cursor.x < sx + 16; cursor.x++) { let block = world.getBlock(cursor, blockProvider, attr)! - if (!INVISIBLE_BLOCKS.has(block.name)) { - const highest = attr.highestBlocks.get(`${cursor.x},${cursor.z}`) - if (!highest || highest.y < cursor.y) { - attr.highestBlocks.set(`${cursor.x},${cursor.z}`, { y: cursor.y, stateId: block.stateId, biomeId: block.biome.id }) - } - } - if (INVISIBLE_BLOCKS.has(block.name)) continue + // if (!INVISIBLE_BLOCKS.has(block.name)) { + // const highest = attr.highestBlocks.get(`${cursor.x},${cursor.z}`) + // if (!highest || highest.y < cursor.y) { + // attr.highestBlocks.set(`${cursor.x},${cursor.z}`, { y: cursor.y, stateId: block.stateId, biomeId: block.biome.id }) + // } + // } + // if (INVISIBLE_BLOCKS.has(block.name)) continue if ((block.name.includes('_sign') || block.name === 'sign') && !world.config.disableSignsMapsSupport) { const key = `${cursor.x},${cursor.y},${cursor.z}` const props: any = block.getProperties() @@ -503,27 +541,30 @@ export function getSectionGeometry (sx, sy, sz, world: World) { if (patchProperties) { block._originalProperties ??= block._properties block._properties = { ...block._originalProperties, ...patchProperties } - if (block.models && JSON.stringify(block._originalProperties) !== JSON.stringify(block._properties)) { - // recompute models - block.models = undefined + const patched = JSON.stringify(block._properties) + block.patchedModels[''] ??= block.models! + block.models = block.patchedModels[patched] + if (!block.models) { + // need to recompute models block = world.getBlock(cursor, blockProvider, attr)! } + block.patchedModels[patched] = block.models! } else { block._properties = block._originalProperties ?? block._properties block._originalProperties = undefined + block.models = block.patchedModels[''] ?? block.models } } const isWaterlogged = isBlockWaterlogged(block) if (block.name === 'water' || isWaterlogged) { const pos = cursor.clone() - // eslint-disable-next-line @typescript-eslint/no-loop-func - delayedRender.push(() => { - renderLiquid(world, pos, blockProvider.getTextureInfo('water_still'), block.type, biome, true, attr) - }) + // delayedRender.push(() => { + renderLiquid(world, pos, blockProvider.getTextureInfo('water_still'), block.type, biome, true, attr, block.stateId) + // }) attr.blocksCount++ } else if (block.name === 'lava') { - renderLiquid(world, cursor, blockProvider.getTextureInfo('lava_still'), block.type, biome, false, attr) + renderLiquid(world, cursor, blockProvider.getTextureInfo('lava_still'), block.type, biome, false, attr, block.stateId) attr.blocksCount++ } if (block.name !== 'water' && block.name !== 'lava' && !INVISIBLE_BLOCKS.has(block.name)) { @@ -535,6 +576,7 @@ export function getSectionGeometry (sx, sy, sz, world: World) { const firstForceVar = world.config.debugModelVariant?.[0] let part = 0 for (const modelVars of models ?? []) { + if (part > 0) continue // todo only webgpu const pos = cursor.clone() // const variantRuntime = mod(Math.floor(pos.x / 16) + Math.floor(pos.y / 16) + Math.floor(pos.z / 16), modelVars.length) const variantRuntime = 0 @@ -544,32 +586,32 @@ export function getSectionGeometry (sx, sy, sz, world: World) { if (!model) continue // #region 10% - let globalMatrix = null as any - let globalShift = null as any - for (const axis of ['x', 'y', 'z'] as const) { - if (axis in model) { - globalMatrix = globalMatrix ? - matmulmat3(globalMatrix, buildRotationMatrix(axis, -(model[axis] ?? 0))) : - buildRotationMatrix(axis, -(model[axis] ?? 0)) - } - } - if (globalMatrix) { - globalShift = [8, 8, 8] - globalShift = vecsub3(globalShift, matmul3(globalMatrix, globalShift)) - } + const globalMatrix = null as any + const globalShift = null as any + // for (const axis of ['x', 'y', 'z'] as const) { + // if (axis in model) { + // globalMatrix = globalMatrix ? + // matmulmat3(globalMatrix, buildRotationMatrix(axis, -(model[axis] ?? 0))) : + // buildRotationMatrix(axis, -(model[axis] ?? 0)) + // } + // } + // if (globalMatrix) { + // globalShift = [8, 8, 8] + // globalShift = vecsub3(globalShift, matmul3(globalMatrix, globalShift)) + // } // #endregion for (const element of model.elements ?? []) { const ao = model.ao ?? true - if (block.transparent) { - const pos = cursor.clone() - delayedRender.push(() => { - renderElement(world, pos, element, ao, attr, globalMatrix, globalShift, block, biome) - }) - } else { - // 60% - renderElement(world, cursor, element, ao, attr, globalMatrix, globalShift, block, biome) - } + // if (block.transparent) { + // const pos = cursor.clone() + // delayedRender.push(() => { + // renderElement(world, pos, element, ao, attr, globalMatrix, globalShift, block, biome) + // }) + // } else { + // 60% + renderElement(world, cursor, element, ao, attr, globalMatrix, globalShift, block, biome) + // } } } if (part > 0) attr.blocksCount++ @@ -621,6 +663,9 @@ export function getSectionGeometry (sx, sy, sz, world: World) { export const setBlockStatesData = (blockstatesModels, blocksAtlas: any, _needTiles = false, useUnknownBlockModel = true, version = 'latest') => { blockProvider = worldBlockProvider(blockstatesModels, blocksAtlas, version) + if (world) { + world.blockCache = {} + } globalThis.blockProvider = blockProvider if (useUnknownBlockModel) { unknownBlockModel = blockProvider.getAllResolvedModels0_1({ name: 'unknown', properties: {} }) diff --git a/prismarine-viewer/viewer/lib/mesher/modelsGeometryCommon.ts b/prismarine-viewer/viewer/lib/mesher/modelsGeometryCommon.ts index 2aec00e29..8293ebf5d 100644 --- a/prismarine-viewer/viewer/lib/mesher/modelsGeometryCommon.ts +++ b/prismarine-viewer/viewer/lib/mesher/modelsGeometryCommon.ts @@ -74,6 +74,8 @@ export function matmulmat3 (a, b) { export const elemFaces = { up: { + side: 0, + webgpuSide: 0, dir: [0, 1, 0], mask1: [1, 1, 0], mask2: [0, 1, 1], @@ -85,6 +87,8 @@ export const elemFaces = { ] }, down: { + side: 1, + webgpuSide: 1, dir: [0, -1, 0], mask1: [1, 1, 0], mask2: [0, 1, 1], @@ -96,6 +100,8 @@ export const elemFaces = { ] }, east: { + side: 2, + webgpuSide: 4, dir: [1, 0, 0], mask1: [1, 1, 0], mask2: [1, 0, 1], @@ -107,6 +113,8 @@ export const elemFaces = { ] }, west: { + side: 3, + webgpuSide: 5, dir: [-1, 0, 0], mask1: [1, 1, 0], mask2: [1, 0, 1], @@ -118,6 +126,8 @@ export const elemFaces = { ] }, north: { + side: 4, + webgpuSide: 3, dir: [0, 0, -1], mask1: [1, 0, 1], mask2: [0, 1, 1], @@ -129,6 +139,8 @@ export const elemFaces = { ] }, south: { + side: 0, + webgpuSide: 2, dir: [0, 0, 1], mask1: [1, 0, 1], mask2: [0, 1, 1], diff --git a/prismarine-viewer/viewer/lib/mesher/world.ts b/prismarine-viewer/viewer/lib/mesher/world.ts index c69da7513..3d25ff78c 100644 --- a/prismarine-viewer/viewer/lib/mesher/world.ts +++ b/prismarine-viewer/viewer/lib/mesher/world.ts @@ -4,8 +4,9 @@ import { Block } from 'prismarine-block' import { Vec3 } from 'vec3' import { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider' import moreBlockDataGeneratedJson from '../moreBlockDataGenerated.json' -import legacyJson from '../../../../src/preflatMap.json' -import { defaultMesherConfig } from './shared' +import type { AllBlocksStateIdToModelIdMap } from '../../../examples/webgpuBlockModels' +import { defaultMesherConfig, MesherGeometryOutput } from './shared' +import { getPreflatBlock } from './getPreflatBlock' const ignoreAoBlocks = Object.keys(moreBlockDataGeneratedJson.noOcclusions) @@ -26,6 +27,7 @@ export type WorldBlock = Omit & { isCube: boolean /** cache */ models?: BlockModelPartsResolved | null + patchedModels: Record _originalProperties?: Record _properties?: Record } @@ -39,6 +41,7 @@ export class World { biomeCache: { [id: number]: mcData.Biome } preflat: boolean erroredBlockModel?: BlockModelPartsResolved + webgpuModelsMapping: AllBlocksStateIdToModelIdMap constructor (version) { this.Chunk = Chunks(version) as any @@ -113,7 +116,7 @@ export class World { return this.getColumn(Math.floor(pos.x / 16) * 16, Math.floor(pos.z / 16) * 16) } - getBlock (pos: Vec3, blockProvider?, attr?): WorldBlock | null { + getBlock (pos: Vec3, blockProvider?, attr?: Partial): WorldBlock | null { // for easier testing if (!(pos instanceof Vec3)) pos = new Vec3(...pos as [number, number, number]) const key = columnKey(Math.floor(pos.x / 16) * 16, Math.floor(pos.z / 16) * 16) @@ -128,6 +131,7 @@ export class World { if (!this.blockCache[stateId]) { const b = column.getBlock(locInChunk) as unknown as WorldBlock + b.patchedModels = {} b.isCube = isCube(b.shapes) this.blockCache[stateId] = b Object.defineProperty(b, 'position', { @@ -136,21 +140,12 @@ export class World { } }) if (this.preflat) { - b._properties = {} - - const namePropsStr = legacyJson.blocks[b.type + ':' + b.metadata] || findClosestLegacyBlockFallback(b.type, b.metadata, pos) - if (namePropsStr) { - b.name = namePropsStr.split('[')[0] - const propsStr = namePropsStr.split('[')?.[1]?.split(']') - if (propsStr) { - const newProperties = Object.fromEntries(propsStr.join('').split(',').map(x => { - let [key, val] = x.split('=') - if (!isNaN(val)) val = parseInt(val, 10) - return [key, val] - })) - b._properties = newProperties - } - } + // patch block + getPreflatBlock(b, () => { + const id = b.type + const { metadata } = b + console.warn(`[mesher] Unknown block with ${id}:${metadata} at ${pos.toString()}, falling back`) // todo has known issues + }) } } @@ -200,15 +195,10 @@ export class World { shouldMakeAo (block: WorldBlock | null) { return block?.isCube && !ignoreAoBlocks.includes(block.name) } -} -const findClosestLegacyBlockFallback = (id, metadata, pos) => { - console.warn(`[mesher] Unknown block with ${id}:${metadata} at ${pos}, falling back`) // todo has known issues - for (const [key, value] of Object.entries(legacyJson.blocks)) { - const [idKey, meta] = key.split(':') - if (idKey === id) return value + setDataForWebgpuRenderer (data: { allBlocksStateIdToModelIdMap: AllBlocksStateIdToModelIdMap }) { + this.webgpuModelsMapping = data.allBlocksStateIdToModelIdMap } - return null } // todo export in chunk instead diff --git a/prismarine-viewer/viewer/lib/ui/newStats.ts b/prismarine-viewer/viewer/lib/ui/newStats.ts index 4f4b5cee7..cbb084fb9 100644 --- a/prismarine-viewer/viewer/lib/ui/newStats.ts +++ b/prismarine-viewer/viewer/lib/ui/newStats.ts @@ -4,11 +4,10 @@ const rightOffset = 0 const stats = {} let lastY = 20 -export const addNewStat = (id: string, width = 80, x = rightOffset, y = lastY) => { +export const addNewStat = (id: string, width = 80, x = rightOffset, y?: number) => { const pane = document.createElement('div') - pane.id = 'fps-counter' pane.style.position = 'fixed' - pane.style.top = `${y}px` + pane.style.top = `${y ?? lastY}px` pane.style.right = `${x}px` // gray bg pane.style.backgroundColor = 'rgba(0, 0, 0, 0.7)' @@ -20,7 +19,7 @@ export const addNewStat = (id: string, width = 80, x = rightOffset, y = lastY) = pane.style.pointerEvents = 'none' document.body.appendChild(pane) stats[id] = pane - if (y === 0) { // otherwise it's a custom position + if (y === undefined && x === rightOffset) { // otherwise it's a custom position // rightOffset += width lastY += 20 } @@ -35,6 +34,50 @@ export const addNewStat = (id: string, width = 80, x = rightOffset, y = lastY) = } } +export const addNewStat2 = (id: string, { top, bottom, right, left, displayOnlyWhenWider }: { top?: number, bottom?: number, right?: number, left?: number, displayOnlyWhenWider?: number }) => { + if (top === undefined && bottom === undefined) top = 0 + const pane = document.createElement('div') + pane.style.position = 'fixed' + if (top !== undefined) { + pane.style.top = `${top}px` + } + if (bottom !== undefined) { + pane.style.bottom = `${bottom}px` + } + if (left !== undefined) { + pane.style.left = `${left}px` + } + if (right !== undefined) { + pane.style.right = `${right}px` + } + // gray bg + pane.style.backgroundColor = 'rgba(0, 0, 0, 0.7)' + 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 + + const resizeCheck = () => { + if (!displayOnlyWhenWider) return + pane.style.display = window.innerWidth > displayOnlyWhenWider ? 'block' : 'none' + } + window.addEventListener('resize', resizeCheck) + resizeCheck() + + return { + updateText (text: string) { + pane.innerText = text + }, + setVisibility (visible: boolean) { + pane.style.display = visible ? 'block' : 'none' + } + } +} + export const updateStatText = (id, text) => { if (!stats[id]) return stats[id].innerText = text diff --git a/prismarine-viewer/viewer/lib/viewer.ts b/prismarine-viewer/viewer/lib/viewer.ts index 73f29fb2d..12b5de031 100644 --- a/prismarine-viewer/viewer/lib/viewer.ts +++ b/prismarine-viewer/viewer/lib/viewer.ts @@ -3,6 +3,7 @@ import * as THREE from 'three' import { Vec3 } from 'vec3' import { generateSpiralMatrix } from 'flying-squid/dist/utils' import worldBlockProvider from 'mc-assets/dist/worldBlockProvider' +import { WorldRendererWebgpu } from './worldrendererWebgpu' import { Entities } from './entities' import { Primitives } from './primitives' import { WorldRendererThree } from './worldrendererThree' @@ -14,17 +15,19 @@ export class Viewer { scene: THREE.Scene ambientLight: THREE.AmbientLight directionalLight: THREE.DirectionalLight - world: WorldRendererCommon + 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 + powerPreference: string | undefined getMineflayerBot (): void | Record {} // to be overridden @@ -43,8 +46,8 @@ export class Viewer { this.scene = new THREE.Scene() this.scene.matrixAutoUpdate = false // for perf - this.threeJsWorld = new WorldRendererThree(this.scene, this.renderer, worldConfig) - this.setWorld() + // this.threeJsWorld = new WorldRendererThree(this.scene, this.renderer, worldConfig) + this.setWorld(worldConfig) this.resetScene() this.entities = new Entities(this.scene) // this.primitives = new Primitives(this.scene, this.camera) @@ -52,8 +55,14 @@ export class Viewer { this.domElement = renderer.domElement } - setWorld () { - this.world = this.threeJsWorld + setWorld (worldConfig: typeof defaultWorldRendererConfig = this.world.config) { + const { version, texturesVersion } = this.world ?? {} + if (this.world) this.world.destroy() + this.webgpuWorld = new WorldRendererWebgpu(worldConfig, this.renderer, { powerPreference: this.powerPreference }) + this.world = this.webgpuWorld + if (version) { + void this.setVersion(version, texturesVersion) + } } resetScene () { @@ -107,7 +116,7 @@ export class Viewer { await this.world.waitForChunkToLoad(pos) } if (!this.world.loadedChunks[`${sectionX},${sectionZ}`]) { - console.debug('[should be unreachable] setBlockStateId called for unloaded chunk', pos) + console.warn('[should be unreachable] setBlockStateId called for unloaded chunk', pos) } this.world.setBlockStateId(pos, stateId) } @@ -233,7 +242,7 @@ export class Viewer { }) // todo remove and use other architecture instead so data flow is clear worldEmitter.on('blockEntities', (blockEntities) => { - if (this.world instanceof WorldRendererThree) (this.world).blockEntities = blockEntities + if (this.world instanceof WorldRendererThree) (this.world as WorldRendererThree).blockEntities = blockEntities }) worldEmitter.on('unloadChunk', ({ x, z }) => { @@ -265,7 +274,7 @@ export class Viewer { }) worldEmitter.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) }) worldEmitter.on('time', (timeOfDay) => { @@ -287,7 +296,7 @@ export class Viewer { if (this.world.mesherConfig.skyLight === skyLight) return this.world.mesherConfig.skyLight = skyLight if (this.world instanceof WorldRendererThree) { - (this.world).rerenderAllChunks?.() + (this.world as WorldRendererThree).rerenderAllChunks?.() } }) @@ -296,7 +305,7 @@ export class Viewer { render () { if (this.world instanceof WorldRendererThree) { - (this.world).render() + (this.world as WorldRendererThree).render() this.entities.render() } } diff --git a/prismarine-viewer/viewer/lib/viewerWrapper.ts b/prismarine-viewer/viewer/lib/viewerWrapper.ts index 52e244fe4..39c0444e1 100644 --- a/prismarine-viewer/viewer/lib/viewerWrapper.ts +++ b/prismarine-viewer/viewer/lib/viewerWrapper.ts @@ -92,16 +92,16 @@ export class ViewerWrapper { } } this.preRender() - statsStart() - // ios bug: viewport dimensions are updated after the resize event - if (this.previousWindowWidth !== window.innerWidth || this.previousWindowHeight !== window.innerHeight) { - this.resizeHandler() - this.previousWindowWidth = window.innerWidth - this.previousWindowHeight = window.innerHeight - } - viewer.render() - this.renderedFps++ - statsEnd() + // statsStart() + // // ios bug: viewport dimensions are updated after the resize event + // if (this.previousWindowWidth !== window.innerWidth || this.previousWindowHeight !== window.innerHeight) { + // this.resizeHandler() + // this.previousWindowWidth = window.innerWidth + // this.previousWindowHeight = window.innerHeight + // } + // viewer.render() + // this.renderedFps++ + // statsEnd() this.postRender() } diff --git a/prismarine-viewer/viewer/lib/worldDataEmitter.ts b/prismarine-viewer/viewer/lib/worldDataEmitter.ts index 0223b855b..d1244fe44 100644 --- a/prismarine-viewer/viewer/lib/worldDataEmitter.ts +++ b/prismarine-viewer/viewer/lib/worldDataEmitter.ts @@ -21,10 +21,8 @@ export class WorldDataEmitter extends EventEmitter { private readonly lastPos: Vec3 private eventListeners: Record = {} private readonly emitter: WorldDataEmitter - keepChunksDistance = 0 addWaitTime = 1 _handDisplay = false - isPlayground = false get handDisplay () { return this._handDisplay } @@ -33,6 +31,10 @@ export class WorldDataEmitter extends EventEmitter { this.eventListeners.heldItemChanged?.() } + /* config */ keepChunksDistance = 0 + /* config */ isPlayground = false + /* config */ allowPositionUpdate = true + constructor (public world: typeof __type_bot['world'], public viewDistance: number, position: Vec3 = new Vec3(0, 0, 0)) { super() this.loadedChunks = {} @@ -236,6 +238,7 @@ export class WorldDataEmitter extends EventEmitter { } async updatePosition (pos: Vec3, force = false) { + if (!this.allowPositionUpdate) return const [lastX, lastZ] = chunkPos(this.lastPos) const [botX, botZ] = chunkPos(pos) if (lastX !== botX || lastZ !== botZ || force) { diff --git a/prismarine-viewer/viewer/lib/worldrendererCommon.ts b/prismarine-viewer/viewer/lib/worldrendererCommon.ts index 71c094b4f..273437d2c 100644 --- a/prismarine-viewer/viewer/lib/worldrendererCommon.ts +++ b/prismarine-viewer/viewer/lib/worldrendererCommon.ts @@ -29,7 +29,8 @@ export const worldCleanup = buildCleanupDecorator('resetWorld') export const defaultWorldRendererConfig = { showChunkBorders: false, - numWorkers: 4 + numWorkers: 4, + isPlayground: false } export type WorldRendererConfig = typeof defaultWorldRendererConfig @@ -45,7 +46,6 @@ export abstract class WorldRendererCommon threejsCursorLineMaterial: LineMaterial @worldCleanup() cursorBlock = null as Vec3 | null - isPlayground = false displayStats = true @worldCleanup() worldConfig = { minY: 0, worldHeight: 256 } @@ -56,18 +56,23 @@ export abstract class WorldRendererCommon active = false version = undefined as string | undefined + // #region CHUNK & SECTIONS TRACKING @worldCleanup() loadedChunks = {} as Record // data is added for these chunks and they might be still processing @worldCleanup() finishedChunks = {} as Record // these chunks are fully loaded into the world (scene) + @worldCleanup() + finishedSections = {} as Record // these sections are fully loaded into the world (scene) + @worldCleanup() // loading sections (chunks) sectionsWaiting = new Map() @worldCleanup() queuedChunks = new Set() + // #endregion @worldCleanup() renderUpdateEmitter = new EventEmitter() as unknown as TypedEmitter<{ @@ -115,8 +120,12 @@ export abstract class WorldRendererCommon workersProcessAverageTime = 0 workersProcessAverageTimeCount = 0 maxWorkersProcessTime = 0 - geometryReceiveCount = {} + geometryReceiveCount = 0 allLoadedIn: undefined | number + messagesDelay = 0 + messageDelayCount = 0 + + // geometryReceiveCount = {} rendererDevice = '...' edgeChunks = {} as Record @@ -140,6 +149,11 @@ export abstract class WorldRendererCommon const loadedChunks = Object.keys(this.finishedChunks).length updateStatText('loaded-chunks', `${loadedChunks}/${this.chunksLength} chunks (${this.lastChunkDistance}/${this.viewDistance})`) }) + + setInterval(() => { + this.geometryReceiveCount = 0 + this.updateChunksStatsText() + }, 1000) } snapshotInitialValues () { } @@ -157,8 +171,9 @@ export abstract class WorldRendererCommon if (!this.active) return this.handleWorkerMessage(data) if (data.type === 'geometry') { - this.geometryReceiveCount[data.workerIndex] ??= 0 - this.geometryReceiveCount[data.workerIndex]++ + // this.geometryReceiveCount[data.workerIndex] ??= 0 + // this.geometryReceiveCount[data.workerIndex]++ + this.geometryReceiveCount++ const geometry = data.geometry as MesherGeometryOutput for (const key in geometry.highestBlocks) { const highest = geometry.highestBlocks[key] @@ -173,6 +188,7 @@ export abstract class WorldRendererCommon if (!this.sectionsWaiting.get(data.key)) throw new Error(`sectionFinished event for non-outstanding section ${data.key}`) this.sectionsWaiting.set(data.key, this.sectionsWaiting.get(data.key)! - 1) if (this.sectionsWaiting.get(data.key) === 0) this.sectionsWaiting.delete(data.key) + this.finishedSections[data.key] = true const chunkCoords = data.key.split(',').map(Number) if (this.loadedChunks[`${chunkCoords[0]},${chunkCoords[2]}`]) { // ensure chunk data was added, not a neighbor chunk update @@ -196,8 +212,12 @@ export abstract class WorldRendererCommon } worker.onmessage = ({ data }) => { if (Array.isArray(data)) { + // const time = data[0] + // this.messagesDelay += Date.now() - time + // this.messageDelayCount++ // eslint-disable-next-line unicorn/no-array-for-each data.forEach(handleMessage) + // data.slice(1).forEach(handleMessage) return } handleMessage(data) @@ -298,14 +318,15 @@ export abstract class WorldRendererCommon } } - async updateTexturesData (resourcePackUpdate = false) { + async updateTexturesData (resourcePackUpdate = false, prioritizeBlockTextures?: string[]) { const blocksAssetsParser = new AtlasParser(this.blocksAtlases, blocksAtlasLatest, blocksAtlasLegacy) const itemsAssetsParser = new AtlasParser(this.itemsAtlases, itemsAtlasLatest, itemsAtlasLegacy) + const customBlockTextures = Object.keys(this.customTextures.blocks?.textures ?? {}).filter(x => x.includes('/')) const { atlas: blocksAtlas, canvas: blocksCanvas } = await blocksAssetsParser.makeNewAtlas(this.texturesVersion ?? this.version ?? 'latest', (textureName) => { const texture = this.customTextures?.blocks?.textures[textureName] if (!texture) return return texture - }, this.customTextures?.blocks?.tileSize) + }, /* this.customTextures?.blocks?.tileSize */undefined, prioritizeBlockTextures, customBlockTextures) const { atlas: itemsAtlas, canvas: itemsCanvas } = await itemsAssetsParser.makeNewAtlas(this.texturesVersion ?? this.version ?? 'latest', (textureName) => { const texture = this.customTextures?.items?.textures[textureName] if (!texture) return @@ -356,7 +377,7 @@ export abstract class WorldRendererCommon } updateChunksStatsText () { - updateStatText('downloaded-chunks', `${Object.keys(this.loadedChunks).length}/${this.chunksLength} chunks D (${this.workers.length}:${this.workersProcessAverageTime.toFixed(0)}ms/${this.allLoadedIn?.toFixed(1) ?? '-'}s)`) + updateStatText('downloaded-chunks', `${Object.keys(this.loadedChunks).length}/${this.chunksLength} chunks D (${this.workers.length}:${this.workersProcessAverageTime.toFixed(0)}ms/${this.geometryReceiveCount}ss/${this.allLoadedIn?.toFixed(1) ?? '-'}s)`) } addColumn (x: number, z: number, chunk: any, isLightUpdate: boolean) { @@ -397,6 +418,7 @@ export abstract class WorldRendererCommon this.allChunksFinished = Object.keys(this.finishedChunks).length === this.chunksLength for (let y = this.worldConfig.minY; y < this.worldConfig.worldHeight; y += 16) { this.setSectionDirty(new Vec3(x, y, z), false) + this.finishedSections[`${x},${y},${z}`] = false } // remove from highestBlocks const startX = Math.floor(x / 16) * 16 @@ -411,26 +433,30 @@ export abstract class WorldRendererCommon } setBlockStateId (pos: Vec3, stateId: number) { - const key = `${Math.floor(pos.x / 16) * 16},${Math.floor(pos.y / 16) * 16},${Math.floor(pos.z / 16) * 16}` - const useChangeWorker = !this.sectionsWaiting[key] for (const worker of this.workers) { worker.postMessage({ type: 'blockUpdate', pos, stateId }) } - this.setSectionDirty(pos, true, useChangeWorker) + this.setSectionDirty(pos, true, true) if (this.neighborChunkUpdates) { - if ((pos.x & 15) === 0) this.setSectionDirty(pos.offset(-16, 0, 0), true, useChangeWorker) - if ((pos.x & 15) === 15) this.setSectionDirty(pos.offset(16, 0, 0), true, useChangeWorker) - if ((pos.y & 15) === 0) this.setSectionDirty(pos.offset(0, -16, 0), true, useChangeWorker) - if ((pos.y & 15) === 15) this.setSectionDirty(pos.offset(0, 16, 0), true, useChangeWorker) - if ((pos.z & 15) === 0) this.setSectionDirty(pos.offset(0, 0, -16), true, useChangeWorker) - if ((pos.z & 15) === 15) this.setSectionDirty(pos.offset(0, 0, 16), true, useChangeWorker) + if ((pos.x & 15) === 0) this.setSectionDirty(pos.offset(-16, 0, 0), true, true) + if ((pos.x & 15) === 15) this.setSectionDirty(pos.offset(16, 0, 0), true, true) + if ((pos.y & 15) === 0) this.setSectionDirty(pos.offset(0, -16, 0), true, true) + if ((pos.y & 15) === 15) this.setSectionDirty(pos.offset(0, 16, 0), true, true) + if ((pos.z & 15) === 0) this.setSectionDirty(pos.offset(0, 0, -16), true, true) + if ((pos.z & 15) === 15) this.setSectionDirty(pos.offset(0, 0, 16), true, true) } } queueAwaited = false messagesQueue = {} as { [workerIndex: string]: any[] } - getWorkerNumber (pos: Vec3) { + getWorkerNumber (pos: Vec3, updateAction = false) { + if (updateAction) { + const key = `${Math.floor(pos.x / 16) * 16},${Math.floor(pos.y / 16) * 16},${Math.floor(pos.z / 16) * 16}` + const useChangeWorker = !this.sectionsWaiting[key] + if (useChangeWorker) return 0 + } + const hash = mod(Math.floor(pos.x / 16) + Math.floor(pos.y / 16) + Math.floor(pos.z / 16), this.workers.length - 1) return hash + 1 } @@ -446,7 +472,7 @@ export abstract class WorldRendererCommon // Dispatch sections to workers based on position // This guarantees uniformity accross workers and that a given section // is always dispatched to the same worker - const hash = useChangeWorker ? 0 : this.getWorkerNumber(pos) + const hash = this.getWorkerNumber(pos, useChangeWorker) this.sectionsWaiting.set(key, (this.sectionsWaiting.get(key) ?? 0) + 1) this.messagesQueue[hash] ??= [] this.messagesQueue[hash].push({ diff --git a/prismarine-viewer/viewer/lib/worldrendererWebgpu.ts b/prismarine-viewer/viewer/lib/worldrendererWebgpu.ts new file mode 100644 index 000000000..c93f2e17c --- /dev/null +++ b/prismarine-viewer/viewer/lib/worldrendererWebgpu.ts @@ -0,0 +1,406 @@ +import { Vec3 } from 'vec3' +// import { addBlocksSection, addWebgpuListener, webgpuChannel } from '../../examples/webgpuRendererMain' +import { pickObj } from '@zardoy/utils' +import { GUI } from 'lil-gui' +import type { WebglData } from '../prepare/webglData' +import { prepareCreateWebgpuBlocksModelsData } from '../../examples/webgpuBlockModels' +import type { workerProxyType } from '../../examples/webgpuRendererWorker' +import { useWorkerProxy } from '../../examples/workerProxy' +import { defaultWebgpuRendererParams, rendererParamsGui } from '../../examples/webgpuRendererShared' +import { loadJSON } from './utils.web' +import { WorldRendererCommon, WorldRendererConfig } from './worldrendererCommon' +import { MesherGeometryOutput } from './mesher/shared' +import { addNewStat, addNewStat2, updateStatText } from './ui/newStats' +import { isMobile } from './simpleUtils' +import { WorldRendererThree } from './worldrendererThree' + +export class WorldRendererWebgpu extends WorldRendererCommon { + outputFormat = 'webgpu' as const + stopBlockUpdate = false + allowUpdates = true + rendering = true + issueReporter = new RendererProblemReporter() + abortController = new AbortController() + worker: Worker | MessagePort | undefined + _readyPromise = Promise.withResolvers() + _readyWorkerPromise = Promise.withResolvers() + readyPromise = this._readyPromise.promise + readyWorkerPromise = this._readyWorkerPromise.promise + postRender = () => {} + preRender = () => {} + rendererParams = defaultWebgpuRendererParams + initCalled = false + + webgpuChannel: typeof workerProxyType['__workerProxy'] = this.getPlaceholderChannel() + rendererDevice = '...' + powerPreference: string | undefined + + constructor (config: WorldRendererConfig, public webglRenderer: THREE.WebGLRenderer, { powerPreference } = {} as any) { + super(config) + this.powerPreference = powerPreference + + void this.readyWorkerPromise.then(() => { + this.addWebgpuListener('rendererProblem', (data) => { + this.issueReporter.reportProblem(data.isContextLost, data.message) + }) + }) + + this.renderUpdateEmitter.on('update', () => { + const loadedChunks = Object.keys(this.finishedChunks).length + updateStatText('loaded-chunks', `${loadedChunks}/${this.chunksLength} chunks (${this.lastChunkDistance}/${this.viewDistance})`) + }) + } + + destroy () { + this.abortController.abort() + this.webgpuChannel.destroy() // still needed in case if running in the same thread + if (this.worker instanceof Worker) { + this.worker.terminate() + } + } + + getPlaceholderChannel () { + return new Proxy({}, { + get: (target, p) => (...args) => { + void this.readyWorkerPromise.then(() => { + this.webgpuChannel[p](...args) + }) + } + }) as any // placeholder to avoid crashes + } + + updateRendererParams (params: Partial) { + this.rendererParams = { ...this.rendererParams, ...params } + this.webgpuChannel.updateConfig(this.rendererParams) + } + + sendCameraToWorker () { + const cameraVectors = ['rotation', 'position'].reduce((acc, key) => { + acc[key] = ['x', 'y', 'z'].reduce((acc2, key2) => { + acc2[key2] = this.camera[key][key2] + return acc2 + }, {}) + return acc + }, {}) as any + this.webgpuChannel.camera({ + ...cameraVectors, + fov: this.camera.fov + }) + } + + addWebgpuListener (type: string, listener: (data: any) => void) { + void this.readyWorkerPromise.then(() => { + this.worker!.addEventListener('message', (e: any) => { + if (e.data.type === type) { + listener(e.data) + } + }) + }) + } + + override async setVersion (version, texturesVersion = version): Promise { + return Promise.all([ + super.setVersion(version, texturesVersion), + this.readyPromise + ]) + } + + setBlockStateId (pos: any, stateId: any): void { + if (this.stopBlockUpdate) return + super.setBlockStateId(pos, stateId) + } + + sendDataForWebgpuRenderer (data) { + for (const worker of this.workers) { + worker.postMessage({ type: 'webgpuData', data }) + } + } + + isWaitingForChunksToRender = false + + override addColumn (x: number, z: number, data: any, _): void { + if (this.initialChunksLoad) { + this.updateRendererParams({ + cameraOffset: [0, this.worldMinYRender < 0 ? Math.abs(this.worldMinYRender) : 0, 0] + }) + } + super.addColumn(x, z, data, _) + } + + allChunksLoaded (): void { + console.log('allChunksLoaded') + this.webgpuChannel.addBlocksSectionDone() + } + + handleWorkerMessage (data: { geometry: MesherGeometryOutput, type, key }): void { + if (data.type === 'geometry' && Object.keys(data.geometry.tiles).length) { + this.addChunksToScene(data.key, data.geometry) + } + } + + addChunksToScene (key: string, geometry: MesherGeometryOutput) { + if (this.finishedChunks[key] && !this.allowUpdates) return + // const chunkCoords = key.split(',').map(Number) as [number, number, number] + if (/* !this.loadedChunks[chunkCoords[0] + ',' + chunkCoords[2]] || */ !this.active) return + + this.webgpuChannel.addBlocksSection(geometry.tiles, key, !this.finishedSections[key]) + } + + updateCamera (pos: Vec3 | null, yaw: number, pitch: number): void { + if (pos) { + // new tweenJs.Tween(this.camera.position).to({ x: pos.x, y: pos.y, z: pos.z }, 50).start() + this.camera.position.set(pos.x, pos.y, pos.z) + } + this.camera.rotation.set(pitch, yaw, 0, 'ZYX') + this.sendCameraToWorker() + } + render (): void { } + + chunksReset () { + this.webgpuChannel.fullDataReset() + } + + updatePosDataChunk (key: string) { + } + + async updateTexturesData (resourcePackUpdate = false): Promise { + const { blocksDataModelDebug: blocksDataModelBefore, interestedTextureTiles } = prepareCreateWebgpuBlocksModelsData() + await super.updateTexturesData(undefined, [...interestedTextureTiles].map(x => x.replace('block/', ''))) + const { blocksDataModel, blocksDataModelDebug, allBlocksStateIdToModelIdMap } = prepareCreateWebgpuBlocksModelsData() + // this.webgpuChannel.updateModels(blocksDataModel) + this.sendDataForWebgpuRenderer({ allBlocksStateIdToModelIdMap }) + void this.initWebgpu(blocksDataModel) + if (resourcePackUpdate) { + const blob = await fetch(this.material.map!.image.src).then(async (res) => res.blob()) + this.webgpuChannel.updateTexture(blob) + } + } + + updateShowChunksBorder (value: boolean) { + // todo + } + + changeBackgroundColor (color: [number, number, number]) { + this.webgpuChannel.updateBackground(color) + } + + setHighlightCursorBlock (position: typeof this.cursorBlock): void { + const useChangeWorker = true + if (this.cursorBlock) { + const worker = this.workers[this.getWorkerNumber(this.cursorBlock, useChangeWorker)] + worker.postMessage({ type: 'specialBlockState', data: { value: null, position: this.cursorBlock } }) + this.setSectionDirty(this.cursorBlock, true, useChangeWorker) + } + + this.cursorBlock = position + if (this.cursorBlock) { + const worker = this.workers[this.getWorkerNumber(this.cursorBlock, useChangeWorker)] + worker.postMessage({ type: 'specialBlockState', data: { value: 'highlight', position: this.cursorBlock } }) + this.setSectionDirty(this.cursorBlock, true, useChangeWorker) + } + } + + + removeColumn (x, z) { + console.log('removeColumn', x, z) + super.removeColumn(x, z) + + for (let y = this.worldConfig.minY; y < this.worldConfig.worldHeight; y += 16) { + this.webgpuChannel.removeBlocksSection(`${x},${y},${z}`) + } + } + + async initWebgpu (blocksDataModel) { + if (this.initCalled) return + this.initCalled = true + // do not use worker in safari, it is bugged + const USE_WORKER = defaultWebgpuRendererParams.webgpuWorker + + const playground = this.config.isPlayground + const { image } = (this.material.map!) + const imageBlob = await fetch(image.src).then(async (res) => res.blob()) + + const existingCanvas = document.getElementById('viewer-canvas') + existingCanvas?.remove() + 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' + + + // replacable by initWebglRenderer + if (USE_WORKER) { + this.worker = new Worker('./webgpuRendererWorker.js') + console.log('starting offscreen') + } else if (globalThis.webgpuRendererChannel) { + this.worker = globalThis.webgpuRendererChannel.port1 as MessagePort + } else { + const messageChannel = new MessageChannel() + globalThis.webgpuRendererChannel = messageChannel + this.worker = messageChannel.port1 + messageChannel.port1.start() + messageChannel.port2.start() + await import('../../examples/webgpuRendererWorker') + } + addWebgpuDebugUi(this.worker, playground, this.webglRenderer) + this.webgpuChannel = useWorkerProxy(this.worker, true) + this._readyWorkerPromise.resolve(undefined) + this.webgpuChannel.canvas( + canvas.transferControlToOffscreen(), + imageBlob, + playground, + pickObj(localStorage, 'vertShader', 'fragShader', 'computeShader'), + blocksDataModel, + { powerPreference: this.powerPreference as GPUPowerPreference } + ) + + if (!USE_WORKER) { + // wait for the .canvas() message to be processed (it's async since we still use message channel) + await new Promise(resolve => { + setTimeout(resolve, 0) + }) + } + + let oldWidth = window.innerWidth + let oldHeight = window.innerHeight + let focused = true + const { signal } = this.abortController + window.addEventListener('focus', () => { + focused = true + this.webgpuChannel.startRender() + }, { signal }) + window.addEventListener('blur', () => { + focused = false + this.webgpuChannel.stopRender() + }, { signal }) + const mainLoop = () => { + if (this.abortController.signal.aborted) return + requestAnimationFrame(mainLoop) + if (!focused || window.stopRender) return + + if (oldWidth !== window.innerWidth || oldHeight !== window.innerHeight) { + oldWidth = window.innerWidth + oldHeight = window.innerHeight + this.webgpuChannel.resize(window.innerWidth * window.devicePixelRatio, window.innerHeight * window.devicePixelRatio) + } + this.preRender() + this.postRender() + this.sendCameraToWorker() + } + + requestAnimationFrame(mainLoop) + + this._readyPromise.resolve(undefined) + } +} + +class RendererProblemReporter { + dom = document.createElement('div') + contextlostDom = document.createElement('div') + mainIssueDom = document.createElement('div') + + constructor () { + document.body.appendChild(this.dom) + this.dom.className = 'renderer-problem-reporter' + this.dom.appendChild(this.contextlostDom) + this.dom.appendChild(this.mainIssueDom) + this.dom.style.fontFamily = 'monospace' + this.dom.style.fontSize = '20px' + this.contextlostDom.style.cssText = ` + position: fixed; + top: 60px; + left: 0; + right: 0; + color: red; + display: flex; + justify-content: center; + z-index: -1; + font-size: 18px; + text-align: center; + ` + this.mainIssueDom.style.cssText = ` + position: fixed; + inset: 0; + color: red; + display: flex; + justify-content: center; + align-items: center; + z-index: -1; + text-align: center; + ` + this.reportProblem(false, 'Waiting for renderer...') + this.mainIssueDom.style.color = 'white' + } + + reportProblem (isContextLost: boolean, message: string) { + this.mainIssueDom.style.color = 'red' + if (isContextLost) { + this.contextlostDom.textContent = `Renderer context lost (try restarting the browser): ${message}` + } else { + this.mainIssueDom.textContent = message + } + } +} + +const addWebgpuDebugUi = (worker, isPlayground, renderer) => { + // todo destroy + const mobile = isMobile() + const { updateText } = addNewStat('fps', 200, undefined, 0) + let prevTimeout + worker.addEventListener('message', (e: any) => { + if (e.data.type === 'fps') { + updateText(`FPS: ${e.data.fps}`) + if (prevTimeout) clearTimeout(prevTimeout) + prevTimeout = setTimeout(() => { + updateText('') + }, 1002) + } + if (e.data.type === 'stats') { + updateTextGpuStats(e.data.stats) + viewer.world.rendererDevice = `${e.data.device} WebGL data: ${WorldRendererThree.getRendererInfo(renderer)}` + } + }) + + const { updateText: updateText2 } = addNewStat('fps-main', 90, 0, 20) + const { updateText: updateTextGpuStats } = addNewStat('gpu-stats', 90, 0, 40) + const leftUi = isPlayground ? 130 : mobile ? 25 : 0 + const { updateText: updateTextBuild } = addNewStat2('build-info', { + left: leftUi, + displayOnlyWhenWider: 700, + }) + updateTextBuild(`WebGPU Renderer Demo by @SA2URAMI. Build: ${process.env.NODE_ENV === 'development' ? 'dev' : process.env.RELEASE_TAG}`) + let updates = 0 + const mainLoop = () => { + requestAnimationFrame(mainLoop) + updates++ + } + mainLoop() + setInterval(() => { + updateText2(`Main Loop: ${updates}`) + updates = 0 + }, 1000) + + if (!isPlayground) { + const gui = new GUI() + gui.domElement.classList.add('webgpu-debug-ui') + gui.title('WebGPU Params') + gui.open(false) + setTimeout(() => { + gui.open(false) + }, 500) + for (const rendererParam of Object.entries(viewer.world.rendererParams)) { + const [key, value] = rendererParam + if (!rendererParamsGui[key]) continue + // eslint-disable-next-line @typescript-eslint/no-loop-func + gui.add(viewer.world.rendererParams, key).onChange((newVal) => { + viewer.world.updateRendererParams({ [key]: newVal }) + if (rendererParamsGui[key]?.qsReload) { + const searchParams = new URLSearchParams(window.location.search) + searchParams.set(key, String(value)) + window.location.search = searchParams.toString() + } + }) + } + } +} diff --git a/prismarine-viewer/viewer/prepare/webglData.ts b/prismarine-viewer/viewer/prepare/webglData.ts new file mode 100644 index 000000000..676e8b63f --- /dev/null +++ b/prismarine-viewer/viewer/prepare/webglData.ts @@ -0,0 +1,28 @@ +import { join } from 'path' +import fs from 'fs' +import { JsonAtlas } from './atlas' + +export type WebglData = ReturnType + +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: Array<{ + index: number, + time: number + } | number> + } + } + return [texture, { + animation: { + ...mcMeta.animation, + framesCount: animatedFrames + } + }] as const + }).filter(Boolean)) +} diff --git a/prismarine-viewer/webgpuShaders/RadialBlur/frag.wgsl b/prismarine-viewer/webgpuShaders/RadialBlur/frag.wgsl new file mode 100644 index 000000000..4b995366e --- /dev/null +++ b/prismarine-viewer/webgpuShaders/RadialBlur/frag.wgsl @@ -0,0 +1,76 @@ +// Fragment shader +@group(0) @binding(0) var tex: texture_depth_2d; + @group(0) @binding(1) var mySampler: sampler; + @group(0) @binding(2) var texColor: texture_2d; + @group(0) @binding(3) var clearColor: vec4; +const sampleDist : f32 = 1.0; +const sampleStrength : f32 = 2.2; + +const SAMPLES: f32 = 24.; +fn hash( p: vec2 ) -> f32 { return fract(sin(dot(p, vec2(41, 289)))*45758.5453); } + +fn lOff() -> vec3{ + + var u = sin(vec2(1.57, 0)); + var a = mat2x2(u.x,u.y, -u.y, u.x); + + var l : vec3 = normalize(vec3(1.5, 1., -0.5)); + var temp = a * l.xz; + l.x = temp.x; + l.z = temp.y; + temp = a * l.xy; + l.x = temp.x; + l.y = temp.y; + + return l; +} + +@fragment +fn main( + @location(0) uv: vec2f, +) -> @location(0) vec4f +{ + var uvs = uv; + uvs.y = 1.0 - uvs.y; + var decay : f32 = 0.93; + // Controls the sample density, which in turn, controls the sample spread. + var density = 0.5; + // Sample weight. Decays as we radiate outwards. + var weight = 0.04; + + var l = lOff(); + + var tuv = uvs-l.xy*.45; + + var dTuv = tuv*density/SAMPLES; + + var temp = textureSample(tex,mySampler, uvs); + var col : f32; + var outTex = textureSample(texColor, mySampler, uvs); + if (temp == 1.0) { + col = temp * 0.25; + } + + uvs += dTuv*(hash(uvs.xy - 1.0) * 2. - 1.); + + for(var i=0.0; i < SAMPLES; i += 1){ + + uvs -= dTuv; + var temp = textureSample(tex, mySampler, uvs); + if (temp == 1.0) { + col +=temp * weight; + } + weight *= decay; + + } + + + //col *= (1. - dot(tuv, tuv)*.75); + let t = clearColor.xyz * sqrt(smoothstep(0.0, 1.0, col)); + if (temp == 1.0) { + return vec4(t, 1.0); + } + + + return outTex + vec4(t, 1.0); +} diff --git a/prismarine-viewer/webgpuShaders/RadialBlur/vert.wgsl b/prismarine-viewer/webgpuShaders/RadialBlur/vert.wgsl new file mode 100644 index 000000000..623aa31f2 --- /dev/null +++ b/prismarine-viewer/webgpuShaders/RadialBlur/vert.wgsl @@ -0,0 +1,18 @@ + +struct VertexOutput { + @builtin(position) Position: vec4f, + @location(0) fragUV: vec2f, +} + + +@vertex +fn main( + @location(0) position: vec4, + @location(1) uv: vec2 +) -> VertexOutput { + var output: VertexOutput; + output.Position = vec4f(position.xy, 0.0 , 1.0); + output.Position = sign(output.Position); + output.fragUV = uv; + return output; +} \ No newline at end of file diff --git a/rsbuild.config.ts b/rsbuild.config.ts index 1085e3fcf..77b91ce00 100644 --- a/rsbuild.config.ts +++ b/rsbuild.config.ts @@ -109,6 +109,12 @@ const appConfig = defineConfig({ } else if (!dev) { await execAsync('pnpm run build-mesher') } + if (fs.existsSync('./prismarine-viewer/dist/webgpuRendererWorker.js')) { + // copy worker + fs.copyFileSync('./prismarine-viewer/dist/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') } diff --git a/src/browserfs.ts b/src/browserfs.ts index 7e66dfe04..6f605376e 100644 --- a/src/browserfs.ts +++ b/src/browserfs.ts @@ -477,6 +477,7 @@ export const copyFilesAsync = async (pathSrc: string, pathDest: string, fileCopi export const openWorldFromHttpDir = async (fileDescriptorUrls: string[]/* | undefined */, baseUrlParam) => { // todo try go guess mode + let indexFileUrl let index let baseUrl for (const url of fileDescriptorUrls) { @@ -502,6 +503,7 @@ export const openWorldFromHttpDir = async (fileDescriptorUrls: string[]/* | und index = file baseUrl = baseUrlParam ?? url.split('/').slice(0, -1).join('/') } + indexFileUrl = url break } if (!index) throw new Error(`The provided mapDir file is not valid descriptor file! ${fileDescriptorUrls.join(', ')}`) @@ -529,6 +531,7 @@ export const openWorldFromHttpDir = async (fileDescriptorUrls: string[]/* | und fsState.syncFs = false fsState.inMemorySave = false fsState.remoteBackend = true + fsState.usingIndexFileUrl = indexFileUrl await loadSave() } diff --git a/src/controls.ts b/src/controls.ts index cb1c132bc..b7703e5a0 100644 --- a/src/controls.ts +++ b/src/controls.ts @@ -457,6 +457,9 @@ export const f3Keybinds = [ console.warn('forcefully removed chunk from scene') } } + + viewer.world.chunksReset() // todo + if (localServer) { //@ts-expect-error not sure why it is private... maybe revisit api? localServer.players[0].world.columns = {} @@ -531,8 +534,20 @@ export const f3Keybinds = [ const proxyPing = await bot['pingProxy']() void showOptionsModal(`${username}: last known total latency (ping): ${playerPing}. Connected to ${lastConnectOptions.value?.proxy} with current ping ${proxyPing}. Player UUID: ${uuid}`, []) }, - mobileTitle: 'Show Proxy & Ping Details' - } + mobileTitle: 'Show Proxy & Ping Details', + show: () => !miscUiState.singleplayer + }, + { + key: 'KeyL', + async action () { + if (viewer.world.rendering) { + viewer.world.webgpuChannel.stopRender() + } else { + viewer.world.webgpuChannel.startRender() + } + }, + mobileTitle: 'Toggle rendering' + }, ] const hardcodedPressedKeys = new Set() @@ -768,6 +783,10 @@ window.addEventListener('keydown', (e) => { if (e.code === 'KeyL' && e.altKey) { console.clear() } + if (e.code === 'KeyK' && e.altKey) { + // eslint-disable-next-line no-debugger + debugger + } }) // #endregion diff --git a/src/devtools.ts b/src/devtools.ts index 16337c1d7..7852506e6 100644 --- a/src/devtools.ts +++ b/src/devtools.ts @@ -19,6 +19,7 @@ window.inspectPlayer = () => require('fs').promises.readFile('/world/playerdata/ Object.defineProperty(window, 'debugSceneChunks', { get () { + if (!(viewer.world instanceof WorldRendererThree)) return return (viewer.world as WorldRendererThree).getLoadedChunksRelative?.(bot.entity.position, true) }, }) diff --git a/src/flyingSquidEvents.ts b/src/flyingSquidEvents.ts index 7231dd276..d66a9e0a8 100644 --- a/src/flyingSquidEvents.ts +++ b/src/flyingSquidEvents.ts @@ -14,6 +14,10 @@ export default () => { }) }) + localServer!.on('newPlayer', (player) => { + player.stopChunkUpdates = !viewer.world.rendererParams.allowChunksViewUpdate + }) + if (options.singleplayerAutoSave) { const autoSaveInterval = setInterval(() => { if (options.singleplayerAutoSave) { diff --git a/src/globals.d.ts b/src/globals.d.ts index 7fc9baecc..d655ba8db 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -33,4 +33,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 { } diff --git a/src/index.ts b/src/index.ts index 93bbe6b90..bc14cfe68 100644 --- a/src/index.ts +++ b/src/index.ts @@ -60,7 +60,8 @@ import { import { pointerLock, toMajorVersion, - setLoadingScreenStatus + setLoadingScreenStatus, + logAction } from './utils' import { isCypress } from './standaloneUtils' @@ -99,10 +100,12 @@ import { signInMessageState } from './react/SignInMessageProvider' import { updateAuthenticatedAccountData, updateLoadedServerData } from './react/ServersListProvider' import { versionToNumber } from 'prismarine-viewer/viewer/prepare/utils' import packetsPatcher from './packetsPatcher' +// import { ViewerBase } from 'prismarine-viewer/viewer/lib/viewerWrapper' import { mainMenuState } from './react/MainMenuRenderApp' import { ItemsRenderer } from 'mc-assets/dist/itemsRenderer' import './mobileShim' import { parseFormattedMessagePacket } from './botUtils' +import { addNewStat } from 'prismarine-viewer/viewer/lib/ui/newStats' window.debug = debug window.THREE = THREE @@ -203,6 +206,7 @@ let lastMouseMove: number const updateCursor = () => { worldInteractions.update() } +let mouseEvents = 0 function onCameraMove (e) { if (e.type !== 'touchmove' && !pointerLock.hasPointerLock) return e.stopPropagation?.() @@ -217,8 +221,14 @@ function onCameraMove (e) { y: e.movementY * mouseSensY * 0.0001 }) updateCursor() + mouseEvents++ + viewer.setFirstPersonCamera(null, bot.entity.yaw, bot.entity.pitch) } -window.addEventListener('mousemove', onCameraMove, { capture: true }) +setInterval(() => { + // console.log('mouseEvents', mouseEvents) + mouseEvents = 0 +}, 1000) +window.addEventListener('mousemove', onCameraMove, { capture: true, passive: false }) contro.on('stickMovement', ({ stick, vector }) => { if (!isGameActive(true)) return if (stick !== 'right') return @@ -303,6 +313,11 @@ async function connect (connectOptions: ConnectOptions) { console.log(`connecting to ${server.host}:${server.port} with ${username}`) + const playType = connectOptions.server ? 'Server' : connectOptions.singleplayer ? 'Singleplayer' : 'P2P Multiplayer' + const info = connectOptions.server ? `${server.host}:${server.port}` : connectOptions.singleplayer ? + (fsState.usingIndexFileUrl || fsState.remoteBackend ? 'remote' : fsState.inMemorySave ? 'IndexedDB' : fsState.syncFs ? 'ZIP' : 'Folder') : '-' + logAction('Play', playType, `v${connectOptions.botVersion} : ${info}`) + const startDisplayViewer = Date.now() hideCurrentScreens() setLoadingScreenStatus('Logging in') @@ -389,6 +404,8 @@ async function connect (connectOptions: ConnectOptions) { const serverOptions = defaultsDeep({}, connectOptions.serverOverrides ?? {}, options.localServerOptions, defaultServerOptions) Object.assign(serverOptions, connectOptions.serverOverridesFlat ?? {}) window._LOAD_MC_DATA() // start loading data (if not loaded yet) + addNewStat('loaded-chunks', undefined, 220, 0) + addNewStat('downloaded-chunks', 90, 200, 20) const downloadMcData = async (version: string) => { if (connectOptions.authenticatedAccount && (versionToNumber(version) < versionToNumber('1.19.4') || versionToNumber(version) >= versionToNumber('1.21'))) { // todo support it (just need to fix .export crash) @@ -421,9 +438,22 @@ async function connect (connectOptions: ConnectOptions) { } } viewer.world.blockstatesModels = await import('mc-assets/dist/blockStatesModels.json') - void viewer.setVersion(version, options.useVersionsTextures === 'latest' ? version : options.useVersionsTextures) + const mcData = MinecraftData(version) + window.loadedData = mcData + const promise = viewer.setVersion(version, options.useVersionsTextures === 'latest' ? version : options.useVersionsTextures) + const isWebgpu = true + if (isWebgpu) { + await promise + } + viewer.world.postRender = () => { + renderWrapper.postRender() + } + viewer.world.preRender = () => { + renderWrapper.preRender() + } } + // serverOptions.version = '1.18.1' const downloadVersion = connectOptions.botVersion || (singleplayer ? serverOptions.version : undefined) if (downloadVersion) { await downloadMcData(downloadVersion) @@ -678,10 +708,8 @@ async function connect (connectOptions: ConnectOptions) { // don't use spawn event, player can be dead bot.once(spawnEarlier ? 'forcedMove' : 'health', () => { errorAbortController.abort() - const mcData = MinecraftData(bot.version) - window.PrismarineBlock = PrismarineBlock(mcData.version.minecraftVersion!) - window.PrismarineItem = PrismarineItem(mcData.version.minecraftVersion!) - window.loadedData = mcData + window.PrismarineBlock = PrismarineBlock(loadedData.version.minecraftVersion!) + window.PrismarineItem = PrismarineItem(loadedData.version.minecraftVersion!) window.Vec3 = Vec3 window.pathfinder = pathfinder @@ -696,6 +724,11 @@ async function connect (connectOptions: ConnectOptions) { if (process.env.NODE_ENV === 'development' && !localStorage.lockUrl && new URLSearchParams(location.search).size === 0) { lockUrl() } + logAction('Joined', playType, `Time: ${Date.now() - startDisplayViewer}ms`) + if (connectOptions.server) { + logAction('Server Version', bot.version) + logAction('Auth', connectOptions.authenticatedAccount ? 'Authenticated' : 'Offline') + } updateDataAfterJoin() if (connectOptions.autoLoginPassword) { bot.chat(`/login ${connectOptions.autoLoginPassword}`) @@ -713,8 +746,8 @@ async function connect (connectOptions: ConnectOptions) { void initVR() - renderWrapper.postRender = () => { - viewer.setFirstPersonCamera(null, bot.entity.yaw, bot.entity.pitch) + renderWrapper.preRender = () => { + // viewer.setFirstPersonCamera(null, bot.entity.yaw, bot.entity.pitch) } @@ -898,13 +931,14 @@ async function connect (connectOptions: ConnectOptions) { }, 600) setLoadingScreenStatus(undefined) - const start = Date.now() + const startLoadingChunks = Date.now() let done = false void viewer.world.renderUpdateEmitter.on('update', () => { // todo might not emit as servers simply don't send chunk if it's empty if (!viewer.world.allChunksFinished || done) return done = true - console.log('All done and ready! In', (Date.now() - start) / 1000, 's') + console.log('All done and ready! In', (Date.now() - startLoadingChunks) / 1000, 's') + logAction('Chunks Loaded', 'All', `Distance: ${viewer.world.viewDistance} Time: ${(Date.now() - startLoadingChunks) / 1000}s`) viewer.render() // ensure the last state is rendered document.dispatchEvent(new Event('cypress-world-ready')) }) diff --git a/src/loadSave.ts b/src/loadSave.ts index 6c7da6bb2..ec257c6f6 100644 --- a/src/loadSave.ts +++ b/src/loadSave.ts @@ -21,7 +21,8 @@ export const fsState = proxy({ saveLoaded: false, openReadOperations: 0, openWriteOperations: 0, - remoteBackend: false + remoteBackend: false, + usingIndexFileUrl: '' }) const PROPOSE_BACKUP = true diff --git a/src/optionsGuiScheme.tsx b/src/optionsGuiScheme.tsx index c6a979c16..5d397b35b 100644 --- a/src/optionsGuiScheme.tsx +++ b/src/optionsGuiScheme.tsx @@ -128,7 +128,7 @@ export const guiOptionsScheme: { id, text: 'Render Distance', unit: '', - max: sp ? 16 : 12, + max: sp ? 32 : 16, min: 1 }} /> diff --git a/src/optionsStorage.ts b/src/optionsStorage.ts index 618d99e88..ef5753509 100644 --- a/src/optionsStorage.ts +++ b/src/optionsStorage.ts @@ -26,7 +26,7 @@ const defaultOptions = { messagesLimit: 200, volume: 50, // fov: 70, - fov: 75, + fov: 90, guiScale: 3, autoRequestCompletions: true, touchButtonsSize: 40, @@ -52,6 +52,7 @@ const defaultOptions = { // antiAliasing: false, + webgpuRendererParams: {} as Record, clipWorldBelowY: undefined as undefined | number, // will be removed disableSignsMapsSupport: false, singleplayerAutoSave: false, @@ -80,6 +81,7 @@ const defaultOptions = { autoParkour: false, vrSupport: true, // doesn't directly affect the VR mode, should only disable the button which is annoying to android users renderDebug: (isDev ? 'advanced' : 'basic') as 'none' | 'advanced' | 'basic', + externalLoggingService: true, // advanced bot options autoRespawn: false, @@ -92,7 +94,7 @@ const defaultOptions = { minimapOptimizations: true, displayBossBars: false, // boss bar overlay was removed for some reason, enable safely disabledUiParts: [] as string[], - neighborChunkUpdates: true + neighborChunkUpdates: false } function getDefaultTouchControlsPositions () { diff --git a/src/react/ButtonWithTooltip.tsx b/src/react/ButtonWithTooltip.tsx index d93720070..e308d8fd5 100644 --- a/src/react/ButtonWithTooltip.tsx +++ b/src/react/ButtonWithTooltip.tsx @@ -9,14 +9,15 @@ interface Props extends React.ComponentProps { localStorageKey?: string | null offset?: number } + alwaysTooltip?: string } const ARROW_HEIGHT = 7 const GAP = 0 -export default ({ initialTooltip, ...args }: Props) => { +export default ({ initialTooltip, alwaysTooltip, ...args }: Props) => { const { localStorageKey = 'firstTimeTooltip', offset = 0 } = initialTooltip - const [showTooltips, setShowTooltips] = useState(localStorageKey ? localStorage[localStorageKey] !== 'false' : true) + const [showTooltips, setShowTooltips] = useState(alwaysTooltip || (localStorageKey ? localStorage[localStorageKey] !== 'false' : true)) useEffect(() => { let timeout @@ -67,7 +68,7 @@ export default ({ initialTooltip, ...args }: Props) => { zIndex: 11 }} > - {initialTooltip.content} + {alwaysTooltip || initialTooltip.content}
diff --git a/src/react/DebugOverlay.module.css b/src/react/DebugOverlay.module.css index 9ea30ebfb..2815a3875 100644 --- a/src/react/DebugOverlay.module.css +++ b/src/react/DebugOverlay.module.css @@ -11,12 +11,12 @@ } .debug-left-side { - top: 1px; + top: 25px; left: 1px; } .debug-right-side { - top: 5px; + top: 25px; right: 1px; /* limit renderer long text width */ width: 50%; diff --git a/src/react/MainMenu.tsx b/src/react/MainMenu.tsx index 45196f4c5..9ca3a7341 100644 --- a/src/react/MainMenu.tsx +++ b/src/react/MainMenu.tsx @@ -59,30 +59,29 @@ export default ({
- - Connect to server -
Singleplayer + mapsProvider && openURL(httpsRegex.test(mapsProvider) ? mapsProvider : 'https://' + mapsProvider, false)} + alwaysTooltip='CHECK MAPS PERF!' + /> +
+ + Multiplayer +
- - {mapsProvider && - openURL(httpsRegex.test(mapsProvider) ? mapsProvider : 'https://' + mapsProvider, false)} - />} ) } diff --git a/src/react/MainMenuRenderApp.tsx b/src/react/MainMenuRenderApp.tsx index f62cf1652..d98b0fb68 100644 --- a/src/react/MainMenuRenderApp.tsx +++ b/src/react/MainMenuRenderApp.tsx @@ -73,8 +73,10 @@ export default () => { } }, []) - let mapsProviderUrl = appConfig?.mapsProvider - if (mapsProviderUrl && location.origin !== 'https://mcraft.fun') mapsProviderUrl = mapsProviderUrl + '?to=' + encodeURIComponent(location.href) + const mapsProviderUrl = appConfig?.mapsProvider && new URL(appConfig?.mapsProvider) + if (mapsProviderUrl && location.origin !== 'https://mcraft.fun') { + mapsProviderUrl.searchParams.set('to', location.href) + } // todo clean, use custom csstransition return @@ -113,7 +115,7 @@ export default () => { openFilePicker() } }} - mapsProvider={mapsProviderUrl} + mapsProvider={mapsProviderUrl?.toString()} versionStatus={versionStatus} versionTitle={versionTitle} onVersionStatusClick={async () => { diff --git a/src/react/PauseScreen.tsx b/src/react/PauseScreen.tsx index 75b94872b..6bbebd18f 100644 --- a/src/react/PauseScreen.tsx +++ b/src/react/PauseScreen.tsx @@ -203,7 +203,10 @@ 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') { const path = await saveToBrowserMemory() if (!path) return @@ -214,6 +217,9 @@ export default () => { // fsState.isReadonly = false // fsState.remoteBackend = false } + if (action === 'Dump loaded chunks') { + // viewer.world.exportLoadedTiles() + } } if (!isModalActive) return null diff --git a/src/resourcePack.ts b/src/resourcePack.ts index 0ab9ec493..9803a221c 100644 --- a/src/resourcePack.ts +++ b/src/resourcePack.ts @@ -381,7 +381,7 @@ const updateTextures = async () => { } } if (viewer.world.active) { - await viewer.world.updateTexturesData() + await viewer.world.updateTexturesData(true) } } diff --git a/src/styles.css b/src/styles.css index 817fae8ca..0034c5cba 100644 --- a/src/styles.css +++ b/src/styles.css @@ -181,6 +181,15 @@ body::xr-overlay #viewer-canvas { color: #999; } +.webgpu-debug-ui { + top: 68px !important; + background: transparent; +} + +.webgpu-debug-ui .title { + background: transparent !important; +} + @media screen and (min-width: 430px) { .span-2 { grid-column: span 2; diff --git a/src/topRightStats.ts b/src/topRightStats.ts index 4bcd7264e..1a31e5d8e 100644 --- a/src/topRightStats.ts +++ b/src/topRightStats.ts @@ -39,6 +39,7 @@ if (hasRamPanel) { addStat(stats2.dom) } +const hideStats = localStorage.hideStats || isCypress() || true export const toggleStatsVisibility = (visible: boolean) => { if (visible) { stats.dom.style.display = 'block' @@ -51,7 +52,6 @@ export const toggleStatsVisibility = (visible: boolean) => { } } -const hideStats = localStorage.hideStats || isCypress() if (hideStats) { toggleStatsVisibility(false) } diff --git a/src/utils.ts b/src/utils.ts index f0d04b55a..04dbafbae 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -65,6 +65,16 @@ export const pointerLock = { } } +export const logAction = (category: string, action: string, value?: string, label?: string) => { + if (!options.externalLoggingService) return + window.loggingServiceChannel?.({ + category, + action, + value, + label + }) +} + window.getScreenRefreshRate = getScreenRefreshRate /** diff --git a/src/watchOptions.ts b/src/watchOptions.ts index 4b69726c9..53d4a9ef6 100644 --- a/src/watchOptions.ts +++ b/src/watchOptions.ts @@ -3,6 +3,8 @@ import { subscribeKey } from 'valtio/utils' import { WorldRendererThree } from 'prismarine-viewer/viewer/lib/worldrendererThree' import { isMobile } from 'prismarine-viewer/viewer/lib/simpleUtils' +import { WorldRendererWebgpu } from 'prismarine-viewer/viewer/lib/worldrendererWebgpu' +import { defaultWebgpuRendererParams } from 'prismarine-viewer/examples/webgpuRendererShared' import { options, watchValue } from './optionsStorage' import { reloadChunks } from './utils' import { miscUiState } from './globalState' @@ -62,18 +64,24 @@ export const watchOptionsAfterViewerInit = () => { viewer.world.mesherConfig.clipWorldBelowY = o.clipWorldBelowY viewer.world.mesherConfig.disableSignsMapsSupport = o.disableSignsMapsSupport if (isChanged) { - (viewer.world as WorldRendererThree).rerenderAllChunks() + if (viewer.world instanceof WorldRendererThree) { + (viewer.world as WorldRendererThree).rerenderAllChunks() + } } }) viewer.world.mesherConfig.smoothLighting = options.smoothLighting subscribeKey(options, 'smoothLighting', () => { - viewer.world.mesherConfig.smoothLighting = options.smoothLighting; - (viewer.world as WorldRendererThree).rerenderAllChunks() + viewer.world.mesherConfig.smoothLighting = options.smoothLighting + if (viewer.world instanceof WorldRendererThree) { + (viewer.world as WorldRendererThree).rerenderAllChunks() + } }) subscribeKey(options, 'newVersionsLighting', () => { - viewer.world.mesherConfig.enableLighting = !bot.supportFeature('blockStateId') || options.newVersionsLighting; - (viewer.world as WorldRendererThree).rerenderAllChunks() + viewer.world.mesherConfig.enableLighting = !bot.supportFeature('blockStateId') || options.newVersionsLighting + if (viewer.world instanceof WorldRendererThree) { + (viewer.world as WorldRendererThree).rerenderAllChunks() + } }) customEvents.on('gameLoaded', () => { viewer.world.mesherConfig.enableLighting = !bot.supportFeature('blockStateId') || options.newVersionsLighting @@ -81,21 +89,48 @@ export const watchOptionsAfterViewerInit = () => { watchValue(options, o => { if (!(viewer.world instanceof WorldRendererThree)) return - viewer.world.starField.enabled = o.starfieldRendering + (viewer.world as WorldRendererThree).starField.enabled = o.starfieldRendering }) watchValue(options, o => { viewer.world.neighborChunkUpdates = o.neighborChunkUpdates }) + watchValue(options, o => { + viewer.powerPreference = o.gpuPreference + }) + + onRendererParamsUpdate() + + if (viewer.world instanceof WorldRendererWebgpu) { + Object.assign(viewer.world.rendererParams, options.webgpuRendererParams) + const oldUpdateRendererParams = viewer.world.updateRendererParams.bind(viewer.world) + viewer.world.updateRendererParams = (...args) => { + oldUpdateRendererParams(...args) + Object.assign(options.webgpuRendererParams, viewer.world.rendererParams) + onRendererParamsUpdate() + } + } +} + +const onRendererParamsUpdate = () => { + if (worldView) { + worldView.allowPositionUpdate = viewer.world.rendererParams.allowChunksViewUpdate + } + if (localServer?.players?.[0]) { + localServer.players[0].stopChunkUpdates = !viewer.world.rendererParams.allowChunksViewUpdate + } } let viewWatched = false export const watchOptionsAfterWorldViewInit = () => { + onRendererParamsUpdate() if (viewWatched) return viewWatched = true watchValue(options, o => { if (!worldView) return worldView.keepChunksDistance = o.keepChunksDistance worldView.handDisplay = o.handDisplay + + // worldView.allowPositionUpdate = o.webgpuRendererParams.allowChunksViewUpdate }) } diff --git a/src/worldInteractions.ts b/src/worldInteractions.ts index d6674ce31..55a981de9 100644 --- a/src/worldInteractions.ts +++ b/src/worldInteractions.ts @@ -6,6 +6,7 @@ import * as THREE from 'three' import { Vec3 } from 'vec3' import { LineMaterial } from 'three-stdlib' import { Entity } from 'prismarine-entity' +import { WorldRendererCommon } from 'prismarine-viewer/viewer/lib/worldrendererCommon' import destroyStage0 from '../assets/destroy_stage_0.png' import destroyStage1 from '../assets/destroy_stage_1.png' import destroyStage2 from '../assets/destroy_stage_2.png' @@ -372,7 +373,7 @@ class WorldInteraction { // Update state if (cursorChanged) { - viewer.world.setHighlightCursorBlock(cursorBlock?.position ?? null, allShapes.map(shape => { + (viewer.world as WorldRendererCommon).setHighlightCursorBlock(cursorBlock?.position ?? null, allShapes.map(shape => { return getDataFromShape(shape) })) } diff --git a/src/worldSaveWorker.ts b/src/worldSaveWorker.ts new file mode 100644 index 000000000..88a0ccafd --- /dev/null +++ b/src/worldSaveWorker.ts @@ -0,0 +1,76 @@ +import './workerWorkaround' +import fs from 'fs' +import './fs2' +import { Anvil } from 'prismarine-provider-anvil' +import WorldLoader from 'prismarine-world' + +import * as browserfs from 'browserfs' +import { generateSpiralMatrix } from 'flying-squid/dist/utils' +import '../dist/mc-data/1.14' +import { oneOf } from '@zardoy/utils' + +console.log('install') +browserfs.install(window) +window.fs = fs + +export interface ReadChunksRequest { + version: string, + +} + +onmessage = (msg) => { + globalThis.readSkylight = false + if (msg.data.type === 'readChunks') { + browserfs.configure({ + fs: 'MountableFileSystem', + options: { + '/data': { fs: 'IndexedDB' }, + }, + }, async () => { + const version = '1.14.4' + const AnvilLoader = Anvil(version) + const World = WorldLoader(version) as any + // const folder = '/data/worlds/Greenfield v0.5.3-3/region' + const { folder } = msg.data + const world = new World(() => { + throw new Error('Not implemented') + }, new AnvilLoader(folder)) + // const chunks = generateSpiralMatrix(20) + const { chunks } = msg.data + // const spawn = { + // x: 113, + // y: 64, + // } + console.log('starting...') + console.time('columns') + const loadedColumns = [] as any[] + const columnToTransfarable = (chunk) => { + return { + biomes: chunk.biomes, + // blockEntities: chunk.blockEntities, + // sectionMask: chunk.sectionMask, + sections: chunk.sections, + // skyLightMask: chunk.skyLightMask, + // blockLightMask: chunk.blockLightMask, + // skyLightSections: chunk.skyLightSections, + // blockLightSections: chunk.blockLightSections + } + } + + for (const chunk of chunks) { + const column = await world.getColumn(chunk[0], chunk[1]) + if (!column) throw new Error(`Column ${chunk[0]} ${chunk[1]} not found`) + postMessage({ + column: columnToTransfarable(column) + }) + } + postMessage({ + type: 'done', + }) + + console.timeEnd('columns') + }) + } +} + +// window.fs = fs diff --git a/tsconfig.json b/tsconfig.json index addc64f19..95f39d697 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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, diff --git a/vitest.config.ts b/vitest.config.ts index c27621d57..d68d160a6 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -8,7 +8,8 @@ export default defineConfig({ '../../src/markdownToFormattedText.test.ts', '../../src/react/parseKeybindingName.test.ts', 'lib/mesher/test/tests.test.ts', - 'sign-renderer/tests.test.ts' + 'sign-renderer/tests.test.ts', // prismarine-viewer/viewer/sign-renderer/tests.test.ts + '../examples/chunksStorage.test.ts' ], }, })