Compare commits
126 commits
next
...
webgpu-tre
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7be450d6d2 | ||
|
|
d46a475e24 | ||
|
|
eeca0e6360 | ||
|
|
c01f8185f6 | ||
|
|
aba18dc896 | ||
|
|
b34a637952 | ||
|
|
c23075b33e | ||
|
|
7d3c046eeb | ||
|
|
2dfd17939f | ||
|
|
e62b4d8cda | ||
|
|
de8e3dea69 | ||
|
|
8e7d7b9cb6 | ||
|
|
16d4f5db19 | ||
|
|
c9b1a9e8c3 | ||
|
|
92afdf54fb | ||
|
|
2ea730e9d1 | ||
|
|
ae4618b1d8 | ||
|
|
5c2be2e147 | ||
|
|
408df667bb | ||
|
|
02a84b83ab | ||
|
|
1c05804398 | ||
|
|
6d375d83e9 | ||
|
|
6f0e238409 | ||
|
|
560d5fb5e3 | ||
|
|
bbe2a6bef1 | ||
|
|
208b026530 | ||
|
|
c1880b137d | ||
|
|
c33ab7ad6c | ||
|
|
fb6b965f98 | ||
|
|
c8310945aa | ||
|
|
4abdead924 | ||
|
|
130daac35e | ||
|
|
137666c65c | ||
|
|
3c4996f1ef | ||
|
|
6c5fdbdc5e | ||
|
|
4e1257cb7c | ||
|
|
e9ca5cf3d2 | ||
|
|
662c823c23 | ||
|
|
a12ab1cd16 | ||
|
|
830424eec2 | ||
|
|
4f9efb4c01 | ||
|
|
a3c414e09e | ||
|
|
a313c5ffc1 | ||
|
|
bb8a8413ae | ||
|
|
890afee8e7 | ||
|
|
40cda86f01 | ||
|
|
0cebd53e3d | ||
|
|
324f07014b | ||
|
|
33d563cf94 | ||
|
|
50efd76595 | ||
|
|
a5f0af8e6b | ||
|
|
f970397886 | ||
|
|
ff5ca18a22 | ||
|
|
e3205ed9c6 | ||
|
|
2bc843959e | ||
|
|
ed74485b01 | ||
|
|
469ef4982e | ||
|
|
de227f08e4 | ||
|
|
0edf51aef3 | ||
|
|
28d55f9cfc | ||
|
|
c5c8f27e18 | ||
|
|
484844b35d | ||
|
|
5476b391ff | ||
|
|
dc9f15e903 | ||
|
|
029a7175ab | ||
|
|
48d3bbeef5 | ||
|
|
889987d908 | ||
|
|
c4d65ee9fd | ||
|
|
db722dba34 | ||
|
|
9f89af613f | ||
|
|
31b419c6ee | ||
|
|
7777307c4f | ||
|
|
183f8e461e | ||
|
|
17cd4bff77 | ||
|
|
9fe0e340f8 | ||
|
|
4c58f4538e | ||
|
|
12dfa9f9a8 | ||
|
|
8b6e1d210b | ||
|
|
97a9e92119 | ||
|
|
16fdd37776 | ||
|
|
349edd17ed | ||
|
|
984a2081a7 | ||
|
|
d0501d331d | ||
|
|
5951e14b68 | ||
|
|
86f1b5e569 | ||
|
|
a75aeb0c92 | ||
|
|
e9ae58612c | ||
|
|
61a8a97dbb | ||
|
|
11c04fb3db | ||
|
|
7b61ff68e8 | ||
|
|
dc625b58d7 | ||
|
|
6275d787a2 | ||
|
|
5b78000697 | ||
|
|
61d0ff46b3 | ||
|
|
f168bdbfa4 | ||
|
|
a7313b3a70 | ||
|
|
506706e88b | ||
|
|
7d5f1c504f | ||
|
|
a19c9ad784 | ||
|
|
f3295593bf | ||
|
|
d0a6941031 | ||
|
|
00b9683e78 | ||
|
|
ce194599d7 | ||
|
|
a3290063e8 | ||
|
|
51ba0eaec5 | ||
|
|
b35bc0882f | ||
|
|
4133eca3a7 | ||
|
|
c8e6475954 | ||
|
|
9e26f70011 | ||
|
|
c6c4ca841b | ||
|
|
b403ed4606 | ||
|
|
a682d4b3da | ||
|
|
70c9686f89 | ||
|
|
ad5a906a95 | ||
|
|
a1db77a21f | ||
|
|
027f5c6917 | ||
|
|
2ee0ef3483 | ||
|
|
73a5e9d8ba | ||
|
|
024891c6d5 | ||
|
|
dee63df232 | ||
|
|
253fb8a2b5 | ||
|
|
6e84db0a7a | ||
|
|
b28f69c126 | ||
|
|
06b2f3bfa8 | ||
|
|
1175d3fae9 | ||
|
|
163290d841 |
42 changed files with 3204 additions and 399 deletions
14
.vscode/tasks.json
vendored
14
.vscode/tasks.json
vendored
|
|
@ -25,10 +25,20 @@
|
|||
},
|
||||
},
|
||||
{
|
||||
"label": "viewer server+esbuild",
|
||||
"label": "webgl-worker",
|
||||
"type": "shell",
|
||||
"command": "node buildWorkers.mjs -w",
|
||||
"problemMatcher": "$esbuild-watch",
|
||||
"presentation": {
|
||||
"reveal": "silent"
|
||||
},
|
||||
},
|
||||
{
|
||||
"label": "viewer server+esbuild+workers",
|
||||
"dependsOn": [
|
||||
"viewer-server",
|
||||
"viewer-esbuild"
|
||||
"viewer-esbuild",
|
||||
"webgl-worker"
|
||||
],
|
||||
"dependsOrder": "parallel",
|
||||
}
|
||||
|
|
|
|||
59
buildWorkers.mjs
Normal file
59
buildWorkers.mjs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
// main worker file intended for computing world geometry is built using prismarine-viewer/buildWorker.mjs
|
||||
import { build, context } from 'esbuild'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
const watch = process.argv.includes('-w')
|
||||
|
||||
const result = await (watch ? context : build)({
|
||||
bundle: true,
|
||||
platform: 'browser',
|
||||
entryPoints: ['prismarine-viewer/examples/webgpuRendererWorker.ts'],
|
||||
outdir: 'prismarine-viewer/public/',
|
||||
sourcemap: watch ? 'inline' : 'external',
|
||||
minify: !watch,
|
||||
treeShaking: true,
|
||||
logLevel: 'info',
|
||||
alias: {
|
||||
'three': './node_modules/three/src/Three.js',
|
||||
events: 'events', // make explicit
|
||||
buffer: 'buffer',
|
||||
'fs': 'browserfs/dist/shims/fs.js',
|
||||
http: 'http-browserify',
|
||||
perf_hooks: './src/perf_hooks_replacement.js',
|
||||
crypto: './src/crypto.js',
|
||||
stream: 'stream-browserify',
|
||||
net: 'net-browserify',
|
||||
assert: 'assert',
|
||||
dns: './src/dns.js'
|
||||
},
|
||||
plugins: [
|
||||
{
|
||||
name: 'writeOutput',
|
||||
setup (build) {
|
||||
build.onEnd(({ outputFiles }) => {
|
||||
for (const file of outputFiles) {
|
||||
for (const dir of ['prismarine-viewer/public', 'dist']) {
|
||||
const baseName = path.basename(file.path)
|
||||
fs.writeFileSync(path.join(dir, baseName), file.contents)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
],
|
||||
loader: {
|
||||
'.vert': 'text',
|
||||
'.frag': 'text',
|
||||
'.wgsl': 'text',
|
||||
},
|
||||
mainFields: [
|
||||
'browser', 'module', 'main'
|
||||
],
|
||||
keepNames: true,
|
||||
write: false,
|
||||
})
|
||||
|
||||
if (watch) {
|
||||
await result.watch()
|
||||
}
|
||||
25
cypress/e2e/performance.spec.ts
Normal file
25
cypress/e2e/performance.spec.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { cleanVisit, setOptions } from './shared'
|
||||
|
||||
it('Loads & renders singleplayer', () => {
|
||||
cleanVisit('/?singleplayer=1')
|
||||
setOptions({
|
||||
renderDistance: 2
|
||||
})
|
||||
// wait for .initial-loader to disappear
|
||||
cy.get('.initial-loader', { timeout: 20_000 }).should('not.exist')
|
||||
cy.window()
|
||||
.its('performance')
|
||||
.invoke('mark', 'worldLoad')
|
||||
|
||||
cy.document().then({ timeout: 20_000 }, doc => {
|
||||
return new Cypress.Promise(resolve => {
|
||||
doc.addEventListener('cypress-world-ready', resolve)
|
||||
})
|
||||
}).then(() => {
|
||||
const duration = cy.window()
|
||||
.its('performance')
|
||||
.invoke('measure', 'modalOpen')
|
||||
.its('duration')
|
||||
cy.log('Duration', duration)
|
||||
})
|
||||
})
|
||||
12
package.json
12
package.json
|
|
@ -6,11 +6,11 @@
|
|||
"dev-rsbuild": "rsbuild dev",
|
||||
"dev-proxy": "node server.js",
|
||||
"start": "run-p dev-rsbuild dev-proxy",
|
||||
"start-watch-script": "nodemon -w rsbuild.config.ts --watch",
|
||||
"build": "rsbuild build",
|
||||
"build-analyze": "BUNDLE_ANALYZE=true rsbuild build",
|
||||
"build": "node buildWorkers.mjs && rsbuild build",
|
||||
"build-analyze": "BUNDLE_ANALYZE=true rsbuild build && node buildWorkers.mjs",
|
||||
"check-build": "tsx scripts/genShims.ts && tsc && pnpm build",
|
||||
"test:cypress": "cypress run",
|
||||
"test:cypress:perf": "cypress run --spec cypress/e2e/perf.spec.ts --browser edge",
|
||||
"test-unit": "vitest",
|
||||
"test:e2e": "start-test http-get://localhost:8080 test:cypress",
|
||||
"prod-start": "node server.js --prod",
|
||||
|
|
@ -20,12 +20,13 @@
|
|||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build && node scripts/build.js moveStorybookFiles",
|
||||
"start-experiments": "vite --config experiments/vite.config.ts --host",
|
||||
"watch-other-workers": "echo NOT IMPLEMENTED",
|
||||
"watch-other-workers": "node buildWorkers.mjs -w",
|
||||
"build-mesher": "node prismarine-viewer/buildMesherWorker.mjs",
|
||||
"watch-mesher": "pnpm build-mesher -w",
|
||||
"run-playground": "run-p watch-mesher watch-other-workers playground-server watch-playground",
|
||||
"run-all": "run-p start run-playground",
|
||||
"playground-server": "live-server --port=9090 prismarine-viewer/public",
|
||||
"start-watch-script": "nodemon -w rsbuild.config.ts --watch",
|
||||
"watch-playground": "node prismarine-viewer/esbuild.mjs -w"
|
||||
},
|
||||
"keywords": [
|
||||
|
|
@ -51,6 +52,7 @@
|
|||
"@types/react": "^18.2.20",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@types/wicg-file-system-access": "^2023.10.2",
|
||||
"@webgpu/types": "^0.1.44",
|
||||
"@xmcl/text-component": "^2.1.3",
|
||||
"@zardoy/react-util": "^0.2.4",
|
||||
"@zardoy/utils": "^0.0.11",
|
||||
|
|
@ -72,6 +74,7 @@
|
|||
"google-drive-browserfs": "github:zardoy/browserfs#google-drive",
|
||||
"jszip": "^3.10.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"math.gl": "^4.0.0",
|
||||
"minecraft-data": "3.65.0",
|
||||
"minecraft-protocol": "github:PrismarineJS/node-minecraft-protocol#master",
|
||||
"mineflayer-item-map-downloader": "github:zardoy/mineflayer-item-map-downloader",
|
||||
|
|
@ -99,6 +102,7 @@
|
|||
"stats.js": "^0.17.0",
|
||||
"tabbable": "^6.2.0",
|
||||
"title-case": "3.x",
|
||||
"twgl.js": "^5.5.4",
|
||||
"ua-parser-js": "^1.0.37",
|
||||
"use-typed-event-listener": "^4.0.2",
|
||||
"valtio": "^1.11.1",
|
||||
|
|
|
|||
1384
pnpm-lock.yaml
generated
1384
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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')) {
|
||||
|
|
|
|||
|
|
@ -46,18 +46,20 @@ const buildOptions = {
|
|||
http: 'http-browserify',
|
||||
stream: 'stream-browserify',
|
||||
net: 'net-browserify',
|
||||
// 'mc-assets': '/Users/vitaly/Documents/mc-assets',
|
||||
'stats.js': 'stats.js/src/Stats.js',
|
||||
},
|
||||
inject: [],
|
||||
metafile: true,
|
||||
loader: {
|
||||
'.png': 'dataurl',
|
||||
'.vert': 'text',
|
||||
'.frag': 'text',
|
||||
'.obj': 'text',
|
||||
},
|
||||
plugins: [
|
||||
{
|
||||
name: 'minecraft-data',
|
||||
setup(build) {
|
||||
setup (build) {
|
||||
build.onLoad({
|
||||
filter: /minecraft-data[\/\\]data.js$/,
|
||||
}, () => {
|
||||
|
|
|
|||
45
prismarine-viewer/examples/Cube.comp.wgsl
Normal file
45
prismarine-viewer/examples/Cube.comp.wgsl
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
struct Cube {
|
||||
position: vec3f,
|
||||
textureIndex: f32,
|
||||
colorBlend: vec3f,
|
||||
//tt: f32
|
||||
}
|
||||
|
||||
struct Uniforms {
|
||||
ViewProjectionMatrix: mat4x4<f32>,
|
||||
}
|
||||
|
||||
struct IndirectDrawParams {
|
||||
vertexCount: u32,
|
||||
instanceCount: atomic<u32>,
|
||||
firstVertex: u32,
|
||||
firstInstance: u32,
|
||||
}
|
||||
|
||||
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
|
||||
@group(0) @binding(1) var<storage, read> cubes: array<Cube>;
|
||||
@group(0) @binding(2) var<storage, read_write> visibleCubes: array<Cube>; // Changed to @binding(4)
|
||||
@group(0) @binding(3) var<storage, read_write> drawParams: IndirectDrawParams;
|
||||
|
||||
@compute @workgroup_size(256)
|
||||
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
|
||||
let index = global_id.x;
|
||||
if (index >= arrayLength(&cubes)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let cube = cubes[index];
|
||||
|
||||
// Transform cube position to clip space
|
||||
let clipPos = uniforms.ViewProjectionMatrix * (vec4<f32>(cube.position, 1.0) + vec4<f32>(0.5, 0.0, 0.5, 0.0));
|
||||
let clipDepth = clipPos.z / clipPos.w; // Obtain depth in clip space
|
||||
let clipX = clipPos.x / clipPos.w;
|
||||
let clipY = clipPos.y / clipPos.w;
|
||||
|
||||
// Check if cube is within the view frustum z-range (depth within near and far planes)
|
||||
let Oversize = 1.2;
|
||||
if (clipDepth >= 0.0 && clipDepth <= 1.0 && clipX > -1.0 * Oversize && clipX < 1.0 * Oversize && clipY > -1.0 * Oversize && clipY < 1.0 * Oversize) { //Small Oversize because binding size
|
||||
let visibleIndex = atomicAdd(&drawParams.instanceCount, 1);
|
||||
visibleCubes[visibleIndex] = cube;
|
||||
}
|
||||
}
|
||||
14
prismarine-viewer/examples/Cube.frag.wgsl
Normal file
14
prismarine-viewer/examples/Cube.frag.wgsl
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
@group(0) @binding(1) var mySampler: sampler;
|
||||
@group(0) @binding(2) var myTexture: texture_2d<f32>;
|
||||
|
||||
@fragment
|
||||
fn main(
|
||||
@location(0) fragUV: vec2f,
|
||||
@location(1) @interpolate(flat) TextureIndex: f32,
|
||||
@location(2) @interpolate(flat) ColorBlend: vec3f
|
||||
) -> @location(0) vec4f {
|
||||
let textureSize: vec2<f32> = vec2<f32>(textureDimensions(myTexture));
|
||||
let tileSize: vec2<f32> = vec2<f32>(16.0f,16.0f);
|
||||
let tilesPerTexture: vec2<f32> = vec2<f32>(textureSize)/tileSize;
|
||||
return textureSample(myTexture, mySampler, fragUV/tilesPerTexture + vec2f(trunc(TextureIndex%tilesPerTexture.y),trunc(TextureIndex/tilesPerTexture.x) )/tilesPerTexture) * vec4f(ColorBlend,1.0);
|
||||
}
|
||||
36
prismarine-viewer/examples/Cube.vert.wgsl
Normal file
36
prismarine-viewer/examples/Cube.vert.wgsl
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
struct Uniforms {
|
||||
ViewProjectionMatrix: mat4x4<f32>,
|
||||
}
|
||||
|
||||
struct Cube {
|
||||
position: vec3f,
|
||||
textureIndex: f32,
|
||||
colorBlend: vec3f,
|
||||
//tt: f32
|
||||
}
|
||||
|
||||
struct VertexOutput {
|
||||
@builtin(position) Position: vec4f,
|
||||
@location(0) fragUV: vec2f,
|
||||
@location(1) @interpolate(flat) TextureIndex: f32,
|
||||
@location(2) @interpolate(flat) ColorBlend: vec3f
|
||||
}
|
||||
|
||||
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
|
||||
@group(0) @binding(3) var<storage, read> visibleCubes: array<Cube>;
|
||||
|
||||
@vertex
|
||||
fn main(
|
||||
@builtin(instance_index) instanceIndex: u32,
|
||||
@location(0) position: vec4<f32>,
|
||||
@location(1) uv: vec2<f32>
|
||||
) -> VertexOutput {
|
||||
let cube = visibleCubes[instanceIndex];
|
||||
//cube.position.x = instance_index * 2;
|
||||
var output: VertexOutput;
|
||||
output.Position = uniforms.ViewProjectionMatrix * (position + vec4<f32>(cube.position, 0.0) + vec4<f32>(0.5, 0.0, 0.5, 0.0));
|
||||
output.fragUV = uv;
|
||||
output.TextureIndex = cube.textureIndex;
|
||||
output.ColorBlend = cube.colorBlend;
|
||||
return output;
|
||||
}
|
||||
95
prismarine-viewer/examples/CubeDef.ts
Normal file
95
prismarine-viewer/examples/CubeDef.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
export const cubeVertexSize = 4 * 5 // Byte size of one cube vertex.
|
||||
export const cubePositionOffset = 0
|
||||
//export const cubeColorOffset = 4 * 3 // Byte offset of cube vertex color attribute.
|
||||
export const cubeUVOffset = 4 * 3
|
||||
export const cubeVertexCount = 36
|
||||
|
||||
//@ts-format-ignore-region
|
||||
export const cubeVertexArray = new Float32Array([
|
||||
-0.5, -0.5, -0.5, 0.0, 0.0, // Bottom-let
|
||||
0.5, -0.5, -0.5, 1.0, 0.0, // bottom-right
|
||||
0.5, 0.5, -0.5, 1.0, 1.0, // top-right
|
||||
0.5, 0.5, -0.5, 1.0, 1.0, // top-right
|
||||
-0.5, 0.5, -0.5, 0.0, 1.0, // top-let
|
||||
-0.5, -0.5, -0.5, 0.0, 0.0, // bottom-let
|
||||
// ront ace
|
||||
-0.5, -0.5, 0.5, 0.0, 0.0, // bottom-let
|
||||
0.5, 0.5, 0.5, 1.0, 1.0, // top-right
|
||||
0.5, -0.5, 0.5, 1.0, 0.0, // bottom-right
|
||||
0.5, 0.5, 0.5, 1.0, 1.0, // top-right
|
||||
-0.5, -0.5, 0.5, 0.0, 0.0, // bottom-let
|
||||
-0.5, 0.5, 0.5, 0.0, 1.0, // top-let
|
||||
// Let ace
|
||||
-0.5, 0.5, 0.5, 1.0, 0.0, // top-right
|
||||
-0.5, -0.5, -0.5, 0.0, 1.0, // bottom-let
|
||||
-0.5, 0.5, -0.5, 1.0, 1.0, // top-let
|
||||
-0.5, -0.5, -0.5, 0.0, 1.0, // bottom-let
|
||||
-0.5, 0.5, 0.5, 1.0, 0.0, // top-right
|
||||
-0.5, -0.5, 0.5, 0.0, 0.0, // bottom-right
|
||||
// Right ace
|
||||
0.5, 0.5, 0.5, 1.0, 0.0, // top-let
|
||||
0.5, 0.5, -0.5, 1.0, 1.0, // top-right
|
||||
0.5, -0.5, -0.5, 0.0, 1.0, // bottom-right
|
||||
0.5, -0.5, -0.5, 0.0, 1.0, // bottom-right
|
||||
0.5, -0.5, 0.5, 0.0, 0.0, // bottom-let
|
||||
0.5, 0.5, 0.5, 1.0, 0.0, // top-let
|
||||
// Bottom ace
|
||||
-0.5, -0.5, -0.5, 0.0, 1.0, // top-right
|
||||
0.5, -0.5, 0.5, 1.0, 0.0, // bottom-let
|
||||
0.5, -0.5, -0.5, 1.0, 1.0, // top-let
|
||||
0.5, -0.5, 0.5, 1.0, 0.0, // bottom-let
|
||||
-0.5, -0.5, -0.5, 0.0, 1.0, // top-right
|
||||
-0.5, -0.5, 0.5, 0.0, 0.0, // bottom-right
|
||||
// Top ace
|
||||
-0.5, 0.5, -0.5, 0.0, 1.0, // top-let
|
||||
0.5, 0.5, -0.5, 1.0, 1.0, // top-right
|
||||
0.5, 0.5, 0.5, 1.0, 0.0, // bottom-right
|
||||
0.5, 0.5, 0.5, 1.0, 0.0, // bottom-right
|
||||
-0.5, 0.5, 0.5, 0.0, 0.0, // bottom-let
|
||||
-0.5, 0.5, -0.5, 0.0, 1.0// top-let˚
|
||||
]);
|
||||
|
||||
|
||||
export const cubeVertexArraySub = new Float32Array([
|
||||
-0.5, -0.5, -0.5, 0.0, 0.0, // Bottom-let
|
||||
0.5, -0.5, -0.5, 1.0, 0.0, // bottom-right
|
||||
0.5, 0.5, -0.5, 1.0, 1.0, // top-right
|
||||
0.5, 0.5, -0.5, 1.0, 1.0, // top-right
|
||||
-0.5, 0.5, -0.5, 0.0, 1.0, // top-let
|
||||
-0.5, -0.5, -0.5, 0.0, 0.0, // bottom-let
|
||||
// ront ace
|
||||
-0.5, -0.5, 0.5, 0.0, 0.0, // bottom-let
|
||||
0.5, 0.5, 0.5, 1.0, 1.0, // top-right
|
||||
0.5, -0.5, 0.5, 1.0, 0.0, // bottom-right
|
||||
0.5, 0.5, 0.5, 1.0, 1.0, // top-right
|
||||
-0.5, -0.5, 0.5, 0.0, 0.0, // bottom-let
|
||||
-0.5, 0.5, 0.5, 0.0, 1.0, // top-let
|
||||
// Let ace
|
||||
-0.5, 0.5, 0.5, 1.0, 0.0, // top-right
|
||||
-0.5, -0.5, -0.5, 0.0, 1.0, // bottom-let
|
||||
-0.5, 0.5, -0.5, 1.0, 1.0, // top-let
|
||||
-0.5, -0.5, -0.5, 0.0, 1.0, // bottom-let
|
||||
-0.5, 0.5, 0.5, 1.0, 0.0, // top-right
|
||||
-0.5, -0.5, 0.5, 0.0, 0.0, // bottom-right
|
||||
// Right ace
|
||||
0.5, 0.5, 0.5, 1.0, 0.0, // top-let
|
||||
0.5, 0.5, -0.5, 1.0, 1.0, // top-right
|
||||
0.5, -0.5, -0.5, 0.0, 1.0, // bottom-right
|
||||
0.5, -0.5, -0.5, 0.0, 1.0, // bottom-right
|
||||
0.5, -0.5, 0.5, 0.0, 0.0, // bottom-let
|
||||
0.5, 0.5, 0.5, 1.0, 0.0, // top-let
|
||||
// Bottom ace
|
||||
-0.5, -0.5, -0.5, 0.0, 1.0, // top-right
|
||||
0.5, -0.5, 0.5, 1.0, 0.0, // bottom-let
|
||||
0.5, -0.5, -0.5, 1.0, 1.0, // top-let
|
||||
0.5, -0.5, 0.5, 1.0, 0.0, // bottom-let
|
||||
-0.5, -0.5, -0.5, 0.0, 1.0, // top-right
|
||||
-0.5, -0.5, 0.5, 0.0, 0.0, // bottom-right
|
||||
// Top ace
|
||||
-0.5, 0.5, -0.5, 0.0, 1.0, // top-let
|
||||
0.5, 0.5, -0.5, 1.0, 1.0, // top-right
|
||||
0.5, 0.5, 0.5, 1.0, 0.0, // bottom-right
|
||||
0.5, 0.5, 0.5, 1.0, 0.0, // bottom-right
|
||||
-0.5, 0.5, 0.5, 0.0, 0.0, // bottom-let
|
||||
-0.5, 0.5, -0.5, 0.0, 1.0, 5.0// top-let˚
|
||||
]);
|
||||
69
prismarine-viewer/examples/TextureAnimation.ts
Normal file
69
prismarine-viewer/examples/TextureAnimation.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
export type AnimationControlSwitches = {
|
||||
tick: number
|
||||
interpolationTick: number // next one
|
||||
}
|
||||
|
||||
type Data = {
|
||||
interpolate: boolean;
|
||||
frametime: number;
|
||||
frames: ({
|
||||
index: number;
|
||||
time: number;
|
||||
} | number)[] | undefined;
|
||||
};
|
||||
|
||||
export class TextureAnimation {
|
||||
data: Data;
|
||||
frameImages: number;
|
||||
frameDelta: number;
|
||||
frameTime: number;
|
||||
framesToSwitch: number;
|
||||
frameIndex: number;
|
||||
|
||||
constructor(public animationControl: AnimationControlSwitches, data: Data, public framesImages: number) {
|
||||
this.data = {
|
||||
interpolate: false,
|
||||
frametime: 1,
|
||||
...data
|
||||
};
|
||||
this.frameImages = 1;
|
||||
this.frameDelta = 0;
|
||||
this.frameTime = this.data.frametime * 50;
|
||||
this.frameIndex = 0;
|
||||
|
||||
this.framesToSwitch = this.frameImages;
|
||||
if (this.data.frames) {
|
||||
this.framesToSwitch = this.data.frames.length;
|
||||
}
|
||||
}
|
||||
|
||||
step (deltaMs: number) {
|
||||
this.frameDelta += deltaMs;
|
||||
|
||||
if (this.frameDelta > this.frameTime) {
|
||||
this.frameDelta -= this.frameTime;
|
||||
this.frameDelta %= this.frameTime;
|
||||
|
||||
this.frameIndex++;
|
||||
this.frameIndex %= this.framesToSwitch;
|
||||
|
||||
const frames = this.data.frames.map(frame => typeof frame === 'number' ? { index: frame, time: this.data.frametime } : frame);
|
||||
if (frames) {
|
||||
let frame = frames[this.frameIndex]
|
||||
let nextFrame = frames[(this.frameIndex + 1) % this.framesToSwitch];
|
||||
|
||||
this.animationControl.tick = frame.index;
|
||||
this.animationControl.interpolationTick = nextFrame.index;
|
||||
this.frameTime = frame.time * 50;
|
||||
} else {
|
||||
this.animationControl.tick = this.frameIndex;
|
||||
this.animationControl.interpolationTick = (this.frameIndex + 1) % this.framesToSwitch;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.data.interpolate) {
|
||||
this.animationControl.interpolationTick = this.frameDelta / this.frameTime;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
63
prismarine-viewer/examples/TouchControls2.tsx
Normal file
63
prismarine-viewer/examples/TouchControls2.tsx
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import React, { useEffect } from 'react'
|
||||
import { LeftTouchArea, RightTouchArea, useInterfaceState } from '@dimaka/interface'
|
||||
import { css } from '@emotion/css'
|
||||
import { Viewer } from '../viewer/lib/viewer'
|
||||
import { renderToDom } from '@zardoy/react-util'
|
||||
import { Vec3 } from 'vec3'
|
||||
import * as THREE from 'three'
|
||||
|
||||
declare const viewer: Viewer
|
||||
const Controls = () => {
|
||||
// todo setting
|
||||
const usingTouch = navigator.maxTouchPoints > 0
|
||||
|
||||
useEffect(() => {
|
||||
let vec3 = new Vec3(0, 0, 0)
|
||||
|
||||
setInterval(() => {
|
||||
viewer.camera.position.add(new THREE.Vector3(vec3.x, vec3.y, vec3.z))
|
||||
}, 1000 / 30)
|
||||
|
||||
useInterfaceState.setState({
|
||||
isFlying: false,
|
||||
uiCustomization: {
|
||||
touchButtonSize: 40,
|
||||
},
|
||||
updateCoord ([coord, state]) {
|
||||
vec3 = new Vec3(0, 0, 0)
|
||||
vec3[coord] = state
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
if (!usingTouch) return null
|
||||
return (
|
||||
<div
|
||||
style={{ zIndex: 8 }}
|
||||
className={css`
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
pointer-events: none;
|
||||
touch-action: none;
|
||||
& > div {
|
||||
pointer-events: auto;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<LeftTouchArea />
|
||||
<div />
|
||||
<RightTouchArea />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const renderPlayground = () => {
|
||||
renderToDom(<Controls />, {
|
||||
// selector: 'body',
|
||||
})
|
||||
}
|
||||
35
prismarine-viewer/examples/newStats.ts
Normal file
35
prismarine-viewer/examples/newStats.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
let rightOffset = 0
|
||||
|
||||
const stats = {}
|
||||
|
||||
export const addNewStat = (id: string, width = 80, x = rightOffset, y = 0) => {
|
||||
const pane = document.createElement('div')
|
||||
pane.id = 'fps-counter'
|
||||
pane.style.position = 'fixed'
|
||||
pane.style.top = `${y}px`
|
||||
pane.style.right = `${x}px`
|
||||
// gray bg
|
||||
pane.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'
|
||||
pane.style.color = 'white'
|
||||
pane.style.padding = '2px'
|
||||
pane.style.fontFamily = 'monospace'
|
||||
pane.style.fontSize = '12px'
|
||||
pane.style.zIndex = '10000'
|
||||
pane.style.pointerEvents = 'none'
|
||||
document.body.appendChild(pane)
|
||||
stats[id] = pane
|
||||
if (y === 0) { // otherwise it's a custom position
|
||||
rightOffset += width
|
||||
}
|
||||
|
||||
return {
|
||||
updateText: (text: string) => {
|
||||
pane.innerText = text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const updateStatText = (id, text) => {
|
||||
if (!stats[id]) return
|
||||
stats[id].innerText = text
|
||||
}
|
||||
|
|
@ -11,13 +11,22 @@ import JSZip from 'jszip'
|
|||
import { TWEEN_DURATION } from '../viewer/lib/entities'
|
||||
import { EntityMesh } from '../viewer/lib/entity/EntityMesh'
|
||||
import blockstatesModels from 'mc-assets/dist/blockStatesModels.json'
|
||||
// import * as Mathgl from 'math.gl'
|
||||
import { initWebgpuRenderer, loadFixtureSides, setAnimationTick, webgpuChannel } from './webgpuRendererMain'
|
||||
import { renderToDom } from '@zardoy/react-util'
|
||||
|
||||
globalThis.THREE = THREE
|
||||
//@ts-ignore
|
||||
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
|
||||
import { toMajorVersion } from '../../src/utils'
|
||||
import { renderPlayground } from './TouchControls2'
|
||||
import { WorldRendererWebgpu } from '../viewer/lib/worldrendererWebgpu'
|
||||
import { TextureAnimation } from './TextureAnimation'
|
||||
import { BlockType } from './shared'
|
||||
import { addNewStat } from './newStats'
|
||||
|
||||
const gui = new GUI()
|
||||
const { updateText: updateTextEvent } = addNewStat('events', 90, 0, 40)
|
||||
|
||||
// initial values
|
||||
const params = {
|
||||
|
|
@ -40,7 +49,8 @@ const params = {
|
|||
camera: '',
|
||||
playSound () { },
|
||||
blockIsomorphicRenderBundle () { },
|
||||
modelVariant: 0
|
||||
modelVariant: 0,
|
||||
animationTick: 0
|
||||
}
|
||||
|
||||
const qs = new URLSearchParams(window.location.search)
|
||||
|
|
@ -60,10 +70,20 @@ const setQs = () => {
|
|||
|
||||
let ignoreResize = false
|
||||
|
||||
const enableControls = new URLSearchParams(window.location.search).get('controls') === 'true'
|
||||
|
||||
async function main () {
|
||||
let continuousRender = false
|
||||
|
||||
const { version } = params
|
||||
// const { version } = params
|
||||
let fixtureUrl = qs.get('fixture')
|
||||
let fixture: undefined | Record<string, any>
|
||||
if (fixtureUrl) {
|
||||
console.log('Loading fixture')
|
||||
fixture = await fetch(fixtureUrl).then(r => r.json())
|
||||
console.log('Loaded fixture')
|
||||
}
|
||||
const version = fixture?.version ?? '1.20.2'
|
||||
// temporary solution until web worker is here, cache data for faster reloads
|
||||
const globalMcData = window['mcData']
|
||||
if (!globalMcData['version']) {
|
||||
|
|
@ -94,6 +114,7 @@ async function main () {
|
|||
gui.add(params, 'skipQs')
|
||||
gui.add(params, 'playSound')
|
||||
gui.add(params, 'blockIsomorphicRenderBundle')
|
||||
const animationController = gui.add(params, 'animationTick', -1, 20, 1).listen()
|
||||
gui.open(false)
|
||||
let metadataFolder = gui.addFolder('metadata')
|
||||
// let entityRotationFolder = gui.addFolder('entity metadata')
|
||||
|
|
@ -125,25 +146,133 @@ async function main () {
|
|||
return chunk
|
||||
})
|
||||
|
||||
let stopUpdate = false
|
||||
// let stopUpdate = true
|
||||
|
||||
// await schem.paste(world, new Vec3(0, 60, 0))
|
||||
|
||||
const worldView = new WorldDataEmitter(world, viewDistance, targetPos)
|
||||
|
||||
// Create three.js context, add to page
|
||||
const renderer = new THREE.WebGLRenderer({ alpha: true, ...localStorage['renderer'] })
|
||||
renderer.setPixelRatio(window.devicePixelRatio || 1)
|
||||
renderer.setSize(window.innerWidth, window.innerHeight)
|
||||
document.body.appendChild(renderer.domElement)
|
||||
|
||||
// Create viewer
|
||||
const viewer = new Viewer(renderer, { numWorkers: 1, showChunkBorders: false, })
|
||||
const nullRenderer = new THREE.WebGLRenderer({ antialias: true })
|
||||
const viewer = new Viewer(nullRenderer, { numWorkers: 1, showChunkBorders: false })
|
||||
viewer.world.blockstatesModels = blockstatesModels
|
||||
viewer.entities.setDebugMode('basic')
|
||||
viewer.world.stopBlockUpdate = stopUpdate
|
||||
viewer.setVersion(version)
|
||||
viewer.entities.onSkinUpdate = () => {
|
||||
viewer.render()
|
||||
globalThis.viewer = viewer
|
||||
|
||||
await initWebgpuRenderer(() => { }, !enableControls && !fixture, true)
|
||||
const simpleControls = () => {
|
||||
let pressedKeys = new Set<string>()
|
||||
const loop = () => {
|
||||
// Create a vector that points in the direction the camera is looking
|
||||
let direction = new THREE.Vector3(0, 0, 0)
|
||||
if (pressedKeys.has('KeyW')) {
|
||||
direction.z = -0.5
|
||||
}
|
||||
if (pressedKeys.has('KeyS')) {
|
||||
direction.z += 0.5
|
||||
}
|
||||
if (pressedKeys.has('KeyA')) {
|
||||
direction.x -= 0.5
|
||||
}
|
||||
if (pressedKeys.has('KeyD')) {
|
||||
direction.x += 0.5
|
||||
}
|
||||
|
||||
|
||||
if (pressedKeys.has('ShiftLeft')) {
|
||||
viewer.camera.position.y -= 0.5
|
||||
}
|
||||
if (pressedKeys.has('Space')) {
|
||||
viewer.camera.position.y += 0.5
|
||||
}
|
||||
direction.applyQuaternion(viewer.camera.quaternion)
|
||||
direction.y = 0
|
||||
|
||||
if (pressedKeys.has('ShiftLeft')) {
|
||||
direction.y *= 2
|
||||
direction.x *= 2
|
||||
direction.z *= 2
|
||||
}
|
||||
// Add the vector to the camera's position to move the camera
|
||||
viewer.camera.position.add(direction)
|
||||
}
|
||||
setInterval(loop, 1000 / 30)
|
||||
const keys = (e) => {
|
||||
const code = e.code
|
||||
const pressed = e.type === 'keydown'
|
||||
if (pressed) {
|
||||
pressedKeys.add(code)
|
||||
} else {
|
||||
pressedKeys.delete(code)
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', keys)
|
||||
window.addEventListener('keyup', keys)
|
||||
window.addEventListener('blur', (e) => {
|
||||
for (const key of pressedKeys) {
|
||||
keys(new KeyboardEvent('keyup', { code: key }))
|
||||
}
|
||||
})
|
||||
|
||||
// mouse
|
||||
const mouse = { x: 0, y: 0 }
|
||||
let mouseMoveCounter = 0
|
||||
const mouseMove = (e: PointerEvent) => {
|
||||
if ((e.target as HTMLElement).closest('.lil-gui')) return
|
||||
if (e.buttons === 1 || e.pointerType === 'touch') {
|
||||
mouseMoveCounter++
|
||||
viewer.camera.rotation.x -= e.movementY / 100
|
||||
//viewer.camera.
|
||||
viewer.camera.rotation.y -= e.movementX / 100
|
||||
if (viewer.camera.rotation.x < -Math.PI / 2) viewer.camera.rotation.x = -Math.PI / 2
|
||||
if (viewer.camera.rotation.x > Math.PI / 2) viewer.camera.rotation.x = Math.PI / 2
|
||||
|
||||
// yaw += e.movementY / 20;
|
||||
// pitch += e.movementX / 20;
|
||||
}
|
||||
if (e.buttons === 2) {
|
||||
viewer.camera.position.set(0, 0, 0)
|
||||
}
|
||||
}
|
||||
setInterval(() => {
|
||||
updateTextEvent(`Mouse Events: ${mouseMoveCounter}`)
|
||||
mouseMoveCounter = 0
|
||||
}, 1000)
|
||||
window.addEventListener('pointermove', mouseMove)
|
||||
}
|
||||
viewer.world.mesherConfig.enableLighting = false
|
||||
viewer.camera.position.set(0, 0, 8)
|
||||
simpleControls()
|
||||
renderPlayground()
|
||||
|
||||
const writeToIndexedDb = async (name, data) => {
|
||||
const db = await window.indexedDB.open(name, 1)
|
||||
db.onupgradeneeded = (e) => {
|
||||
const db = (e.target as any).result
|
||||
db.createObjectStore(name)
|
||||
}
|
||||
db.onsuccess = (e) => {
|
||||
const db = (e.target as any).result
|
||||
const tx = db.transaction(name, 'readwrite')
|
||||
const store = tx.objectStore(name)
|
||||
store.add(data, name)
|
||||
}
|
||||
}
|
||||
|
||||
if (fixture) {
|
||||
loadFixtureSides(fixture.sides)
|
||||
const pos = fixture.camera[0]
|
||||
viewer.camera.position.set(pos[0], pos[1], pos[2])
|
||||
}
|
||||
|
||||
let blocks: Record<string, BlockType> = {}
|
||||
let i = 0
|
||||
console.log('generating random data')
|
||||
webgpuChannel.generateRandom(490_000)
|
||||
|
||||
return
|
||||
|
||||
// Create viewer
|
||||
|
||||
viewer.listen(worldView)
|
||||
// Load chunks
|
||||
|
|
@ -151,127 +280,17 @@ async function main () {
|
|||
window['worldView'] = worldView
|
||||
window['viewer'] = viewer
|
||||
|
||||
params.blockIsomorphicRenderBundle = () => {
|
||||
const canvas = renderer.domElement
|
||||
const onlyCurrent = !confirm('Ok - render all blocks, Cancel - render only current one')
|
||||
const sizeRaw = prompt('Size', '512')
|
||||
if (!sizeRaw) return
|
||||
const size = parseInt(sizeRaw)
|
||||
// const size = 512
|
||||
|
||||
ignoreResize = true
|
||||
canvas.width = size
|
||||
canvas.height = size
|
||||
renderer.setSize(size, size)
|
||||
|
||||
//@ts-ignore
|
||||
viewer.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 10)
|
||||
viewer.scene.background = null
|
||||
|
||||
const rad = THREE.MathUtils.degToRad(-120)
|
||||
viewer.directionalLight.position.set(
|
||||
Math.cos(rad),
|
||||
Math.sin(rad),
|
||||
0.2
|
||||
).normalize()
|
||||
viewer.directionalLight.intensity = 1
|
||||
|
||||
const cameraPos = targetPos.offset(2, 2, 2)
|
||||
const pitch = THREE.MathUtils.degToRad(-30)
|
||||
const yaw = THREE.MathUtils.degToRad(45)
|
||||
viewer.camera.rotation.set(pitch, yaw, 0, 'ZYX')
|
||||
// viewer.camera.lookAt(center.x + 0.5, center.y + 0.5, center.z + 0.5)
|
||||
viewer.camera.position.set(cameraPos.x + 1, cameraPos.y + 0.5, cameraPos.z + 1)
|
||||
|
||||
const allBlocks = mcData.blocksArray.map(b => b.name)
|
||||
// const allBlocks = ['stone', 'warped_slab']
|
||||
|
||||
let blockCount = 1
|
||||
let blockName = allBlocks[0]
|
||||
|
||||
const updateBlock = () => {
|
||||
|
||||
//@ts-ignore
|
||||
// viewer.setBlockStateId(targetPos, mcData.blocksByName[blockName].minStateId)
|
||||
params.block = blockName
|
||||
// todo cleanup (introduce getDefaultState)
|
||||
onUpdate.block()
|
||||
applyChanges(false, true)
|
||||
}
|
||||
viewer.waitForChunksToRender().then(async () => {
|
||||
// wait for next macro task
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, 0)
|
||||
})
|
||||
if (onlyCurrent) {
|
||||
viewer.render()
|
||||
onWorldUpdate()
|
||||
} else {
|
||||
// will be called on every render update
|
||||
viewer.world.renderUpdateEmitter.addListener('update', onWorldUpdate)
|
||||
updateBlock()
|
||||
}
|
||||
})
|
||||
|
||||
const zip = new JSZip()
|
||||
zip.file('description.txt', 'Generated with prismarine-viewer')
|
||||
|
||||
const end = async () => {
|
||||
// download zip file
|
||||
|
||||
const a = document.createElement('a')
|
||||
const blob = await zip.generateAsync({ type: 'blob' })
|
||||
const dataUrlZip = URL.createObjectURL(blob)
|
||||
a.href = dataUrlZip
|
||||
a.download = 'blocks_render.zip'
|
||||
a.click()
|
||||
URL.revokeObjectURL(dataUrlZip)
|
||||
console.log('end')
|
||||
|
||||
viewer.world.renderUpdateEmitter.removeListener('update', onWorldUpdate)
|
||||
}
|
||||
|
||||
async function onWorldUpdate () {
|
||||
// await new Promise(resolve => {
|
||||
// setTimeout(resolve, 50)
|
||||
// })
|
||||
const dataUrl = canvas.toDataURL('image/png')
|
||||
|
||||
zip.file(`${blockName}.png`, dataUrl.split(',')[1], { base64: true })
|
||||
|
||||
if (onlyCurrent) {
|
||||
end()
|
||||
} else {
|
||||
nextBlock()
|
||||
}
|
||||
}
|
||||
const nextBlock = async () => {
|
||||
blockName = allBlocks[blockCount++]
|
||||
console.log(allBlocks.length, '/', blockCount, blockName)
|
||||
if (blockCount % 5 === 0) {
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, 100)
|
||||
})
|
||||
}
|
||||
if (blockName) {
|
||||
updateBlock()
|
||||
} else {
|
||||
end()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//@ts-ignore
|
||||
const controls = new OrbitControls(viewer.camera, renderer.domElement)
|
||||
controls.target.set(targetPos.x + 0.5, targetPos.y + 0.5, targetPos.z + 0.5)
|
||||
// const controls = new OrbitControls(viewer.camera, nullRenderer.domElement)
|
||||
// controls.target.set(targetPos.x + 0.5, targetPos.y + 0.5, targetPos.z + 0.5)
|
||||
|
||||
const cameraPos = targetPos.offset(2, 2, 2)
|
||||
const pitch = THREE.MathUtils.degToRad(-45)
|
||||
const yaw = THREE.MathUtils.degToRad(45)
|
||||
viewer.camera.rotation.set(pitch, yaw, 0, 'ZYX')
|
||||
viewer.camera.lookAt(targetPos.x + 0.5, targetPos.y + 0.5, targetPos.z + 0.5)
|
||||
viewer.camera.position.set(cameraPos.x + 0.5, cameraPos.y + 0.5, cameraPos.z + 0.5)
|
||||
controls.update()
|
||||
// viewer.camera.lookAt(targetPos.x + 0.5, targetPos.y + 0.5, targetPos.z + 0.5)
|
||||
viewer.camera.position.set(cameraPos.x, cameraPos.y, cameraPos.z)
|
||||
// controls.update()
|
||||
|
||||
let blockProps = {}
|
||||
let entityOverrides = {}
|
||||
|
|
@ -302,6 +321,9 @@ async function main () {
|
|||
}, TWEEN_DURATION)
|
||||
}
|
||||
|
||||
params.block ||= 'stone'
|
||||
|
||||
let textureAnimation: TextureAnimation | undefined
|
||||
const onUpdate = {
|
||||
version (initialUpdate) {
|
||||
if (initialUpdate) return
|
||||
|
|
@ -381,6 +403,27 @@ async function main () {
|
|||
},
|
||||
modelVariant () {
|
||||
viewer.world.mesherConfig.debugModelVariant = params.modelVariant === 0 ? undefined : [params.modelVariant]
|
||||
},
|
||||
animationTick () {
|
||||
// TODO
|
||||
const webgl = (viewer.world as WorldRendererWebgpu).playgroundGetWebglData() as unknown as { animation: any }
|
||||
if (!webgl?.animation) {
|
||||
setAnimationTick(0)
|
||||
return
|
||||
}
|
||||
if (params.animationTick === -1) {
|
||||
textureAnimation = new TextureAnimation(new Proxy({} as any, {
|
||||
set (t, p, v) {
|
||||
if (p === 'tick') {
|
||||
setAnimationTick(v)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}), webgl.animation, webgl.animation.framesCount)
|
||||
} else {
|
||||
setAnimationTick(params.animationTick)
|
||||
textureAnimation = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -418,7 +461,9 @@ async function main () {
|
|||
if (object === params) {
|
||||
if (property === 'camera') return
|
||||
onUpdate[property]?.()
|
||||
applyChanges(property === 'metadata')
|
||||
if (property !== 'animationTick') {
|
||||
applyChanges(property === 'metadata')
|
||||
}
|
||||
} else {
|
||||
applyChanges()
|
||||
}
|
||||
|
|
@ -432,25 +477,35 @@ async function main () {
|
|||
update(true)
|
||||
}
|
||||
applyChanges()
|
||||
gui.openAnimated()
|
||||
// gui.openAnimated()
|
||||
})
|
||||
|
||||
const animate = () => {
|
||||
const animate = () => { }
|
||||
const animate2 = () => {
|
||||
// if (controls) controls.update()
|
||||
// worldView.updatePosition(controls.target)
|
||||
viewer.render()
|
||||
// window.requestAnimationFrame(animate)
|
||||
window.requestAnimationFrame(animate2)
|
||||
}
|
||||
viewer.world.renderUpdateEmitter.addListener('update', () => {
|
||||
animate()
|
||||
// const frames = viewer.world.hasWithFrames ? viewer.world.hasWithFrames - 1 : 0;
|
||||
const webgl = (viewer.world as WorldRendererWebgpu).playgroundGetWebglData()
|
||||
// if (webgl?.animation) {
|
||||
// params.animationTick = -1
|
||||
// animationController.show()
|
||||
// animationController.max(webgl.animation.framesCount)
|
||||
// } else {
|
||||
// animationController.hide()
|
||||
// }
|
||||
onUpdate.animationTick()
|
||||
})
|
||||
animate()
|
||||
animate2()
|
||||
|
||||
// #region camera rotation param
|
||||
if (params.camera) {
|
||||
const [x, y] = params.camera.split(',')
|
||||
viewer.camera.rotation.set(parseFloat(x), parseFloat(y), 0, 'ZYX')
|
||||
controls.update()
|
||||
// controls.update()
|
||||
console.log(viewer.camera.rotation.x, parseFloat(x))
|
||||
}
|
||||
const throttledCamQsUpdate = _.throttle(() => {
|
||||
|
|
@ -458,16 +513,16 @@ async function main () {
|
|||
// params.camera = `${camera.rotation.x.toFixed(2)},${camera.rotation.y.toFixed(2)}`
|
||||
setQs()
|
||||
}, 200)
|
||||
controls.addEventListener('change', () => {
|
||||
throttledCamQsUpdate()
|
||||
animate()
|
||||
})
|
||||
// controls.addEventListener('change', () => {
|
||||
// throttledCamQsUpdate()
|
||||
// animate()
|
||||
// })
|
||||
// #endregion
|
||||
|
||||
let time = performance.now()
|
||||
const continuousUpdate = () => {
|
||||
if (continuousRender) {
|
||||
animate()
|
||||
}
|
||||
textureAnimation?.step(performance.now() - time)
|
||||
time = performance.now()
|
||||
requestAnimationFrame(continuousUpdate)
|
||||
}
|
||||
continuousUpdate()
|
||||
|
|
@ -482,7 +537,7 @@ async function main () {
|
|||
const { camera } = viewer
|
||||
viewer.camera.aspect = window.innerWidth / window.innerHeight
|
||||
viewer.camera.updateProjectionMatrix()
|
||||
renderer.setSize(window.innerWidth, window.innerHeight)
|
||||
nullRenderer.setSize(window.innerWidth, window.innerHeight)
|
||||
|
||||
animate()
|
||||
}
|
||||
|
|
|
|||
11
prismarine-viewer/examples/shared.ts
Normal file
11
prismarine-viewer/examples/shared.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
export type BlockFaceType = {
|
||||
side: number
|
||||
textureIndex: number
|
||||
textureName?: string
|
||||
tint?: [number, number, number]
|
||||
isTransparent?: boolean
|
||||
}
|
||||
|
||||
export type BlockType = {
|
||||
faces: BlockFaceType[]
|
||||
}
|
||||
511
prismarine-viewer/examples/webgpuRenderer.ts
Normal file
511
prismarine-viewer/examples/webgpuRenderer.ts
Normal file
|
|
@ -0,0 +1,511 @@
|
|||
import * as THREE from 'three';
|
||||
import { BlockFaceType } from './shared';
|
||||
import * as tweenJs from '@tweenjs/tween.js';
|
||||
import { cubePositionOffset, cubeUVOffset, cubeVertexArray, cubeVertexCount, cubeVertexSize } from './CubeDef';
|
||||
import VertShader from './Cube.vert.wgsl';
|
||||
import FragShader from './Cube.frag.wgsl';
|
||||
import ComputeShader from './Cube.comp.wgsl';
|
||||
import { updateSize, allSides } from './webgpuRendererWorker';
|
||||
|
||||
export class WebgpuRenderer {
|
||||
rendering = true
|
||||
NUMBER_OF_CUBES = 490_000;
|
||||
renderedFrames = 0
|
||||
localStorage: any = {}
|
||||
|
||||
ready = false;
|
||||
|
||||
device: GPUDevice;
|
||||
renderPassDescriptor: GPURenderPassDescriptor;
|
||||
uniformBindGroup: GPUBindGroup;
|
||||
UniformBuffer: GPUBuffer;
|
||||
ViewUniformBuffer: GPUBuffer;
|
||||
ProjectionUniformBuffer: GPUBuffer;
|
||||
ctx: GPUCanvasContext;
|
||||
verticesBuffer: GPUBuffer;
|
||||
InstancedModelBuffer: GPUBuffer;
|
||||
pipeline: GPURenderPipeline;
|
||||
InstancedTextureIndexBuffer: GPUBuffer;
|
||||
InstancedColorBuffer: GPUBuffer;
|
||||
notRenderedAdditions = 0;
|
||||
|
||||
// Add these properties to the WebgpuRenderer class
|
||||
computePipeline: GPUComputePipeline;
|
||||
indirectDrawBuffer: GPUBuffer;
|
||||
cubesBuffer: GPUBuffer;
|
||||
visibleCubesBuffer: GPUBuffer;
|
||||
computeBindGroup: GPUBindGroup;
|
||||
computeBindGroupLayout: GPUBindGroupLayout;
|
||||
indirectDrawParams: Uint32Array;
|
||||
maxBufferSize: number
|
||||
|
||||
constructor(public canvas: HTMLCanvasElement, public imageBlob: ImageBitmapSource, public isPlayground: boolean, public camera: THREE.PerspectiveCamera) {
|
||||
this.init();
|
||||
}
|
||||
|
||||
async init () {
|
||||
const { canvas, imageBlob, isPlayground, localStorage } = this;
|
||||
|
||||
updateSize(canvas.width, canvas.height);
|
||||
const textureBitmap = await createImageBitmap(imageBlob);
|
||||
const textureWidth = textureBitmap.width;
|
||||
const textureHeight = textureBitmap.height;
|
||||
|
||||
const adapter = await navigator.gpu.requestAdapter();
|
||||
if (!adapter) throw new Error('WebGPU not supported');
|
||||
this.device = await adapter.requestDevice();
|
||||
const { device } = this;
|
||||
this.maxBufferSize = device.limits.maxStorageBufferBindingSize;
|
||||
this.renderedFrames = device.limits.maxComputeWorkgroupSizeX;
|
||||
console.log('max buffer size', this.maxBufferSize / 1024 / 1024, 'MB')
|
||||
|
||||
const ctx = this.ctx = canvas.getContext('webgpu')!;
|
||||
|
||||
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
|
||||
|
||||
ctx.configure({
|
||||
device,
|
||||
format: presentationFormat,
|
||||
alphaMode: 'premultiplied',
|
||||
});
|
||||
|
||||
const verticesBuffer = device.createBuffer({
|
||||
size: cubeVertexArray.byteLength,
|
||||
usage: GPUBufferUsage.VERTEX,
|
||||
mappedAtCreation: true,
|
||||
});
|
||||
this.verticesBuffer = verticesBuffer;
|
||||
new Float32Array(verticesBuffer.getMappedRange()).set(cubeVertexArray);
|
||||
verticesBuffer.unmap();
|
||||
|
||||
const pipeline = device.createRenderPipeline({
|
||||
label: 'mainPipeline',
|
||||
layout: 'auto',
|
||||
vertex: {
|
||||
module: device.createShaderModule({
|
||||
code: localStorage.vertShader || VertShader,
|
||||
}),
|
||||
buffers: [
|
||||
{
|
||||
arrayStride: cubeVertexSize,
|
||||
attributes: [
|
||||
{
|
||||
shaderLocation: 0,
|
||||
offset: cubePositionOffset,
|
||||
format: 'float32x3',
|
||||
},
|
||||
{
|
||||
shaderLocation: 1,
|
||||
offset: cubeUVOffset,
|
||||
format: 'float32x2',
|
||||
},
|
||||
],
|
||||
},
|
||||
// {
|
||||
// arrayStride: 3 * 4,
|
||||
// attributes: [
|
||||
// {
|
||||
// shaderLocation: 2,
|
||||
// offset: 0,
|
||||
// format: 'float32x3',
|
||||
// },
|
||||
// ],
|
||||
// stepMode: 'instance',
|
||||
// },
|
||||
// {
|
||||
// arrayStride: 1 * 4,
|
||||
// attributes: [
|
||||
// {
|
||||
// shaderLocation: 3,
|
||||
// offset: 0,
|
||||
// format: 'float32',
|
||||
// },
|
||||
// ],
|
||||
// stepMode: 'instance',
|
||||
// },
|
||||
// {
|
||||
// arrayStride: 3 * 4,
|
||||
// attributes: [
|
||||
// {
|
||||
// shaderLocation: 4,
|
||||
// offset: 0,
|
||||
// format: 'float32x3',
|
||||
// },
|
||||
// ],
|
||||
// stepMode: 'instance',
|
||||
// },
|
||||
],
|
||||
},
|
||||
fragment: {
|
||||
module: device.createShaderModule({
|
||||
code: localStorage.fragShader || FragShader,
|
||||
}),
|
||||
targets: [
|
||||
{
|
||||
format: presentationFormat,
|
||||
blend: {
|
||||
color: {
|
||||
srcFactor: 'src-alpha',
|
||||
dstFactor: 'one-minus-src-alpha',
|
||||
operation: 'add',
|
||||
},
|
||||
alpha: {
|
||||
srcFactor: 'src-alpha',
|
||||
dstFactor: 'one-minus-src-alpha',
|
||||
operation: 'add',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
primitive: {
|
||||
topology: 'triangle-list',
|
||||
cullMode: 'front',
|
||||
},
|
||||
depthStencil: {
|
||||
depthWriteEnabled: true,
|
||||
depthCompare: 'less',
|
||||
format: 'depth24plus',
|
||||
},
|
||||
});
|
||||
this.pipeline = pipeline;
|
||||
|
||||
const depthTexture = device.createTexture({
|
||||
size: [canvas.width, canvas.height],
|
||||
format: 'depth24plus',
|
||||
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
||||
});
|
||||
|
||||
const uniformBufferSize = 4 * (4 * 4); // 4x4 matrix
|
||||
this.UniformBuffer = device.createBuffer({
|
||||
size: uniformBufferSize,
|
||||
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
||||
});
|
||||
|
||||
// Fetch the image and upload it into a GPUTexture.
|
||||
let cubeTexture: GPUTexture;
|
||||
{
|
||||
cubeTexture = device.createTexture({
|
||||
size: [textureBitmap.width, textureBitmap.height, 1],
|
||||
format: 'rgb10a2unorm',
|
||||
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
|
||||
});
|
||||
device.queue.copyExternalImageToTexture(
|
||||
{ source: textureBitmap },
|
||||
{ texture: cubeTexture },
|
||||
[textureBitmap.width, textureBitmap.height]
|
||||
);
|
||||
}
|
||||
|
||||
const sampler = device.createSampler({
|
||||
magFilter: 'nearest',
|
||||
minFilter: 'nearest',
|
||||
});
|
||||
|
||||
this.renderPassDescriptor = {
|
||||
label: 'MainRenderPassDescriptor',
|
||||
colorAttachments: [
|
||||
{
|
||||
view: undefined as any, // Assigned later
|
||||
clearValue: [0.6784313725490196, 0.8470588235294118, 0.9019607843137255, 1],
|
||||
loadOp: 'clear',
|
||||
storeOp: 'store',
|
||||
},
|
||||
],
|
||||
depthStencilAttachment: {
|
||||
view: depthTexture.createView(),
|
||||
depthClearValue: 1,
|
||||
depthLoadOp: 'clear',
|
||||
depthStoreOp: 'store',
|
||||
},
|
||||
};
|
||||
|
||||
// Create compute pipeline
|
||||
const computeShaderModule = device.createShaderModule({
|
||||
code: localStorage.computeShader || ComputeShader,
|
||||
label: 'Culled Instance',
|
||||
});
|
||||
|
||||
const computeBindGroupLayout = device.createBindGroupLayout({
|
||||
label: 'computeBindGroupLayout',
|
||||
entries: [
|
||||
{ binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'uniform' } },
|
||||
{ binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } },
|
||||
{ binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } },
|
||||
{ binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } },
|
||||
],
|
||||
});
|
||||
|
||||
const computePipelineLayout = device.createPipelineLayout({
|
||||
label: 'computePipelineLayout',
|
||||
bindGroupLayouts: [computeBindGroupLayout],
|
||||
|
||||
})
|
||||
|
||||
this.computePipeline = device.createComputePipeline({
|
||||
label: 'Culled Instance',
|
||||
layout: computePipelineLayout,
|
||||
// layout: 'auto',
|
||||
compute: {
|
||||
module: computeShaderModule,
|
||||
entryPoint: 'main',
|
||||
},
|
||||
});
|
||||
|
||||
// Create buffers for compute shader and indirect drawing
|
||||
this.cubesBuffer = device.createBuffer({
|
||||
label: 'cubesBuffer',
|
||||
size: this.NUMBER_OF_CUBES * 32, // 8 floats per cube - minimum buffer size
|
||||
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
||||
});
|
||||
|
||||
this.visibleCubesBuffer = device.createBuffer({
|
||||
label: 'visibleCubesBuffer',
|
||||
size: this.NUMBER_OF_CUBES * 32,
|
||||
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.VERTEX,
|
||||
});
|
||||
|
||||
this.indirectDrawBuffer = device.createBuffer({
|
||||
label: 'indirectDrawBuffer',
|
||||
size: 16, // 4 uint32 values
|
||||
usage: GPUBufferUsage.INDIRECT | GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
||||
});
|
||||
|
||||
// Initialize indirect draw parameters
|
||||
const indirectDrawParams = new Uint32Array([cubeVertexCount, 0, 0, 0]);
|
||||
device.queue.writeBuffer(this.indirectDrawBuffer, 0, indirectDrawParams);
|
||||
|
||||
// const vertexBindGroupLayout = device.createBindGroupLayout({
|
||||
// label: 'vertexBindGroupLayout',
|
||||
// entries: [
|
||||
// { binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: 'uniform' } },
|
||||
// { binding: 2, visibility: GPUShaderStage.VERTEX, buffer: { type: 'read-only-storage' } } // Read-only storage
|
||||
// ]
|
||||
// });
|
||||
|
||||
|
||||
// Create bind group for render pass
|
||||
this.uniformBindGroup = device.createBindGroup({
|
||||
label: 'uniformBindGroups',
|
||||
//layout: vertexBindGroupLayout,
|
||||
layout: pipeline.getBindGroupLayout(0),
|
||||
entries: [
|
||||
{
|
||||
binding: 0,
|
||||
resource: {
|
||||
buffer: this.UniformBuffer,
|
||||
},
|
||||
},
|
||||
{
|
||||
binding: 1,
|
||||
resource: sampler,
|
||||
},
|
||||
{
|
||||
binding: 2,
|
||||
resource: cubeTexture.createView(),
|
||||
},
|
||||
{
|
||||
binding: 3,
|
||||
resource: {
|
||||
buffer: this.visibleCubesBuffer
|
||||
}
|
||||
|
||||
}
|
||||
],
|
||||
});
|
||||
|
||||
// // Create bind group for compute shader
|
||||
// this.computeBindGroupLayout = device.createBindGroupLayout({
|
||||
// label: 'computeBindGroupLayout',
|
||||
// entries: [
|
||||
// {
|
||||
// binding: 0,
|
||||
// visibility: GPUShaderStage.COMPUTE,
|
||||
// buffer: {
|
||||
// type: 'uniform',
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// binding: 1,
|
||||
// visibility: GPUShaderStage.COMPUTE,
|
||||
// buffer: {
|
||||
// type: 'storage',
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// binding: 2,
|
||||
// visibility: GPUShaderStage.COMPUTE,
|
||||
// buffer: {
|
||||
// type: '',
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// binding: 3,
|
||||
// visibility: GPUShaderStage.COMPUTE,
|
||||
// buffer: {
|
||||
// type: 'storage',
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
// });
|
||||
|
||||
this.computeBindGroup = device.createBindGroup({
|
||||
//layout: this.computeBindGroupLayout,
|
||||
layout: this.computePipeline.getBindGroupLayout(0),
|
||||
label: 'computeBindGroup',
|
||||
entries: [
|
||||
{
|
||||
binding: 0,
|
||||
resource: { buffer: this.UniformBuffer },
|
||||
},
|
||||
{
|
||||
binding: 1,
|
||||
resource: { buffer: this.cubesBuffer },
|
||||
},
|
||||
{
|
||||
binding: 2,
|
||||
resource: { buffer: this.visibleCubesBuffer },
|
||||
},
|
||||
{
|
||||
binding: 3,
|
||||
resource: { buffer: this.indirectDrawBuffer },
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
this.indirectDrawParams = new Uint32Array([cubeVertexCount, 0, 0, 0]);
|
||||
|
||||
// always last!
|
||||
this.rendering = false;
|
||||
console.log('init finish')
|
||||
this.updateSides()
|
||||
this.loop();
|
||||
this.ready = true;
|
||||
return canvas;
|
||||
}
|
||||
|
||||
removeOne () { }
|
||||
|
||||
realNumberOfCubes = 0;
|
||||
|
||||
updateSides (startOffset = 0) {
|
||||
console.time('updateSides')
|
||||
this.rendering = true;
|
||||
const positions = [] as number[];
|
||||
let textureIndexes = [] as number[];
|
||||
let colors = [] as number[];
|
||||
const blocksPerFace = {} as Record<string, BlockFaceType>;
|
||||
for (const side of allSides.slice(startOffset)) {
|
||||
if (!side) continue;
|
||||
const [x, y, z] = side.slice(0, 3);
|
||||
const key = `${x},${y},${z}`;
|
||||
if (blocksPerFace[key]) continue;
|
||||
blocksPerFace[key] = side[3];
|
||||
}
|
||||
for (const key in blocksPerFace) {
|
||||
const side = key.split(',').map(Number);
|
||||
positions.push(...[side[0], side[1], side[2]]);
|
||||
const face = blocksPerFace[key];
|
||||
textureIndexes.push(face.textureIndex);
|
||||
colors.push(1, 1, 1);
|
||||
}
|
||||
|
||||
const NUMBER_OF_CUBES_NEEDED = Math.ceil(positions.length / 3);
|
||||
this.realNumberOfCubes = NUMBER_OF_CUBES_NEEDED;
|
||||
if (NUMBER_OF_CUBES_NEEDED > this.NUMBER_OF_CUBES) {
|
||||
console.warn('extending number of cubes', NUMBER_OF_CUBES_NEEDED, this.NUMBER_OF_CUBES)
|
||||
this.NUMBER_OF_CUBES = NUMBER_OF_CUBES_NEEDED + 5000;
|
||||
}
|
||||
|
||||
const BYTES_PER_ELEMENT = 8;
|
||||
const cubeData = new Float32Array(this.NUMBER_OF_CUBES * BYTES_PER_ELEMENT);
|
||||
for (let i = 0; i < this.NUMBER_OF_CUBES; i++) {
|
||||
const offset = i * BYTES_PER_ELEMENT;
|
||||
cubeData[offset] = positions[i * 3];
|
||||
cubeData[offset + 1] = positions[i * 3 + 1];
|
||||
cubeData[offset + 2] = positions[i * 3 + 2];
|
||||
cubeData[offset + 3] = textureIndexes[i];
|
||||
cubeData[offset + 4] = colors[i * 3];
|
||||
cubeData[offset + 5] = colors[i * 3 + 1];
|
||||
cubeData[offset + 6] = colors[i * 3 + 2];
|
||||
//cubeData[offset + 7] = 0.5; // Sphere radius
|
||||
}
|
||||
|
||||
console.time('writeCubes buffer')
|
||||
this.device.queue.writeBuffer(this.cubesBuffer, 0, cubeData);
|
||||
console.timeEnd('writeCubes buffer')
|
||||
|
||||
// Reset indirect draw parameters
|
||||
// this.indirectDrawParams = new Uint32Array([cubeVertexCount, 0, 0, 0]);
|
||||
//this.device.queue.writeBuffer(this.indirectDrawBuffer, 0, this.indirectDrawParams);
|
||||
|
||||
this.notRenderedAdditions++;
|
||||
console.timeEnd('updateSides')
|
||||
}
|
||||
|
||||
lastCall = performance.now();
|
||||
logged = false;
|
||||
loop () {
|
||||
if (!this.rendering) {
|
||||
requestAnimationFrame(() => this.loop());
|
||||
return;
|
||||
}
|
||||
|
||||
const { device, UniformBuffer: uniformBuffer, renderPassDescriptor, uniformBindGroup, pipeline, ctx, verticesBuffer } = this;
|
||||
|
||||
const now = Date.now();
|
||||
tweenJs.update();
|
||||
|
||||
const ViewProjectionMat4 = new THREE.Matrix4();
|
||||
this.camera.updateMatrix();
|
||||
const projectionMatrix = this.camera.projectionMatrix;
|
||||
ViewProjectionMat4.multiplyMatrices(projectionMatrix, this.camera.matrix.invert());
|
||||
const ViewProjection = new Float32Array(ViewProjectionMat4.elements);
|
||||
device.queue.writeBuffer(
|
||||
uniformBuffer,
|
||||
0,
|
||||
ViewProjection
|
||||
);
|
||||
|
||||
// const EmptyVisibleCubes = new Float32Array([36, 0, 0, 0]) ;
|
||||
|
||||
device.queue.writeBuffer(
|
||||
this.indirectDrawBuffer, 0, this.indirectDrawParams);
|
||||
|
||||
renderPassDescriptor.colorAttachments[0].view = ctx
|
||||
.getCurrentTexture()
|
||||
.createView();
|
||||
|
||||
let commandEncoder = device.createCommandEncoder();
|
||||
// Compute pass for occlusion culling
|
||||
commandEncoder.label = "Main Comand Encoder"
|
||||
const computePass = commandEncoder.beginComputePass();
|
||||
computePass.label = "ComputePass"
|
||||
computePass.setPipeline(this.computePipeline);
|
||||
//computePass.setBindGroup(0, this.uniformBindGroup);
|
||||
computePass.setBindGroup(0, this.computeBindGroup);
|
||||
computePass.dispatchWorkgroups(Math.ceil(this.NUMBER_OF_CUBES / 256));
|
||||
computePass.end();
|
||||
device.queue.submit([commandEncoder.finish()]);
|
||||
commandEncoder = device.createCommandEncoder();
|
||||
//device.queue.submit([commandEncoder.finish()]);
|
||||
// Render pass
|
||||
//console.log(this.indirectDrawBuffer.getMappedRange());
|
||||
const renderPass = commandEncoder.beginRenderPass(this.renderPassDescriptor);
|
||||
renderPass.label = "RenderPass"
|
||||
renderPass.setPipeline(pipeline);
|
||||
renderPass.setBindGroup(0, this.uniformBindGroup);
|
||||
renderPass.setVertexBuffer(0, verticesBuffer);
|
||||
|
||||
// Use indirect drawing
|
||||
renderPass.drawIndirect(this.indirectDrawBuffer, 0);
|
||||
|
||||
renderPass.end();
|
||||
device.queue.submit([commandEncoder.finish()]);
|
||||
|
||||
this.renderedFrames++;
|
||||
requestAnimationFrame(() => this.loop());
|
||||
this.notRenderedAdditions = 0;
|
||||
}
|
||||
}
|
||||
204
prismarine-viewer/examples/webgpuRendererMain.ts
Normal file
204
prismarine-viewer/examples/webgpuRendererMain.ts
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
import { generateSpiralMatrix } from 'flying-squid/dist/utils'
|
||||
import { Viewer } from '../viewer/lib/viewer'
|
||||
import { addNewStat } from './newStats'
|
||||
import type { workerProxyType } from './webgpuRendererWorker'
|
||||
import { useWorkerProxy } from './workerProxy'
|
||||
import { MesherGeometryOutput } from '../viewer/lib/mesher/shared'
|
||||
import { pickObj } from '@zardoy/utils'
|
||||
|
||||
let worker: Worker
|
||||
|
||||
export let webgpuChannel: typeof workerProxyType['__workerProxy'] = new Proxy({}, {
|
||||
get: () => () => { }
|
||||
}) as any // placeholder to avoid crashes
|
||||
|
||||
declare const viewer: Viewer
|
||||
|
||||
let allReceived = false
|
||||
declare const customEvents
|
||||
declare const bot
|
||||
if (typeof customEvents !== 'undefined') {
|
||||
customEvents.on('gameLoaded', () => {
|
||||
const chunksExpected = generateSpiralMatrix(globalThis.options.renderDistance)
|
||||
let received = 0
|
||||
bot.on('chunkColumnLoad', (data) => {
|
||||
received++
|
||||
if (received === chunksExpected.length) {
|
||||
allReceived = true
|
||||
// addBlocksSection('all', viewer.world.newChunks)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
let isWaitingToUpload = false
|
||||
globalThis.tiles = {}
|
||||
export const addBlocksSection = (key, data: MesherGeometryOutput) => {
|
||||
if (globalThis.tiles[key]) return
|
||||
globalThis.tiles[key] = data.tiles
|
||||
webgpuChannel.addBlocksSection(data.tiles, key, false)
|
||||
if (playground && !isWaitingToUpload) {
|
||||
isWaitingToUpload = true
|
||||
// viewer.waitForChunksToRender().then(() => {
|
||||
// isWaitingToUpload = false
|
||||
// sendWorkerMessage({
|
||||
// type: 'addBlocksSectionDone'
|
||||
// })
|
||||
// })
|
||||
}
|
||||
}
|
||||
|
||||
export const loadFixtureSides = (json) => {
|
||||
webgpuChannel.loadFixture(json)
|
||||
}
|
||||
|
||||
export const sendCameraToWorker = () => {
|
||||
const cameraVectors = ['rotation', 'position'].reduce((acc, key) => {
|
||||
acc[key] = ['x', 'y', 'z'].reduce((acc2, key2) => {
|
||||
acc2[key2] = viewer.camera[key][key2]
|
||||
return acc2
|
||||
}, {})
|
||||
return acc
|
||||
}, {}) as any
|
||||
webgpuChannel.camera({
|
||||
...cameraVectors,
|
||||
fov: viewer.camera.fov
|
||||
})
|
||||
}
|
||||
|
||||
export const removeBlocksSection = (key) => {
|
||||
webgpuChannel.removeBlocksSection(key)
|
||||
}
|
||||
|
||||
let playground = false
|
||||
export const initWebgpuRenderer = async (postRender = () => { }, playgroundModeInWorker = false, actuallyPlayground = false) => {
|
||||
playground = actuallyPlayground
|
||||
await new Promise(resolve => {
|
||||
// console.log('viewer.world.material.map!.image', viewer.world.material.map!.image)
|
||||
// viewer.world.material.map!.image.onload = () => {
|
||||
// console.log(this.material.map!.image)
|
||||
// resolve()
|
||||
// }
|
||||
viewer.world.renderUpdateEmitter.once('textureDownloaded', resolve)
|
||||
})
|
||||
const image = viewer.world.material.map!.image
|
||||
const imageBlob = await fetch(image.src).then((res) => res.blob())
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = window.innerWidth * window.devicePixelRatio
|
||||
canvas.height = window.innerHeight * window.devicePixelRatio
|
||||
document.body.appendChild(canvas)
|
||||
canvas.id = 'viewer-canvas'
|
||||
console.log('starting offscreen')
|
||||
|
||||
const offscreen = canvas.transferControlToOffscreen()
|
||||
|
||||
// replacable by initWebglRenderer
|
||||
worker = new Worker('./webgpuRendererWorker.js')
|
||||
addFpsCounters()
|
||||
webgpuChannel = useWorkerProxy<typeof workerProxyType>(worker, true)
|
||||
webgpuChannel.canvas(offscreen, imageBlob, playgroundModeInWorker, pickObj(localStorage, 'vertShader', 'fragShader', 'computeShader'))
|
||||
|
||||
let oldWidth = window.innerWidth
|
||||
let oldHeight = window.innerHeight
|
||||
let oldCamera = {
|
||||
position: { x: 0, y: 0, z: 0 },
|
||||
rotation: { x: 0, y: 0, z: 0 }
|
||||
}
|
||||
let focused = true
|
||||
window.addEventListener('focus', () => {
|
||||
focused = true
|
||||
webgpuChannel.startRender()
|
||||
})
|
||||
window.addEventListener('blur', () => {
|
||||
focused = false
|
||||
webgpuChannel.stopRender()
|
||||
})
|
||||
const mainLoop = () => {
|
||||
requestAnimationFrame(mainLoop)
|
||||
//@ts-ignore
|
||||
if (!focused || window.stopRender) return
|
||||
|
||||
if (oldWidth !== window.innerWidth || oldHeight !== window.innerHeight) {
|
||||
oldWidth = window.innerWidth
|
||||
oldHeight = window.innerHeight
|
||||
webgpuChannel.resize(window.innerWidth * window.devicePixelRatio, window.innerHeight * window.devicePixelRatio)
|
||||
}
|
||||
postRender()
|
||||
// TODO! do it in viewer to avoid possible delays
|
||||
if (actuallyPlayground && ['rotation', 'position'].some((key) => oldCamera[key] !== viewer.camera[key])) {
|
||||
// TODO fix
|
||||
for (const [key, val] of Object.entries(oldCamera)) {
|
||||
for (const key2 of Object.keys(val)) {
|
||||
oldCamera[key][key2] = viewer.camera[key][key2]
|
||||
}
|
||||
}
|
||||
sendCameraToWorker()
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(mainLoop)
|
||||
}
|
||||
|
||||
export const setAnimationTick = (tick: number, frames?: number) => {
|
||||
webgpuChannel.animationTick(tick, frames)
|
||||
}
|
||||
|
||||
export const exportLoadedTiles = () => {
|
||||
webgpuChannel.exportData()
|
||||
const controller = new AbortController()
|
||||
worker.addEventListener('message', async (e) => {
|
||||
const receivedData = e.data.data
|
||||
console.log('received fixture')
|
||||
// await new Promise(resolve => {
|
||||
// setTimeout(resolve, 0)
|
||||
// })
|
||||
try {
|
||||
const a = document.createElement('a')
|
||||
type Vec3 = [number, number, number]
|
||||
type PlayTimeline = [pos: Vec3, rot: Vec3, time: number]
|
||||
const vec3ToArr = (vec3: { x, y, z }) => [vec3.x, vec3.y, vec3.z] as Vec3
|
||||
// const dataObj = {
|
||||
// ...receivedData,
|
||||
// version: viewer.version,
|
||||
// camera: [vec3ToArr(viewer.camera.position), vec3ToArr(viewer.camera.rotation)],
|
||||
// playTimeline: [] as PlayTimeline[]
|
||||
// }
|
||||
// split into two chunks
|
||||
const objectURL = URL.createObjectURL(new Blob([receivedData.sides.buffer], { type: 'application/octet-stream' }))
|
||||
a.href = objectURL
|
||||
a.download = 'dumped-chunks-tiles.bin'
|
||||
a.click()
|
||||
URL.revokeObjectURL(objectURL)
|
||||
} finally {
|
||||
controller.abort()
|
||||
}
|
||||
}, { signal: controller.signal })
|
||||
}
|
||||
|
||||
|
||||
const addFpsCounters = () => {
|
||||
const { updateText } = addNewStat('fps')
|
||||
let prevTimeout
|
||||
worker.addEventListener('message', (e) => {
|
||||
if (e.data.type === 'fps') {
|
||||
updateText(`FPS: ${e.data.fps}`)
|
||||
if (prevTimeout) clearTimeout(prevTimeout)
|
||||
prevTimeout = setTimeout(() => {
|
||||
updateText('<hanging>')
|
||||
}, 1002)
|
||||
}
|
||||
})
|
||||
|
||||
const { updateText: updateText2 } = addNewStat('fps-main', 90, 0, 20)
|
||||
let updates = 0
|
||||
const mainLoop = () => {
|
||||
requestAnimationFrame(mainLoop)
|
||||
updates++
|
||||
}
|
||||
mainLoop()
|
||||
setInterval(() => {
|
||||
updateText2(`Main Loop: ${updates}`)
|
||||
updates = 0
|
||||
}, 1000)
|
||||
}
|
||||
241
prismarine-viewer/examples/webgpuRendererWorker.ts
Normal file
241
prismarine-viewer/examples/webgpuRendererWorker.ts
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
/// <reference types="@webgpu/types" />
|
||||
import * as THREE from 'three'
|
||||
import { BlockFaceType, BlockType } from './shared'
|
||||
import * as tweenJs from '@tweenjs/tween.js'
|
||||
//@ts-ignore
|
||||
//@ts-ignore
|
||||
import { createWorkerProxy } from './workerProxy'
|
||||
import { WebgpuRenderer } from './webgpuRenderer'
|
||||
|
||||
export let allSides = [] as ([number, number, number, BlockFaceType] | undefined)[]
|
||||
globalThis.allSides = allSides
|
||||
let allSidesAdded = 0
|
||||
let needsSidesUpdate = false
|
||||
|
||||
let chunksArrIndexes = {}
|
||||
let freeArrayIndexes = [] as [number, number][]
|
||||
let sidePositions
|
||||
let lastNotUpdatedIndex
|
||||
let lastNotUpdatedArrSize
|
||||
let animationTick = 0
|
||||
|
||||
const camera = new THREE.PerspectiveCamera(75, 1 / 1, 0.1, 10_000)
|
||||
globalThis.camera = camera
|
||||
|
||||
let webgpuRenderer: WebgpuRenderer | undefined
|
||||
|
||||
setInterval(() => {
|
||||
if (!webgpuRenderer) return
|
||||
// console.log('FPS:', renderedFrames)
|
||||
postMessage({ type: 'fps', fps: webgpuRenderer.renderedFrames })
|
||||
webgpuRenderer.renderedFrames = 0
|
||||
}, 1000)
|
||||
|
||||
export const updateSize = (width, height) => {
|
||||
camera.aspect = width / height
|
||||
camera.updateProjectionMatrix()
|
||||
}
|
||||
|
||||
|
||||
let fullReset
|
||||
|
||||
const updateCubesWhenAvailable = (pos) => {
|
||||
if (webgpuRenderer?.ready) {
|
||||
webgpuRenderer.updateSides(pos)
|
||||
} else {
|
||||
setTimeout(updateCubesWhenAvailable, 100)
|
||||
}
|
||||
}
|
||||
|
||||
let started = false
|
||||
let newWidth: number | undefined
|
||||
let newHeight: number | undefined
|
||||
let autoTickUpdate = undefined as number | undefined
|
||||
export const workerProxyType = createWorkerProxy({
|
||||
canvas (canvas, imageBlob, isPlayground, localStorage) {
|
||||
started = true
|
||||
webgpuRenderer = new WebgpuRenderer(canvas, imageBlob, isPlayground, camera)
|
||||
webgpuRenderer.localStorage
|
||||
globalThis.webgpuRenderer = webgpuRenderer
|
||||
},
|
||||
startRender () {
|
||||
if (!webgpuRenderer) return
|
||||
webgpuRenderer.rendering = true
|
||||
},
|
||||
stopRender () {
|
||||
if (!webgpuRenderer) return
|
||||
webgpuRenderer.rendering = false
|
||||
},
|
||||
resize (newWidth, newHeight) {
|
||||
newWidth = newWidth
|
||||
newHeight = newHeight
|
||||
updateSize(newWidth, newHeight)
|
||||
},
|
||||
generateRandom (count: number) {
|
||||
const square = Math.sqrt(count)
|
||||
if (square % 1 !== 0) throw new Error('square must be a whole number')
|
||||
const blocks = {}
|
||||
const getFace = (face: number) => {
|
||||
return {
|
||||
side: face,
|
||||
textureIndex: Math.floor(Math.random() * 512)
|
||||
}
|
||||
}
|
||||
for (let x = 0; x < square; x++) {
|
||||
for (let z = 0; z < square; z++) {
|
||||
blocks[`${x},${0},${z}`] = {
|
||||
faces: [
|
||||
getFace(0),
|
||||
getFace(1),
|
||||
getFace(2),
|
||||
getFace(3),
|
||||
getFace(4),
|
||||
getFace(5)
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log('data ready')
|
||||
this.addBlocksSection(blocks, `0,0,0`)
|
||||
},
|
||||
addBlocksSection (tiles: Record<string, BlockType>, key: string, update = true) {
|
||||
const currentLength = allSides.length
|
||||
// in: object - name, out: [x, y, z, name]
|
||||
const newData = Object.entries(tiles).flatMap(([key, value]) => {
|
||||
const [x, y, z] = key.split(',').map(Number)
|
||||
const block = value as BlockType
|
||||
return block.faces.map((side) => {
|
||||
return [x, y, z, side] as [number, number, number, BlockFaceType]
|
||||
})
|
||||
})
|
||||
// find freeIndexes if possible
|
||||
const freeArea = freeArrayIndexes.find(([startIndex, endIndex]) => endIndex - startIndex >= newData.length)
|
||||
if (freeArea) {
|
||||
const [startIndex, endIndex] = freeArea
|
||||
allSides.splice(startIndex, newData.length, ...newData)
|
||||
lastNotUpdatedIndex ??= startIndex
|
||||
const freeAreaIndex = freeArrayIndexes.indexOf(freeArea)
|
||||
freeArrayIndexes[freeAreaIndex] = [startIndex + newData.length, endIndex]
|
||||
if (freeArrayIndexes[freeAreaIndex][0] >= freeArrayIndexes[freeAreaIndex][1]) {
|
||||
freeArrayIndexes.splice(freeAreaIndex, 1)
|
||||
// todo merge
|
||||
}
|
||||
lastNotUpdatedArrSize = newData.length
|
||||
console.log('using free area', freeArea)
|
||||
}
|
||||
|
||||
chunksArrIndexes[key] = [currentLength, currentLength + newData.length]
|
||||
let i = 0;
|
||||
while (i < newData.length) {
|
||||
allSides.splice(currentLength + i, 0, ...newData.slice(i, i + 1024));
|
||||
i += 1024;
|
||||
}
|
||||
lastNotUpdatedIndex ??= currentLength
|
||||
// if (webglRendererWorker && webglRendererWorker.notRenderedAdditions < 5) {
|
||||
if (update) {
|
||||
updateCubesWhenAvailable(currentLength)
|
||||
}
|
||||
},
|
||||
addBlocksSectionDone () {
|
||||
updateCubesWhenAvailable(lastNotUpdatedIndex)
|
||||
lastNotUpdatedIndex = undefined
|
||||
lastNotUpdatedArrSize = undefined
|
||||
},
|
||||
removeBlocksSection (key) {
|
||||
return
|
||||
// fill data with 0
|
||||
const [startIndex, endIndex] = chunksArrIndexes[key]
|
||||
for (let i = startIndex; i < endIndex; i++) {
|
||||
allSides[i] = undefined
|
||||
}
|
||||
lastNotUpdatedArrSize = endIndex - startIndex
|
||||
updateCubesWhenAvailable(startIndex)
|
||||
|
||||
// freeArrayIndexes.push([startIndex, endIndex])
|
||||
|
||||
// // merge freeArrayIndexes TODO
|
||||
// if (freeArrayIndexes.at(-1)[0] === freeArrayIndexes.at(-2)?.[1]) {
|
||||
// const [startIndex, endIndex] = freeArrayIndexes.pop()!
|
||||
// const [startIndex2, endIndex2] = freeArrayIndexes.pop()!
|
||||
// freeArrayIndexes.push([startIndex2, endIndex])
|
||||
// }
|
||||
},
|
||||
camera (newCam: { rotation: { x: number, y: number, z: number }, position: { x: number, y: number, z: number }, fov: number }) {
|
||||
// if (webgpuRenderer?.isPlayground) {
|
||||
// camera.rotation.order = 'ZYX'
|
||||
// new tweenJs.Tween(camera.rotation).to({ x: newCam.rotation.x, y: newCam.rotation.y, z: newCam.rotation.z }, 50).start()
|
||||
// } else {
|
||||
camera.rotation.set(newCam.rotation.x, newCam.rotation.y, newCam.rotation.z, 'ZYX')
|
||||
// }
|
||||
if (newCam.position.x === 0 && newCam.position.y === 0 && newCam.position.z === 0) {
|
||||
// initial camera position
|
||||
camera.position.set(newCam.position.x, newCam.position.y, newCam.position.z)
|
||||
} else {
|
||||
new tweenJs.Tween(camera.position).to({ x: newCam.position.x, y: newCam.position.y, z: newCam.position.z }, 50).start()
|
||||
}
|
||||
|
||||
if (newCam.fov !== camera.fov) {
|
||||
camera.fov = newCam.fov
|
||||
camera.updateProjectionMatrix()
|
||||
}
|
||||
},
|
||||
animationTick (frames, tick) {
|
||||
if (frames <= 0) {
|
||||
autoTickUpdate = undefined
|
||||
animationTick = 0
|
||||
return
|
||||
}
|
||||
if (tick === -1) {
|
||||
autoTickUpdate = frames
|
||||
} else {
|
||||
autoTickUpdate = undefined
|
||||
animationTick = tick % 20 // todo update automatically in worker
|
||||
}
|
||||
},
|
||||
fullReset () {
|
||||
fullReset()
|
||||
},
|
||||
exportData () {
|
||||
const exported = exportData()
|
||||
postMessage({ type: 'exportData', data: exported }, undefined as any, [exported.sides.buffer])
|
||||
},
|
||||
loadFixture (json) {
|
||||
// allSides = json.map(([x, y, z, face, textureIndex]) => {
|
||||
// return [x, y, z, { face, textureIndex }] as [number, number, number, BlockFaceType]
|
||||
// })
|
||||
const dataSize = json.length / 5
|
||||
for (let i = 0; i < json.length; i += 5) {
|
||||
allSides.push([json[i], json[i + 1], json[i + 2], { side: json[i + 3], textureIndex: json[i + 4] }])
|
||||
}
|
||||
updateCubesWhenAvailable(0)
|
||||
},
|
||||
})
|
||||
|
||||
globalThis.testDuplicates = () => {
|
||||
const duplicates = allSides.filter((value, index, self) => self.indexOf(value) !== index)
|
||||
console.log('duplicates', duplicates)
|
||||
}
|
||||
|
||||
const exportData = () => {
|
||||
// Calculate the total length of the final array
|
||||
const totalLength = allSides.length * 5
|
||||
|
||||
// Create a new Int16Array with the total length
|
||||
const flatData = new Int16Array(totalLength)
|
||||
|
||||
// Fill the flatData array
|
||||
for (let i = 0; i < allSides.length; i++) {
|
||||
const sideData = allSides[i]
|
||||
if (!sideData) continue
|
||||
const [x, y, z, side] = sideData
|
||||
flatData.set([x, y, z, side.side, side.textureIndex], i * 5)
|
||||
}
|
||||
|
||||
return { sides: flatData }
|
||||
}
|
||||
|
||||
setInterval(() => {
|
||||
if (autoTickUpdate) {
|
||||
animationTick = (animationTick + 1) % autoTickUpdate
|
||||
}
|
||||
}, 1000 / 20)
|
||||
58
prismarine-viewer/examples/workerProxy.ts
Normal file
58
prismarine-viewer/examples/workerProxy.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
export function createWorkerProxy<T extends Record<string, (...args: any[]) => void>> (handlers: T): { __workerProxy: T } {
|
||||
addEventListener('message', (event) => {
|
||||
const { type, args } = event.data
|
||||
if (handlers[type]) {
|
||||
handlers[type](...args)
|
||||
}
|
||||
})
|
||||
return null as any
|
||||
}
|
||||
|
||||
/**
|
||||
* in main thread
|
||||
* ```ts
|
||||
* // either:
|
||||
* import type { importedTypeWorkerProxy } from './worker'
|
||||
* // or:
|
||||
* type importedTypeWorkerProxy = import('./worker').importedTypeWorkerProxy
|
||||
*
|
||||
* const workerChannel = useWorkerProxy<typeof importedTypeWorkerProxy>(worker)
|
||||
* ```
|
||||
*/
|
||||
export const useWorkerProxy = <T extends { __workerProxy: Record<string, (...args: any[]) => void> }> (worker: Worker, autoTransfer = true): T['__workerProxy'] & {
|
||||
transfer: (...args: Transferable[]) => T['__workerProxy']
|
||||
} => {
|
||||
// in main thread
|
||||
return new Proxy({} as any, {
|
||||
get: (target, prop) => {
|
||||
if (prop === 'transfer') return (...transferable: Transferable[]) => {
|
||||
return new Proxy({}, {
|
||||
get: (target, prop) => {
|
||||
return (...args: any[]) => {
|
||||
worker.postMessage({
|
||||
type: prop,
|
||||
args,
|
||||
}, transferable)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
return (...args: any[]) => {
|
||||
const transfer = autoTransfer ? args.filter(arg => arg instanceof ArrayBuffer || arg instanceof MessagePort || arg instanceof ImageBitmap || arg instanceof OffscreenCanvas || arg instanceof ImageData) : []
|
||||
worker.postMessage({
|
||||
type: prop,
|
||||
args,
|
||||
}, transfer)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// const workerProxy = createWorkerProxy({
|
||||
// startRender (canvas: HTMLCanvasElement) {
|
||||
// },
|
||||
// })
|
||||
|
||||
// const worker = useWorkerProxy(null, workerProxy)
|
||||
|
||||
// worker.
|
||||
9
prismarine-viewer/globals.d.ts
vendored
Normal file
9
prismarine-viewer/globals.d.ts
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
type StringKeys<T extends object> = Extract<keyof T, string>
|
||||
|
||||
interface ObjectConstructor {
|
||||
keys<T extends object> (obj: T): Array<StringKeys<T>>
|
||||
entries<T extends object> (obj: T): Array<[StringKeys<T>, T[keyof T]]>
|
||||
// todo review https://stackoverflow.com/questions/57390305/trying-to-get-fromentries-type-right
|
||||
fromEntries<T extends Array<[string, any]>> (obj: T): Record<T[number][0], T[number][1]>
|
||||
assign<T extends Record<string, any>, K extends Record<string, any>> (target: T, source: K): asserts target is T & K
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@
|
|||
html {
|
||||
overflow: hidden;
|
||||
background: black;
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
html, body {
|
||||
|
|
@ -31,6 +33,7 @@
|
|||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/javascript" src="playground.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
3
prismarine-viewer/sharedBuildOptions.mjs
Normal file
3
prismarine-viewer/sharedBuildOptions.mjs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export const sharedPlaygroundMainOptions = {
|
||||
alias: {}
|
||||
}
|
||||
|
|
@ -84,7 +84,7 @@ const handleMessage = data => {
|
|||
}
|
||||
|
||||
if (data.type === 'mesherData') {
|
||||
setMesherData(data.blockstatesModels, data.blocksAtlas)
|
||||
setMesherData(data.blockstatesModels, data.blocksAtlas, data.config.outputFormat === 'webgpu')
|
||||
allDataReady = true
|
||||
} else if (data.type === 'dirty') {
|
||||
const loc = new Vec3(data.x, data.y, data.z)
|
||||
|
|
@ -128,7 +128,7 @@ setInterval(() => {
|
|||
const chunk = world.getColumn(x, z)
|
||||
if (chunk?.getSection(new Vec3(x, y, z))) {
|
||||
const geometry = getSectionGeometry(x, y, z, world)
|
||||
const transferable = [geometry.positions.buffer, geometry.normals.buffer, geometry.colors.buffer, geometry.uvs.buffer]
|
||||
const transferable = [geometry.positions?.buffer, geometry.normals?.buffer, geometry.colors?.buffer, geometry.uvs?.buffer].filter(Boolean)
|
||||
//@ts-ignore
|
||||
postMessage({ type: 'geometry', key, geometry }, transferable)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import worldBlockProvider, { WorldBlockProvider } from 'mc-assets/dist/worldBloc
|
|||
import { BlockElement, buildRotationMatrix, elemFaces, matmul3, matmulmat3, vecadd3, vecsub3 } from './modelsGeometryCommon'
|
||||
|
||||
let blockProvider: WorldBlockProvider
|
||||
import { BlockType } from '../../../examples/shared'
|
||||
import { MesherGeometryOutput } from './shared'
|
||||
|
||||
const tints: any = {}
|
||||
let needTiles = false
|
||||
|
|
@ -20,6 +22,19 @@ for (const key of Object.keys(tintsData)) {
|
|||
tints[key] = prepareTints(tintsData[key])
|
||||
}
|
||||
|
||||
type TestTileData = {
|
||||
block: string
|
||||
faces: {
|
||||
face: string
|
||||
neighbor: string
|
||||
light?: number
|
||||
}[]
|
||||
}
|
||||
|
||||
type Tiles = {
|
||||
[blockPos: string]: BlockType & TestTileData
|
||||
}
|
||||
|
||||
function prepareTints (tints) {
|
||||
const map = new Map()
|
||||
const defaultValue = tintToGl(tints.default)
|
||||
|
|
@ -104,18 +119,23 @@ function getLiquidRenderHeight (world, block, type, pos) {
|
|||
return ((block.metadata >= 8 ? 8 : 7 - block.metadata) + 1) / 9
|
||||
}
|
||||
|
||||
const everyArray = (array, callback) => {
|
||||
if (!array?.length) return false
|
||||
return array.every(callback)
|
||||
}
|
||||
|
||||
const isCube = (block: Block) => {
|
||||
const isCube = (block) => {
|
||||
if (!block || block.transparent) return false
|
||||
if (block.isCube) return true
|
||||
if (!block.models?.length || block.models.length !== 1) return false
|
||||
// all variants
|
||||
return block.models[0].every(v => v.elements!.every(e => {
|
||||
// TODO!
|
||||
// if (!block.variant) block.variant = getModelVariants(block)
|
||||
if (!block.variant?.length) return false
|
||||
return block.variant.every(v => everyArray(v?.model?.elements, e => {
|
||||
return e.from[0] === 0 && e.from[1] === 0 && e.from[2] === 0 && e.to[0] === 16 && e.to[1] === 16 && e.to[2] === 16
|
||||
}))
|
||||
}
|
||||
|
||||
function renderLiquid (world: World, cursor: Vec3, texture: any | undefined, type: number, biome: string, water: boolean, attr: Record<string, any>) {
|
||||
function renderLiquid (world, cursor, texture, type, biome, water, attr) {
|
||||
const heights: number[] = []
|
||||
for (let z = -1; z <= 1; z++) {
|
||||
for (let x = -1; x <= 1; x++) {
|
||||
|
|
@ -134,12 +154,12 @@ function renderLiquid (world: World, cursor: Vec3, texture: any | undefined, typ
|
|||
const { dir, corners } = elemFaces[face]
|
||||
const isUp = dir[1] === 1
|
||||
|
||||
const neighborPos = cursor.offset(...dir as [number, number, number])
|
||||
const neighborPos = cursor.offset(...dir)
|
||||
const neighbor = world.getBlock(neighborPos)
|
||||
if (!neighbor) continue
|
||||
if (neighbor.type === type) continue
|
||||
const isGlass = neighbor.name.includes('glass')
|
||||
if ((isCube(neighbor) && !isUp) || neighbor.getProperties().waterlogged) continue
|
||||
if ((isCube(neighbor) && !isUp) || neighbor.material === 'plant' || neighbor.getProperties().waterlogged) continue
|
||||
|
||||
let tint = [1, 1, 1]
|
||||
if (water) {
|
||||
|
|
@ -151,11 +171,12 @@ function renderLiquid (world: World, cursor: Vec3, texture: any | undefined, typ
|
|||
}
|
||||
|
||||
if (needTiles) {
|
||||
attr.tiles[`${cursor.x},${cursor.y},${cursor.z}`] ??= {
|
||||
const tiles = attr.tiles as Tiles
|
||||
tiles[`${cursor.x},${cursor.y},${cursor.z}`] ??= {
|
||||
block: 'water',
|
||||
faces: [],
|
||||
}
|
||||
attr.tiles[`${cursor.x},${cursor.y},${cursor.z}`].faces.push({
|
||||
tiles[`${cursor.x},${cursor.y},${cursor.z}`].faces.push({
|
||||
face,
|
||||
neighbor: `${neighborPos.x},${neighborPos.y},${neighborPos.z}`,
|
||||
// texture: eFace.texture.name,
|
||||
|
|
@ -182,7 +203,7 @@ function renderLiquid (world: World, cursor: Vec3, texture: any | undefined, typ
|
|||
|
||||
let needRecompute = false
|
||||
|
||||
function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO: boolean, attr: Record<string, any>, globalMatrix: any, globalShift: any, block: Block, biome: string) {
|
||||
function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO: boolean, attr: MesherGeometryOutput, globalMatrix: any, globalShift: any, block: Block, biome: string) {
|
||||
const position = cursor
|
||||
// const key = `${position.x},${position.y},${position.z}`
|
||||
// if (!globalThis.allowedBlocks.includes(key)) return
|
||||
|
|
@ -190,7 +211,7 @@ function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO:
|
|||
|
||||
for (const face in element.faces) {
|
||||
const eFace = element.faces[face]
|
||||
const { corners, mask1, mask2 } = elemFaces[face]
|
||||
const { corners, mask1, mask2, side } = elemFaces[face]
|
||||
const dir = matmul3(globalMatrix, elemFaces[face].dir)
|
||||
|
||||
if (eFace.cullface) {
|
||||
|
|
@ -322,17 +343,24 @@ function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO:
|
|||
attr.colors.push(baseLight * tint[0] * light, baseLight * tint[1] * light, baseLight * tint[2] * light)
|
||||
}
|
||||
|
||||
const lightWithColor = [baseLight * tint[0], baseLight * tint[1], baseLight * tint[2]]
|
||||
|
||||
if (needTiles) {
|
||||
attr.tiles[`${cursor.x},${cursor.y},${cursor.z}`] ??= {
|
||||
const tiles = attr.tiles as Tiles
|
||||
tiles[`${cursor.x},${cursor.y},${cursor.z}`] ??= {
|
||||
block: block.name,
|
||||
faces: [],
|
||||
}
|
||||
attr.tiles[`${cursor.x},${cursor.y},${cursor.z}`].faces.push({
|
||||
tiles[`${cursor.x},${cursor.y},${cursor.z}`].faces.push({
|
||||
face,
|
||||
side,
|
||||
textureIndex: eFace.texture.tileIndex,
|
||||
neighbor: `${neighborPos.x},${neighborPos.y},${neighborPos.z}`,
|
||||
light: baseLight
|
||||
// texture: eFace.texture.name,
|
||||
})
|
||||
light: baseLight,
|
||||
// color: lightWithColor,
|
||||
//@ts-ignore debug prop
|
||||
texture: eFace.texture.debugName || block.name,
|
||||
} satisfies BlockType['faces'][number] & TestTileData['faces'][number] as any)
|
||||
}
|
||||
|
||||
if (doAO && aos[0] + aos[3] >= aos[1] + aos[2]) {
|
||||
|
|
@ -357,7 +385,7 @@ let unknownBlockModel: BlockModelPartsResolved
|
|||
export function getSectionGeometry (sx, sy, sz, world: World) {
|
||||
let delayedRender = [] as (() => void)[]
|
||||
|
||||
const attr = {
|
||||
const attr: MesherGeometryOutput = {
|
||||
sx: sx + 8,
|
||||
sy: sy + 8,
|
||||
sz: sz + 8,
|
||||
|
|
@ -373,7 +401,7 @@ export function getSectionGeometry (sx, sy, sz, world: World) {
|
|||
tiles: {},
|
||||
// todo this can be removed here
|
||||
signs: {}
|
||||
} as Record<string, any>
|
||||
}
|
||||
|
||||
const cursor = new Vec3(0, 0, 0)
|
||||
for (cursor.y = sy; cursor.y < sy + 16; cursor.y++) {
|
||||
|
|
@ -492,7 +520,7 @@ export function getSectionGeometry (sx, sy, sz, world: World) {
|
|||
delayedRender = []
|
||||
|
||||
let ndx = attr.positions.length / 3
|
||||
for (let i = 0; i < attr.t_positions.length / 12; i++) {
|
||||
for (let i = 0; i < attr.t_positions!.length / 12; i++) {
|
||||
attr.indices.push(
|
||||
ndx, ndx + 1, ndx + 2,
|
||||
ndx + 2, ndx + 1, ndx + 3,
|
||||
|
|
@ -503,10 +531,10 @@ export function getSectionGeometry (sx, sy, sz, world: World) {
|
|||
ndx += 4
|
||||
}
|
||||
|
||||
attr.positions.push(...attr.t_positions)
|
||||
attr.normals.push(...attr.t_normals)
|
||||
attr.colors.push(...attr.t_colors)
|
||||
attr.uvs.push(...attr.t_uvs)
|
||||
attr.positions.push(...attr.t_positions!)
|
||||
attr.normals.push(...attr.t_normals!)
|
||||
attr.colors.push(...attr.t_colors!)
|
||||
attr.uvs.push(...attr.t_uvs!)
|
||||
|
||||
delete attr.t_positions
|
||||
delete attr.t_normals
|
||||
|
|
@ -518,6 +546,13 @@ export function getSectionGeometry (sx, sy, sz, world: World) {
|
|||
attr.colors = new Float32Array(attr.colors) as any
|
||||
attr.uvs = new Float32Array(attr.uvs) as any
|
||||
|
||||
if (needTiles) {
|
||||
delete attr.positions
|
||||
delete attr.normals
|
||||
delete attr.colors
|
||||
delete attr.uvs
|
||||
}
|
||||
|
||||
return attr
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ export function matmulmat3 (a, b) {
|
|||
|
||||
export const elemFaces = {
|
||||
up: {
|
||||
side: 0,
|
||||
dir: [0, 1, 0],
|
||||
mask1: [1, 1, 0],
|
||||
mask2: [0, 1, 1],
|
||||
|
|
@ -85,6 +86,7 @@ export const elemFaces = {
|
|||
]
|
||||
},
|
||||
down: {
|
||||
side: 1,
|
||||
dir: [0, -1, 0],
|
||||
mask1: [1, 1, 0],
|
||||
mask2: [0, 1, 1],
|
||||
|
|
@ -96,6 +98,7 @@ export const elemFaces = {
|
|||
]
|
||||
},
|
||||
east: {
|
||||
side: 2,
|
||||
dir: [1, 0, 0],
|
||||
mask1: [1, 1, 0],
|
||||
mask2: [1, 0, 1],
|
||||
|
|
@ -107,6 +110,7 @@ export const elemFaces = {
|
|||
]
|
||||
},
|
||||
west: {
|
||||
side: 3,
|
||||
dir: [-1, 0, 0],
|
||||
mask1: [1, 1, 0],
|
||||
mask2: [1, 0, 1],
|
||||
|
|
@ -118,6 +122,7 @@ export const elemFaces = {
|
|||
]
|
||||
},
|
||||
north: {
|
||||
side: 4,
|
||||
dir: [0, 0, -1],
|
||||
mask1: [1, 0, 1],
|
||||
mask2: [0, 1, 1],
|
||||
|
|
@ -129,6 +134,7 @@ export const elemFaces = {
|
|||
]
|
||||
},
|
||||
south: {
|
||||
side: 5,
|
||||
dir: [0, 0, 1],
|
||||
mask1: [1, 0, 1],
|
||||
mask2: [0, 1, 1],
|
||||
|
|
|
|||
|
|
@ -1,11 +1,32 @@
|
|||
import { BlockType } from '../../../examples/shared'
|
||||
|
||||
export const defaultMesherConfig = {
|
||||
version: '',
|
||||
enableLighting: true,
|
||||
skyLight: 15,
|
||||
smoothLighting: true,
|
||||
outputFormat: 'threeJs' as 'threeJs' | 'webgl',
|
||||
outputFormat: 'threeJs' as 'threeJs' | 'webgpu',
|
||||
textureSize: 1024, // for testing
|
||||
debugModelVariant: undefined as undefined | number[]
|
||||
}
|
||||
|
||||
export type MesherConfig = typeof defaultMesherConfig
|
||||
|
||||
export type MesherGeometryOutput = {
|
||||
sx: number,
|
||||
sy: number,
|
||||
sz: number,
|
||||
// resulting: float32array
|
||||
positions: any,
|
||||
normals: any,
|
||||
colors: any,
|
||||
uvs: any,
|
||||
t_positions?: number[],
|
||||
t_normals?: number[],
|
||||
t_colors?: number[],
|
||||
t_uvs?: number[],
|
||||
|
||||
indices: number[],
|
||||
tiles: Record<string, BlockType>,
|
||||
signs: Record<string, any>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
const THREE = require('three')
|
||||
|
||||
const textureCache = {}
|
||||
function loadTexture (texture, cb, onLoad) {
|
||||
function loadTexture(texture, cb, onLoad) {
|
||||
const cached = textureCache[texture]
|
||||
if (!cached) {
|
||||
textureCache[texture] = new THREE.TextureLoader().load(texture, onLoad)
|
||||
|
|
@ -11,7 +11,7 @@ function loadTexture (texture, cb, onLoad) {
|
|||
if (cached) onLoad?.()
|
||||
}
|
||||
|
||||
function loadJSON (url, callback) {
|
||||
function loadJSON(url, callback) {
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.open('GET', url, true)
|
||||
xhr.responseType = 'json'
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import * as THREE from 'three'
|
||||
import { Vec3 } from 'vec3'
|
||||
import { WorldRendererWebgpu } from './worldrendererWebgpu'
|
||||
import { Entities } from './entities'
|
||||
import { Primitives } from './primitives'
|
||||
import EventEmitter from 'events'
|
||||
import { WorldRendererThree } from './worldrendererThree'
|
||||
import { generateSpiralMatrix } from 'flying-squid/dist/utils'
|
||||
import { WorldRendererCommon, WorldRendererConfig, defaultWorldRendererConfig } from './worldrendererCommon'
|
||||
import { defaultWorldRendererConfig } from './worldrendererCommon'
|
||||
import { sendCameraToWorker } from '../../examples/webgpuRendererMain'
|
||||
import { WorldRendererThree } from './worldrendererThree'
|
||||
import { versionToNumber } from '../prepare/utils'
|
||||
import worldBlockProvider from 'mc-assets/dist/worldBlockProvider'
|
||||
import { renderBlockThree } from './mesher/standaloneRenderer'
|
||||
|
|
@ -14,33 +16,36 @@ export class Viewer {
|
|||
scene: THREE.Scene
|
||||
ambientLight: THREE.AmbientLight
|
||||
directionalLight: THREE.DirectionalLight
|
||||
world: WorldRendererCommon
|
||||
camera: THREE.PerspectiveCamera
|
||||
world: WorldRendererWebgpu/* | WorldRendererThree */
|
||||
entities: Entities
|
||||
// primitives: Primitives
|
||||
domElement: HTMLCanvasElement
|
||||
playerHeight = 1.62
|
||||
isSneaking = false
|
||||
threeJsWorld: WorldRendererThree
|
||||
// threeJsWorld: WorldRendererThree
|
||||
cameraObjectOverride?: THREE.Object3D // for xr
|
||||
audioListener: THREE.AudioListener
|
||||
renderingUntilNoUpdates = false
|
||||
processEntityOverrides = (e, overrides) => overrides
|
||||
webgpuWorld: WorldRendererWebgpu
|
||||
|
||||
get camera () {
|
||||
return this.world.camera
|
||||
}
|
||||
set camera (camera) {
|
||||
this.world.camera = camera
|
||||
}
|
||||
// get camera () {
|
||||
// return this.world.camera
|
||||
// }
|
||||
// set camera (camera) {
|
||||
// this.world.camera = camera
|
||||
// }
|
||||
|
||||
constructor (public renderer: THREE.WebGLRenderer, worldConfig = defaultWorldRendererConfig) {
|
||||
constructor(public renderer: THREE.WebGLRenderer, worldConfig = defaultWorldRendererConfig) {
|
||||
// https://discourse.threejs.org/t/updates-to-color-management-in-three-js-r152/50791
|
||||
THREE.ColorManagement.enabled = false
|
||||
renderer.outputColorSpace = THREE.LinearSRGBColorSpace
|
||||
|
||||
this.scene = new THREE.Scene()
|
||||
this.scene.matrixAutoUpdate = false // for perf
|
||||
this.threeJsWorld = new WorldRendererThree(this.scene, this.renderer, worldConfig)
|
||||
// this.threeJsWorld = new WorldRendererThree(this.scene, this.renderer, worldConfig)
|
||||
this.webgpuWorld = new WorldRendererWebgpu(worldConfig)
|
||||
this.setWorld()
|
||||
this.resetScene()
|
||||
this.entities = new Entities(this.scene)
|
||||
|
|
@ -50,7 +55,7 @@ export class Viewer {
|
|||
}
|
||||
|
||||
setWorld () {
|
||||
this.world = this.threeJsWorld
|
||||
this.world = this.webgpuWorld
|
||||
}
|
||||
|
||||
resetScene () {
|
||||
|
|
@ -132,11 +137,14 @@ export class Viewer {
|
|||
|
||||
setFirstPersonCamera (pos: Vec3 | null, yaw: number, pitch: number, roll = 0) {
|
||||
const cam = this.cameraObjectOverride || this.camera
|
||||
let yOffset = this.playerHeight
|
||||
if (this.isSneaking) yOffset -= 0.3
|
||||
|
||||
if (this.world instanceof WorldRendererThree) this.world.camera = cam as THREE.PerspectiveCamera
|
||||
this.world.updateCamera(pos?.offset(0, yOffset, 0) ?? null, yaw, pitch)
|
||||
if (pos) {
|
||||
let y = pos.y + this.playerHeight
|
||||
if (this.isSneaking) y -= 0.3
|
||||
// new tweenJs.Tween(cam.position).to({ x: pos.x, y, z: pos.z }, 50).start()
|
||||
cam.position.set(pos.x, y, pos.z)
|
||||
}
|
||||
cam.rotation.set(pitch, yaw, roll, 'ZYX')
|
||||
sendCameraToWorker()
|
||||
}
|
||||
|
||||
playSound (position: Vec3, path: string, volume = 1, pitch = 1) {
|
||||
|
|
@ -184,7 +192,7 @@ export class Viewer {
|
|||
})
|
||||
// todo remove and use other architecture instead so data flow is clear
|
||||
emitter.on('blockEntities', (blockEntities) => {
|
||||
if (this.world instanceof WorldRendererThree) this.world.blockEntities = blockEntities
|
||||
if (this.world instanceof WorldRendererThree) (this.world as WorldRendererThree).blockEntities = blockEntities
|
||||
})
|
||||
|
||||
emitter.on('unloadChunk', ({ x, z }) => {
|
||||
|
|
@ -199,6 +207,16 @@ export class Viewer {
|
|||
this.world.updateViewerPosition(pos)
|
||||
})
|
||||
|
||||
emitter.on('renderDistance', (d) => {
|
||||
this.world.viewDistance = d
|
||||
this.world.chunksLength = d === 0 ? 1 : generateSpiralMatrix(d).length
|
||||
})
|
||||
|
||||
emitter.on('renderDistance', (d) => {
|
||||
this.world.viewDistance = d
|
||||
this.world.chunksLength = d === 0 ? 1 : generateSpiralMatrix(d).length
|
||||
})
|
||||
|
||||
emitter.on('renderDistance', (d) => {
|
||||
this.world.viewDistance = d
|
||||
this.world.chunksLength = d === 0 ? 1 : generateSpiralMatrix(d).length
|
||||
|
|
@ -206,7 +224,7 @@ export class Viewer {
|
|||
})
|
||||
|
||||
emitter.on('updateLight', ({ pos }) => {
|
||||
if (this.world instanceof WorldRendererThree) this.world.updateLight(pos.x, pos.z)
|
||||
if (this.world instanceof WorldRendererThree) (this.world as WorldRendererThree).updateLight(pos.x, pos.z)
|
||||
})
|
||||
|
||||
emitter.on('time', (timeOfDay) => {
|
||||
|
|
@ -227,15 +245,24 @@ export class Viewer {
|
|||
|
||||
if (this.world.mesherConfig.skyLight === skyLight) return
|
||||
this.world.mesherConfig.skyLight = skyLight
|
||||
; (this.world as WorldRendererThree).rerenderAllChunks?.()
|
||||
if (this.world instanceof WorldRendererThree) {
|
||||
(this.world as WorldRendererThree).rerenderAllChunks?.()
|
||||
}
|
||||
})
|
||||
|
||||
emitter.emit('listening')
|
||||
}
|
||||
|
||||
loadChunksFixture () { }
|
||||
|
||||
render () {
|
||||
this.world.render()
|
||||
this.entities.render()
|
||||
// if (this.composer) {
|
||||
// this.renderPass.camera = this.camera
|
||||
// this.composer.render()
|
||||
// } else {
|
||||
// this.renderer.render(this.scene, this.camera)
|
||||
// }
|
||||
// this.entities.render()
|
||||
}
|
||||
|
||||
async waitForChunksToRender () {
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
blocks?: CustomTexturesData
|
||||
} = {}
|
||||
|
||||
abstract outputFormat: 'threeJs' | 'webgl'
|
||||
abstract outputFormat: 'threeJs' | 'webgpu'
|
||||
|
||||
constructor (public config: WorldRendererConfig) {
|
||||
// this.initWorkers(1) // preload script on page load
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@ import nbt from 'prismarine-nbt'
|
|||
import PrismarineChatLoader from 'prismarine-chat'
|
||||
import { renderSign } from '../sign-renderer/'
|
||||
import { chunkPos, sectionPos } from './simpleUtils'
|
||||
|
||||
function mod (x, n) {
|
||||
return ((x % n) + n) % n
|
||||
}
|
||||
import { WorldRendererCommon, WorldRendererConfig } from './worldrendererCommon'
|
||||
import * as tweenJs from '@tweenjs/tween.js'
|
||||
import { BloomPass, RenderPass, UnrealBloomPass, EffectComposer, WaterPass, GlitchPass } from 'three-stdlib'
|
||||
|
|
|
|||
103
prismarine-viewer/viewer/lib/worldrendererWebgpu.ts
Normal file
103
prismarine-viewer/viewer/lib/worldrendererWebgpu.ts
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import { Vec3 } from 'vec3'
|
||||
import { updateStatText } from '../../examples/newStats'
|
||||
import { addBlocksSection, removeBlocksSection, webgpuChannel } from '../../examples/webgpuRendererMain'
|
||||
import type { WebglData } from '../prepare/webglData'
|
||||
import { loadJSON } from './utils.web'
|
||||
import { WorldRendererCommon } from './worldrendererCommon'
|
||||
import { MesherGeometryOutput } from './mesher/shared'
|
||||
|
||||
export class WorldRendererWebgpu extends WorldRendererCommon {
|
||||
outputFormat = 'webgpu' as const
|
||||
newChunks = {} as Record<string, any>
|
||||
// webglData: WebglData
|
||||
stopBlockUpdate = false
|
||||
lastChunkDistance = 0
|
||||
|
||||
constructor(config) {
|
||||
super(config)
|
||||
|
||||
this.renderUpdateEmitter.on('update', () => {
|
||||
const loadedChunks = Object.keys(this.finishedChunks).length
|
||||
updateStatText('loaded-chunks', `${loadedChunks}/${this.chunksLength} chunks (${this.lastChunkDistance})`)
|
||||
})
|
||||
}
|
||||
|
||||
playgroundGetWebglData () {
|
||||
const playgroundChunk = Object.values(this.newChunks).filter((x: any) => Object.keys(x?.blocks ?? {}).length > 0)?.[0] as any
|
||||
if (!playgroundChunk) return
|
||||
const block = Object.values(playgroundChunk.blocks)?.[0] as any
|
||||
if (!block) return
|
||||
const { textureName } = block
|
||||
if (!textureName) return
|
||||
// return this.webglData[textureName]
|
||||
}
|
||||
|
||||
setBlockStateId (pos: any, stateId: any): void {
|
||||
if (this.stopBlockUpdate) return
|
||||
super.setBlockStateId(pos, stateId)
|
||||
}
|
||||
|
||||
isWaitingForChunksToRender = false
|
||||
|
||||
allChunksLoaded (): void {
|
||||
console.log('allChunksLoaded')
|
||||
webgpuChannel.addBlocksSectionDone()
|
||||
}
|
||||
|
||||
handleWorkerMessage (data: { geometry: MesherGeometryOutput, type, key }): void {
|
||||
if (data.type === 'geometry' && Object.keys(data.geometry.tiles).length) {
|
||||
|
||||
const chunkCoords = data.key.split(',').map(Number) as [number, number, number]
|
||||
if (/* !this.loadedChunks[chunkCoords[0] + ',' + chunkCoords[2]] || */ !this.active) return
|
||||
|
||||
addBlocksSection(data.key, data.geometry)
|
||||
this.lastChunkDistance = Math.max(...this.getDistance(new Vec3(chunkCoords[0], 0, chunkCoords[2])))
|
||||
|
||||
// todo
|
||||
this.newChunks[data.key] = data.geometry
|
||||
}
|
||||
}
|
||||
|
||||
updateCamera (pos: Vec3 | null, yaw: number, pitch: number): void { }
|
||||
render (): void { }
|
||||
|
||||
chunksReset () {
|
||||
webgpuChannel.fullReset()
|
||||
}
|
||||
|
||||
updatePosDataChunk (key: string) {
|
||||
}
|
||||
|
||||
async updateTexturesData (): Promise<void> {
|
||||
await super.updateTexturesData()
|
||||
}
|
||||
|
||||
updateShowChunksBorder (value: boolean) {
|
||||
// todo
|
||||
}
|
||||
|
||||
|
||||
removeColumn (x, z) {
|
||||
console.log('removeColumn', x, z)
|
||||
super.removeColumn(x, z)
|
||||
for (const key of Object.keys(this.newChunks)) {
|
||||
const [xSec, _ySec, zSec] = key.split(',').map(Number)
|
||||
// if (Math.floor(x / 16) === x && Math.floor(z / 16) === z) {
|
||||
if (x === xSec && z === zSec) {
|
||||
// foundSections.push(key)
|
||||
removeBlocksSection(key)
|
||||
}
|
||||
}
|
||||
// for (let y = this.worldConfig.minY; y < this.worldConfig.worldHeight; y += 16) {
|
||||
// this.setSectionDirty(new Vec3(x, y, z), false)
|
||||
// const key = `${x},${y},${z}`
|
||||
// const mesh = this.sectionObjects[key]
|
||||
// if (mesh) {
|
||||
// this.scene.remove(mesh)
|
||||
// dispose3(mesh)
|
||||
// }
|
||||
// delete this.sectionObjects[key]
|
||||
// }
|
||||
}
|
||||
|
||||
}
|
||||
28
prismarine-viewer/viewer/prepare/webglData.ts
Normal file
28
prismarine-viewer/viewer/prepare/webglData.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { JsonAtlas } from './atlas';
|
||||
import { join } from 'path'
|
||||
import fs from 'fs'
|
||||
|
||||
export type WebglData = ReturnType<typeof prepareWebglData>
|
||||
|
||||
export const prepareWebglData = (blockTexturesDir: string, atlas: JsonAtlas) => {
|
||||
// todo
|
||||
return Object.fromEntries(Object.entries(atlas.textures).map(([texture, { animatedFrames }]) => {
|
||||
if (!animatedFrames) return null!
|
||||
const mcMeta = JSON.parse(fs.readFileSync(join(blockTexturesDir, texture + '.png.mcmeta'), 'utf8')) as {
|
||||
animation: {
|
||||
interpolate: boolean,
|
||||
frametime: number,
|
||||
frames: ({
|
||||
index: number,
|
||||
time: number
|
||||
} | number)[]
|
||||
}
|
||||
}
|
||||
return [texture, {
|
||||
animation: {
|
||||
...mcMeta.animation,
|
||||
framesCount: animatedFrames
|
||||
}
|
||||
}] as const
|
||||
}).filter(Boolean))
|
||||
}
|
||||
|
|
@ -121,6 +121,12 @@ export default defineConfig({
|
|||
} else {
|
||||
await execAsync('pnpm run build-mesher')
|
||||
}
|
||||
if (fs.existsSync('./prismarine-viewer/public/webgpuRendererWorker.js')) {
|
||||
// copy worker
|
||||
fs.copyFileSync('./prismarine-viewer/public/webgpuRendererWorker.js', './dist/webgpuRendererWorker.js')
|
||||
} else {
|
||||
await execAsync('pnpm run build-other-workers')
|
||||
}
|
||||
fs.writeFileSync('./dist/version.txt', buildingVersion, 'utf-8')
|
||||
console.timeEnd('total-prep')
|
||||
}
|
||||
|
|
@ -166,6 +172,10 @@ export default defineConfig({
|
|||
test: /\.obj$/,
|
||||
type: 'asset/source',
|
||||
},
|
||||
{
|
||||
test: /\.wgsl$/,
|
||||
type: 'asset/source',
|
||||
},
|
||||
{
|
||||
test: /\.mp3$/,
|
||||
type: 'asset/source',
|
||||
|
|
|
|||
|
|
@ -438,6 +438,10 @@ export const f3Keybinds = [
|
|||
console.warn('forcefully removed chunk from scene')
|
||||
}
|
||||
}
|
||||
|
||||
viewer.world.chunksReset() // todo
|
||||
viewer.world.newChunks = {}
|
||||
|
||||
if (localServer) {
|
||||
//@ts-expect-error not sure why it is private... maybe revisit api?
|
||||
localServer.players[0].world.columns = {}
|
||||
|
|
|
|||
13
src/globals.d.ts
vendored
13
src/globals.d.ts
vendored
|
|
@ -32,4 +32,17 @@ declare interface Document {
|
|||
exitPointerLock?(): void
|
||||
}
|
||||
|
||||
declare module '*.frag' {
|
||||
const png: string
|
||||
export default png
|
||||
}
|
||||
declare module '*.vert' {
|
||||
const png: string
|
||||
export default png
|
||||
}
|
||||
declare module '*.wgsl' {
|
||||
const png: string
|
||||
export default png
|
||||
}
|
||||
|
||||
declare interface Window extends Record<string, any> { }
|
||||
|
|
|
|||
|
|
@ -99,6 +99,9 @@ import { signInMessageState } from './react/SignInMessageProvider'
|
|||
import { updateAuthenticatedAccountData, updateLoadedServerData } from './react/ServersListProvider'
|
||||
import { versionToNumber } from 'prismarine-viewer/viewer/prepare/utils'
|
||||
import packetsPatcher from './packetsPatcher'
|
||||
import { initWebgpuRenderer } from 'prismarine-viewer/examples/webgpuRendererMain'
|
||||
import { addNewStat } from 'prismarine-viewer/examples/newStats'
|
||||
// import { ViewerBase } from 'prismarine-viewer/viewer/lib/viewerWrapper'
|
||||
import blockstatesModels from 'mc-assets/dist/blockStatesModels.json'
|
||||
import { mainMenuState } from './react/MainMenuRenderApp'
|
||||
import { ItemsRenderer } from 'mc-assets/dist/itemsRenderer'
|
||||
|
|
@ -414,10 +417,15 @@ async function connect (connectOptions: ConnectOptions) {
|
|||
viewer.setVersion(version, options.useVersionsTextures === 'latest' ? version : options.useVersionsTextures)
|
||||
}
|
||||
|
||||
// serverOptions.version = '1.18.1'
|
||||
const downloadVersion = connectOptions.botVersion || (singleplayer ? serverOptions.version : undefined)
|
||||
if (downloadVersion) {
|
||||
await downloadMcData(downloadVersion)
|
||||
}
|
||||
await initWebgpuRenderer(() => {
|
||||
renderWrapper.postRender()
|
||||
})
|
||||
addNewStat('loaded-chunks')
|
||||
|
||||
if (singleplayer) {
|
||||
// SINGLEPLAYER EXPLAINER:
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useEffect } from 'react'
|
|||
import { useSnapshot } from 'valtio'
|
||||
import { usedServerPathsV1 } from 'flying-squid/dist/lib/modules/world'
|
||||
import { openURL } from 'prismarine-viewer/viewer/lib/simpleUtils'
|
||||
import { exportLoadedTiles } from 'prismarine-viewer/examples/webgpuRendererMain'
|
||||
import {
|
||||
activeModalStack,
|
||||
showModal,
|
||||
|
|
@ -98,10 +99,16 @@ export default () => {
|
|||
if (fsStateSnap.inMemorySave || !singleplayer) {
|
||||
return showOptionsModal('World actions...', [])
|
||||
}
|
||||
const action = await showOptionsModal('World actions...', ['Save to browser memory'])
|
||||
const action = await showOptionsModal('World actions...', [
|
||||
...!fsStateSnap.inMemorySave && singleplayer ? ['Save to browser memory'] : [],
|
||||
'Dump loaded chunks'
|
||||
])
|
||||
if (action === 'Save to browser memory') {
|
||||
await saveToBrowserMemory()
|
||||
}
|
||||
if (action === 'Dump loaded chunks') {
|
||||
exportLoadedTiles()
|
||||
}
|
||||
}
|
||||
|
||||
if (!isModalActive) return null
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ if (hasRamPanel) {
|
|||
addStat(stats2.dom)
|
||||
}
|
||||
|
||||
const hideStats = localStorage.hideStats || isCypress()
|
||||
const hideStats = localStorage.hideStats || isCypress() || true
|
||||
if (hideStats) {
|
||||
stats.dom.style.display = 'none'
|
||||
stats2.dom.style.display = 'none'
|
||||
|
|
|
|||
2
src/worldSaveWorker.ts
Normal file
2
src/worldSaveWorker.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
import './workerWorkaround'
|
||||
import './browserfs'
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue