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;
-
+
+
+