importScripts("../lib/improved-noise.js"); importScripts("../lib/cannon-es.js"); importScripts("../lib/ammo.js"); var neighborData = [ { "position": [-1, 0, 0], "corners": [ [0, 0, 0, 0, 0], [0, 0, 1, 1, 0], [0, 1, 0, 0, 1], [0, 1, 1, 1, 1] ] }, { "position": [1, 0, 0], "corners": [ [1, 0, 1, 0, 0], [1, 0, 0, 1, 0], [1, 1, 1, 0, 1], [1, 1, 0, 1, 1] ] }, { "position": [0, -1, 0], "corners": [ [0, 0, 0, 0, 1], [1, 0, 0, 1, 1], [0, 0, 1, 0, 0], [1, 0, 1, 1, 0] ] }, { "position": [0, 1, 0], "corners": [ [0, 1, 1, 0, 0], [1, 1, 1, 1, 0], [0, 1, 0, 0, 1], [1, 1, 0, 1, 1] ] }, { "position": [0, 0, -1], "corners": [ [1, 0, 0, 0, 0], [0, 0, 0, 1, 0], [1, 1, 0, 0, 1], [0, 1, 0, 1, 1] ] }, { "position": [0, 0, 1], "corners": [ [0, 0, 1, 0, 0], [1, 0, 1, 1, 0], [0, 1, 1, 0, 1], [1, 1, 1, 1, 1] ] } ]; let worldConfig = { "chunkSize": [16, 16, 16] }; let textureData = { "resolution": [16, 16], "size": [256, 256], "blocks": { } }; var tasks = { "physics": { "running": false, "interval": setInterval(async function () { if (!tasks["physics"].running && (tasks["physics"].queue.length > 0)) { tasks["physics"].running = true; while (tasks["physics"].queue.length > 0) { tasks["physics"].queue[0].run(...tasks["physics"].queue[0].arguments); tasks["physics"].queue.splice(0, 1); } tasks["physics"].running = false; } }, 256), "queue": [] } }; var ammoIsReady = false; let world; var worlds = {}; var bodies = {}; var onmessage = function (event) { var command = event.data["command"]; var arguments = event.data["arguments"]; var callbackID = arguments["callbackID"]; switch (command) { case "config": if (arguments["world"]) worldConfig = arguments["world"]; textureData = arguments["textureData"]; break; case "generate": var generator = self.generators[arguments["generator"]]; var data = arguments["data"]; if (!generator) return postMessage({ "event": "generated", "data": { "callbackID": callbackID, "blocks": [] } }); var position = arguments["position"]; var blocks = []; var chunkWidth = worldConfig.chunkSize[0]; var chunkHeight = worldConfig.chunkSize[1]; var chunkDepth = worldConfig.chunkSize[2]; switch (generator.type) { case "stepped": for (var x = 0; x < chunkWidth; x++) { for (var z = 0; z < chunkDepth; z++) { for (var y = 0; y < chunkHeight; y++) { var block = generator.run([ (position[0] * chunkWidth) + x, (position[1] * chunkHeight) + y, (position[2] * chunkDepth) + z ], data, worldConfig.chunkSize); var blockIndex = computeBlockIndex( worldConfig.chunkSize, [ x, y, z ] ); if (block) blocks[blockIndex] = block; } } } break; case "full": var blocks = generator.run(position, data, worldConfig.chunkSize); break; default: } postMessage({ "event": "generated", "data": { "callbackID": callbackID, "blocks": blocks } }); break; case "construct": var chunk = arguments["chunk"]; var neighbors = arguments["neighbors"]; postMessage({ "event": "constructed", "data": { "callbackID": callbackID, "properties": construct(chunk, neighbors) } }); break; case "create-world": // worlds[arguments["uuid"]] = new CANNON.World({ // "allowSleep": arguments["allowSleep"] || true, // "gravity": ("gravity" in arguments) ? new CANNON.Vec3( // arguments["gravity"][0], // arguments["gravity"][1], // arguments["gravity"][2] // ) : new CANNON.Vec3(0, -9.82, 0), // "solver": new CANNON.GSSolver() // }); var collisionConfiguration = new Ammo.btDefaultCollisionConfiguration(); var dispatcher = new Ammo.btCollisionDispatcher(collisionConfiguration); var overlappingPairCache = new Ammo.btDbvtBroadphase(); var solver = new Ammo.btSequentialImpulseConstraintSolver(); world = new Ammo.btDiscreteDynamicsWorld(dispatcher, overlappingPairCache, solver, collisionConfiguration); world.setGravity(new Ammo.btVector3(0, -9.82, 0)); postMessage({ "event": "world-created", "data": { "callbackID": callbackID } }); break; case "create-body": let bodyType; switch (arguments["type"]) { case "dynamic": bodyType = CANNON.Body.DYNAMIC; break; case "static": bodyType = CANNON.Body.STATIC; break; case "kinematic": bodyType = CANNON.Body.KINEMATIC; break; default: bodyType = CANNON.Body.DYNAMIC; } var bodyOptions = { "allowSleep": arguments["allowSleep"] || true, "fixedRotation": arguments["fixedRotation"] || false, "mass": arguments["mass"] || 0, "position": ("position" in arguments) ? new CANNON.Vec3( arguments["position"][0], arguments["position"][1], arguments["position"][2] ) : new CANNON.Vec3(0, 0, 0), "quaternion": ("quaternion" in arguments) ? new CANNON.Quaternion( arguments["quaternion"][0], arguments["quaternion"][1], arguments["quaternion"][2], arguments["quaternion"][3] ) : new CANNON.Quaternion(0, 0, 0, 1), "type": bodyType, "material": new CANNON.Material() /*"shape": ("shape" in arguments) ? new CANNON.ConvexPolyhedron({ "vertices": (function (positions) { var vertices = []; for (var v = 0; v < (positions.length / 3); v++) { var vertexOffset = v * 3; vertices[v] = new CANNON.Vec3( positions[vertexOffset + 0], positions[vertexOffset + 1], positions[vertexOffset + 2] ); } return vertices; })(arguments["shape"].vertices), "faces": (function (indices) { var faces = []; for (var f = 0; f < (indices.length / 3); f++) { var faceOffset = f * 3; faces[f] = indices.slice(faceOffset, faceOffset + 3); } return faces; })(arguments["shape"].indices) }) : new CANNON.Box(new CANNON.Vec3(1, 1, 1))*/ }; var shape = ("shape" in arguments) ? new CANNON.Trimesh( arguments["shape"].vertices, arguments["shape"].indices ) : new CANNON.Box(new CANNON.Vec3(1, 1, 1)); postMessage({ "event": "body-created", "data": { "callbackID": callbackID } }); bodies[arguments["uuid"]] = (shape instanceof CANNON.Trimesh) ? trimeshToPolyhedron(bodyOptions, shape) : new CANNON.Body(bodyOptions); if (shape instanceof CANNON.Box) bodies[arguments["uuid"]].addShape(shape); bodies[arguments["uuid"]].uuid = arguments["uuid"]; break; case "set-body": if ("shape" in arguments) { var convexPolyhedronBody = trimeshToPolyhedron({}, new CANNON.Trimesh( arguments["shape"].vertices, arguments["shape"].indices )); for (var s = bodies[arguments["uuid"]].shapes.length - 1; s >= 0; s--) { bodies[arguments["uuid"]].removeShape(bodies[arguments["uuid"]].shapes[s]); } for (var s = 0; s < convexPolyhedronBody.shapes.length; s++) { bodies[arguments["uuid"]].addShape(convexPolyhedronBody.shapes[s]); } bodies[arguments["uuid"]].updateBoundingRadius(); bodies[arguments["uuid"]].updateAABB(); /*bodies[arguments["uuid"]].shapes[0] = new CANNON.ConvexPolyhedron({ "vertices": (function (positions) { var vertices = []; for (var v = 0; v < (positions.length / 3); v++) { var vertexOffset = v * 3; vertices[v] = new CANNON.Vec3( positions[vertexOffset + 0], positions[vertexOffset + 1], positions[vertexOffset + 2] ); } return vertices; })(arguments["shape"].vertices), "faces": (function (indices) { var faces = []; for (var f = 0; f < (indices.length / 3); f++) { var faceOffset = f * 3; faces[f] = indices.slice(faceOffset, faceOffset + 3); } return faces; })(arguments["shape"].indices) });*/ } if ("position" in arguments) { bodies[arguments["uuid"]].position.set( arguments["position"][0], arguments["position"][1], arguments["position"][2] ); bodies[arguments["uuid"]].aabbNeedsUpdate = true; } if ("quaternion" in arguments) { bodies[arguments["uuid"]].quaternion.set( arguments["quaternion"][0], arguments["quaternion"][1], arguments["quaternion"][2], arguments["quaternion"][3] ); } break; case "add-body": worlds[arguments["worldUUID"]].addBody(bodies[arguments["bodyUUID"]]); postMessage({ "event": "body-added", "data": { "callbackID": callbackID } }); break; case "step-simulation": worlds[arguments["uuid"]].step(arguments["delta"]); var updates = {}; for (var b = 0; b < worlds[arguments["uuid"]].bodies.length; b++) { var body = worlds[arguments["uuid"]].bodies[b]; updates[body.uuid] = { "position": body.position.toArray(), "quaternion": body.quaternion.toArray() } } postMessage({ "event": "simulation-stepped", "data": { "callbackID": callbackID, "updates": updates } }); break; case "remove-body": worlds[arguments["worldUUID"]].removeBody(bodies[arguments["bodyUUID"]]); postMessage({ "event": "body-removed", "data": { "callbackID": callbackID } }); break; case "destroy-body": delete bodies[arguments["uuid"]]; postMessage({ "event": "body-destroyed", "data": { "callbackID": callbackID } }); break; case "put-voxels": tasks["physics"].queue.push({ "run": function (args) { var worldUUID = args["worldUUID"]; var chunkUUID = args["chunkUUID"]; var chunkPosition = args["chunkPosition"]; var voxelIndices = args["indices"]; var bodyOptions = { "mass": 0, "type": CANNON.Body.STATIC, "material": new CANNON.Material(), "position": new CANNON.Vec3( chunkPosition[0] * worldConfig.chunkSize[0], chunkPosition[1] * worldConfig.chunkSize[1], chunkPosition[2] * worldConfig.chunkSize[2] ) }; var chunk = bodies[chunkUUID] = new CANNON.Body(bodyOptions); var shapes = []; for (var i = 0; i < voxelIndices.length; i++) { var shape = shapes[i] = new CANNON.Box(new CANNON.Vec3(0.5, 0.5, 0.5)); var shapePosition = computeBlockPosition(worldConfig.chunkSize, voxelIndices[i]); chunk.addShape(shape, new CANNON.Vec3( shapePosition[0], shapePosition[1], shapePosition[2] )); } worlds[worldUUID].addBody(chunk); }, "arguments": [arguments] }); break; case "remove-voxels": tasks["physics"].queue.push({ "run": function (args) { var worldUUID = args["worldUUID"]; var chunkUUID = args["chunkUUID"]; worlds[worldUUID].removeBody(bodies[chunkUUID]); delete bodies[chunkUUID]; }, "arguments": [arguments] }); break; case "destroy-world": delete worlds[arguments["uuid"]]; postMessage({ "event": "world-destroyed", "data": { "callbackID": callbackID } }); break; default: } }; self.generators = { "flat": { "type": "stepped", "run": function (position, data = {}) { if (position[1] >= 0 && position[1] <= 5) return ["glitch", {}]; if (position[1] >= 6 && position[1] <= 57) return ["stone", {}]; if (position[1] >= 58 && position[1] <= 62) return ["dirt", {}]; if (position[1] == 63) return ["grass", {}]; } }, "sphere": { "type": "stepped", "run": function (position, data = {}) { var mustGenerate = ((position[0] ** 2) + (position[1] ** 2) + (position[2] ** 2)) <= 16 * 16; if (mustGenerate) return ["stone", {}]; } }, "xr-flat": { "type": "stepped", "run": function (position, data = {}) { if (position[1] >= 0 && position[1] <= 1) return ["glitch", {}]; if (position[1] >= 2 && position[1] <= 4) return ["stone", {}]; if (position[1] >= 5 && position[1] <= 6) return ["dirt", {}]; if (position[1] == 7) return ["grass", {}]; } }, "terrain": { "type": "full", "run": function (position, data = {}, chunkSize) { var chunkWidth = chunkSize[0]; var chunkHeight = chunkSize[1]; var chunkDepth = chunkSize[2]; var blocks = new Array(chunkSize[0] * chunkSize[1] * chunkSize[2]), perlin = new ImprovedNoise(), averageY = data["averageY"] || 64; let quality = 25; for (var x = 0; x < chunkWidth; x++) { for (var z = 0; z < chunkDepth; z++) { var absoluteCoords = [ (position[0] * chunkWidth) + x, (position[2] * chunkDepth) + z ]; var maxY = Math.round( perlin.noise( absoluteCoords[0] / quality, averageY / quality, absoluteCoords[1] / quality ) * quality ) + averageY; for (var y = 0; y < chunkHeight; y++) { var absoluteY = (position[1] * chunkHeight) + y; if (absoluteY >= (position[1] * chunkHeight) && absoluteY <= ((position[1] * chunkHeight) + chunkHeight)) { if (absoluteY >= 0 && absoluteY <= 3) { blocks[computeBlockIndex(worldConfig.chunkSize, [ x, y, z ])] = ["glitch", {}]; } else if (absoluteY >= 4 && absoluteY <= (maxY - 4)) { blocks[computeBlockIndex(worldConfig.chunkSize, [ x, y, z ])] = ["stone", {}]; } else if (absoluteY >= (maxY - 3) && absoluteY <= (maxY - 1)) { blocks[computeBlockIndex(worldConfig.chunkSize, [ x, y, z ])] = ["dirt", {}]; } else if (absoluteY == maxY) { blocks[computeBlockIndex(worldConfig.chunkSize, [ x, y, z ])] = ["grass", {}]; } } } } } return blocks; } } }; var construct = function (chunk, neighbors, chunkSize) { var positions = []; var normals = []; var uvs = []; var indexes = []; let texUV = []; for (var b = 0; b < chunk.blocks.length; b++) { var blockPosition = computeBlockPosition(worldConfig.chunkSize, b); if (chunk.blocks[b]) { var type = chunk.blocks[b][0]; var blockTexData = textureData.textures[type]; for (var n = 0; n < neighborData.length; n++) { var neighbor = neighborData[n]; var adjacentPosition = addPositions(blockPosition, neighbor.position); let adjacentInNeighbor = false; if ( adjacentPosition[0] < 0 || adjacentPosition[0] >= worldConfig.chunkSize[0] || adjacentPosition[1] < 0 || adjacentPosition[1] >= worldConfig.chunkSize[1] || adjacentPosition[2] < 0 || adjacentPosition[2] >= worldConfig.chunkSize[2] ) adjacentInNeighbor = true; let hasAdjacent = true; if (adjacentInNeighbor) { if (!neighbors[n]) hasAdjacent = false; var positionInNeighbor = neighborPosition(adjacentPosition, worldConfig.chunkSize); var indexInNeighbor = computeBlockIndex(worldConfig.chunkSize, positionInNeighbor); if (!neighbors[n] || !neighbors[n].blocks[indexInNeighbor]) hasAdjacent = false; } else { var blockIndex = computeBlockIndex(worldConfig.chunkSize, adjacentPosition); if (!chunk.blocks[blockIndex]) hasAdjacent = false; } if (!hasAdjacent) { var index = positions.length / 3; if (blockTexData.sided) { let sideMap; for (var sm = 0; sm < blockTexData.sideMaps.length; sm++) { if (blockTexData.sideMaps[sm].includes(n)) sideMap = sm; } texUV[0] = blockTexData.positions[sideMap][0] / blockTexData.sizes[sideMap][0]; texUV[1] = blockTexData.positions[sideMap][1] / blockTexData.sizes[sideMap][1]; } else { texUV[0] = blockTexData.position[0] / blockTexData.size[0]; texUV[1] = blockTexData.position[1] / blockTexData.size[1]; } var sideTexData = blockTexData[n]; // texUV[0] = sideTexData.end[0] - sideTexData.start[0]; // texUV[1] = sideTexData.end[1] - sideTexData.start[1]; for (var c = 0; c < neighbor.corners.length; c++) { var corner = neighbor.corners[c]; var position = addPositions(blockPosition, corner); positions.push( position[0], position[1], position[2] ); normals.push( neighbor.position[0], neighbor.position[1], neighbor.position[2] ); var uv = corner.slice(3, 5); uvs.push( (texUV[0] + uv[0]) * textureData.resolution[0] / textureData.size[0], 1 - (texUV[1] + 1 - uv[1]) * textureData.resolution[1] / textureData.size[1] ); } indexes.push( index + 0, index + 1, index + 2, index + 2, index + 1, index + 3 ); } } } } return {positions, normals, uvs, indexes}; }; var computeBlockIndex = function (chunkSize, blockPosition) { var x = blockPosition[0] % chunkSize[0]; var z = blockPosition[2] % chunkSize[2]; var y = blockPosition[1] % chunkSize[1]; return (x * (chunkSize[1] * chunkSize[2])) + y + (z * chunkSize[1]); }; var computeBlockPosition = function (chunkSize, blockIndex) { var blockPosition = [0, 0, 0]; var subtractedIndex = 0 + blockIndex; blockPosition[1] = subtractedIndex % chunkSize[1]; subtractedIndex -= blockPosition[1]; blockPosition[2] = (subtractedIndex / chunkSize[1]) % chunkSize[2]; subtractedIndex -= blockPosition[2] * chunkSize[1]; blockPosition[0] = (subtractedIndex / (chunkSize[1] * chunkSize[2])) % chunkSize[0]; subtractedIndex -= blockPosition[0] * chunkSize[0]; return blockPosition; }; var neighborPosition = function (position, chunkSize) { var chunkPosition = position.slice(0, 3); if (position[0] < 0) chunkPosition[0] = chunkSize[0] + position[0]; if (position[0] >= chunkSize[0]) chunkPosition[0] = position[0] - chunkSize[0]; if (position[1] < 0) chunkPosition[1] = chunkSize[1] + position[1]; if (position[1] >= chunkSize[1]) chunkPosition[1] = position[1] - chunkSize[1]; if (position[2] < 0) chunkPosition[2] = chunkSize[2] + position[2]; if (position[2] >= chunkSize[2]) chunkPosition[2] = position[2] - chunkSize[2]; return chunkPosition; }; var addPositions = function (positionA, positionB) { var position = new Array(positionA.length); for (var p = 0; p < position.length; p++) { position[p] = positionA[p] + positionB[p]; } return position; }; // Thanks to https://github.com/pmndrs/cannon-es/issues/21 (I slightly modified it) var trimeshToPolyhedron = function (bodyOptions, trimesh, upvector) { let p1 = new CANNON.Vec3(), p2 = new CANNON.Vec3(), p3 = new CANNON.Vec3(), p4 = new CANNON.Vec3(), mp = new CANNON.Vec3(), tmp = new CANNON.Vec3(), e1 = new CANNON.Vec3(), e2 = new CANNON.Vec3(); const body = new CANNON.Body(bodyOptions); for (let i = 0; i < trimesh.indices.length / 3; i++) { mp.set(0, 0, 0); trimesh.getTriangleVertices(i, p1, p2, p3); trimesh.getNormal(i, p4); // if (upvector && p4.dot(upvector) < 0) p4.scale(-1, p4); p4.normalize(); mp = mp.vadd(p1).vadd(p2).vadd(p3).scale(1/3); const vertices = [ new CANNON.Vec3().copy(p1), new CANNON.Vec3().copy(p2), new CANNON.Vec3().copy(p3), mp.vadd(p4.scale(-1 / 2)) ]; const faces = [ [0, 1, 2], [0, 3, 1], [1, 3, 2], [2, 3, 0] ]; const normals = [ new CANNON.Vec3().copy(p4) ]; for (let j = 0; j < 3; j++) { vertices[j].vsub(vertices[(j + 1) % 3], e1); vertices[(j + 1) % 3].vsub(p4, e2); tmp.set(0, 0, 0); const points = faces[j + 1]; for (let p = 0; p < points.length; p++) { tmp.vadd(vertices[points[p]], tmp); } tmp.scale(1 / points.length, tmp); const normal = e1.cross(e2); normal.normalize(); normal.scale(-1, normal); const angle = normal.dot(tmp); if(angle < 0) normal.scale(-1, normal); normals.push(normal); } const polyhedron = new CANNON.ConvexPolyhedron({vertices, faces, normals}); body.addShape(polyhedron); } return body; }; Ammo().then(function () { ammoIsReady = true; }).catch(function (error) { });