diff --git a/.circleci/config.yml b/.circleci/config.yml index 605d2e211c8..452836d8c64 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -171,6 +171,13 @@ workflows: filters: tags: only: /.*/ + - trigger-performance-tests: + context: + - "sdk-cicd/circleci-api" + filters: + branches: + only: + - internal linux-defaults: &linux-defaults docker: @@ -584,3 +591,13 @@ jobs: path: test/integration/render-tests - store_artifacts: path: "test/integration/render-tests/index.html" + + trigger-performance-tests: + <<: *linux-defaults + steps: + - checkout + - run: + name: Trigger SLA performance tests + command: | + sha=$(git rev-parse HEAD) + curl --location --request POST 'https://circleci.com/api/v2/project/github/mapbox/mapbox-gl-js-performance-internal/pipeline' --header 'Content-Type: application/json' -u $CIRCLECI_API_TOKEN: -d "{ \"parameters\": { \"setup_sha\": \"$sha\", \"setup_source_branch\": \"internal\" } }" \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 9aebed38055..89c93f48f80 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,14 +1,10 @@ ## Launch Checklist - - - - [ ] briefly describe the changes in this PR - - [ ] include before/after visuals or gifs if this PR includes visual changes - - [ ] write tests for all new functionality - - [ ] document any changes to public APIs - - [ ] post benchmark scores - - [ ] manually test the debug page - - [ ] tagged `@mapbox/map-design-team` `@mapbox/static-apis` if this PR includes style spec API or visual changes - - [ ] tagged `@mapbox/gl-native` if this PR includes shader changes or needs a native port - - [ ] apply changelog label ('bug', 'feature', 'docs', etc) or use the label 'skip changelog' - - [ ] add an entry inside this element for inclusion in the `mapbox-gl-js` changelog: `` + - [ ] Make sure the PR title is descriptive and preferably reflects the change from the user's perspective. + - [ ] Add additional detail and context in the PR description (with screenshots/videos if there are visual changes). + - [ ] Manually test the debug page. + - [ ] Write tests for all new functionality and make sure the CI checks pass. + - [ ] Document any changes to public APIs. + - [ ] Post benchmark scores if the change could affect performance. + - [ ] Tag `@mapbox/map-design-team` `@mapbox/static-apis` if this PR includes style spec API or visual changes. + - [ ] Tag `@mapbox/gl-native` if this PR includes shader changes or needs a native port. diff --git a/.github/actions/check-changelog/action.yml b/.github/actions/check-changelog/action.yml deleted file mode 100644 index e7904d49c6c..00000000000 --- a/.github/actions/check-changelog/action.yml +++ /dev/null @@ -1,12 +0,0 @@ -name: 'Check Changelog' -description: 'Ensure changelog meets requirements' -inputs: - pr-body: - description: 'PR body from the github event' - required: true -outputs: - approved-changelog-entry: - description: 'Changelog entry has passed requirements' -runs: - using: 'node20' - main: 'index.js' diff --git a/.github/actions/check-changelog/index.js b/.github/actions/check-changelog/index.js deleted file mode 100644 index 585b44e5795..00000000000 --- a/.github/actions/check-changelog/index.js +++ /dev/null @@ -1,15 +0,0 @@ -const core = require('@actions/core'); -const github = require('@actions/github'); - -try { - const prBody = core.getInput('pr-body'); - const changelogEntry = prBody.match(/\(.+)<\/changelog>/); - // Should create a standard of at least # characters long ("I am" is shortest English sentence). - if (changelogEntry && changelogEntry[1].length > 3) { - core.setOutput('Changelog entry requirement is completed'); - } else { - core.setFailed('Changelog entry is not completed or does not pass basic requirements'); - } -} catch (error) { - core.setFailed(error.message); -} diff --git a/.github/actions/package.json b/.github/actions/package.json deleted file mode 100644 index 847d66efd4e..00000000000 --- a/.github/actions/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "actions", - "version": "1.0.0", - "main": "index.js", - "license": "MIT", - "dependencies": { - "@actions/core": "^1.10.1", - "@actions/github": "^5.1.1" - } -} diff --git a/.github/actions/yarn.lock b/.github/actions/yarn.lock deleted file mode 100644 index a134025350d..00000000000 --- a/.github/actions/yarn.lock +++ /dev/null @@ -1,193 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@actions/core@^1.10.1": - version "1.10.1" - resolved "https://registry.yarnpkg.com/@actions/core/-/core-1.10.1.tgz#61108e7ac40acae95ee36da074fa5850ca4ced8a" - integrity sha512-3lBR9EDAY+iYIpTnTIXmWcNbX3T2kCkAEQGIQx4NVQ0575nk2k3GRZDTPQG+vVtS2izSLmINlxXf0uLtnrTP+g== - dependencies: - "@actions/http-client" "^2.0.1" - uuid "^8.3.2" - -"@actions/github@^5.1.1": - version "5.1.1" - resolved "https://registry.yarnpkg.com/@actions/github/-/github-5.1.1.tgz#40b9b9e1323a5efcf4ff7dadd33d8ea51651bbcb" - integrity sha512-Nk59rMDoJaV+mHCOJPXuvB1zIbomlKS0dmSIqPGxd0enAXBnOfn4VWF+CGtRCwXZG9Epa54tZA7VIRlJDS8A6g== - dependencies: - "@actions/http-client" "^2.0.1" - "@octokit/core" "^3.6.0" - "@octokit/plugin-paginate-rest" "^2.17.0" - "@octokit/plugin-rest-endpoint-methods" "^5.13.0" - -"@actions/http-client@^2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@actions/http-client/-/http-client-2.0.1.tgz#873f4ca98fe32f6839462a6f046332677322f99c" - integrity sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw== - dependencies: - tunnel "^0.0.6" - -"@octokit/auth-token@^2.4.4": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.5.0.tgz#27c37ea26c205f28443402477ffd261311f21e36" - integrity sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g== - dependencies: - "@octokit/types" "^6.0.3" - -"@octokit/core@^3.6.0": - version "3.6.0" - resolved "https://registry.yarnpkg.com/@octokit/core/-/core-3.6.0.tgz#3376cb9f3008d9b3d110370d90e0a1fcd5fe6085" - integrity sha512-7RKRKuA4xTjMhY+eG3jthb3hlZCsOwg3rztWh75Xc+ShDWOfDDATWbeZpAHBNRpm4Tv9WgBMOy1zEJYXG6NJ7Q== - dependencies: - "@octokit/auth-token" "^2.4.4" - "@octokit/graphql" "^4.5.8" - "@octokit/request" "^5.6.3" - "@octokit/request-error" "^2.0.5" - "@octokit/types" "^6.0.3" - before-after-hook "^2.2.0" - universal-user-agent "^6.0.0" - -"@octokit/endpoint@^6.0.1": - version "6.0.12" - resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-6.0.12.tgz#3b4d47a4b0e79b1027fb8d75d4221928b2d05658" - integrity sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA== - dependencies: - "@octokit/types" "^6.0.3" - is-plain-object "^5.0.0" - universal-user-agent "^6.0.0" - -"@octokit/graphql@^4.5.8": - version "4.8.0" - resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-4.8.0.tgz#664d9b11c0e12112cbf78e10f49a05959aa22cc3" - integrity sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg== - dependencies: - "@octokit/request" "^5.6.0" - "@octokit/types" "^6.0.3" - universal-user-agent "^6.0.0" - -"@octokit/openapi-types@^11.2.0": - version "11.2.0" - resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-11.2.0.tgz#b38d7fc3736d52a1e96b230c1ccd4a58a2f400a6" - integrity sha512-PBsVO+15KSlGmiI8QAzaqvsNlZlrDlyAJYcrXBCvVUxCp7VnXjkwPoFHgjEJXx3WF9BAwkA6nfCUA7i9sODzKA== - -"@octokit/openapi-types@^12.11.0": - version "12.11.0" - resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-12.11.0.tgz#da5638d64f2b919bca89ce6602d059f1b52d3ef0" - integrity sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ== - -"@octokit/plugin-paginate-rest@^2.17.0": - version "2.21.3" - resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.21.3.tgz#7f12532797775640dbb8224da577da7dc210c87e" - integrity sha512-aCZTEf0y2h3OLbrgKkrfFdjRL6eSOo8komneVQJnYecAxIej7Bafor2xhuDJOIFau4pk0i/P28/XgtbyPF0ZHw== - dependencies: - "@octokit/types" "^6.40.0" - -"@octokit/plugin-rest-endpoint-methods@^5.13.0": - version "5.16.2" - resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.16.2.tgz#7ee8bf586df97dd6868cf68f641354e908c25342" - integrity sha512-8QFz29Fg5jDuTPXVtey05BLm7OB+M8fnvE64RNegzX7U+5NUXcOcnpTIK0YfSHBg8gYd0oxIq3IZTe9SfPZiRw== - dependencies: - "@octokit/types" "^6.39.0" - deprecation "^2.3.1" - -"@octokit/request-error@^2.0.5", "@octokit/request-error@^2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-2.1.0.tgz#9e150357831bfc788d13a4fd4b1913d60c74d677" - integrity sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg== - dependencies: - "@octokit/types" "^6.0.3" - deprecation "^2.0.0" - once "^1.4.0" - -"@octokit/request@^5.6.0", "@octokit/request@^5.6.3": - version "5.6.3" - resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.6.3.tgz#19a022515a5bba965ac06c9d1334514eb50c48b0" - integrity sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A== - dependencies: - "@octokit/endpoint" "^6.0.1" - "@octokit/request-error" "^2.1.0" - "@octokit/types" "^6.16.1" - is-plain-object "^5.0.0" - node-fetch "^2.6.7" - universal-user-agent "^6.0.0" - -"@octokit/types@^6.0.3", "@octokit/types@^6.16.1": - version "6.34.0" - resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.34.0.tgz#c6021333334d1ecfb5d370a8798162ddf1ae8218" - integrity sha512-s1zLBjWhdEI2zwaoSgyOFoKSl109CUcVBCc7biPJ3aAf6LGLU6szDvi31JPU7bxfla2lqfhjbbg/5DdFNxOwHw== - dependencies: - "@octokit/openapi-types" "^11.2.0" - -"@octokit/types@^6.39.0", "@octokit/types@^6.40.0": - version "6.41.0" - resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.41.0.tgz#e58ef78d78596d2fb7df9c6259802464b5f84a04" - integrity sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg== - dependencies: - "@octokit/openapi-types" "^12.11.0" - -before-after-hook@^2.2.0: - version "2.2.2" - resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.2.tgz#a6e8ca41028d90ee2c24222f201c90956091613e" - integrity sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ== - -deprecation@^2.0.0, deprecation@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" - integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== - -is-plain-object@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" - integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== - -node-fetch@^2.6.7: - version "2.6.7" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" - integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== - dependencies: - whatwg-url "^5.0.0" - -once@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= - dependencies: - wrappy "1" - -tr46@~0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" - integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= - -tunnel@^0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" - integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== - -universal-user-agent@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee" - integrity sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w== - -uuid@^8.3.2: - version "8.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" - integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== - -webidl-conversions@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" - integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= - -whatwg-url@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" - integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0= - dependencies: - tr46 "~0.0.3" - webidl-conversions "^3.0.0" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= diff --git a/.github/workflows/pull-request-requirements.yml b/.github/workflows/pull-request-requirements.yml deleted file mode 100644 index 1622aab82e7..00000000000 --- a/.github/workflows/pull-request-requirements.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Check PR Requirements - -on: - pull_request: - types: [opened, reopened, edited, labeled, unlabeled, synchronize] - -jobs: - check-changelog: - if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip changelog') }} - runs-on: ubuntu-latest - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Use Node.js 20.x - uses: actions/setup-node@v3 - with: - node-version: 20.x - - name: Install dependencies - run: yarn install --cwd ./.github/actions - - name: Check if changelog entry exists - uses: ./.github/actions/check-changelog # uses action in .github/actions - id: check-changelog - with: - pr-body: ${{ github.event.pull_request.body }} diff --git a/3d-style/data/bucket/tiled_3d_model_bucket.js b/3d-style/data/bucket/tiled_3d_model_bucket.js index ae88f0fbc1a..c3d19d55ef5 100644 --- a/3d-style/data/bucket/tiled_3d_model_bucket.js +++ b/3d-style/data/bucket/tiled_3d_model_bucket.js @@ -237,7 +237,7 @@ class Tiled3dModelBucket implements Bucket { if (terrain && demTile && demTile.dem && demTile.tileID.overscaledZ !== this.elevationReadFromZ) { this.elevationReadFromZ = demTile.tileID.overscaledZ; - const dem = DEMSampler.create(terrain, this.id, demTile); + const dem = DEMSampler.create(terrain, coord, demTile); if (!dem) return; if (this.modelTraits & ModelTraits.HasMapboxMeshFeatures) { this.updateDEM(terrain, dem, coord, source); @@ -267,8 +267,11 @@ class Tiled3dModelBucket implements Bucket { return; } + // Resolution of the DEM data. + const demRes = dem._dem.dim; + tiles.push(coord.canonical); - assert(lookup.length <= dem._dem.dim * dem._dem.dim); + assert(lookup.length <= demRes * demRes); let changed = false; for (const nodeInfo of this.getNodesInfo()) { @@ -277,31 +280,41 @@ class Tiled3dModelBucket implements Bucket { continue; } + // Convert the bounds of the footprint for this node from its tile coordinates to DEM pixel coordinates. const grid = node.footprint.grid; const minDem = dem.tileCoordToPixel(grid.min.x, grid.min.y); const maxDem = dem.tileCoordToPixel(grid.max.x, grid.max.y); - const distanceToBorder = Math.min(Math.min(dem._dem.dim - maxDem.y, minDem.x), Math.min(minDem.y, dem._dem.dim - maxDem.x)); + const distanceToBorder = Math.min(Math.min(demRes - maxDem.y, minDem.x), Math.min(minDem.y, demRes - maxDem.x)); if (distanceToBorder < 0) { continue; // don't deal with neighbors and landmarks crossing tile borders, fix terrain only for buildings within the tile } // demAtt is a number of pixels we use to propagate attenuated change to surrounding pixels. // this is clamped further when sampling near tile border. + // The footprint covers a certain region of DEM pixels as indicated with 'minDem' and 'maxDem' (region A). + // This region is further padded by demAtt pixels to form the region B. + // First mark all the DEM pixels in region B as unchanged (using 'passLookup' array). + // +------------+ + // | +-----+ | + // | | A | | + // | +-----+ B | + // +------------+ const demAtt = clamp(distanceToBorder, 2, 5); - let heightAcc = 0; - let min = Number.POSITIVE_INFINITY; - let max = Number.NEGATIVE_INFINITY; - let count = 0; let minx = Math.max(0, minDem.x - demAtt); let miny = Math.max(0, minDem.y - demAtt); - let maxx = Math.min(maxDem.x + demAtt, dem._dem.dim - 1); - let maxy = Math.min(maxDem.y + demAtt, dem._dem.dim - 1); - for (let y = miny; y <= maxy + demAtt; ++y) { - for (let x = minx - demAtt; x <= maxx + demAtt; ++x) { - passLookup[y * dem._dem.dim + x] = 255; + let maxx = Math.min(maxDem.x + demAtt, demRes - 1); + let maxy = Math.min(maxDem.y + demAtt, demRes - 1); + for (let y = miny; y <= maxy; ++y) { + for (let x = minx; x <= maxx; ++x) { + passLookup[y * demRes + x] = 255; } } + // Next go through all eligible DEM pixels in region A, mark them as changed and calculate the average height(elevation). + // Some pixels may be skipped (and therefore aren't eligible) because no footprint geometry overlaps them. + // This is indicated by the existence of a 'Cell' at a given pixel's position. + let heightAcc = 0; + let count = 0; for (let celly = 0; celly < grid.cellsY; ++celly) { for (let cellx = 0; cellx < grid.cellsX; ++cellx) { const cell = grid.cells[celly * grid.cellsX + cellx]; @@ -310,13 +323,11 @@ class Tiled3dModelBucket implements Bucket { } const demP = dem.tileCoordToPixel(grid.min.x + cellx / grid.xScale, grid.min.y + celly / grid.yScale); const demPMax = dem.tileCoordToPixel(grid.min.x + (cellx + 1) / grid.xScale, grid.min.y + (celly + 1) / grid.yScale); - for (let y = demP.y; y <= Math.min(demPMax.y + 1, dem._dem.dim - 1); ++y) { - for (let x = demP.x; x <= Math.min(demPMax.x + 1, dem._dem.dim - 1); ++x) { - if (passLookup[y * dem._dem.dim + x] === 255) { - passLookup[y * dem._dem.dim + x] = 0; + for (let y = demP.y; y <= Math.min(demPMax.y + 1, demRes - 1); ++y) { + for (let x = demP.x; x <= Math.min(demPMax.x + 1, demRes - 1); ++x) { + if (passLookup[y * demRes + x] === 255) { + passLookup[y * demRes + x] = 0; const height = dem.getElevationAtPixel(x, y); - min = Math.min(height, min); - max = Math.max(height, max); heightAcc += height; count++; } @@ -327,28 +338,36 @@ class Tiled3dModelBucket implements Bucket { assert(count); const avgHeight = heightAcc / count; + // See https://github.com/mapbox/mapbox-gl-js-internal/pull/804#issuecomment-1738720351 + // for explanation why bounds should be clamped to 1 and demRes - 2 respectively. minx = Math.max(1, minDem.x - demAtt); miny = Math.max(1, minDem.y - demAtt); - maxx = Math.min(maxDem.x + demAtt, dem._dem.dim - 1); - maxy = Math.min(maxDem.y + demAtt, dem._dem.dim - 1); + maxx = Math.min(maxDem.x + demAtt, demRes - 2); + maxy = Math.min(maxDem.y + demAtt, demRes - 2); + // Next, update the DEM pixels in region A (which the footprint overlaps with) by the average height. + // This effectively flattens the terrain for the given footprint/building. + // Store the difference of the original height with the average height in 'lookup' array. changed = true; for (let y = miny; y <= maxy; ++y) { for (let x = minx; x <= maxx; ++x) { - if (passLookup[y * dem._dem.dim + x] === 0) { - lookup[y * dem._dem.dim + x] = dem._dem.set(x, y, avgHeight); + if (passLookup[y * demRes + x] === 0) { + lookup[y * demRes + x] = dem._dem.set(x, y, avgHeight); } } } + // Finally propagate the flattened out values to the remaining surrounding pixels (as goverened by demAtt padding) in region B. + // This ensures a smooth transition between the flattened and the non-flattened regions. for (let p = 1; p < demAtt; ++p) { minx = Math.max(1, minDem.x - p); miny = Math.max(1, minDem.y - p); - maxx = Math.min(maxDem.x + p, dem._dem.dim - 1); - maxy = Math.min(maxDem.y + p, dem._dem.dim - 1); + maxx = Math.min(maxDem.x + p, demRes - 2); + maxy = Math.min(maxDem.y + p, demRes - 2); for (let y = miny; y <= maxy; ++y) { for (let x = minx; x <= maxx; ++x) { - const indexThis = y * dem._dem.dim + x; + const indexThis = y * demRes + x; + // If DEM pixel is not modified. if (passLookup[indexThis] === 255) { let maxDiff = 0; let maxDiffAbs = 0; @@ -356,7 +375,7 @@ class Tiled3dModelBucket implements Bucket { let yoffset = -1; for (let j = -1; j <= 1; ++j) { for (let i = -1; i <= 1; ++i) { - const index = (y + j) * dem._dem.dim + x + i; + const index = (y + j) * demRes + x + i; if (passLookup[index] >= p) { continue; } diff --git a/3d-style/render/draw_model.js b/3d-style/render/draw_model.js index 8a0d8419377..202310bbcd7 100644 --- a/3d-style/render/draw_model.js +++ b/3d-style/render/draw_model.js @@ -1,6 +1,7 @@ // @flow import type Painter from '../../src/render/painter.js'; +import type {UseProgramParams} from '../../src/render/painter.js'; import type SourceCache from '../../src/source/source_cache.js'; import type ModelStyleLayer from '../style/style_layer/model_style_layer.js'; @@ -29,6 +30,8 @@ import {DEMSampler} from '../../src/terrain/elevation.js'; import {OverscaledTileID} from '../../src/source/tile_id.js'; import {Aabb} from '../../src/util/primitives.js'; import {getCutoffParams} from '../../src/render/cutoff.js'; +import {FOG_OPACITY_THRESHOLD} from '../../src/style/fog_helpers.js'; +import {ZoomDependentExpression} from '../../src/style-spec/expression/index.js'; export default drawModels; @@ -47,6 +50,7 @@ type SortedMesh = { type RenderData = { shadowUniformsInitialized: boolean; + useSingleShadowCascade: boolean; tileMatrix: Float64Array; shadowTileMatrix: Float32Array; aabb: Aabb; @@ -126,10 +130,12 @@ function drawMesh(sortedMesh: SortedMesh, painter: Painter, layer: ModelStyleLay assert(opacity > 0); const context = painter.context; const depthMode = new DepthMode(painter.context.gl.LEQUAL, DepthMode.ReadWrite, painter.depthRangeFor3D); + const tr = painter.transform; const mesh = sortedMesh.mesh; const material = mesh.material; const pbr = material.pbrMetallicRoughness; + const fog = painter.style.fog; let lightingMatrix; if (painter.transform.projection.zAxisUnit === "pixels") { @@ -154,26 +160,36 @@ function drawMesh(sortedMesh: SortedMesh, painter: Painter, layer: ModelStyleLay material, layer); - const definesValues = []; + const programOptions: UseProgramParams = { + defines: [] + }; + // Extra buffers (colors, normals, texCoords) const dynamicBuffers = []; - setupMeshDraw(definesValues, dynamicBuffers, mesh, painter); + setupMeshDraw(((programOptions.defines: any): Array), dynamicBuffers, mesh, painter); const shadowRenderer = painter.shadowRenderer; if (shadowRenderer) { shadowRenderer.useNormalOffset = false; } let fogMatrixArray = null; - if (painter.style.fog) { + if (fog) { const fogMatrix = fogMatrixForModel(sortedMesh.nodeModelMatrix, painter.transform); - definesValues.push('FOG', 'FOG_DITHERING'); fogMatrixArray = new Float32Array(fogMatrix); + + if (tr.projection.name !== 'globe') { + const min = mesh.aabb.min; + const max = mesh.aabb.max; + const [minOpacity, maxOpacity] = fog.getOpacityForBounds(fogMatrix, min[0], min[1], max[0], max[1]); + programOptions.overrideFog = minOpacity >= FOG_OPACITY_THRESHOLD || maxOpacity >= FOG_OPACITY_THRESHOLD; + } } const cutoffParams = getCutoffParams(painter, layer.paint.get('model-cutoff-fade-range')); if (cutoffParams.shouldRenderCutoff) { - definesValues.push('RENDER_CUTOFF'); + (programOptions.defines: any).push('RENDER_CUTOFF'); } - const program = painter.useProgram('model', null, ((definesValues: any): DynamicDefinesType[])); + + const program = painter.useProgram('model', programOptions); painter.uploadCommonUniforms(context, program, null, fogMatrixArray, cutoffParams); @@ -252,7 +268,7 @@ function drawShadowCaster(mesh: Mesh, matrix: Mat4, painter: Painter, layer: Mod const shadowMatrix = shadowRenderer.calculateShadowPassMatrixFromMatrix(matrix); const uniformValues = modelDepthUniformValues(shadowMatrix); const definesValues = ['DEPTH_TEXTURE']; - const program = painter.useProgram('modelDepth', null, ((definesValues: any): DynamicDefinesType[])); + const program = painter.useProgram('modelDepth', {defines: ((definesValues: any): DynamicDefinesType[])}); const context = painter.context; program.draw(painter, context.gl.TRIANGLES, depthMode, StencilMode.disabled, colorMode, CullFaceMode.backCCW, uniformValues, layer.id, mesh.vertexBuffer, mesh.indexBuffer, mesh.segments, layer.paint, painter.transform.zoom, @@ -269,8 +285,20 @@ function drawModels(painter: Painter, sourceCache: SourceCache, layer: ModelStyl return; } const castShadows = layer.paint.get('model-cast-shadows'); - if (painter.renderPass === 'shadow' && !castShadows) { - return; + if (painter.renderPass === 'shadow') { + if (!castShadows) { + return; + } + if (painter.terrain) { + const noShadowCutoff = 0.65; + if (opacity < noShadowCutoff) { + const expression = layer._transitionablePaint._values['model-opacity'].value.expression; + if (expression instanceof ZoomDependentExpression) { + // avoid rendering shadows during fade in / fade out on terrain + return; + } + } + } } const shadowRenderer = painter.shadowRenderer; const receiveShadows = layer.paint.get('model-receive-shadows'); @@ -294,12 +322,14 @@ function drawModels(painter: Painter, sourceCache: SourceCache, layer: ModelStyl return; } - if (!modelSource.loaded()) return; if (modelSource.type === 'vector' || modelSource.type === 'geojson') { drawInstancedModels(painter, sourceCache, layer, coords); cleanup(); return; } + + if (!modelSource.loaded()) return; + if (modelSource.type === 'batched-model') { drawBatchedModels(painter, sourceCache, layer, coords); cleanup(); @@ -428,6 +458,7 @@ function updateModelBucketsElevation(painter: Painter, bucket: ModelBucket, buck // preallocate structure used to reduce re-allocation during rendering and flow checks const renderData: RenderData = { shadowUniformsInitialized: false, + useSingleShadowCascade: false, tileMatrix: new Float64Array(16), shadowTileMatrix: new Float32Array(16), aabb: new Aabb([0, 0, 0], [EXTENT, EXTENT, 0]) @@ -474,6 +505,7 @@ function drawInstancedModels(painter: Painter, source: SourceCache, layer: Model const mercCameraPos = (tr.getFreeCameraOptions().position: any); if (!painter.modelManager) return; const modelManager = painter.modelManager; + const shadowRenderer = painter.shadowRenderer; if (!layer._unevaluatedLayout._values.hasOwnProperty('model-id')) return; const modelIdUnevaluatedProperty = layer._unevaluatedLayout._values['model-id']; const evaluationParameters = {...layer.layout.get("model-id").parameters}; @@ -489,8 +521,8 @@ function drawInstancedModels(painter: Painter, source: SourceCache, layer: Model updateModelBucketsElevation(painter, bucket, coord); renderData.shadowUniformsInitialized = false; - if (painter.renderPass === 'shadow' && painter.shadowRenderer) { - const shadowRenderer = painter.shadowRenderer; + renderData.useSingleShadowCascade = !!shadowRenderer && shadowRenderer.getMaxCascadeForTile(coord.toUnwrapped()) === 0; + if (painter.renderPass === 'shadow' && shadowRenderer) { if (painter.currentShadowCascade === 1 && bucket.isInsideFirstShadowMapFrustum) continue; const tileMatrix = tr.calculatePosMatrix(coord.toUnwrapped(), tr.worldSize); @@ -517,7 +549,7 @@ function drawInstancedModels(painter: Painter, source: SourceCache, layer: Model modelId = modelIdProperty.evaluate(modelInstances.features[0].feature, {}); } const model = modelManager.getModel(modelId, layer.scope); - if (!model) continue; + if (!model || !model.uploaded) continue; for (const node of model.nodes) { drawInstancedNode(painter, layer, node, modelInstances, cameraPos, coord, renderData); } @@ -532,6 +564,7 @@ function drawInstancedNode(painter: Painter, layer: ModelStyleLayer, node: Node, const isShadowPass = painter.renderPass === 'shadow'; const shadowRenderer = painter.shadowRenderer; const depthMode = isShadowPass && shadowRenderer ? shadowRenderer.getShadowPassDepthMode() : new DepthMode(context.gl.LEQUAL, DepthMode.ReadWrite, painter.depthRangeFor3D); + const affectedByFog = painter.isTileAffectedByFog(coord); if (node.meshes) { for (const mesh of node.meshes) { @@ -550,12 +583,12 @@ function drawInstancedNode(painter: Painter, layer: ModelStyleLayer, node: Node, definesValues.push('RENDER_CUTOFF'); } if (isShadowPass && shadowRenderer) { - program = painter.useProgram('modelDepth', null, ((definesValues: any): DynamicDefinesType[])); + program = painter.useProgram('modelDepth', {defines: ((definesValues: any): DynamicDefinesType[])}); uniformValues = modelDepthUniformValues(renderData.shadowTileMatrix, renderData.shadowTileMatrix, Float32Array.from(node.matrix)); colorMode = shadowRenderer.getShadowPassColorMode(); } else { setupMeshDraw(definesValues, dynamicBuffers, mesh, painter); - program = painter.useProgram('model', null, ((definesValues: any): DynamicDefinesType[])); + program = painter.useProgram('model', {defines: ((definesValues: any): DynamicDefinesType[]), overrideFog: affectedByFog}); const material = mesh.material; const pbr = material.pbrMetallicRoughness; const layerOpacity = layer.paint.get('model-opacity'); @@ -640,6 +673,8 @@ function prepareBatched(painter: Painter, source: SourceCache, layer: ModelStyle function drawBatchedModels(painter: Painter, source: SourceCache, layer: ModelStyleLayer, coords: Array) { const context = painter.context; const tr = painter.transform; + const fog = painter.style.fog; + const shadowRenderer = painter.shadowRenderer; if (tr.projection.name !== 'mercator') { warnOnce(`Drawing 3D landmark models for ${tr.projection.name} projection is not yet implemented`); return; @@ -664,6 +699,11 @@ function drawBatchedModels(painter: Painter, source: SourceCache, layer: ModelSt const tile = source.getTile(coord); const bucket: ?Tiled3dModelBucket = (tile.getBucket(layer): any); if (!bucket || !bucket.uploaded) continue; + + let singleCascade = false; + if (shadowRenderer) { + singleCascade = shadowRenderer.getMaxCascadeForTile(coord.toUnwrapped()) === 0; + } const tileMatrix = tr.calculatePosMatrix(coord.toUnwrapped(), tr.worldSize); const modelTraits = bucket.modelTraits; @@ -715,12 +755,18 @@ function drawBatchedModels(painter: Painter, source: SourceCache, layer: ModelSt continue; } - const definesValues = []; + const programOptions: UseProgramParams = { + defines: [] + }; const dynamicBuffers = []; - setupMeshDraw(definesValues, dynamicBuffers, mesh, painter); + setupMeshDraw(((programOptions.defines: any): Array), dynamicBuffers, mesh, painter); if (!(modelTraits & ModelTraits.HasMapboxMeshFeatures)) { - definesValues.push('DIFFUSE_SHADED'); + (programOptions.defines: any).push('DIFFUSE_SHADED'); + } + + if (singleCascade) { + (programOptions.defines: any).push('SHADOWS_SINGLE_CASCADE'); } const isShadowPass = painter.renderPass === 'shadow'; @@ -730,14 +776,19 @@ function drawBatchedModels(painter: Painter, source: SourceCache, layer: ModelSt } let fogMatrixArray = null; - if (painter.style.fog) { + if (fog) { const fogMatrix = fogMatrixForModel(modelMatrix, painter.transform); - definesValues.push('FOG', 'FOG_DITHERING'); fogMatrixArray = new Float32Array(fogMatrix); + + if (tr.projection.name !== 'globe') { + const min = mesh.aabb.min; + const max = mesh.aabb.max; + const [minOpacity, maxOpacity] = fog.getOpacityForBounds(fogMatrix, min[0], min[1], max[0], max[1]); + programOptions.overrideFog = minOpacity >= FOG_OPACITY_THRESHOLD || maxOpacity >= FOG_OPACITY_THRESHOLD; + } } - const program = painter.useProgram('model', null, ((definesValues: any): DynamicDefinesType[])); - const shadowRenderer = painter.shadowRenderer; + const program = painter.useProgram('model', programOptions); if (!isShadowPass && shadowRenderer) { shadowRenderer.useNormalOffset = !!mesh.normalBuffer; diff --git a/3d-style/render/shadow_renderer.js b/3d-style/render/shadow_renderer.js index a08589309cb..b940f39b9b9 100644 --- a/3d-style/render/shadow_renderer.js +++ b/3d-style/render/shadow_renderer.js @@ -56,11 +56,91 @@ type ShadowNormalOffsetMode = 'vector-tile' | 'model-tile'; const cascadeCount = 2; const shadowMapResolution = 2048; +class ShadowReceiver { + constructor(aabb: Aabb, lastCascade: ?number) { + this.aabb = aabb; + this.lastCascade = lastCascade; + } + + aabb: Aabb; + lastCascade: ?number; +} + +class ShadowReceivers { + add(tileId: UnwrappedTileID, aabb: Aabb) { + const receiver = this.receivers[tileId.key]; + + if (receiver !== undefined) { + receiver.aabb.min[0] = Math.min(receiver.aabb.min[0], aabb.min[0]); + receiver.aabb.min[1] = Math.min(receiver.aabb.min[1], aabb.min[1]); + receiver.aabb.min[2] = Math.min(receiver.aabb.min[2], aabb.min[2]); + receiver.aabb.max[0] = Math.max(receiver.aabb.max[0], aabb.max[0]); + receiver.aabb.max[1] = Math.max(receiver.aabb.max[1], aabb.max[1]); + receiver.aabb.max[2] = Math.max(receiver.aabb.max[2], aabb.max[2]); + } else { + this.receivers[tileId.key] = new ShadowReceiver(aabb, null); + } + } + clear() { + this.receivers = {}; + } + + get(tileId: UnwrappedTileID): ?ShadowReceiver { + return this.receivers[tileId.key]; + } + + // Returns the number of cascades that need to be rendered based on visibility on screen. + // Cascades that need to be rendered always include the first cascade. + computeRequiredCascades(frustum: Frustum, worldSize: number, cascades: Array): number { + const frustumAabb = Aabb.fromPoints((frustum.points: any)); + let lastCascade = 0; + + for (const receiverKey in this.receivers) { + const receiver = (this.receivers[receiverKey]: ?ShadowReceiver); + if (!receiver) continue; + + if (!frustumAabb.intersectsAabb(receiver.aabb)) continue; + + receiver.aabb.min = frustumAabb.closestPoint(receiver.aabb.min); + receiver.aabb.max = frustumAabb.closestPoint(receiver.aabb.max); + const clampedTileAabbPoints = receiver.aabb.getCorners(); + + for (let i = 0; i < cascades.length; i++) { + let aabbInsideCascade = true; + + for (const point of clampedTileAabbPoints) { + const p = [point[0] * worldSize, point[1] * worldSize, point[2]]; + vec3.transformMat4(p, p, cascades[i].matrix); + + if (p[0] < -1.0 || p[0] > 1.0 || p[1] < -1.0 || p[1] > 1.0) { + aabbInsideCascade = false; + break; + } + } + + receiver.lastCascade = i; + lastCascade = Math.max(lastCascade, i); + + if (aabbInsideCascade) { + break; + } + } + } + + return lastCascade + 1; + } + + receivers: {number: ShadowReceiver}; +} + export class ShadowRenderer { painter: Painter; _enabled: boolean; _shadowLayerCount: number; + _numCascadesToRender: number; _cascades: Array; + _groundShadowTiles: Array; + _receivers: ShadowReceivers; _depthMode: DepthMode; _uniformValues: UniformValues; shadowDirection: Vec3; @@ -70,7 +150,10 @@ export class ShadowRenderer { this.painter = painter; this._enabled = false; this._shadowLayerCount = 0; + this._numCascadesToRender = 0; this._cascades = []; + this._groundShadowTiles = []; + this._receivers = new ShadowReceivers(); this._depthMode = new DepthMode(painter.context.gl.LEQUAL, DepthMode.ReadWrite, [0, 1]); this._uniformValues = defaultShadowUniformValues(); @@ -91,6 +174,7 @@ export class ShadowRenderer { this._enabled = false; this._shadowLayerCount = 0; + this._receivers.clear(); if (!directionalLight || !directionalLight.properties) { return; @@ -195,6 +279,24 @@ export class ShadowRenderer { this._uniformValues['u_shadow_map_resolution'] = shadowMapResolution; this._uniformValues['u_shadowmap_0'] = TextureSlots.ShadowMap0; this._uniformValues['u_shadowmap_1'] = TextureSlots.ShadowMap0 + 1; + + // Render shadows on the ground plane as an extra layer of blended "tiles" + const tileCoverOptions = { + tileSize: 512, + renderWorldCopies: true + }; + + this._groundShadowTiles = painter.transform.coveringTiles(tileCoverOptions); + + const elevation = painter.transform.elevation; + for (const tileId of this._groundShadowTiles) { + let tileHeight = {min: 0, max: 0}; + if (elevation) { + const minMax = elevation.getMinMaxForTile(tileId); + if (minMax) tileHeight = minMax; + } + this.addShadowReceiver(tileId.toUnwrapped(), tileHeight.min, tileHeight.max); + } } get enabled(): boolean { @@ -216,9 +318,14 @@ export class ShadowRenderer { assert(painter.renderPass === 'shadow'); + // For each shadow receiver, compute how many cascades would need to be + // sampled for the VISIBLE part of the receiver to be fully covered by + // shadows. + this._numCascadesToRender = this._receivers.computeRequiredCascades(painter.transform.getFrustum(0), painter.transform.worldSize, this._cascades); + context.viewport.set([0, 0, shadowMapResolution, shadowMapResolution]); - for (let cascade = 0; cascade < cascadeCount; ++cascade) { + for (let cascade = 0; cascade < this._numCascadesToRender; ++cascade) { painter.currentShadowCascade = cascade; context.bindFramebuffer.set(this._cascades[cascade].framebuffer.framebuffer); @@ -259,21 +366,15 @@ export class ShadowRenderer { if (cutoffParams.shouldRenderCutoff) { baseDefines.push('RENDER_CUTOFF'); } - const program = painter.useProgram('groundShadow', null, baseDefines); - - // Render shadows on the ground plane as an extra layer of blended "tiles" - const tileCoverOptions = { - tileSize: 512, - renderWorldCopies: true - }; - const tiles = painter.transform.coveringTiles(tileCoverOptions); const shadowColor = calculateGroundShadowFactor(directionalLight, ambientLight); const depthMode = new DepthMode(context.gl.LEQUAL, DepthMode.ReadOnly, painter.depthRangeFor3D); - for (const id of tiles) { + for (const id of this._groundShadowTiles) { const unwrapped = id.toUnwrapped(); + const affectedByFog = painter.isTileAffectedByFog(id); + const program = painter.useProgram('groundShadow', {defines: baseDefines, overrideFog: affectedByFog}); this.setupShadows(unwrapped, program); @@ -426,6 +527,15 @@ export class ShadowRenderer { computePlane(corners[0], corners[3], corners[7]) ]; return tileShadowVolume; } + + addShadowReceiver(tileId: UnwrappedTileID, minHeight: number, maxHeight: number) { + this._receivers.add(tileId, Aabb.fromTileIdAndHeight(tileId, minHeight, maxHeight)); + } + + getMaxCascadeForTile(tileId: UnwrappedTileID): number { + const receiver = this._receivers.get(tileId); + return !!receiver && !!receiver.lastCascade ? receiver.lastCascade : 0; + } } function tileAabb(id: UnwrappedTileID, height: number, worldSize: number): Aabb { diff --git a/3d-style/source/model_loader.js b/3d-style/source/model_loader.js index 3635e26bb99..c522e966e8a 100644 --- a/3d-style/source/model_loader.js +++ b/3d-style/source/model_loader.js @@ -489,7 +489,7 @@ export default function convertModel(gltf: Object): Array { return resultNodes; } -export function convertB3dm(gltf: Object, zScale: number): Array { +export function process3DTile(gltf: Object, zScale: number): Array { const nodes = convertModel(gltf); for (const node of nodes) { for (const mesh of node.meshes) { diff --git a/3d-style/source/tiled_3d_model_worker_source.js b/3d-style/source/tiled_3d_model_worker_source.js index 8c28ef85f1f..3f13e02bbc2 100644 --- a/3d-style/source/tiled_3d_model_worker_source.js +++ b/3d-style/source/tiled_3d_model_worker_source.js @@ -11,7 +11,7 @@ import type { TileParameters, WorkerTileResult } from '../../src/source/worker_source.js'; -import {convertB3dm} from './model_loader.js'; +import {process3DTile} from './model_loader.js'; import {tileToMeter} from '../../src/geo/mercator_coordinate.js'; import Tiled3dModelBucket from '../data/bucket/tiled_3d_model_bucket.js'; import type {Bucket} from '../../src/data/bucket.js'; @@ -59,7 +59,7 @@ class Tiled3dWorkerTile { featureIndex.bucketLayerIDs = []; const gltf = await load3DTile(data).catch((err) => callback(new Error(err.message))); if (!gltf) return callback(new Error('Could not parse tile')); - const nodes = convertB3dm(gltf, 1.0 / tileToMeter(params.tileID.canonical)); + const nodes = process3DTile(gltf, 1.0 / tileToMeter(params.tileID.canonical)); const hasMapboxMeshFeatures = gltf.json.extensionsUsed && gltf.json.extensionsUsed.includes('MAPBOX_mesh_features'); for (const sourceLayerId in layerFamilies) { for (const family of layerFamilies[sourceLayerId]) { diff --git a/3d-style/util/loaders.js b/3d-style/util/loaders.js index 0d765ba39d3..daaba8f0ae6 100644 --- a/3d-style/util/loaders.js +++ b/3d-style/util/loaders.js @@ -276,11 +276,15 @@ export async function loadGLTF(url: string): Promise { } export async function load3DTile(data: ArrayBuffer): Promise { - const header = new Uint32Array(data, 0, 7); - const [/*magic*/, /*version*/, byteLen, featureTableJsonLen, featureTableBinLen, batchTableJsonLen/*, batchTableBinLen*/] = header; - const gltfOffset = header.byteLength + featureTableJsonLen + featureTableBinLen + batchTableJsonLen + featureTableBinLen; - if (byteLen !== data.byteLength || gltfOffset >= data.byteLength) { - warnOnce('Invalid b3dm header information.'); + const magic = new Uint32Array(data, 0, 1)[0]; + let gltfOffset = 0; + if (magic !== MAGIC_GLTF) { + const header = new Uint32Array(data, 0, 7); + const [/*magic*/, /*version*/, byteLen, featureTableJsonLen, featureTableBinLen, batchTableJsonLen/*, batchTableBinLen*/] = header; + gltfOffset = header.byteLength + featureTableJsonLen + featureTableBinLen + batchTableJsonLen + featureTableBinLen; + if (byteLen !== data.byteLength || gltfOffset >= data.byteLength) { + warnOnce('Invalid b3dm header information.'); + } } return decodeGLTF(data, gltfOffset); } diff --git a/CHANGELOG.md b/CHANGELOG.md index fd093997be8..a15358f9fc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,30 @@ -## 3.0.0-beta.4 +## 3.0.0-beta.5 Mapbox GL JS v3 enables the Mapbox Standard Style, a new realistic 3D lighting system, building shadows and many other visual enhancements, and an ergonomic API for using a new kind of rich, evolving, configurable map styles and seamless integration with custom data. You can get more information about the new features in the [Mapbox GL JS v3 migration guide](./MIGRATION_GUIDE_v3.md). Changes since `v3.0.0-beta.3`: ### ✨ Features and improvements +- Improve shadow and fog rendering performance. +- Slightly improve performance of 3D layers on highly pitched views by rendering front to back. +- Make zooming over dynamic terrain (that pops in as you zoom) smoother. +- Add `renderstart` event, which combined with `render` event can be used to measure rendering frame duration. + +### Bug fixes 🐞 + +- Fix shadows sometimes flickering when zooming in on the Standard style. +- Fix flickering when using GeoJSON `setData` to animate 3D models. +- Fix symbols elevated over 3D layers jumping to ground level and back during zoom. +- Fix an error when loading a 3D tile where multiple materials reference the same texture. +- Fix several edge cases when smoothing terrain under 3D landmarks. +- Fix `hillshade-illumination-direction` to align with light direction if `hillshade-illumination-anchor` is not set to `viewport`. +- Fix precision issues when rendering ground flood light. +- Fix styles with `fragment: false` not to be loaded as basemap imports. +- Fix an error on `map` `hasImage` and `updateImage` after the map was removed. + +## 3.0.0-beta.4 + +### ✨ Features and improvements + - Significantly improve map loading performance on views with 3D models. - Significantly reduce the loaded bundle size on 3D styles by implementing a custom 3D model parsing pipeline. - Improve shadow rendering performance. diff --git a/bench/benchmarks/worker_transfer.js b/bench/benchmarks/worker_transfer.js index fd9db65c7d0..693f92179ea 100644 --- a/bench/benchmarks/worker_transfer.js +++ b/bench/benchmarks/worker_transfer.js @@ -50,7 +50,7 @@ export default class WorkerTransfer extends Benchmark { }).then((tileResults) => { const payload = tileResults .concat(values(this.parser.icons)) - .concat(values(this.parser.glyphs)).map((obj) => serialize(obj, [])); + .concat(values(this.parser.glyphs)).map((obj) => serialize(obj, new Set())); this.payloadJSON = payload.map(barePayload); this.payloadTiles = payload.slice(0, tileResults.length); }); diff --git a/bench/gl-stats.html b/bench/gl-stats.html index 7e580e4da35..57da37649a5 100644 --- a/bench/gl-stats.html +++ b/bench/gl-stats.html @@ -8,12 +8,13 @@ let now = performance.now(); window.performance.now = () => now; - + + +