Merge branch 'main' into edge_line_unique_VAO

This commit is contained in:
Gabby Getz 2025-09-09 14:03:36 -04:00 committed by GitHub
commit 3d801d1727
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1329 changed files with 70962 additions and 484 deletions

6
.git-blame-ignore-revs Normal file
View File

@ -0,0 +1,6 @@
# Format all code with prettier
2fd0e8f7e4212bd1e7084299187f70597a6bbfd8
# var -> const/let
8143df4436b79260e9861f63c6ac134d7910a818
# run prettier v3
09a719b8fb4616ecbcd7370e81dcdc998b64b6e2

View File

@ -15,7 +15,9 @@ cp cesium*.tgz ../test
cp Specs/test.*js ../test
cd ../test
npm install cesium*.tgz
npm install cesium-engine*.tgz
npm install cesium-widgets*.tgz
npm install cesium-1.*.tgz
NODE_ENV=development node test.cjs
NODE_ENV=production node test.cjs
node test.mjs
node test.mjs

View File

@ -22,6 +22,8 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPO: ${{ github.repository }}
GITHUB_SHA: ${{ github.sha }}
BASE_URL: /cesium/${{ github.ref_name }}/
DEPLOYED_URL: https://ci-builds.cesium.com/cesium/${{ github.ref_name }}/
steps:
- uses: actions/checkout@v5
- name: install node 22
@ -40,6 +42,8 @@ jobs:
run: npm pack --workspaces &> /dev/null
- name: build apps
run: npm run build-apps
- name: build sandcastle v2
run: npm run build-ci -w packages/sandcastle -- -l warn
- uses: ./.github/actions/verify-package
- name: deploy to s3
if: ${{ env.AWS_ACCESS_KEY_ID != '' }}

35
.github/workflows/sandcastle-dev.yml vendored Normal file
View File

@ -0,0 +1,35 @@
name: sandcastle-dev
on:
push:
branches:
- 'cesium.com'
jobs:
deploy:
runs-on: ubuntu-latest
env:
PROD: true
AWS_ACCESS_KEY_ID: ${{ secrets.PROD_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.PROD_AWS_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
BRANCH: ${{ github.ref_name }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPO: ${{ github.repository }}
GITHUB_SHA: ${{ github.sha }}
steps:
- uses: actions/checkout@v5
- name: install node 22
uses: actions/setup-node@v4
with:
node-version: '22'
- name: npm install
run: npm install
- name: build website release
run: npm run website-release
- name: build types
run: npm run build-ts
- name: build prod sandcastle
run: npm run build-prod -w packages/sandcastle -- -l warn
- name: deploy to dev-sandcastle.cesium.com
if: ${{ env.AWS_ACCESS_KEY_ID != '' }}
run: |
aws s3 sync Build/Sandcastle2/ s3://cesium-dev-sandcastle-website/ --cache-control "public, max-age=1800" --delete

View File

@ -1,4 +1,5 @@
/node_modules
packages/sandcastle/node_modules
/ThirdParty
/Tools/**

View File

@ -18,6 +18,7 @@
!**/*.html
!**/*.md
!**/*.ts
!**/*.tsx
# Re-ignore a few things caught above
@ -33,6 +34,9 @@ packages/widgets/Build/**
packages/widgets/index.js
packages/widgets/Source/ThirdParty/**
packages/sandcastle/node_modules/**
Apps/Sandcastle2/**
Specs/jasmine/**
Apps/Sandcastle/ThirdParty

View File

@ -6,6 +6,7 @@
"cesium.gltf-vscode",
"bierner.github-markdown-preview",
"DavidAnson.vscode-markdownlint",
"streetsidesoftware.code-spell-checker"
"streetsidesoftware.code-spell-checker",
"eamodio.gitlens"
]
}

View File

@ -29,5 +29,9 @@
"javascript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": false,
"javascript.format.placeOpenBraceOnNewLineForFunctions": false,
"javascript.format.placeOpenBraceOnNewLineForControlBlocks": false,
"glTF.defaultV2Engine": "Cesium"
"glTF.defaultV2Engine": "Cesium",
"gitlens.advanced.blame.customArguments": [
"--ignore-revs-file",
"${workspaceFolder}/.git-blame-ignore-revs"
],
}

13530
Apps/SampleData/Mars.czml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,276 @@
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"text": "Acidalia Planitia",
"destination": [2482963.8874656125, -1390493.34713116, 2003606.997020314],
"orientation": [2.9706346891645325, -0.39288074608352463, 0.00013399155612248137],
"description": "Acidalia Planitia is a plain on Mars, between the Tharsis volcanic province and Arabia Terra to the north of Valles Marineris, centered at 49.8°N 339.3°E. Most of this region is found in the Mare Acidalium quadrangle, but a small part is in the Ismenius Lacus quadrangle. The plain contains the famous Cydonia region at the contact with the heavily cratered highland terrain.",
"sourceURL": "https://en.wikipedia.org/wiki/Acidalia_Planitia",
"source": "Wikipedia",
"imageURL": "https://upload.wikimedia.org/wikipedia/commons/3/36/Acidalia_planitia_topo.jpg"
},
"geometry": {
"type": "Point",
"coordinates": [-28.039999999999999, 31.210000000000001]
}
},
{
"type": "Feature",
"properties": {
"text": "Alba Mons",
"destination": [-468768.84888828284, -2636983.410363647, 3193355.1570078987],
"orientation": [3.8060647475551765, -0.9801861222357124, 6.281240302469402],
"description": "Alba Mons is a volcano located in the northern Tharsis region of the planet Mars. It is the biggest volcano on Mars in terms of surface area, with volcanic flow fields that extend for at least 1,350 km (840 mi) from its summit. Although the volcano has a span comparable to that of the United States, it reaches an elevation of only 6.8 km (22,000 ft) at its highest point.[6] This is about one-third the height of Olympus Mons, the tallest volcano on the planet.",
"sourceURL": "https://en.wikipedia.org/wiki/Alba_Mons",
"source": "Wikipedia",
"imageURL": "https://upload.wikimedia.org/wikipedia/commons/a/a9/Alba_Mons_Viking_DIM.jpg"
},
"geometry": {
"type": "Point",
"coordinates": [250.4, 40.5]
}
},
{
"type": "Feature",
"properties": {
"text": "Arabia Terra",
"destination": [3173625.979675759, -925087.3184430292, 1281260.6050768741],
"orientation": [2.210739895697892, -0.4557150936475227, 0.0003490992512000801],
"description": "Arabia Terra is a large upland region in the north of Mars that lies mostly in the Arabia quadrangle, but a small part is in the Mare Acidalium quadrangle. It is densely cratered and heavily eroded. This battered topography indicates great age, and Arabia Terra is presumed to be one of the oldest terrains on the planet. It covers as much as 4,500 km (2,800 mi) at its longest extent, centered roughly at 21°N 6°E with its eastern and southern regions rising 4 km (13,000 ft) above the north-west. Alongside its many craters, canyons wind through the Arabia Terra, many emptying into the large northern lowlands of the planet, which borders Arabia Terra to the north.",
"sourceURL": "https://en.wikipedia.org/wiki/Arabia_Terra",
"source": "Wikipedia",
"imageURL": "https://upload.wikimedia.org/wikipedia/commons/thumb/4/41/Eden_Patera_THEMIS_day_IR.jpg/2560px-Eden_Patera_THEMIS_day_IR.jpg"
},
"geometry": {
"type": "Point",
"coordinates": [-11.870000000000001, 18.02]
}
},
{
"type": "Feature",
"properties": {
"text": "Elysium Volcanic Region",
"destination": [-3390705.18743154, 1723996.087561009, 1009387.4502178654],
"orientation": [5.95184286595793, -0.8844046974415081, 0.005041989289460425],
"description": "Elysium, located in the Elysium and Cebrenia quadrangles, is the second largest volcanic region on Mars, after Tharsis. The region includes the volcanoes (from north to south) Hecates Tholus, Elysium Mons and Albor Tholus. The province is centered roughly on Elysium Mons at 24.7°N 150°E. Elysium Planitia is a broad plain to the south of Elysium, centered at 3.0°N 154.7°E. Another large volcano, Apollinaris Mons, lies south of Elysium Planitia and is not part of the province. Besides having large volcanoes, Elysium has several areas with long trenches, called fossa or fossae (plural) on Mars.",
"sourceURL": "https://en.wikipedia.org/wiki/Elysium_(volcanic_province)",
"source": "Wikipedia",
"imageURL": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/96/Elysium_MOLA_zoom_64.jpg/2560px-Elysium_MOLA_zoom_64.jpg"
},
"geometry": {
"type": "Point",
"coordinates": [150, 24.7]
}
},
{
"type": "Feature",
"properties": {
"text": "Gale Crater (Curiosity)",
"destination": [-2791535.3207, 2189446.3646, -616541.9385],
"orientation": [5.5684, -0.636, 0.0],
"description": "Curiosity is a car-sized Mars rover that is exploring Gale crater and Mount Sharp on Mars as part of NASA's Mars Science Laboratory (MSL) mission. Launched in 2011 and landed the following year, the rover continues to operate more than a decade after its original two-year mission. Mission goals include an investigation of the Martian climate and geology, an assessment of whether the selected field site inside Gale has ever offered environmental conditions favorable for microbial life (including investigation of the role of water), and planetary habitability studies in preparation for human exploration.",
"sourceURL": "https://en.wikipedia.org/wiki/Curiosity_(rover)",
"source": "Wikipedia",
"imageURL": "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f3/Curiosity_Self-Portrait_at_%27Big_Sky%27_Drilling_Site.jpg/1280px-Curiosity_Self-Portrait_at_%27Big_Sky%27_Drilling_Site.jpg"
},
"geometry": {
"type": "Point",
"coordinates": [137.8, -5.4]
}
},
{
"type": "Feature",
"properties": {
"text": "Jezero Crater (Perseverance)",
"destination": [596565.83388002, 3353778.00627982, 923026.06801835],
"orientation": [5.59008567, -0.59978189, 0.00060998],
"description": "Perseverance is a car-sized Mars rover designed to explore the Jezero crater on Mars as part of NASA's Mars 2020 mission. Perseverance has a similar design to its predecessor rover, Curiosity, although it was moderately upgraded. It carries seven primary payload instruments, nineteen cameras, and two microphones. The rover also carried the mini-helicopter Ingenuity to Mars, an experimental technology testbed that made the first powered aircraft flight on another planet on April 19, 2021.",
"sourceURL": "https://en.wikipedia.org/wiki/Perseverance_(rover)",
"source": "Wikipedia",
"imageURL": "https://upload.wikimedia.org/wikipedia/commons/a/a4/Perseverance-Selfie-at-Rochette-Horizontal-V2.gif"
},
"geometry": {
"type": "Point",
"coordinates": [77.58, 18.38]
}
},
{
"type": "Feature",
"properties": {
"text": "Marth Crater",
"destination": [3327216.4788566492, -218466.2408178681, 946551.8768631018],
"orientation": [3.331697871424215, -0.29366873064196347, 6.281659377334691],
"description": "In the best-selling novel \"The Martian\" and the movie based on it, stranded astronaut Mark Watney's adventures take him to the rim of Mawrth Crater. The crater rim is not very distinct, and from the Martian surface it would be quite difficult to tell that you are even on the rim of a crater. The terrain is hummocky and rolling, punctuated by smaller impact craters and wind-blown drifts of sand or dust.",
"sourceURL": "https://science.nasa.gov/resource/western-edge-of-mars-marth-crater-a-movie-location/",
"source": "NASA",
"imageURL": "https://assets.science.nasa.gov/dynamicimage/assets/science/psd/mars/resources/detail_files/7/7500_mars-the-martian-marth-crater-PIA19915-full2.jpg?w=1163&h=720&fit=clip&crop=faces%2Cfocalpoint"
},
"geometry": {
"type": "Point",
"coordinates": [-4.2680995373239057, 12.660926420007353]
}
},
{
"type": "Feature",
"properties": {
"text": "Mawrth Vallis",
"destination": [2880337.0763509087, -1422487.4666314998, 1778960.0256092676],
"orientation": [2.059623110400561, -0.6237026798827308, 0.0017318644099715286],
"description": "Mawrth Vallis (Mawrth means \"Mars\" in Welsh) is a valley on Mars, located in the Oxia Palus quadrangle at 22.3°N, 343.5°E with an elevation approximately two kilometers below datum. Situated between the southern highlands and northern lowlands, the valley is a channel formed by massive flooding which occurred in Mars' ancient past. It is an ancient water outflow channel with light-colored clay-rich rocks.",
"sourceURL": "https://en.wikipedia.org/wiki/Mawrth_Vallis",
"source": "Wikipedia",
"imageURL": "https://upload.wikimedia.org/wikipedia/commons/thumb/8/8f/Mawrth_Vallis_martian_mosaic.jpg/1280px-Mawrth_Vallis_martian_mosaic.jpg"
},
"geometry": {
"type": "Point",
"coordinates": [-20.5, 26.350000000000001]
}
},
{
"type": "Feature",
"properties": {
"text": "Olympus Mons",
"destination": [-2549089.8672, -2720744.9822, 604987.2427],
"orientation": [6.2232, -0.7135, 0.0],
"description": "Olympus Mons is a large shield volcano on Mars. It is over 21.9 km high as measured by the Mars Orbiter Laser Altimeter (MOLA), about 2.5 times the elevation of Mount Everest above sea level. It is Mars's tallest volcano, its tallest planetary mountain, and is approximately tied with Rheasilvia on Vesta as the tallest mountain currently discovered in the Solar System. It is associated with the volcanic region of Tharsis Montes. It last erupted 25 million years ago.",
"sourceURL": "https://en.wikipedia.org/wiki/Olympus_Mons",
"source": "Wikipedia",
"imageURL": "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c2/Olympus_Mons_-_ESA_Mars_Express_-_Flickr_-_Andrea_Luck.png/2880px-Olympus_Mons_-_ESA_Mars_Express_-_Flickr_-_Andrea_Luck.png"
},
"geometry": {
"type": "Point",
"coordinates": [226.2, 18.65]
}
},
{
"type": "Feature",
"properties": {
"text": "Meridiani Planum (Opportunity)",
"destination": [3528252.21590355, -6454.59498861, -190095.70073928],
"orientation": [4.91589709, -0.43114195, 5.97503936],
"description": "Opportunity, also known as MER-B (Mars Exploration Rover - B) or MER-1, and nicknamed Oppy, is a robotic rover that was active on Mars from 2004 until 2018. Opportunity was operational on Mars for 5111 sols (14 years, 138 days on Earth). Launched on July 7, 2003, as part of NASA's Mars Exploration Rover program, it landed in Meridiani Planum on January 25, 2004, three weeks after its twin, Spirit (MER-A), touched down on the other side of the planet. With a planned 90-sol duration of activity (slightly less than 92.5 Earth days), Spirit functioned until it got stuck in 2009 and ceased communications in 2010, while Opportunity was able to stay operational for 5111 sols after landing, maintaining its power and key systems through continual recharging of its batteries using solar power, and hibernating during events such as dust storms to save power. This careful operation allowed Opportunity to operate for 57 times its designed lifespan, exceeding the initial plan by 14 years, 47 days (in Earth time). By June 10, 2018, when it last contacted NASA, the rover had traveled a distance of 45.16 kilometers (28.06 miles).",
"sourceURL": "https://en.wikipedia.org/wiki/Opportunity_(rover)",
"source": "Wikipedia",
"imageURL": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d8/NASA_Mars_Rover.jpg/2560px-NASA_Mars_Rover.jpg"
},
"geometry": {
"type": "Point",
"coordinates": [354.4734, -1.9462]
}
},
{
"type": "Feature",
"properties": {
"text": "Schiaparelli Crater",
"destination": [3513269.5521, 664880.5211, -498902.5607],
"orientation": [0.8318, -0.5309, 0.0],
"description": "Schiaparelli is an impact crater on Mars, located near the planet's equator at latitude 3° south and longitude 344° in the Sinus Sabaeus quadrangle. It measures approximately 459 kilometers (285-miles) in diameter and was named after Italian astronomer Giovanni Schiaparelli. A crater within Schiaparelli shows many layers that may have formed by the wind, volcanoes, or deposition under water.",
"sourceURL": "https://en.wikipedia.org/wiki/Schiaparelli_(Martian_crater)",
"source": "Wikipedia",
"imageURL": "https://upload.wikimedia.org/wikipedia/commons/6/6e/SchiaparelliMOLA.jpeg"
},
"geometry": {
"type": "Point",
"coordinates": [16.7, -2.7]
}
},
{
"type": "Feature",
"properties": {
"text": "Ares Vallis (Sojourner)",
"destination": [3002746.35818072, -1674150.97664144, 1015018.06707550],
"orientation": [5.27982625, -0.63635512, 0.00167013],
"description": "The robotic Sojourner rover reached Mars on July 4, 1997 as part of the Mars Pathfinder mission. Sojourner was operational on Mars for 92 sols (95 Earth days), and was the first wheeled vehicle to operate on an astronomical object other than the Earth or Moon. The landing site was in the Ares Vallis channel in the Chryse Planitia region of the Oxia Palus quadrangle.",
"sourceURL": "https://en.wikipedia.org/wiki/Sojourner_(rover)",
"source": "Wikipedia",
"imageURL": "https://upload.wikimedia.org/wikipedia/commons/3/3a/Sojourner_on_Mars_PIA01122.jpg"
},
"geometry": {
"type": "Point",
"coordinates": [-33.22, 19.13]
}
},
{
"type": "Feature",
"properties": {
"text": "Gusev Crater (Spirit)",
"destination": [-3318104.44189717, 143085.57854143, -986175.00294181],
"orientation": [5.53211415, -0.38597730, 0.00036546],
"description": "Spirit, also known as MER-A (Mars Exploration Rover - A) or MER-2, is a Mars robotic rover, active from 2004 to 2010. Spirit was operational on Mars for 2208 sols or 3.3 Martian years (2269 days; 6 years, 77 days). It was one of two rovers of NASA's Mars Exploration Rover Mission managed by the Jet Propulsion Laboratory (JPL). Spirit landed successfully within the impact crater Gusev on Mars at 04:35 Ground UTC on January 4, 2004, three weeks before its twin, Opportunity (MER-B), which landed on the other side of the planet. Its name was chosen through a NASA-sponsored student essay competition. The rover got stuck in a \"sand trap\" in late 2009 at an angle that hampered recharging of its batteries; its last communication with Earth was on March 22, 2010.",
"sourceURL": "https://en.wikipedia.org/wiki/Spirit_(rover)",
"source": "Wikipedia",
"imageURL": "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f8/KSC-03PD-0786.jpg/2880px-KSC-03PD-0786.jpg"
},
"geometry": {
"type": "Point",
"coordinates": [175.472636, -14.5684]
}
},
{
"type": "Feature",
"properties": {
"text": "Terra Meridiani",
"destination": [3527603.695520859, -689644.450481964, 483920.0646853684],
"orientation": [2.04380225538318, -0.5700974895821371, 6.2830672295985615],
"description": "Terra Meridiani is a large plain straddling the equator of Mars. The plain sits on top of an enormous body of sediments that contains bound water. The iron oxide in the spherules is crystalline (grey) hematite (Fe2O3). The Meridiani Planum is one of the most thoroughly investigated regions of Mars. Many studies were carried out by the scientists involved with NASA's Mars Exploration Rover (MER) Opportunity.",
"sourceURL": "https://en.wikipedia.org/wiki/Terra_Meridiani",
"imageURL": "https://upload.wikimedia.org/wikipedia/commons/6/6b/Sol322B.Smooth.Sheet.bedforms.close.to.heat.shield.crp.jpg"
},
"geometry": {
"type": "Point",
"coordinates": [-4.8900000000000006, 4.6100000000000003]
}
},
{
"type": "Feature",
"properties": {
"text": "\"The Hab\"",
"destination": [2482963.8874656125, -1390493.34713116, 2003606.997020314],
"orientation": [2.9706346891645325, -0.39288074608352463, 0.00013399155612248137],
"description": "(Spoilers!) In the novel and movie The Martian, \"The Hab\" is a habitat on Mars, in Acidalia Planitia, where astronaut Mark Watney lives and works after being stranded there alone following a dust storm on Sol 18 of the Ares III mission.",
"sourceURL": "https://en.wikipedia.org/wiki/The_Martian_(film)",
"source": "Wikipedia",
"imageURL": "https://upload.wikimedia.org/wikipedia/commons/a/a7/PIA19913-MarsLandingSite-Ares3Mission-TheMartian-2015Film-20150517.jpg"
},
"geometry": {
"type": "Point",
"coordinates": [-28.57, 31.27]
}
},
{
"type": "Feature",
"properties": {
"text": "Valles Marineris",
"destination": [1962773.9022, -2712650.0794, -1111175.4271],
"orientation": [5.4566, -0.4563, 0.0],
"description": "Valles Marineris (Latin for Mariner Valleys, named after the Mariner 9 Mars orbiter of 1971-72 which discovered it) is a system of canyons that runs along the Martian surface east of the Tharsis region. At more than 4,000 km (2,500 mi) long, 200 km (120 mi) wide and up to 7 km (23,000 ft) deep, Valles Marineris is the largest canyon in the Solar System.",
"sourceURL": "https://en.wikipedia.org/wiki/Valles_Marineris",
"source": "Wikipedia",
"imageURL": "https://upload.wikimedia.org/wikipedia/commons/thumb/5/56/Mars_Valles_Marineris.jpeg/1200px-Mars_Valles_Marineris.jpeg"
},
"geometry": {
"type": "Point",
"coordinates": [-59.2, -13.9]
}
},
{
"type": "Feature",
"properties": {
"text": "\"Watney Triangle\"",
"destination": [3286701.2244228586, -654675.1932169931, 1071261.3465401786],
"orientation": [2.215118283105494, -0.4393789184089627, 6.283005739536628],
"description": "(Spoilers!) In the novel and movie The Martian, the \"Watney Triangle\" is a region of treacherous craters (Trouvelot, Becquerel, and Marth) that astronaut Mark Watney must traverse on his way to the Ares 4 landing site at Schiaparelli Crater.",
"sourceURL": "https://en.wikipedia.org/wiki/The_Martian_(film)",
"source": "Wikipedia",
"imageURL": "https://upload.wikimedia.org/wikipedia/commons/9/99/TrouvelotMartianCrater.jpg"
},
"geometry": {
"type": "Point",
"coordinates": [-7.6717428056727446, 15.243062604535314]
}
}
]
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -49,6 +49,15 @@ body {
float: right;
}
#banner {
text-align: center;
background: #daf6ff;
a {
color: blue;
}
}
#codeContainer {
width: 40%;
}
@ -186,25 +195,6 @@ a.linkButton:hover {
overflow: auto !important;
}
.feedback {
width: 250px;
.future-banner {
display: flex;
justify-content: center;
align-items: stretch;
text-align: center;
flex-direction: column;
h3 {
margin: 0 1em;
}
p {
margin: 1em 1.5em;
}
}
}
.claro .dijitTabContainerTop-tabs .dijitTabChecked .dijitTabContent {
background-position: 0 -103px;
}

View File

@ -0,0 +1,555 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no"
/>
<meta
name="description"
content="Mars terrain visualized using 3D Tiles, tiled and hosted by Cesium ion, and shown with points of interest and additional data layers."
/>
<meta name="cesium-sandcastle-labels" content="Showcases, ion Assets, 3D Tiles" />
<title>Cesium Mars</title>
<script type="text/javascript" src="../Sandcastle-header.js"></script>
<script
type="text/javascript"
src="../../../Build/CesiumUnminified/Cesium.js"
nomodule
></script>
<script type="module" src="../load-cesium-es6.js"></script>
</head>
<body class="sandcastle-loading" data-sandcastle-bucket="bucket-requirejs.html">
<style>
@import url(../templates/bucket.css);
#toolbar {
background: rgba(42, 42, 42, 0.8);
padding: 4px;
border-radius: 4px;
}
#toolbar input {
vertical-align: middle;
padding-top: 2px;
padding-bottom: 2px;
}
#toolbar .header {
font-weight: bold;
}
/* Styles for indicating to the user to press the play button */
.cesium-animation-rectButton.highlight-animation .cesium-animation-buttonGlow {
display: block;
fill: #fff;
/* keep the built-in blur and add extra glow layers */
filter: url(#animation_blurred) drop-shadow(0 0 3px #aef)
drop-shadow(0 0 3px #fff);
animation: highlight-animation-button 1.2s ease-in-out infinite;
}
.cesium-animation-rectButton.highlight-animation .cesium-animation-buttonMain {
stroke: #fff;
stroke-width: 3;
}
.cesium-animation-rectButton.highlight-animation .cesium-animation-buttonPath {
fill: #fff;
}
.cesium-animation-shuttleRingG.highlight-animation
.cesium-animation-shuttleRingBack {
stroke: #fff;
stroke-width: 6;
filter: drop-shadow(0 0 3px #aef) drop-shadow(0 0 3px rgba(174, 238, 255, 0.95));
animation: highlight-animation-ring 1.2s ease-in-out infinite;
}
.cesium-animation-shuttleRingG.highlight-animation
.cesium-animation-shuttleRingSwoosh
line {
stroke: #fff;
stroke-opacity: 0.8;
}
@keyframes highlight-animation-button {
0% {
opacity: 0.3;
stroke-width: 0;
}
50% {
opacity: 1;
stroke-width: 4;
}
100% {
opacity: 0.3;
stroke-width: 0;
}
}
@keyframes highlight-animation-ring {
0% {
stroke-opacity: 0.25;
stroke: #333;
}
50% {
stroke-opacity: 1;
stroke: #fff;
}
100% {
stroke-opacity: 0.25;
stroke: #333;
}
}
</style>
<div id="cesiumContainer" class="fullSize"></div>
<div id="loadingOverlay"><h1>Loading...</h1></div>
<div id="toolbar"></div>
<template id="roverHelpRowTemplate1">
<tr>
<td>
<img
data-src="Widgets/Images/NavigationHelp/MouseLeft.svg"
style="height: 48px; width: 48px"
alt="Left mouse button"
/>
</td>
<td>
<div class="cesium-navigation-help-pan">Track Rover</div>
<div class="cesium-navigation-help-detail">
Double click on a rover to track it
</div>
</td>
</tr>
</template>
<template id="roverHelpRowTemplate2">
<tr>
<td>
<svg width="48" height="48" viewBox="0 0 32 32" aria-label="Play" role="img">
<path
transform="translate(32,32) scale(0.85) translate(-32,-32)"
d="M6.684,25.682L24.316,15.5L6.684,5.318V25.682z"
fill="#ffffff"
/>
</svg>
</td>
<td>
<div class="cesium-navigation-help-zoom">Play Animation</div>
<div class="cesium-navigation-help-detail">
Press play on the timeline to watch the rover move
</div>
</td>
</tr>
</template>
<script id="cesium_sandcastle_script">
window.startup = async function (Cesium) {
"use strict";
//Sandcastle_Begin
Cesium.Ellipsoid.default = Cesium.Ellipsoid.MARS;
const viewer = new Cesium.Viewer("cesiumContainer", {
terrainProvider: false,
baseLayer: false,
baseLayerPicker: false,
geocoder: false,
shadows: false,
globe: new Cesium.Globe(Cesium.Ellipsoid.MARS),
skyBox: Cesium.SkyBox.createEarthSkyBox(),
skyAtmosphere: new Cesium.SkyAtmosphere(Cesium.Ellipsoid.MARS),
});
viewer.scene.globe.show = false;
const scene = viewer.scene;
const clock = viewer.clock;
const navHelp = viewer.navigationHelpButton;
// Adjust the default atmosphere coefficients to be more Mars-like
scene.skyAtmosphere.atmosphereMieCoefficient = new Cesium.Cartesian3(
9.0e-5,
2.0e-5,
1.0e-5,
);
scene.skyAtmosphere.atmosphereRayleighCoefficient = new Cesium.Cartesian3(
9.0e-6,
2.0e-6,
1.0e-6,
);
scene.skyAtmosphere.atmosphereRayleighScaleHeight = 9000;
scene.skyAtmosphere.atmosphereMieScaleHeight = 2700.0;
scene.skyAtmosphere.saturationShift = -0.1;
scene.skyAtmosphere.perFragmentAtmosphere = true;
// Adjust postprocess settings for brighter and richer features
const bloom = viewer.scene.postProcessStages.bloom;
bloom.enabled = true;
bloom.uniforms.brightness = -0.5;
bloom.uniforms.stepSize = 1.0;
bloom.uniforms.sigma = 3.0;
bloom.uniforms.delta = 1.5;
scene.highDynamicRange = true;
viewer.scene.postProcessStages.exposure = 1.5;
// Load Mars tileset
try {
const tileset = await Cesium.Cesium3DTileset.fromIonAssetId(3644333, {
enableCollision: true,
});
viewer.scene.primitives.add(tileset);
} catch (error) {
console.log(error);
}
// Load the rovers and path from The Martian (by Andy Weir), from CZML data source.
let curiosity, perseverance, ingenuity, theMartianJourney;
try {
const dataSource = await Cesium.CzmlDataSource.load(
"../../SampleData/Mars.czml",
);
viewer.dataSources.add(dataSource);
const roverMenuEntries = [
{
text: "Fly to rover...",
onselect: () => {},
},
];
const onSelectRover = (rover) => {
reset();
const roverAnimStartIso = rover.properties.animationStartTime.getValue(
Cesium.JulianDate.now(),
);
clock.multiplier = 604800;
clock.currentTime = new Cesium.JulianDate.fromIso8601(roverAnimStartIso);
viewer.timeline.zoomTo(rover.availability.start, rover.availability.stop);
const boundingSphere = new Cesium.BoundingSphere(
rover.position.getValue(clock.currentTime),
5000.0,
);
scene.camera.flyToBoundingSphere(boundingSphere, {
offset: new Cesium.HeadingPitchRoll(4.9791, -0.5294, 0.0),
easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT,
maximumHeight: 5e6,
pitchAdjustHeight: 2.5e6,
duration: 3.0,
complete: function () {
highlightAnimationViewModel(); // Draw attention to the play button
navHelp.viewModel.showInstructions = true;
},
});
};
const setupRover = function (entityId, startSol, outRover) {
outRover = dataSource.entities.getById(entityId);
const julianDateToSol = createJulianDateToSolConverter(
outRover.availability.start,
startSol,
);
outRover.label.text = new Cesium.CallbackProperty(function (time) {
return julianDateToSol(time);
}, false);
const roverPath = dataSource.entities.getById(`${entityId}Path`);
roverPath.polyline.width = createWidthCallbackProperty(
new Cesium.NearFarScalar(0.0, 15.0, 1.0e5, 0.0),
);
roverMenuEntries.push({
text: entityId,
onselect: () => onSelectRover(outRover),
});
return outRover;
};
curiosity = setupRover("Curiosity", 3, curiosity);
perseverance = setupRover("Perseverance", 13, perseverance);
ingenuity = dataSource.entities.getById("Ingenuity"); // Only for viewing - no data for flight paths
theMartianJourney = dataSource.entities.getById("TheMartianJourney");
theMartianJourney.polyline.width = createWidthCallbackProperty(
new Cesium.NearFarScalar(0.0, 10.0, 1.0e7, 0.0),
);
theMartianJourney.rectangle.material = new Cesium.ImageMaterialProperty({
image: createCanvasAsTexture('Mark Watney\'s Journey in "The Martian"'),
transparent: true,
});
roverMenuEntries.push({
text: '"The Martian" Journey',
onselect: () => {
reset();
viewer.zoomTo(theMartianJourney);
},
});
Sandcastle.addToolbarMenu(roverMenuEntries);
} catch (error) {
console.log(`Error loading CZML: ${error}`);
}
// For changing the width of polylines based on distance from the camera
function createWidthCallbackProperty(nearFarScalar) {
return new Cesium.CallbackProperty(function () {
const distance = viewer.camera.positionCartographic.height;
let t =
(distance - nearFarScalar.near) / (nearFarScalar.far - nearFarScalar.near);
t = Cesium.Math.clamp(t, 0.0, 1.0);
return Cesium.Math.lerp(nearFarScalar.nearValue, nearFarScalar.farValue, t);
}, false);
}
// Converts a Julian date to a Mars Sol number, given a start date / sol number
function createJulianDateToSolConverter(startJulianDate, startSol) {
return function (julianDate) {
const secondsPerSol = 24 * 60 * 60 + 39 * 60 + 35;
const differenceInSeconds = Cesium.JulianDate.secondsDifference(
julianDate,
startJulianDate,
);
const solNumber = Math.floor(differenceInSeconds / secondsPerSol) + startSol;
return `Sol ${solNumber}`;
};
}
// Load points of interest from GeoJSON data source
try {
const dataSource = await Cesium.GeoJsonDataSource.load(
"../../SampleData/MarsPointsOfInterest.geojson",
);
viewer.dataSources.add(dataSource);
const onSelectLandmark = (landmark) => {
reset();
scene.camera.flyTo(landmark);
};
const landmarkMenuEntries = [
{
text: "Fly to landmark...",
onselect: () => {},
},
];
const entities = dataSource.entities.values;
entities.forEach((entity) => {
entity.label = new Cesium.LabelGraphics({
text: entity.properties.text,
font: "18pt Verdana",
outlineColor: Cesium.Color.DARKSLATEGREY,
outlineWidth: 2,
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
pixelOffset: new Cesium.Cartesian2(0, -22),
scaleByDistance: new Cesium.NearFarScalar(1.5e2, 1.0, 1.5e7, 0.5),
translucencyByDistance: new Cesium.NearFarScalar(2.5e7, 1.0, 4.0e7, 0.0),
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
disableDepthTestDistance: new Cesium.CallbackProperty(() => {
return Cesium.Cartesian3.magnitude(scene.camera.positionWC);
}, false),
});
entity.point = new Cesium.PointGraphics({
pixelSize: 10,
color: Cesium.Color.fromBytes(243, 242, 99),
outlineColor: Cesium.Color.fromBytes(219, 218, 111),
outlineWidth: 2,
scaleByDistance: new Cesium.NearFarScalar(1.5e3, 1.0, 4.0e7, 0.1),
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
disableDepthTestDistance: new Cesium.CallbackProperty(() => {
return Cesium.Cartesian3.magnitude(scene.camera.positionWC);
}, false),
});
entity.name = entity.properties.text.getValue();
entity.description = createPickedFeatureDescription(entity);
const flyToDestination = new Cesium.Cartesian3.fromArray(
entity.properties.destination.getValue(),
);
const orientationArray = entity.properties.orientation.getValue();
const flyToOrientation = new Cesium.HeadingPitchRoll(
orientationArray[0],
orientationArray[1],
orientationArray[2],
);
landmarkMenuEntries.push({
text: entity.properties.text.getValue(),
onselect: () =>
onSelectLandmark({
destination: flyToDestination,
orientation: flyToOrientation,
easingFunction: Cesium.EasingFunction.CUBIC_IN_OUT,
maximumHeight: 5e6,
pitchAdjustHeight: 2.5e6,
duration: 3.0,
complete: function () {
viewer.selectedEntity = entity;
viewer.infoBox.viewModel.showInfo = true;
},
}),
});
});
Sandcastle.addToolbarMenu(landmarkMenuEntries);
} catch (error) {
console.log(`Error loading GeoJSON: ${error}`);
}
// Spin Mars on first load but disable the spinning upon any input
const rotationSpeed = Cesium.Math.toRadians(0.1);
const removeRotation = viewer.scene.postRender.addEventListener(
function (scene, time) {
viewer.scene.camera.rotateRight(rotationSpeed);
},
);
const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
handler.setInputAction(
() => removeRotation(),
Cesium.ScreenSpaceEventType.LEFT_DOWN,
);
handler.setInputAction(
() => removeRotation(),
Cesium.ScreenSpaceEventType.RIGHT_DOWN,
);
handler.setInputAction(
() => removeRotation(),
Cesium.ScreenSpaceEventType.MIDDLE_DOWN,
);
handler.setInputAction(() => removeRotation(), Cesium.ScreenSpaceEventType.WHEEL);
// For drawing attention to the play button in the animation view model
function highlightAnimationViewModel() {
if (clock.shouldAnimate) {
// Animation already playing
return;
}
const playPath =
viewer.animation.container.querySelector("#animation_pathPlay");
const playButton = playPath.closest("g.cesium-animation-rectButton");
const ringG = viewer.animation.container.querySelector(
".cesium-animation-shuttleRingG",
);
playButton.classList.add("highlight-animation");
ringG.classList.add("highlight-animation");
playButton.addEventListener("click", removeHighlight, { once: true });
setTimeout(removeHighlight, 30000); // Remove after 30 seconds if not clicked
}
function removeHighlight() {
const playPath =
viewer.animation.container.querySelector("#animation_pathPlay");
const playButton = playPath.closest("g.cesium-animation-rectButton");
const ringG = viewer.animation.container.querySelector(
".cesium-animation-shuttleRingG",
);
playButton.classList.remove("highlight-animation");
ringG.classList.remove("highlight-animation");
}
function reset() {
clock.multiplier = 1;
viewer.selectedEntity = undefined;
viewer.trackedEntity = undefined;
viewer.timeline.zoomTo(clock.startTime, clock.stopTime);
removeRotation();
removeHighlight();
}
// Add a listener for when the home button is clicked.
viewer.homeButton.viewModel.command.beforeExecute.addEventListener(
function (commandInfo) {
reset();
},
);
// When animating, if the multiplier is very high (which is necessary to see rover movement),
// model lighting flickers distractingly, so disable it
const entitiesToDisableLightingFor = [curiosity, perseverance, ingenuity];
Cesium.knockout
.getObservable(viewer.clockViewModel, "shouldAnimate")
.subscribe(function (shouldAnimate) {
if (shouldAnimate && clock.multiplier >= 100000) {
entitiesToDisableLightingFor.forEach(function (entity) {
entity.model.lightColor = new Cesium.Color(0, 0, 0);
});
} else {
entitiesToDisableLightingFor.forEach(function (entity) {
entity.model.lightColor = new Cesium.Color(1, 1, 1);
});
}
});
// To create a rectangle with text that conforms to the terrain, we can create a canvas
// with text and use it as a texture on a rectangle entity.
function createCanvasAsTexture(text) {
const canvas = document.createElement("canvas");
canvas.width = 1024;
canvas.height = 256;
const ctx = canvas.getContext("2d");
// Background
ctx.fillStyle = "rgba(0, 0, 0, 0)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Text
ctx.font = "36px sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.strokeStyle = "rgba(0,0,0,0.1)";
ctx.lineWidth = 1;
ctx.fillStyle = "#ffffff";
ctx.strokeText(text, canvas.width / 2, canvas.height / 2);
ctx.fillText(text, canvas.width / 2, canvas.height / 2);
return canvas;
}
// Create the HTML that will be put into the info box that shows
// information about the currently selected feature
function createPickedFeatureDescription(entity) {
return `<img\
width="50%"\
style="float:left; margin: 0 1em 1em 0;"\
src=${entity.properties.imageURL}>\
<p>${entity.properties.description}</p>\
<p>\
Source: \
<a style="color: WHITE"\
target="_blank"\
href="${entity.properties.sourceURL}">${entity.properties.source}</a>\
</p>`;
}
// Inject instructions for interacting with the rovers into the navigation help menu
function addRoverInstructionsToNavMenu() {
const div = document.querySelector(
".cesium-click-navigation-help.cesium-navigation-help-instructions",
);
const table = div.querySelector("table");
const instructions1 = document.getElementById("roverHelpRowTemplate1");
const instructions1Clone = instructions1.content.cloneNode(true);
const img = instructions1Clone.querySelector("img[data-src]");
img.src = Cesium.buildModuleUrl(img.dataset.src);
table.tBodies[0].appendChild(instructions1Clone);
const instructions2 = document.getElementById("roverHelpRowTemplate2");
const instructions2Clone = instructions2.content.cloneNode(true);
table.tBodies[0].appendChild(instructions2Clone);
}
addRoverInstructionsToNavMenu();
//Sandcastle_End
};
if (typeof Cesium !== "undefined") {
window.startupCalled = true;
window.startup(Cesium).catch((error) => {
"use strict";
console.error(error);
});
Sandcastle.finishedLoading();
}
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -76,6 +76,15 @@
data-dojo-type="dijit.layout.BorderContainer"
data-dojo-props="design: 'headline', gutters: true, liveSplitters: true"
>
<div
id="banner"
data-dojo-type="dijit.layout.ContentPane"
data-dojo-props="region: 'top'"
>
<a href="https://dev-sandcastle.cesium.com"
>Try the new version of Sandcastle today!</a
>
</div>
<div
id="toolbar"
data-dojo-type="dijit.Toolbar"
@ -341,36 +350,8 @@
</div>
</div>
</div>
<div
class="feedback"
data-dojo-type="dijit.layout.TabContainer"
data-dojo-props="region: 'right'"
>
<div
class="future-banner"
data-dojo-type="dijit.layout.ContentPane"
data-dojo-props="title: 'Feedback'"
>
<h3>Help needed</h3>
<p>
We're currently gathering user feedback to make Sandcastle even
better.
</p>
<button data-dojo-type="dijit/form/Button" type="button">
Please share your thoughts!
<script
type="dojo/on"
data-dojo-event="click"
data-dojo-args="evt"
>
window.open("https://community.cesium.com/t/upgrading-sandcastle-we-need-your-input/39715/", "_blank")
</script>
</button>
</div>
</div>
</div>
</div>
<div class="dijitTooltip dijitTooltipBelow" id="docPopup">
<div
class="dijitTooltipContainer dijitTooltipContents"

View File

@ -1,28 +1,47 @@
# Change Log
## 1.134 - 2025-10-01
### @cesium/engine
#### Fixes :wrench:
- Materials loaded from type now respect submaterials present in the referenced material type. [#10566](https://github.com/CesiumGS/cesium/issues/10566)
- Reverts `createImageBitmap` options update to continue support for older browsers [#12846](https://github.com/CesiumGS/cesium/issues/12846)
- Fix flickering artifact in Gaussian splat models caused by incorrect sorting results. [#12662](https://github.com/CesiumGS/cesium/issues/12662)
#### Additions :tada:
- Added support for the [EXT_mesh_primitive_edge_visibility](https://github.com/KhronosGroup/glTF/pull/2479) glTF extension. [#12765](https://github.com/CesiumGS/cesium/issues/12765)
- Adds an async factory method for the Material class that allows callers to wait on resource loading. [#10566](https://github.com/CesiumGS/cesium/issues/10566)
## 1.133 - 2025-09-02
- Give the [new version of Sandcastle](https://dev-sandcastle.cesium.com/) a try today!
### @cesium/engine
#### Breaking Changes :mega:
- Removed the argument fallback in `ITwinData.*` functions. Please switch to the new options argument signature [#12778](https://github.com/CesiumGS/cesium/issues/12778)
- Allow passing tileset constructor options to the tileset that is created with `ITwinData.createTilesetForRealityDataId` [#12709](https://github.com/CesiumGS/cesium/issues/12709)
#### Fixes :wrench:
- Removes the minimum tile threshold of four for WMTS. [#4372](https://github.com/CesiumGS/cesium/issues/4372)
- Fixes issue where a Gaussian splat tileset would be rendered even if out of current camera view. [#12840](https://github.com/CesiumGS/cesium/pull/12840)
- Fixed a crash that could happen when loading PNTS (point cloud) data that contained a batch table without a binary part. [#11166](https://github.com/CesiumGS/cesium/issues/11166)
- Removed the argument fallback in `ITwinData.*` functions. Instead, use the new options argument signature. [#12778](https://github.com/CesiumGS/cesium/issues/12778)
#### Additions :tada:
- Added support for the [EXT_mesh_primitive_restart](https://github.com/KhronosGroup/glTF/pull/2478) glTF extension. [#12764](https://github.com/CesiumGS/cesium/issues/12764)
- Added support for the [EXT_mesh_primitive_edge_visibility](https://github.com/KhronosGroup/glTF/pull/2479) glTF extension. [#12765](https://github.com/CesiumGS/cesium/issues/12765)
- Added spherical harmonics support for Gaussian splats, supported with the SPZ compression format. [#12790](https://github.com/CesiumGS/cesium/pull/12790)
- Added `Ellipsoid.MARS` for use with Mars terrain and imagery. [#12828](https://github.com/CesiumGS/cesium/pull/12828)
- Allow passing `Cesium3DTileset` constructor options to the tileset that is created with `ITwinData.createTilesetForRealityDataId`. [#12709](https://github.com/CesiumGS/cesium/issues/12709)
#### Fixes :wrench:
- Fixed issue where a Gaussian splat tileset would be rendered even if out of current camera view. [#12840](https://github.com/CesiumGS/cesium/pull/12840)
- Removes the minimum tile threshold of four for WMTS. [#4372](https://github.com/CesiumGS/cesium/issues/4372)
- Fixed a crash when loading PNTS (point cloud) data that contained a batch table without a binary part. [#11166](https://github.com/CesiumGS/cesium/issues/11166)
- Fixed an error picking an area hidden by a `ClippingPolygon`. [#12725](https://github.com/CesiumGS/cesium/issues/12725)
#### Deprecated :hourglass_flowing_sand:
- Deprecated support of the `KHR_spz_gaussian_splats_compression` extension in favor of the latest 3D Gaussian Splatting extensions for glTF, `KHR_gaussian_splatting` and `KHR_gaussian_splatting_compression_spz_2`. The deprecated extension will be removed in version 1.135. Please retile your existing 3D Tiles using Gaussian splatting before that time. [#12837](https://github.com/CesiumGS/cesium/issues/12837)
- Deprecated support for the `KHR_spz_gaussian_splats_compression` extension in favor of the latest 3D Gaussian splatting extensions for glTF, `KHR_gaussian_splatting` and `KHR_gaussian_splatting_compression_spz_2`. The deprecated extension will be removed in version 1.135. To ensure support in CesiumJS 1.135 and beyond, Please re-tile existing Gaussian splatting 3D Tiles before November 1, 2025. [#12837](https://github.com/CesiumGS/cesium/issues/12837)
## 1.132 - 2025-08-01

View File

@ -430,3 +430,4 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute to Cesiu
- [Matt Schwartz](https://github.com/mzschwartz5)
- [Easy Mahaffey](https://github.com/easymaahffey)
- [Pamela Augustine](https://github.com/pamelaAugustine)
- [宋时旺](https://github.com/BlockCnFuture)

View File

@ -39,6 +39,7 @@
- Double-check your settings for name and email: `git config --get-regexp user.*`.
- Recommended Git settings:
- `git config --global fetch.prune true` - when fetching remote changes, remove any remote branches that no longer exist on the remote.
- `git config blame.ignoreRevsFile .git-blame-ignore-revs` - uses the ignore file to skip certain noisy revisions (like formatting) when running git blame. Alternatively, for VSCode users, install the GitLens extension, which will automatically use the ignore file.
- Have [commit access](https://github.com/CesiumGS/cesium/blob/main/Documentation/Contributors/CommittersGuide/README.md) to CesiumJS?
- No
- Fork [cesium](https://github.com/CesiumGS/cesium).

View File

@ -0,0 +1 @@
{"asset":{"extras":{"ion":{"georeferenced":false,"movable":true}},"version":"1.1"},"extensions":{"3DTILES_content_gltf":{"extensionsRequired":["KHR_gaussian_splatting","KHR_gaussian_splatting_compression_spz_2"],"extensionsUsed":["KHR_gaussian_splatting","KHR_gaussian_splatting_compression_spz_2"]}},"extensionsUsed":["3DTILES_content_gltf"],"geometricError":173.20508075688772,"root":{"boundingVolume":{"box":[50.0,-50.0,-50.0,50.0,0.0,0.0,0.0,50.0,0.0,0.0,0.0,50.0]},"content":{"uri":"0/0.glb"},"geometricError":0.0,"refine":"REPLACE"}}

View File

@ -12,7 +12,7 @@
"license": [
"BSD-3-Clause"
],
"version": "2.7.71",
"version": "2.7.73",
"url": "https://www.npmjs.com/package/@zip.js/zip.js"
},
{
@ -109,7 +109,7 @@
"license": [
"MIT"
],
"version": "1.0.1",
"version": "1.1.0",
"url": "https://www.npmjs.com/package/ktx-parse"
},
{
@ -133,7 +133,7 @@
"license": [
"MIT"
],
"version": "0.24.0",
"version": "0.25.0",
"url": "https://www.npmjs.com/package/meshoptimizer"
},
{
@ -158,7 +158,7 @@
"license": [
"BSD-3-Clause"
],
"version": "7.5.3",
"version": "7.5.4",
"url": "https://www.npmjs.com/package/protobufjs"
},
{

View File

@ -1,6 +1,9 @@
import globals from "globals";
import html from "eslint-plugin-html";
import configCesium from "@cesium/eslint-config";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from "typescript-eslint";
export default [
{
@ -15,6 +18,11 @@ export default [
"Apps/HelloWorld.html",
"Apps/Sandcastle/jsHintOptions.js",
"Apps/Sandcastle/gallery/gallery-index.js",
"Apps/Sandcastle2/",
"packages/sandcastle/public/",
"packages/sandcastle/templates/Sandcastle.d.ts",
"packages/sandcastle/templates/Sandcastle.js",
"packages/sandcastle/gallery/pagefind/",
"packages/engine/Source/Scene/GltfPipeline/**/*",
"packages/engine/Source/Shaders/**/*",
"Specs/jasmine/*",
@ -32,7 +40,13 @@ export default [
...configCesium.configs.node,
},
{
files: [".github/**/*.js", "scripts/**/*.js", "gulpfile.js", "server.js"],
files: [
".github/**/*.js",
"scripts/**/*.js",
"packages/sandcastle/scripts/**/*.js",
"gulpfile.js",
"server.js",
],
...configCesium.configs.node,
languageOptions: {
...configCesium.configs.node.languageOptions,
@ -41,6 +55,7 @@ export default [
},
{
files: ["packages/**/*.js", "Apps/**/*.js", "Specs/**/*.js", "**/*.html"],
ignores: ["packages/sandcastle/scripts/**/*.js"],
...configCesium.configs.browser,
plugins: { html },
rules: {
@ -86,6 +101,48 @@ export default [
sourceType: "module",
},
},
...[...tseslint.configs.recommended].map((config) => ({
// This is needed to restrict to a specific path unless using the tseslint.config function
// https://typescript-eslint.io/packages/typescript-eslint#config
...config,
files: ["packages/sandcastle/**/*.{ts,tsx}"],
})),
{
// This config came from the vite project generation
files: ["packages/sandcastle/**/*.{ts,tsx}"],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
"react-hooks": reactHooks,
"react-refresh": reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
],
},
},
{
files: ["packages/sandcastle/gallery/**/*.js"],
languageOptions: {
ecmaVersion: 2022,
sourceType: "module",
},
rules: {
"no-alert": "off",
},
},
{
files: ["packages/sandcastle/gallery/hello-world/main.js"],
rules: {
// ignore this rule here to avoid the excessive eslint-disable comment in our bare minimum example
"no-unused-vars": "off",
},
},
{
files: ["Specs/**/*", "packages/**/Specs/**/*"],
languageOptions: {

View File

@ -43,8 +43,18 @@ if (/\.0$/.test(version)) {
version = version.substring(0, version.length - 2);
}
const karmaConfigFile = resolve("./Specs/karma.conf.cjs");
function getWorkspaces(onlyDependencies = false) {
const dependencies = Object.keys(packageJson.dependencies);
return onlyDependencies
? packageJson.workspaces.filter((workspace) => {
return dependencies.includes(
workspace.replace("packages", `@${scope}`),
);
})
: packageJson.workspaces;
}
const devDeployUrl = "https://ci-builds.cesium.com/cesium/";
const devDeployUrl = process.env.DEPLOYED_URL;
const isProduction = process.env.PROD;
//Gulp doesn't seem to have a way to get the currently running tasks for setting
@ -247,7 +257,7 @@ export async function buildTs() {
} else if (argv.workspace) {
workspaces = argv.workspace;
} else {
workspaces = packageJson.workspaces;
workspaces = getWorkspaces(true);
}
// Generate types for passed packages in order.
@ -353,18 +363,14 @@ export async function prepare() {
"node_modules/@cesium/wasm-splats/wasm_splats_bg.wasm",
"packages/engine/Source/ThirdParty/wasm_splats_bg.wasm",
);
// Copy pako and zip.js worker files to Source/ThirdParty
// Copy zip.js worker and wasm files to Source/ThirdParty
copyFileSync(
"node_modules/pako/dist/pako_inflate.min.js",
"packages/engine/Source/ThirdParty/Workers/pako_inflate.min.js",
"node_modules/@zip.js/zip.js/dist/zip-web-worker.js",
"packages/engine/Source/ThirdParty/Workers/zip-web-worker.js",
);
copyFileSync(
"node_modules/pako/dist/pako_deflate.min.js",
"packages/engine/Source/ThirdParty/Workers/pako_deflate.min.js",
);
copyFileSync(
"node_modules/@zip.js/zip.js/dist/z-worker-pako.js",
"packages/engine/Source/ThirdParty/Workers/z-worker-pako.js",
"node_modules/@zip.js/zip.js/dist/zip-module.wasm",
"packages/engine/Source/ThirdParty/zip-module.wasm",
);
// Copy prism.js and prism.css files into Tools
@ -400,7 +406,7 @@ export async function buildDocs() {
stdio: "inherit",
env: Object.assign({}, process.env, {
CESIUM_VERSION: version,
CESIUM_PACKAGES: packageJson.workspaces,
CESIUM_PACKAGES: getWorkspaces(true),
}),
},
);
@ -533,6 +539,7 @@ async function pruneScriptsForZip(packageJsonPath) {
delete scripts["build-ts"];
delete scripts["build-third-party"];
delete scripts["build-apps"];
delete scripts["build-sandcastle"];
delete scripts.clean;
delete scripts.cloc;
delete scripts["build-docs"];
@ -698,12 +705,10 @@ export async function deployStatus() {
const status = argv.status;
const message = argv.message;
const deployUrl = `${devDeployUrl + process.env.BRANCH}/`;
const deployUrl = `${devDeployUrl}`;
const zipUrl = `${deployUrl}Cesium-${version}.zip`;
const npmUrl = `${deployUrl}cesium-${version}.tgz`;
const coverageUrl = `${
devDeployUrl + process.env.BRANCH
}/Build/Coverage/index.html`;
const coverageUrl = `${devDeployUrl}Build/Coverage/index.html`;
return Promise.all([
setStatus(status, deployUrl, message, "deployment"),
@ -1490,8 +1495,8 @@ async function getLicenseDataFromThirdPartyExtra(path, discoveredDependencies) {
return result;
}
// Resursively check the workspaces
for (const workspace of packageJson.workspaces) {
// Recursively check the workspaces
for (const workspace of getWorkspaces(true)) {
const workspacePackageJson = require(`./${workspace}/package.json`);
result = await getLicenseDataFromPackage(
workspacePackageJson,

View File

@ -33,7 +33,12 @@
</li>
<li>
<a href="Apps/Sandcastle/index.html">Sandcastle</a>
(<a href="Build/Apps/Sandcastle/index.html">built version</a>)
<ul>
<li>
<a href="Build/Apps/Sandcastle/index.html">Built Sandcastle</a>
</li>
<li><a href="Apps/Sandcastle2/index.html">Sandcastle v2</a></li>
</ul>
</li>
<li>
<a href="Apps/CesiumViewer/index.html?inspector=true"

View File

@ -1,6 +1,6 @@
{
"name": "cesium",
"version": "1.132.0",
"version": "1.133.0",
"description": "CesiumJS is a JavaScript library for creating 3D globes and 2D maps in a web browser without a plugin.",
"homepage": "http://cesium.com/cesiumjs/",
"license": "Apache-2.0",
@ -51,8 +51,8 @@
"./Specs/**/*"
],
"dependencies": {
"@cesium/engine": "^19.0.0",
"@cesium/widgets": "^13.0.0"
"@cesium/engine": "^20.0.0",
"@cesium/widgets": "^13.1.0"
},
"devDependencies": {
"@cesium/eslint-config": "^12.0.0",
@ -63,6 +63,8 @@
"esbuild": "^0.25.8",
"eslint": "^9.32.0",
"eslint-plugin-html": "^8.1.1",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"express": "^5.1.0",
"globals": "^16.0.0",
"globby": "^14.0.0",
@ -99,7 +101,8 @@
"prismjs": "^1.28.0",
"rimraf": "^6.0.1",
"tsd-jsdoc": "^2.5.0",
"typescript": "^5.3.2",
"typescript": "~5.8.3",
"typescript-eslint": "^8.30.1",
"yargs": "^18.0.0"
},
"scripts": {
@ -112,12 +115,13 @@
"build-ts": "gulp buildTs",
"build-third-party": "gulp buildThirdParty",
"build-apps": "gulp buildApps",
"build-sandcastle": "npm run build-app --workspace packages/sandcastle",
"clean": "gulp clean",
"cloc": "gulp cloc",
"coverage": "gulp coverage",
"build-docs": "gulp buildDocs",
"build-docs-watch": "gulp buildDocsWatch",
"eslint": "eslint \"./**/*.*js\" \"./**/*.html\" --cache --quiet",
"eslint": "eslint \"./**/*.*js\" \"./**/*.*ts*\" \"./**/*.html\" --cache --quiet",
"make-zip": "gulp makeZip",
"markdownlint": "markdownlint \"**/*.md\"",
"release": "gulp release",
@ -145,7 +149,7 @@
"node": ">=20.19.0"
},
"lint-staged": {
"*.{js,cjs,mjs,css,html}": [
"*.{js,cjs,mjs,ts,tsx,css,html}": [
"eslint --cache --quiet",
"prettier --write"
],
@ -156,6 +160,15 @@
},
"workspaces": [
"packages/engine",
"packages/widgets"
]
}
"packages/widgets",
"packages/sandcastle"
],
"overrides": {
"allotment": {
"use-resize-observer": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}
}
}

View File

@ -7,7 +7,7 @@ Source/ThirdParty/Shaders/**/*.js
Source/ThirdParty/_commonjsHelpers*
Source/ThirdParty/wasm_splats_bg.wasm
Source/ThirdParty/draco_decoder.wasm
Source/ThirdParty/zip-module.wasm
Source/ThirdParty/Workers/draco_decoder_nodejs.js
Source/ThirdParty/Workers/pako_inflate.min.js
Source/ThirdParty/Workers/pako_deflate.min.js
Source/ThirdParty/Workers/zip-web-worker.js
Source/ThirdParty/Workers/z-worker-pako.js

View File

@ -254,6 +254,15 @@ Ellipsoid.MOON = Object.freeze(
),
);
/**
* An Ellipsoid instance initialized to a sphere with the mean radii of Mars.
* Source: https://epsg.io/104905
*
* @type {Ellipsoid}
* @constant
*/
Ellipsoid.MARS = Object.freeze(new Ellipsoid(3396190.0, 3396190.0, 3376200.0));
Ellipsoid._default = Ellipsoid.WGS84;
Object.defineProperties(Ellipsoid, {
/**

View File

@ -4,7 +4,7 @@ import Resource from "./Resource.js";
let defaultTokenCredit;
const defaultAccessToken =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJhNzg4MTAwYi1kMTg5LTRjNDItYTVlMi0wOTlhNGM0NTc5Y2YiLCJpZCI6MjU5LCJpYXQiOjE3NTQwNjAzNjJ9.5ei_XzXku4PefU_uHUlbhQnPS1sbv-BHo28oU2fj0Ig";
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI2NTEwZTU2Yi0wOGEyLTQyZjgtOTJjNi04Mzc2NGRlNzA4NTkiLCJpZCI6MjU5LCJpYXQiOjE3NTY4NDExOTJ9._Y3MIsYgGKTVTpkEpKPNT0cQSa_hUocY0DdH7h0U-xM";
/**
* Default settings for accessing the Cesium ion API.
*

View File

@ -231,6 +231,8 @@ Resource.supportsImageBitmapOptions = function () {
})
.then(function (blob) {
const imageBitmapOptions = {
// 'from-image' is deprecated, new option is 'none'. However, we still need to support older browsers,
// and there's no good way to detect support for these options. For now, continue to use 'from-image'. See: https://github.com/CesiumGS/cesium/issues/12846
imageOrientation: "flipY", // default is "from-image"
premultiplyAlpha: "none", // default is "default"
colorSpaceConversion: "none", // default is "default"
@ -2036,7 +2038,9 @@ Resource.createImageBitmapFromBlob = function (blob, options) {
);
return createImageBitmap(blob, {
imageOrientation: options.flipY ? "flipY" : "from-image",
// 'from-image' is deprecated, new option is 'none'. However, we still need to support older browsers,
// and there's no good way to detect support for these options. For now, continue to use 'from-image'. See: https://github.com/CesiumGS/cesium/issues/12846
imageOrientation: options.flipY ? "flipY" : "none",
premultiplyAlpha: options.premultiplyAlpha ? "premultiply" : "none",
colorSpaceConversion: options.skipColorSpaceConversion ? "none" : "default",
});

View File

@ -42,7 +42,13 @@ import LabelStyle from "../Scene/LabelStyle.js";
import SceneMode from "../Scene/SceneMode.js";
import Autolinker from "autolinker";
import Uri from "urijs";
import * as zip from "@zip.js/zip.js/lib/zip-no-worker.js";
import {
configure,
BlobReader,
Data64URIWriter,
TextWriter,
ZipReader,
} from "@zip.js/zip.js/lib/zip-core.js";
import getElement from "./getElement.js";
import BillboardGraphics from "./BillboardGraphics.js";
import CompositePositionProperty from "./CompositePositionProperty.js";
@ -390,24 +396,18 @@ function removeDuplicateNamespaces(text) {
return text;
}
function loadXmlFromZip(entry, uriResolver) {
return Promise.resolve(entry.getData(new zip.TextWriter())).then(
function (text) {
text = insertNamespaces(text);
text = removeDuplicateNamespaces(text);
uriResolver.kml = parser.parseFromString(text, "application/xml");
},
);
async function loadXmlFromZip(entry, uriResolver) {
let text = await entry.getData(new TextWriter());
text = insertNamespaces(text);
text = removeDuplicateNamespaces(text);
uriResolver.kml = parser.parseFromString(text, "application/xml");
}
function loadDataUriFromZip(entry, uriResolver) {
async function loadDataUriFromZip(entry, uriResolver) {
const mimeType =
MimeTypes.detectFromFilename(entry.filename) ?? "application/octet-stream";
return Promise.resolve(entry.getData(new zip.Data64URIWriter(mimeType))).then(
function (dataUri) {
uriResolver[entry.filename] = dataUri;
},
);
const dataUri = await entry.getData(new Data64URIWriter(mimeType));
uriResolver[entry.filename] = dataUri;
}
function embedDataUris(div, elementType, attributeName, uriResolver) {
@ -3287,69 +3287,68 @@ function loadKml(
});
}
function loadKmz(
async function loadKmz(
dataSource,
entityCollection,
blob,
sourceResource,
screenOverlayContainer,
) {
const zWorkerUrl = buildModuleUrl("ThirdParty/Workers/z-worker-pako.js");
zip.configure({
workerScripts: {
deflate: [zWorkerUrl, "./pako_deflate.min.js"],
inflate: [zWorkerUrl, "./pako_inflate.min.js"],
},
const zWorkerUri = buildModuleUrl("ThirdParty/Workers/zip-web-worker.js");
const zWasmUri = buildModuleUrl("ThirdParty/zip-module.wasm");
configure({
workerURI: zWorkerUri,
wasmURI: zWasmUri,
});
const reader = new zip.ZipReader(new zip.BlobReader(blob));
return Promise.resolve(reader.getEntries()).then(function (entries) {
const promises = [];
const uriResolver = {};
let docEntry;
for (let i = 0; i < entries.length; i++) {
const entry = entries[i];
if (!entry.directory) {
if (/\.kml$/i.test(entry.filename)) {
// We use the first KML document we come across
// https://developers.google.com/kml/documentation/kmzarchives
// Unless we come across a .kml file at the root of the archive because GE does this
if (!defined(docEntry) || !/\//i.test(entry.filename)) {
if (defined(docEntry)) {
// We found one at the root so load the initial kml as a data uri
promises.push(loadDataUriFromZip(docEntry, uriResolver));
}
docEntry = entry;
} else {
// Wasn't the first kml and wasn't at the root
promises.push(loadDataUriFromZip(entry, uriResolver));
const reader = new ZipReader(new BlobReader(blob));
const entries = await reader.getEntries();
const promises = [];
const uriResolver = {};
let docEntry;
for (let i = 0; i < entries.length; i++) {
const entry = entries[i];
if (!entry.directory) {
if (/\.kml$/i.test(entry.filename)) {
// We use the first KML document we come across
// https://developers.google.com/kml/documentation/kmzarchives
// Unless we come across a .kml file at the root of the archive because GE does this
if (!defined(docEntry) || !/\//i.test(entry.filename)) {
if (defined(docEntry)) {
// We found one at the root so load the initial kml as a data uri
promises.push(loadDataUriFromZip(docEntry, uriResolver));
}
docEntry = entry;
} else {
// Wasn't the first kml and wasn't at the root
promises.push(loadDataUriFromZip(entry, uriResolver));
}
} else {
promises.push(loadDataUriFromZip(entry, uriResolver));
}
}
}
// Now load the root KML document
if (defined(docEntry)) {
promises.push(loadXmlFromZip(docEntry, uriResolver));
}
return Promise.all(promises).then(function () {
reader.close();
if (!defined(uriResolver.kml)) {
throw new RuntimeError("KMZ file does not contain a KML document.");
}
uriResolver.keys = Object.keys(uriResolver);
return loadKml(
dataSource,
entityCollection,
uriResolver.kml,
sourceResource,
uriResolver,
screenOverlayContainer,
);
});
});
// Now load the root KML document
if (defined(docEntry)) {
promises.push(loadXmlFromZip(docEntry, uriResolver));
}
await Promise.all(promises);
reader.close();
if (!defined(uriResolver.kml)) {
throw new RuntimeError("KMZ file does not contain a KML document.");
}
uriResolver.keys = Object.keys(uriResolver);
return loadKml(
dataSource,
entityCollection,
uriResolver.kml,
sourceResource,
uriResolver,
screenOverlayContainer,
);
}
function load(dataSource, entityCollection, data, options) {

View File

@ -20,7 +20,13 @@ import TimeIntervalCollection from "../Core/TimeIntervalCollection.js";
import HeightReference from "../Scene/HeightReference.js";
import HorizontalOrigin from "../Scene/HorizontalOrigin.js";
import VerticalOrigin from "../Scene/VerticalOrigin.js";
import * as zip from "@zip.js/zip.js/lib/zip-no-worker.js";
import {
configure,
BlobReader,
BlobWriter,
TextReader,
ZipWriter,
} from "@zip.js/zip.js/lib/zip-core.js";
import BillboardGraphics from "./BillboardGraphics.js";
import CompositePositionProperty from "./CompositePositionProperty.js";
import ModelGraphics from "./ModelGraphics.js";
@ -322,43 +328,34 @@ function exportKml(options) {
});
}
function createKmz(kmlString, externalFiles) {
const zWorkerUrl = buildModuleUrl("ThirdParty/Workers/z-worker-pako.js");
zip.configure({
workerScripts: {
deflate: [zWorkerUrl, "./pako_deflate.min.js"],
inflate: [zWorkerUrl, "./pako_inflate.min.js"],
},
async function createKmz(kmlString, externalFiles) {
const zWorkerUri = buildModuleUrl("ThirdParty/Workers/zip-web-worker.js");
const zWasmUri = buildModuleUrl("ThirdParty/zip-module.wasm");
configure({
workerURI: zWorkerUri,
wasmURI: zWasmUri,
});
const blobWriter = new zip.BlobWriter();
const writer = new zip.ZipWriter(blobWriter);
const blobWriter = new BlobWriter("application/vnd.google-earth.kmz");
const writer = new ZipWriter(blobWriter);
// We need to only write one file at a time so the zip doesn't get corrupted
return writer
.add("doc.kml", new zip.TextReader(kmlString))
.then(function () {
const keys = Object.keys(externalFiles);
return addExternalFilesToZip(writer, keys, externalFiles, 0);
})
.then(function () {
return writer.close();
})
.then(function (blob) {
return {
kmz: blob,
};
});
await writer.add("doc.kml", new TextReader(kmlString));
const keys = Object.keys(externalFiles);
await addExternalFilesToZip(writer, keys, externalFiles, 0);
await writer.close();
const blob = await blobWriter.getData();
return {
kmz: blob,
};
}
function addExternalFilesToZip(writer, keys, externalFiles, index) {
async function addExternalFilesToZip(writer, keys, externalFiles, index) {
if (keys.length === index) {
return;
}
const filename = keys[index];
return writer
.add(filename, new zip.BlobReader(externalFiles[filename]))
.then(function () {
return addExternalFilesToZip(writer, keys, externalFiles, index + 1);
});
await writer.add(filename, new BlobReader(externalFiles[filename]));
return addExternalFilesToZip(writer, keys, externalFiles, index + 1);
}
exportKml._createState = function (options) {

View File

@ -4,7 +4,7 @@ import Resource from "../Core/Resource.js";
let defaultTokenCredit;
const defaultAccessToken =
"AAPTxy8BH1VEsoebNVZXo8HurEOF051kAEKlhkOhBEc9BmRpOZfBt2Ic5blmnx9xwwyG_Mt0EmBxpEa6xCqXn5V_qFEgJ0edE9ixiefKjMiv986NGSn2HB8y6x0GtSOCBkg19K6rBgZ7Upl7ABEnaFh4dD73GW0gUAJ9hHWhqd1qgHkBuctWiFEJUmQjs_52tdx6l5bDGoeXVvmelklmEJOshCRV_s3kLPC19ENRNLc18eA.AT1_EXUMUBsi";
"AAPTxy8BH1VEsoebNVZXo8HurEOF051kAEKlhkOhBEc9BmRXcPUeuOd6ZTh-Z86vRv6teqZdBYLUWIxB6HTYajPhFtAlXhMdqTg07UwlBQ59KbiReC5wCeOF4LzLTW4oaSPp-t4pEcJHnFePlnFcIPXM7R565gTy6f6fMuZAXPvUEuZSMf8dvTqHyV9-Zd9eZGWokDzKC4uEtHME7OeAk3oyZ7vzkjEo8fpuIfFw0sH8vQU.AT1_PLgWE6Lc";
/**
* Default options for accessing the ArcGIS image tile service.
*

View File

@ -17,7 +17,6 @@ import deprecationWarning from "../Core/deprecationWarning.js";
*
* @alias GaussianSplat3DTileContent
* @constructor
* @private
*/
function GaussianSplat3DTileContent(loader, tileset, tile, resource) {
this._tileset = tileset;
@ -47,7 +46,7 @@ function GaussianSplat3DTileContent(loader, tileset, tile, resource) {
* @type {undefined|Primitive}
* @private
*/
this.splatPrimitive = undefined;
this.gltfPrimitive = undefined;
/**
* Transform matrix to turn model coordinates into world coordinates.
@ -79,6 +78,27 @@ function GaussianSplat3DTileContent(loader, tileset, tile, resource) {
* @private
*/
this._transformed = false;
/**
* The degree of the spherical harmonics used for the Gaussian splats.
* @type {number}
* @private
*/
this._sphericalHarmonicsDegree = 0;
/**
* The number of spherical harmonic coefficients used for the Gaussian splats.
* @type {number}
* @private
*/
this._sphericalHarmonicsCoefficientCount = 0;
/**
* Spherical Harmonic data that has been packed for use in a texture or shader.
* @type {undefined|Uint32Array}
* @private
*/
this._packedSphericalHarmonicsData = undefined;
}
/**
@ -144,7 +164,7 @@ Object.defineProperties(GaussianSplat3DTileContent.prototype, {
*/
pointsLength: {
get: function () {
return this.splatPrimitive.attributes[0].count;
return this.gltfPrimitive.attributes[0].count;
},
},
/**
@ -169,7 +189,7 @@ Object.defineProperties(GaussianSplat3DTileContent.prototype, {
*/
geometryByteLength: {
get: function () {
return this.splatPrimitive.attributes.reduce((totalLength, attribute) => {
return this.gltfPrimitive.attributes.reduce((totalLength, attribute) => {
return totalLength + attribute.byteLength;
}, 0);
},
@ -347,8 +367,188 @@ Object.defineProperties(GaussianSplat3DTileContent.prototype, {
this._group = value;
},
},
/**
* The number of spherical harmonic coefficients used for the Gaussian splats.
* @type {number}
* @private
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
sphericalHarmonicsCoefficientCount: {
get: function () {
return this._sphericalHarmonicsCoefficientCount;
},
},
/**
* The degree of the spherical harmonics used for the Gaussian splats.
* @type {number}
* @private
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
sphericalHarmonicsDegree: {
get: function () {
return this._sphericalHarmonicsDegree;
},
},
/**
* The packed spherical harmonic data for the Gaussian splats for use a shader or texture.
* @type {number}
* @private
* @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
*/
packedSphericalHarmonicsData: {
get: function () {
return this._packedSphericalHarmonicsData;
},
},
});
function getShAttributePrefix(attribute) {
const prefix = attribute.startsWith("KHR_gaussian_splatting:")
? "KHR_gaussian_splatting:"
: "_";
return `${prefix}SH_DEGREE_`;
}
/**
* Determine Spherical Harmonics degree and coefficient count from attributes
* @param {Attribute[]} attributes - The list of glTF attributes.
* @returns {object} An object containing the degree (l) and coefficient (n).
* @private
*/
function degreeAndCoefFromAttributes(attributes) {
const shAttributes = attributes.filter((attr) =>
attr.name.includes("SH_DEGREE_"),
);
switch (shAttributes.length) {
default:
case 0:
return { l: 0, n: 0 };
case 3:
return { l: 1, n: 9 };
case 8:
return { l: 2, n: 24 };
case 15:
return { l: 3, n: 45 };
}
}
/**
* Converts a 32-bit floating point number to a 16-bit floating point number.
* @param {number} float32 input
* @returns {number} Half precision float
* @private
*/
const buffer = new ArrayBuffer(4);
const floatView = new Float32Array(buffer);
const intView = new Uint32Array(buffer);
function float32ToFloat16(float32) {
floatView[0] = float32;
const bits = intView[0];
const sign = (bits >> 31) & 0x1;
const exponent = (bits >> 23) & 0xff;
const mantissa = bits & 0x7fffff;
let half;
if (exponent === 0xff) {
half = (sign << 15) | (0x1f << 10) | (mantissa ? 0x200 : 0);
} else if (exponent === 0) {
half = sign << 15;
} else {
const newExponent = exponent - 127 + 15;
if (newExponent >= 31) {
half = (sign << 15) | (0x1f << 10);
} else if (newExponent <= 0) {
half = sign << 15;
} else {
half = (sign << 15) | (newExponent << 10) | (mantissa >>> 13);
}
}
return half;
}
/**
* Extracts the spherical harmonic degree and coefficient from the attribute name.
* @param {string} attribute - The attribute name.
* @returns {object} An object containing the degree (l) and coefficient (n).
* @private
*/
function extractSHDegreeAndCoef(attribute) {
const prefix = getShAttributePrefix(attribute);
const separator = "_COEF_";
const lStart = prefix.length;
const coefIndex = attribute.indexOf(separator, lStart);
const l = parseInt(attribute.slice(lStart, coefIndex), 10);
const n = parseInt(attribute.slice(coefIndex + separator.length), 10);
return { l, n };
}
/**
* Packs spherical harmonic data into half-precision floats.
* @param {GaussianSplat3DTileContent} tileContent - The tile content containing the spherical harmonic data.
* @returns {Uint32Array} - The Float16 packed spherical harmonic data.
* @private
*/
function packSphericalHarmonicsData(tileContent) {
const degree = tileContent.sphericalHarmonicsDegree;
const coefs = tileContent.sphericalHarmonicsCoefficientCount;
const totalLength = tileContent.pointsLength * (coefs * (2 / 3)); //3 packs into 2
const packedData = new Uint32Array(totalLength);
const shAttributes = tileContent.gltfPrimitive.attributes.filter((attr) =>
attr.name.includes("SH_DEGREE_"),
);
let stride = 0;
const base = [0, 9, 24];
switch (degree) {
case 1:
stride = 9;
break;
case 2:
stride = 24;
break;
case 3:
stride = 45;
break;
}
shAttributes.sort((a, b) => {
if (a.name < b.name) {
return -1;
}
if (a.name > b.name) {
return 1;
}
return 0;
});
const packedStride = stride * (2 / 3);
for (let i = 0; i < shAttributes.length; i++) {
const { l, n } = extractSHDegreeAndCoef(shAttributes[i].name);
for (let j = 0; j < tileContent.pointsLength; j++) {
//interleave the data
const packedBase = (base[l - 1] * 2) / 3;
const idx = j * packedStride + packedBase + n * 2;
const src = j * 3;
packedData[idx] =
float32ToFloat16(shAttributes[i].typedArray[src]) |
(float32ToFloat16(shAttributes[i].typedArray[src + 1]) << 16);
packedData[idx + 1] = float32ToFloat16(
shAttributes[i].typedArray[src + 2],
);
}
}
return packedData;
}
/**
* Creates a new instance of {@link GaussianSplat3DTileContent} from a glTF or glb resource.
*
@ -425,31 +625,37 @@ GaussianSplat3DTileContent.prototype.update = function (primitive, frameState) {
}
if (this._resourcesLoaded) {
this.splatPrimitive = loader.components.scene.nodes[0].primitives[0];
this.gltfPrimitive = loader.components.scene.nodes[0].primitives[0];
this.worldTransform = loader.components.scene.nodes[0].matrix;
this._ready = true;
this._originalPositions = new Float32Array(
ModelUtility.getAttributeBySemantic(
this.splatPrimitive,
this.gltfPrimitive,
VertexAttributeSemantic.POSITION,
).typedArray,
);
this._originalRotations = new Float32Array(
ModelUtility.getAttributeBySemantic(
this.splatPrimitive,
this.gltfPrimitive,
VertexAttributeSemantic.ROTATION,
).typedArray,
);
this._originalScales = new Float32Array(
ModelUtility.getAttributeBySemantic(
this.splatPrimitive,
this.gltfPrimitive,
VertexAttributeSemantic.SCALE,
).typedArray,
);
const { l, n } = degreeAndCoefFromAttributes(this.gltfPrimitive.attributes);
this._sphericalHarmonicsDegree = l;
this._sphericalHarmonicsCoefficientCount = n;
this._packedSphericalHarmonicsData = packSphericalHarmonicsData(this);
return;
}

View File

@ -31,10 +31,12 @@ import Cartesian3 from "../Core/Cartesian3.js";
import Quaternion from "../Core/Quaternion.js";
import SplitDirection from "./SplitDirection.js";
import destroyObject from "../Core/destroyObject.js";
import ContextLimits from "../Renderer/ContextLimits.js";
const scratchMatrix4A = new Matrix4();
const scratchMatrix4B = new Matrix4();
const scratchMatrix4C = new Matrix4();
const scratchMatrix4D = new Matrix4();
const GaussianSplatSortingState = {
IDLE: 0,
@ -44,6 +46,25 @@ const GaussianSplatSortingState = {
ERROR: 4,
};
function createSphericalHarmonicsTexture(context, shData) {
const texture = new Texture({
context: context,
source: {
width: shData.width,
height: shData.height,
arrayBufferView: shData.data,
},
preMultiplyAlpha: false,
skipColorSpaceConversion: true,
pixelFormat: PixelFormat.RG_INTEGER,
pixelDatatype: PixelDatatype.UNSIGNED_INT,
flipY: false,
sampler: Sampler.NEAREST,
});
return texture;
}
function createGaussianSplatTexture(context, splatTextureData) {
return new Texture({
context: context,
@ -148,6 +169,14 @@ function GaussianSplatPrimitive(options) {
* @see {@link GaussianSplatTextureGenerator}
*/
this.gaussianSplatTexture = undefined;
/**
* The texture used to store the spherical harmonics coefficients for the Gaussian splats.
* @type {undefined|Texture}
* @private
*/
this.sphericalHarmonicsTexture = undefined;
/**
* The last width of the Gaussian splat texture.
* This is used to track changes in the texture size and update the primitive accordingly.
@ -395,7 +424,7 @@ GaussianSplatPrimitive.prototype.onTileVisible = function (tile) {};
*/
GaussianSplatPrimitive.transformTile = function (tile) {
const computedTransform = tile.computedTransform;
const splatPrimitive = tile.content.splatPrimitive;
const gltfPrimitive = tile.content.gltfPrimitive;
const gaussianSplatPrimitive = tile.tileset.gaussianSplatPrimitive;
const computedModelMatrix = Matrix4.multiplyTransformation(
@ -425,17 +454,17 @@ GaussianSplatPrimitive.transformTile = function (tile) {
const rotations = tile.content._originalRotations;
const scales = tile.content._originalScales;
const attributePositions = ModelUtility.getAttributeBySemantic(
splatPrimitive,
gltfPrimitive,
VertexAttributeSemantic.POSITION,
).typedArray;
const attributeRotations = ModelUtility.getAttributeBySemantic(
splatPrimitive,
gltfPrimitive,
VertexAttributeSemantic.ROTATION,
).typedArray;
const attributeScales = ModelUtility.getAttributeBySemantic(
splatPrimitive,
gltfPrimitive,
VertexAttributeSemantic.SCALE,
).typedArray;
@ -537,7 +566,7 @@ GaussianSplatPrimitive.generateSplatTexture = function (primitive, frameState) {
},
});
}
primitive._vertexArray = undefined;
primitive._lastTextureHeight = splatTextureData.height;
primitive._lastTextureWidth = splatTextureData.width;
@ -601,10 +630,68 @@ GaussianSplatPrimitive.buildGSplatDrawCommand = function (
ShaderDestination.VERTEX,
);
shaderBuilder.addUniform(
"float",
"u_sphericalHarmonicsDegree",
ShaderDestination.VERTEX,
);
shaderBuilder.addUniform("float", "u_splatScale", ShaderDestination.VERTEX);
shaderBuilder.addUniform(
"vec3",
"u_cameraPositionWC",
ShaderDestination.VERTEX,
);
shaderBuilder.addUniform(
"mat3",
"u_inverseModelRotation",
ShaderDestination.VERTEX,
);
const uniformMap = renderResources.uniformMap;
const textureCache = primitive.gaussianSplatTexture;
uniformMap.u_splatAttributeTexture = function () {
return primitive.gaussianSplatTexture;
return textureCache;
};
if (primitive._sphericalHarmonicsDegree > 0) {
shaderBuilder.addDefine(
"HAS_SPHERICAL_HARMONICS",
"1",
ShaderDestination.VERTEX,
);
shaderBuilder.addUniform(
"highp usampler2D",
"u_sphericalHarmonicsTexture",
ShaderDestination.VERTEX,
);
uniformMap.u_sphericalHarmonicsTexture = function () {
return primitive.sphericalHarmonicsTexture;
};
}
uniformMap.u_sphericalHarmonicsDegree = function () {
return primitive._sphericalHarmonicsDegree;
};
uniformMap.u_cameraPositionWC = function () {
return Cartesian3.clone(frameState.camera.positionWC);
};
uniformMap.u_inverseModelRotation = function () {
const tileset = primitive._tileset;
const modelMatrix = Matrix4.multiply(
tileset.modelMatrix,
Matrix4.fromArray(tileset.root.transform),
scratchMatrix4A,
);
const inverseModelRotation = Matrix4.getRotation(
Matrix4.inverse(modelMatrix, scratchMatrix4C),
scratchMatrix4D,
);
return inverseModelRotation;
};
uniformMap.u_splitDirection = function () {
@ -682,12 +769,13 @@ GaussianSplatPrimitive.buildGSplatDrawCommand = function (
scratchMatrix4B,
);
const vertexArrayCache = primitive._vertexArray;
const command = new DrawCommand({
boundingVolume: tileset.boundingSphere,
modelMatrix: modelMatrix,
uniformMap: uniformMap,
renderState: renderState,
vertexArray: primitive._vertexArray,
vertexArray: vertexArrayCache,
shaderProgram: shaderProgram,
cull: renderStateOptions.cull.enabled,
pass: Pass.GAUSSIAN_SPLATS,
@ -760,6 +848,7 @@ GaussianSplatPrimitive.prototype.update = function (frameState) {
this._scales = undefined;
this._colors = undefined;
this._indexes = undefined;
this._shData = undefined;
this._needsGaussianSplatTexture = true;
this._gaussianSplatTexturePending = false;
@ -775,7 +864,7 @@ GaussianSplatPrimitive.prototype.update = function (frameState) {
let aggregate;
let offset = 0;
for (const tile of tiles) {
const primitive = tile.content.splatPrimitive;
const primitive = tile.content.gltfPrimitive;
const attribute = getAttributeCallback(primitive);
if (!defined(aggregate)) {
aggregate = ComponentDatatype.createTypedArray(
@ -790,42 +879,71 @@ GaussianSplatPrimitive.prototype.update = function (frameState) {
return aggregate;
};
const aggregateShData = () => {
let offset = 0;
for (const tile of tiles) {
const shData = tile.content.packedSphericalHarmonicsData;
if (tile.content.sphericalHarmonicsDegree > 0) {
if (!defined(this._shData)) {
let coefs;
switch (tile.content.sphericalHarmonicsDegree) {
case 1:
coefs = 9;
break;
case 2:
coefs = 24;
break;
case 3:
coefs = 45;
}
this._shData = new Uint32Array(totalElements * (coefs * (2 / 3)));
}
this._shData.set(shData, offset);
offset += shData.length;
}
}
};
this._positions = aggregateAttributeValues(
ComponentDatatype.FLOAT,
(splatPrimitive) =>
(gltfPrimitive) =>
ModelUtility.getAttributeBySemantic(
splatPrimitive,
gltfPrimitive,
VertexAttributeSemantic.POSITION,
),
);
this._scales = aggregateAttributeValues(
ComponentDatatype.FLOAT,
(splatPrimitive) =>
(gltfPrimitive) =>
ModelUtility.getAttributeBySemantic(
splatPrimitive,
gltfPrimitive,
VertexAttributeSemantic.SCALE,
),
);
this._rotations = aggregateAttributeValues(
ComponentDatatype.FLOAT,
(splatPrimitive) =>
(gltfPrimitive) =>
ModelUtility.getAttributeBySemantic(
splatPrimitive,
gltfPrimitive,
VertexAttributeSemantic.ROTATION,
),
);
this._colors = aggregateAttributeValues(
ComponentDatatype.UNSIGNED_BYTE,
(splatPrimitive) =>
(gltfPrimitive) =>
ModelUtility.getAttributeBySemantic(
splatPrimitive,
gltfPrimitive,
VertexAttributeSemantic.COLOR,
),
);
aggregateShData();
this._sphericalHarmonicsDegree =
tiles[0].content.sphericalHarmonicsDegree;
this._numSplats = totalElements;
this.selectedTileLength = tileset._selectedTiles.length;
}
@ -837,6 +955,38 @@ GaussianSplatPrimitive.prototype.update = function (frameState) {
if (this._needsGaussianSplatTexture) {
if (!this._gaussianSplatTexturePending) {
GaussianSplatPrimitive.generateSplatTexture(this, frameState);
if (defined(this._shData)) {
const oldTex = this.sphericalHarmonicsTexture;
const width = ContextLimits.maximumTextureSize;
const dims =
tileset._selectedTiles[0].content
.sphericalHarmonicsCoefficientCount / 3;
const splatsPerRow = Math.floor(width / dims);
const floatsPerRow = splatsPerRow * (dims * 2);
const texBuf = new Uint32Array(
width * Math.ceil(this._numSplats / splatsPerRow) * 2,
);
let dataIndex = 0;
for (let i = 0; dataIndex < this._shData.length; i += width * 2) {
texBuf.set(
this._shData.subarray(dataIndex, dataIndex + floatsPerRow),
i,
);
dataIndex += floatsPerRow;
}
this.sphericalHarmonicsTexture = createSphericalHarmonicsTexture(
frameState.context,
{
data: texBuf,
width: width,
height: Math.ceil(this._numSplats / splatsPerRow),
},
);
if (defined(oldTex)) {
oldTex.destroy();
}
}
}
return;
}

View File

@ -2075,6 +2075,7 @@ function loadPrimitive(loader, gltfPrimitive, hasInstances, frameState) {
}
}
//support the latest glTF spec and the legacy extension
const spzExtension = fetchSpzExtensionFrom(extensions);
if (defined(spzExtension)) {

View File

@ -306,6 +306,26 @@ async function loadFromSpz(vertexBufferLoader) {
}
}
function getShAttributePrefix(attribute) {
const prefix = attribute.startsWith("KHR_gaussian_splatting:")
? "KHR_gaussian_splatting:"
: "_";
return `${prefix}SH_DEGREE_`;
}
function extractSHDegreeAndCoef(attribute) {
const prefix = getShAttributePrefix(attribute);
const separator = "_COEF_";
const lStart = prefix.length;
const coefIndex = attribute.indexOf(separator, lStart);
const l = parseInt(attribute.slice(lStart, coefIndex), 10);
const n = parseInt(attribute.slice(coefIndex + separator.length), 10);
return { l, n };
}
function processSpz(vertexBufferLoader) {
vertexBufferLoader._state = ResourceLoaderState.PROCESSING;
const spzLoader = vertexBufferLoader._spzLoader;
@ -351,6 +371,33 @@ function processSpz(vertexBufferLoader) {
255.0,
);
}
} else if (vertexBufferLoader._attributeSemantic.includes("SH_DEGREE_")) {
const { l, n } = extractSHDegreeAndCoef(
vertexBufferLoader._attributeSemantic,
);
const sphericalHarmonicDegree = gcloudData.shDegree;
let stride = 0;
const base = [0, 9, 24];
switch (sphericalHarmonicDegree) {
case 1:
stride = 9;
break;
case 2:
stride = 24;
break;
case 3:
stride = 45;
break;
}
const count = gcloudData.numPoints;
const sh = gcloudData.sh;
vertexBufferLoader._typedArray = new Float32Array(count * 3);
for (let i = 0; i < count; i++) {
const idx = i * stride + base[l - 1] + n * 3;
vertexBufferLoader._typedArray[i * 3] = sh[idx];
vertexBufferLoader._typedArray[i * 3 + 1] = sh[idx + 1];
vertexBufferLoader._typedArray[i * 3 + 2] = sh[idx + 2];
}
}
}

View File

@ -327,12 +327,58 @@ function Material(options) {
this._defaultTexture = undefined;
/**
* Any and all promises that are created when initializing the material.
* Examples: loading images and cubemaps.
*
* @type {Promise[]}
* @private
*/
this._initializationPromises = [];
/**
* An error that occurred in async operations during material initialization.
* Only one error is stored.
*
* @type {Error|undefined}
* @private
*/
this._initializationError = undefined;
initializeMaterial(options, this);
Object.defineProperties(this, {
type: {
value: this.type,
writable: false,
},
/**
* The {@link TextureMinificationFilter} to apply to this material's textures.
* @type {TextureMinificationFilter}
* @default TextureMinificationFilter.LINEAR
*/
minificationFilter: {
get: function () {
return this._minificationFilter;
},
set: function (value) {
this._minificationFilter = value;
},
},
/**
* The {@link TextureMagnificationFilter} to apply to this material's textures.
* @type {TextureMagnificationFilter}
* @default TextureMagnificationFilter.LINEAR
*/
magnificationFilter: {
get: function () {
return this._magnificationFilter;
},
set: function (value) {
this._magnificationFilter = value;
},
},
});
if (!defined(Material._uniformList[this.type])) {
@ -384,6 +430,68 @@ Material.fromType = function (type, uniforms) {
return material;
};
/**
* Creates a new material using an existing material type and returns a promise that resolves when
* all of the material's resources have been loaded.
*
* @param {string} type The base material type.
* @param {object} [uniforms] Overrides for the default uniforms.
* @returns {Promise<Material>} A promise that resolves to a new material object when all resources are loaded.
*
* @exception {DeveloperError} material with that type does not exist.
*
* @example
* const material = await Cesium.Material.fromTypeAsync('Image', {
* image: '../Images/Cesium_Logo_overlay.png'
* });
*/
Material.fromTypeAsync = async function (type, uniforms) {
//>>includeStart('debug', pragmas.debug);
if (!defined(Material._materialCache.getMaterial(type))) {
throw new DeveloperError(`material with type '${type}' does not exist.`);
}
//>>includeEnd('debug');
const initializationPromises = [];
// Unlike Material.fromType, we need to specify the uniforms in the Material constructor up front,
// or else anything that needs to be async loaded won't be kicked off until the next Update call.
const material = new Material({
fabric: {
type: type,
uniforms: uniforms,
},
});
// Recursively collect initialization promises for this material and its submaterials.
getInitializationPromises(material, initializationPromises);
await Promise.all(initializationPromises);
initializationPromises.length = 0;
if (defined(material._initializationError)) {
throw material._initializationError;
}
return material;
};
/**
* Recursively traverses the material and its submaterials to collect all initialization promises.
* @param {Material} material The material to traverse.
* @param {Promise[]} initializationPromises The array to collect promises into.
*
* @private
*/
function getInitializationPromises(material, initializationPromises) {
initializationPromises.push(...material._initializationPromises);
const submaterials = material.materials;
for (const name in submaterials) {
if (submaterials.hasOwnProperty(name)) {
const submaterial = submaterials[name];
getInitializationPromises(submaterial, initializationPromises);
}
}
}
/**
* Gets whether or not this material is translucent.
* @returns {boolean} <code>true</code> if this material is translucent, <code>false</code> otherwise.
@ -586,6 +694,7 @@ function initializeMaterial(options, result) {
result._strict = options.strict ?? false;
result._count = options.count ?? 0;
result._template = clone(options.fabric ?? Frozen.EMPTY_OBJECT);
result.fabric = clone(options.fabric ?? Frozen.EMPTY_OBJECT);
result._template.uniforms = clone(
result._template.uniforms ?? Frozen.EMPTY_OBJECT,
);
@ -616,15 +725,15 @@ function initializeMaterial(options, result) {
// Make sure the template has no obvious errors. More error checking happens later.
checkForTemplateErrors(result);
createMethodDefinition(result);
createUniforms(result);
createSubMaterials(result);
// If the material has a new type, add it to the cache.
if (!defined(cachedMaterial)) {
Material._materialCache.addMaterial(result.type, result);
}
createMethodDefinition(result);
createUniforms(result);
createSubMaterials(result);
const defaultTranslucent =
result._translucentFunctions.length === 0 ? true : undefined;
translucent = translucent ?? defaultTranslucent;
@ -858,10 +967,10 @@ function createTexture2DUpdateFunction(uniformId) {
texture.destroy();
}
texture = undefined;
material._texturePaths[uniformId] = undefined;
}
if (!defined(texture)) {
material._texturePaths[uniformId] = undefined;
texture = material._textures[uniformId] = material._defaultTexture;
uniformDimensionsName = `${uniformId}Dimensions`;
@ -876,59 +985,90 @@ function createTexture2DUpdateFunction(uniformId) {
return;
}
// When using the entity layer, the Resource objects get recreated on getValue because
// they are clonable. That's why we check the url property for Resources
// because the instances aren't the same and we keep trying to load the same
// image if it fails to load.
const isResource = uniformValue instanceof Resource;
if (
!defined(material._texturePaths[uniformId]) ||
(isResource &&
uniformValue.url !== material._texturePaths[uniformId].url) ||
(!isResource && uniformValue !== material._texturePaths[uniformId])
) {
if (typeof uniformValue === "string" || isResource) {
const resource = isResource
? uniformValue
: Resource.createIfNeeded(uniformValue);
let promise;
if (ktx2Regex.test(resource.url)) {
promise = loadKTX2(resource.url);
} else {
promise = resource.fetchImage();
}
Promise.resolve(promise)
.then(function (image) {
material._loadedImages.push({
id: uniformId,
image: image,
});
})
.catch(function () {
if (defined(texture) && texture !== material._defaultTexture) {
texture.destroy();
}
material._textures[uniformId] = material._defaultTexture;
});
} else if (
uniformValue instanceof HTMLCanvasElement ||
(uniformValue instanceof HTMLCanvasElement ||
uniformValue instanceof HTMLImageElement ||
uniformValue instanceof ImageBitmap ||
uniformValue instanceof OffscreenCanvas
) {
material._loadedImages.push({
id: uniformId,
image: uniformValue,
});
}
uniformValue instanceof OffscreenCanvas) &&
uniformValue !== material._texturePaths[uniformId]
) {
material._loadedImages.push({
id: uniformId,
image: uniformValue,
});
material._texturePaths[uniformId] = uniformValue;
return;
}
// If we get to this point, the image should be a string URL or Resource.
// Don't wait on the promise to resolve, just start loading the image and poll status from the update loop.
loadTexture2DImageForUniform(material, uniformId);
};
}
/**
* For a given uniform ID, potentially loads a texture image for the material, if the uniform value is a Resource or string URL,
* and has changed since the last time this was called (either on construction or update).
*
* @param {Material} material The material to load the texture for.
* @param {string} uniformId The ID of the uniform of the image.
* @returns A promise that resolves when the image is loaded, or a resolved promise if image loading is not necessary.
*
* @private
*/
function loadTexture2DImageForUniform(material, uniformId) {
const uniforms = material.uniforms;
const uniformValue = uniforms[uniformId];
if (uniformValue === Material.DefaultImageId) {
return Promise.resolve();
}
// Attempt to make a resource from the uniform value. If it's not already a resource or string, this returns the original object.
const resource = Resource.createIfNeeded(uniformValue);
if (!(resource instanceof Resource)) {
return Promise.resolve();
}
// When using the entity layer, the Resource objects get recreated on getValue because
// they are clonable. That's why we check the url property for Resources
// because the instances aren't the same and we keep trying to load the same
// image if it fails to load.
const oldResource = Resource.createIfNeeded(
material._texturePaths[uniformId],
);
const uniformHasChanged =
!defined(oldResource) || oldResource.url !== resource.url;
if (!uniformHasChanged) {
return Promise.resolve();
}
let promise;
if (ktx2Regex.test(resource.url)) {
promise = loadKTX2(resource.url);
} else {
promise = resource.fetchImage();
}
Promise.resolve(promise)
.then(function (image) {
material._loadedImages.push({
id: uniformId,
image: image,
});
})
.catch(function (error) {
material._initializationError = error;
const texture = material._textures[uniformId];
if (defined(texture) && texture !== material._defaultTexture) {
texture.destroy();
}
material._textures[uniformId] = material._defaultTexture;
});
material._texturePaths[uniformId] = uniformValue;
return promise;
}
function createCubeMapUpdateFunction(uniformId) {
return function (material, context) {
const uniformValue = material.uniforms[uniformId];
@ -944,44 +1084,66 @@ function createCubeMapUpdateFunction(uniformId) {
}
if (!defined(material._textures[uniformId])) {
material._texturePaths[uniformId] = undefined;
material._textures[uniformId] = context.defaultCubeMap;
}
if (uniformValue === Material.DefaultCubeMapId) {
return;
}
const path =
uniformValue.positiveX +
uniformValue.negativeX +
uniformValue.positiveY +
uniformValue.negativeY +
uniformValue.positiveZ +
uniformValue.negativeZ;
if (path !== material._texturePaths[uniformId]) {
const promises = [
Resource.createIfNeeded(uniformValue.positiveX).fetchImage(),
Resource.createIfNeeded(uniformValue.negativeX).fetchImage(),
Resource.createIfNeeded(uniformValue.positiveY).fetchImage(),
Resource.createIfNeeded(uniformValue.negativeY).fetchImage(),
Resource.createIfNeeded(uniformValue.positiveZ).fetchImage(),
Resource.createIfNeeded(uniformValue.negativeZ).fetchImage(),
];
Promise.all(promises).then(function (images) {
material._loadedCubeMaps.push({
id: uniformId,
images: images,
});
});
material._texturePaths[uniformId] = path;
}
loadCubeMapImagesForUniform(material, uniformId);
};
}
/**
* Loads the images for a cubemap uniform, if it has changed since the last time this was called.
*
* @param {Material} material The material to load the cubemap images for.
* @param {string} uniformId The ID of the uniform that corresponds to the cubemap images.
* @returns A promise that resolves when the images are loaded, or a resolved promise if image loading is not necessary.
*/
function loadCubeMapImagesForUniform(material, uniformId) {
const uniforms = material.uniforms;
const uniformValue = uniforms[uniformId];
if (uniformValue === Material.DefaultCubeMapId) {
return Promise.resolve();
}
const path =
uniformValue.positiveX +
uniformValue.negativeX +
uniformValue.positiveY +
uniformValue.negativeY +
uniformValue.positiveZ +
uniformValue.negativeZ;
// The uniform value is unchanged, no update / image load necessary.
if (path === material._texturePaths[uniformId]) {
return Promise.resolve();
}
const promises = [
Resource.createIfNeeded(uniformValue.positiveX).fetchImage(),
Resource.createIfNeeded(uniformValue.negativeX).fetchImage(),
Resource.createIfNeeded(uniformValue.positiveY).fetchImage(),
Resource.createIfNeeded(uniformValue.negativeY).fetchImage(),
Resource.createIfNeeded(uniformValue.positiveZ).fetchImage(),
Resource.createIfNeeded(uniformValue.negativeZ).fetchImage(),
];
const allPromise = Promise.all(promises);
allPromise
.then(function (images) {
material._loadedCubeMaps.push({
id: uniformId,
images: images,
});
})
.catch(function (error) {
material._initializationError = error;
});
material._texturePaths[uniformId] = path;
return allPromise;
}
function createUniforms(material) {
const uniforms = material._template.uniforms;
for (const uniformId in uniforms) {
@ -1059,11 +1221,17 @@ function createUniform(material, uniformId) {
return material._textures[uniformId];
};
material._updateFunctions.push(createTexture2DUpdateFunction(uniformId));
material._initializationPromises.push(
loadTexture2DImageForUniform(material, uniformId),
);
} else if (uniformType === "samplerCube") {
material._uniforms[newUniformId] = function () {
return material._textures[uniformId];
};
material._updateFunctions.push(createCubeMapUpdateFunction(uniformId));
material._initializationPromises.push(
loadCubeMapImagesForUniform(material, uniformId),
);
} else if (uniformType.indexOf("mat") !== -1) {
const scratchMatrix = new matrixMap[uniformType]();
material._uniforms[newUniformId] = function () {

View File

@ -79,10 +79,16 @@ ModelClippingPolygonsPipelineStage.process = function (
const uniformMap = {
model_clippingDistance: function () {
return clippingPolygons.clippingTexture;
return (
// The later should never happen during a render pass, see https://github.com/CesiumGS/cesium/issues/12725
clippingPolygons.clippingTexture ?? frameState.context.defaultTexture
);
},
model_clippingExtents: function () {
return clippingPolygons.extentsTexture;
return (
// The later should never happen during a render pass, see https://github.com/CesiumGS/cesium/issues/12725
clippingPolygons.extentsTexture ?? frameState.context.defaultTexture
);
},
};

View File

@ -8,11 +8,11 @@ void main() {
if (v_splitDirection < 0.0 && gl_FragCoord.x > czm_splitPosition) discard;
if (v_splitDirection > 0.0 && gl_FragCoord.x < czm_splitPosition) discard;
mediump float A = dot(v_vertPos, v_vertPos);
if(A > 1.0) {
float A = -dot(v_vertPos, v_vertPos);
if (A < -4.) {
discard;
}
mediump float scale = 4.0;
mediump float B = exp(-A * scale) * (v_splatColor.a);
out_FragColor = vec4(v_splatColor.rgb * B, B);
float B = exp(A * 4.) * v_splatColor.a ;
out_FragColor = vec4(v_splatColor.rgb * B , B);
}

View File

@ -7,18 +7,110 @@
//
// Discards splats outside the view frustum or with negligible screen size.
//
#if defined(HAS_SPHERICAL_HARMONICS)
const uint coefficientCount[3] = uint[3](3u,8u,15u);
const float SH_C1 = 0.48860251;
const float SH_C2[5] = float[5](
1.092548430,
-1.09254843,
0.315391565,
-1.09254843,
0.546274215
);
const float SH_C3[7] = float[7](
-0.59004358,
2.890611442,
-0.45704579,
0.373176332,
-0.45704579,
1.445305721,
-0.59004358
);
//Retrieve SH coefficient. Currently RG32UI format
uvec2 loadSHCoeff(uint splatID, int index) {
ivec2 shTexSize = textureSize(u_sphericalHarmonicsTexture, 0);
uint dims = coefficientCount[uint(u_sphericalHarmonicsDegree)-1u];
uint splatsPerRow = uint(shTexSize.x) / dims;
uint shIndex = (splatID%splatsPerRow) * dims + uint(index);
ivec2 shPosCoord = ivec2(shIndex, splatID / splatsPerRow);
return texelFetch(u_sphericalHarmonicsTexture, shPosCoord, 0).rg;
}
//Unpack RG32UI half float coefficients to vec3
vec3 halfToVec3(uvec2 packed) {
return vec3(unpackHalf2x16(packed.x), unpackHalf2x16(packed.y).x);
}
vec3 loadAndExpandSHCoeff(uint splatID, int index) {
uvec2 coeff = loadSHCoeff(splatID, index);
return halfToVec3(coeff);
}
vec3 evaluateSH(uint splatID, vec3 viewDir) {
vec3 result = vec3(0.0);
int coeffIndex = 0;
float x = viewDir.x, y = viewDir.y, z = viewDir.z;
if (u_sphericalHarmonicsDegree >= 1.) {
vec3 sh1 = loadAndExpandSHCoeff(splatID, coeffIndex++);
vec3 sh2 = loadAndExpandSHCoeff(splatID, coeffIndex++);
vec3 sh3 = loadAndExpandSHCoeff(splatID, coeffIndex++);
result += -SH_C1 * y * sh1 + SH_C1 * z * sh2 - SH_C1 * x * sh3;
if (u_sphericalHarmonicsDegree >= 2.) {
float xx = x * x;
float yy = y * y;
float zz = z * z;
float xy = x * y;
float yz = y * z;
float xz = x * z;
vec3 sh4 = loadAndExpandSHCoeff(splatID, coeffIndex++);
vec3 sh5 = loadAndExpandSHCoeff(splatID, coeffIndex++);
vec3 sh6 = loadAndExpandSHCoeff(splatID, coeffIndex++);
vec3 sh7 = loadAndExpandSHCoeff(splatID, coeffIndex++);
vec3 sh8 = loadAndExpandSHCoeff(splatID, coeffIndex++);
result += SH_C2[0] * xy * sh4 +
SH_C2[1] * yz * sh5 +
SH_C2[2] * (2.0f * zz - xx - yy) * sh6 +
SH_C2[3] * xz * sh7 +
SH_C2[4] * (xx - yy) * sh8;
if (u_sphericalHarmonicsDegree >= 3.) {
vec3 sh9 = loadAndExpandSHCoeff(splatID, coeffIndex++);
vec3 sh10 = loadAndExpandSHCoeff(splatID, coeffIndex++);
vec3 sh11 = loadAndExpandSHCoeff(splatID, coeffIndex++);
vec3 sh12 = loadAndExpandSHCoeff(splatID, coeffIndex++);
vec3 sh13 = loadAndExpandSHCoeff(splatID, coeffIndex++);
vec3 sh14 = loadAndExpandSHCoeff(splatID, coeffIndex++);
vec3 sh15 = loadAndExpandSHCoeff(splatID, coeffIndex++);
result += SH_C3[0] * y * (3.0f * xx - yy) * sh9 +
SH_C3[1] * xy * z * sh10 +
SH_C3[2] * y * (4.0f * zz - xx - yy) * sh11 +
SH_C3[3] * z * (2.0f * zz - 3.0f * xx - 3.0f * yy) * sh12 +
SH_C3[4] * x * (4.0f * zz - xx - yy) * sh13 +
SH_C3[5] * z * (xx - yy) * sh14 +
SH_C3[6] * x * (xx - 3.0f * yy) * sh15;
}
}
}
return result;
}
#endif
// Transforms and projects splat covariance into screen space and extracts the major and minor axes of the Gaussian ellipsoid
// which is used to calculate the vertex position in clip space.
vec4 calcCovVectors(vec3 viewPos, mat3 Vrk) {
vec4 t = vec4(viewPos, 1.0);
float focal = czm_viewport.z * czm_projection[0][0];
vec2 focal = vec2(czm_projection[0][0] * czm_viewport.z, czm_projection[1][1] * czm_viewport.w);
float J1 = focal / t.z;
vec2 J2 = -J1 / t.z * t.xy;
vec2 J1 = focal / t.z;
vec2 J2 = -focal * vec2(t.x, t.y) / (t.z * t.z);
mat3 J = mat3(
J1, 0.0, J2.x,
0.0, J1, J2.y,
J1.x, 0.0, J2.x,
0.0, J1.y, J2.y,
0.0, 0.0, 0.0
);
@ -87,6 +179,11 @@ void main() {
v_vertPos = corner ;
v_splatColor = vec4(covariance.w & 0xffu, (covariance.w >> 8) & 0xffu, (covariance.w >> 16) & 0xffu, (covariance.w >> 24) & 0xffu) / 255.0;
#if defined(HAS_SPHERICAL_HARMONICS)
vec4 splatWC = czm_inverseView * splatViewPos;
vec3 viewDirModel = normalize(u_inverseModelRotation * (splatWC.xyz - u_cameraPositionWC.xyz));
v_splatColor.rgb += evaluateSH(texIdx, viewDirModel).rgb;
#endif
v_splitDirection = u_splitDirection;
}

View File

@ -59,7 +59,7 @@ describe("Core/loadImageFromTypedArray", function () {
})
.then(function () {
expect(window.createImageBitmap).toHaveBeenCalledWith(blob, {
imageOrientation: "from-image",
imageOrientation: "none",
premultiplyAlpha: "none",
colorSpaceConversion: "default",
});
@ -91,7 +91,7 @@ describe("Core/loadImageFromTypedArray", function () {
return loadImageFromTypedArray(options)
.then(function () {
expect(window.createImageBitmap).toHaveBeenCalledWith(blob, {
imageOrientation: "from-image",
imageOrientation: "none",
premultiplyAlpha: "none",
colorSpaceConversion: "none",
});
@ -102,7 +102,7 @@ describe("Core/loadImageFromTypedArray", function () {
})
.then(function () {
expect(window.createImageBitmap).toHaveBeenCalledWith(blob, {
imageOrientation: "from-image",
imageOrientation: "none",
premultiplyAlpha: "none",
colorSpaceConversion: "default",
});

View File

@ -65,26 +65,26 @@ describe(
expect(content).toBeDefined();
expect(content instanceof GaussianSplat3DTileContent).toBe(true);
const splatPrimitive = content.splatPrimitive;
expect(splatPrimitive).toBeDefined();
expect(splatPrimitive.attributes.length).toBeGreaterThan(0);
const gltfPrimitive = content.gltfPrimitive;
expect(gltfPrimitive).toBeDefined();
expect(gltfPrimitive.attributes.length).toBeGreaterThan(0);
const positions = ModelUtility.getAttributeBySemantic(
splatPrimitive,
gltfPrimitive,
VertexAttributeSemantic.POSITION,
).typedArray;
const rotations = ModelUtility.getAttributeBySemantic(
splatPrimitive,
gltfPrimitive,
VertexAttributeSemantic.ROTATION,
).typedArray;
const scales = ModelUtility.getAttributeBySemantic(
splatPrimitive,
gltfPrimitive,
VertexAttributeSemantic.SCALE,
).typedArray;
const colors = ModelUtility.getAttributeBySemantic(
splatPrimitive,
gltfPrimitive,
VertexAttributeSemantic.COLOR,
).typedArray;
@ -174,26 +174,26 @@ describe(
expect(content).toBeDefined();
expect(content instanceof GaussianSplat3DTileContent).toBe(true);
const splatPrimitive = content.splatPrimitive;
expect(splatPrimitive).toBeDefined();
expect(splatPrimitive.attributes.length).toBeGreaterThan(0);
const gltfPrimitive = content.gltfPrimitive;
expect(gltfPrimitive).toBeDefined();
expect(gltfPrimitive.attributes.length).toBeGreaterThan(0);
const positions = ModelUtility.getAttributeBySemantic(
splatPrimitive,
gltfPrimitive,
VertexAttributeSemantic.POSITION,
).typedArray;
const rotations = ModelUtility.getAttributeBySemantic(
splatPrimitive,
gltfPrimitive,
VertexAttributeSemantic.ROTATION,
).typedArray;
const scales = ModelUtility.getAttributeBySemantic(
splatPrimitive,
gltfPrimitive,
VertexAttributeSemantic.SCALE,
).typedArray;
const colors = ModelUtility.getAttributeBySemantic(
splatPrimitive,
gltfPrimitive,
VertexAttributeSemantic.COLOR,
).typedArray;

View File

@ -5,21 +5,31 @@ import {
RequestScheduler,
HeadingPitchRange,
GaussianSplat3DTileContent,
Transforms,
} from "../../index.js";
import Cesium3DTilesTester from "../../../../Specs/Cesium3DTilesTester.js";
import createScene from "../../../../Specs/createScene.js";
import createCanvas from "../../../../Specs/createCanvas.js";
import pollToPromise from "../../../../Specs/pollToPromise.js";
describe(
"Scene/GaussianSplatPrimitive",
function () {
const tilesetUrl = "./Data/Cesium3DTiles/GaussianSplats/tower/tileset.json";
const sphericalHarmonicUrl =
"./Data/Cesium3DTiles/GaussianSplats/sh_unit_cube/tileset.json";
let scene;
let options;
let camera;
const canvassize = { width: 512, height: 512 };
const samplePosition =
((canvassize.width / 2) * canvassize.height + canvassize.width / 2) * 4;
beforeAll(function () {
scene = createScene();
const canvas = createCanvas(canvassize.width, canvassize.height);
scene = createScene({ canvas });
});
afterAll(function () {
@ -30,7 +40,7 @@ describe(
RequestScheduler.clearForSpecs();
scene.morphTo3D(0.0);
const camera = scene.camera;
camera = scene.camera;
camera.frustum = new PerspectiveFrustum();
camera.frustum.aspectRatio =
scene.drawingBufferWidth / scene.drawingBufferHeight;
@ -50,7 +60,7 @@ describe(
it("loads a Gaussian splats tileset", async function () {
const tileset = await Cesium3DTilesTester.loadTileset(
scene,
tilesetUrl,
sphericalHarmonicUrl,
options,
);
scene.camera.lookAt(
@ -58,7 +68,6 @@ describe(
new HeadingPitchRange(0.0, -1.57, tileset.boundingSphere.radius),
);
expect(tileset.hasExtension("3DTILES_content_gltf")).toBe(true);
expect(tileset.isGltfExtensionUsed("KHR_gaussian_splatting")).toBe(true);
expect(
tileset.isGltfExtensionUsed("KHR_gaussian_splatting_compression_spz_2"),
).toBe(true);
@ -80,26 +89,127 @@ describe(
expect(tile.content instanceof GaussianSplat3DTileContent).toBe(true);
});
xit("loads a Gaussian splats tileset and toggles visibility", async function () {
it("loads a Gaussian splats tileset and toggles visibility", async function () {
const tileset = await Cesium3DTilesTester.loadTileset(
scene,
tilesetUrl,
sphericalHarmonicUrl,
options,
);
scene.camera.lookAt(
tileset.boundingSphere.center,
new HeadingPitchRange(0.0, -1.57, tileset.boundingSphere.radius),
);
const tile = await Cesium3DTilesTester.waitForTileContentReady(
scene,
tileset.root,
);
expect(tile.content).toBeDefined();
expect(tileset.gaussianSplatPrimitive).toBeDefined();
expect(scene).notToRender([0, 0, 0, 255]);
const gsPrim = tileset.gaussianSplatPrimitive;
expect(gsPrim).toBeDefined();
await pollToPromise(function () {
scene.renderForSpecs();
return gsPrim._dirty === false && gsPrim._sorterPromise === undefined;
});
scene.renderForSpecs();
expect(scene).toRenderAndCall(function (rgba) {
expect(rgba[samplePosition + 0]).not.toBe(0);
expect(rgba[samplePosition + 1]).not.toBe(0);
expect(rgba[samplePosition + 2]).not.toBe(0);
});
tileset.show = false;
expect(scene).toRender([0, 0, 0, 255]);
scene.renderForSpecs();
expect(scene).toRenderAndCall(function (rgba) {
expect(rgba[samplePosition + 0]).toBe(0);
expect(rgba[samplePosition + 1]).toBe(0);
expect(rgba[samplePosition + 2]).toBe(0);
});
});
it("Check Spherical Harmonic specular on a Gaussian splats tileset", async function () {
const tileset = await Cesium3DTilesTester.loadTileset(
scene,
sphericalHarmonicUrl,
options,
);
const boundingSphere = tileset.boundingSphere;
const yellowish = new HeadingPitchRange(
CesiumMath.toRadians(231),
CesiumMath.toRadians(-75),
tileset.boundingSphere.radius / 10,
);
const orangeish = new HeadingPitchRange(
CesiumMath.toRadians(2),
CesiumMath.toRadians(-76),
tileset.boundingSphere.radius / 10,
);
const purplish = new HeadingPitchRange(
CesiumMath.toRadians(100),
CesiumMath.toRadians(66),
tileset.boundingSphere.radius / 10,
);
const targetOrange = { red: 210, green: 156, blue: 98 };
const targetYellow = { red: 189, green: 173, blue: 97 };
const targetPurple = { red: 127, green: 80, blue: 141 };
tileset.show = true;
const enu = Transforms.eastNorthUpToFixedFrame(boundingSphere.center);
scene.camera.lookAtTransform(enu, yellowish);
await Cesium3DTilesTester.waitForTileContentReady(scene, tileset.root);
const gsPrim = tileset.gaussianSplatPrimitive;
await pollToPromise(function () {
scene.renderForSpecs();
return gsPrim._dirty === false && gsPrim._sorterPromise === undefined;
});
for (let i = 0; i < 100; ++i) {
scene.renderForSpecs();
}
scene.renderForSpecs();
expect(scene).toRenderAndCall(function (rgba) {
expect(rgba[samplePosition + 0]).toBeCloseTo(targetYellow.red, -1);
expect(rgba[samplePosition + 1]).toBeCloseTo(targetYellow.green, -1);
expect(rgba[samplePosition + 2]).toBeCloseTo(targetYellow.blue, -1);
});
scene.camera.lookAtTransform(enu, orangeish);
await Cesium3DTilesTester.waitForTileContentReady(scene, tileset.root);
await pollToPromise(function () {
scene.renderForSpecs();
return gsPrim._dirty === false && gsPrim._sorterState === 0;
});
scene.renderForSpecs();
expect(scene).toRenderAndCall(function (rgba) {
expect(rgba[samplePosition + 0]).toBeCloseTo(targetOrange.red, -1);
expect(rgba[samplePosition + 1]).toBeCloseTo(targetOrange.green, -1);
expect(rgba[samplePosition + 2]).toBeCloseTo(targetOrange.blue, -1);
});
scene.camera.lookAtTransform(enu, purplish);
await Cesium3DTilesTester.waitForTileContentReady(scene, tileset.root);
await pollToPromise(function () {
scene.renderForSpecs();
return gsPrim._dirty === false && gsPrim._sorterState === 0;
});
scene.renderForSpecs();
expect(scene).toRenderAndCall(function (rgba) {
expect(rgba[samplePosition + 0]).toBeCloseTo(targetPurple.red, -1);
expect(rgba[samplePosition + 1]).toBeCloseTo(targetPurple.green, -1);
expect(rgba[samplePosition + 2]).toBeCloseTo(targetPurple.blue, -1);
});
});
},
"WebGL",

View File

@ -14,6 +14,7 @@ import {
Primitive,
TextureMagnificationFilter,
TextureMinificationFilter,
DeveloperError,
} from "../../index.js";
import createScene from "../../../../Specs/createScene.js";
@ -24,7 +25,6 @@ describe(
function () {
let scene;
const rectangle = Rectangle.fromDegrees(-10.0, -10.0, 10.0, 10.0);
let polygon;
const backgroundColor = [0, 0, 128, 255];
let polylines;
@ -40,7 +40,9 @@ describe(
scene.backgroundColor,
);
scene.primitives.destroyPrimitives = false;
scene.camera.setView({ destination: rectangle });
scene.camera.setView({
destination: Rectangle.fromDegrees(-10.0, -10.0, 10.0, 10.0),
});
});
afterAll(function () {
@ -54,7 +56,7 @@ describe(
geometryInstances: new GeometryInstance({
geometry: new RectangleGeometry({
vertexFormat: vertexFormat,
rectangle: rectangle,
rectangle: Rectangle.fromDegrees(-10.0, -10.0, 10.0, 10.0),
}),
}),
asynchronous: false,
@ -82,6 +84,19 @@ describe(
polylines = polylines && polylines.destroy();
});
function itRenders(initialColor = backgroundColor) {
it("renders", function () {
expect(scene).toRender(initialColor);
scene.primitives.removeAll();
scene.primitives.add(polygon);
expect(scene).toRenderAndCall(function (rgba) {
expect(rgba).not.toEqual(backgroundColor);
});
});
}
function renderMaterial(material, ignoreBackground, callback) {
ignoreBackground = ignoreBackground ?? false;
polygon.appearance.material = material;
@ -100,116 +115,63 @@ describe(
});
}
function renderPolylineMaterial(material) {
polyline.material = material;
expect(scene).toRender(backgroundColor);
scene.primitives.removeAll();
scene.primitives.add(polylines);
let result;
expect(scene).toRenderAndCall(function (rgba) {
result = rgba;
expect(rgba).not.toEqual(backgroundColor);
});
return result;
}
function verifyMaterial(type) {
const material = new Material({
strict: true,
fabric: {
type: type,
},
describe(`${type} built-in material`, function () {
beforeEach(function () {
const material = new Material({
strict: true,
fabric: {
type: type,
},
});
polygon.appearance.material = material;
});
itRenders();
});
renderMaterial(material);
}
function verifyPolylineMaterial(type) {
const material = new Material({
strict: true,
fabric: {
type: type,
},
describe(`${type} built-in material`, function () {
it("renders", function () {
const material = new Material({
strict: true,
fabric: {
type: type,
},
});
polyline.material = material;
expect(scene).toRender(backgroundColor);
scene.primitives.removeAll();
scene.primitives.add(polylines);
expect(scene).notToRender(backgroundColor);
});
});
renderPolylineMaterial(material);
}
it("draws Color built-in material", function () {
verifyMaterial("Color");
});
verifyMaterial("Color");
verifyMaterial("Image");
verifyMaterial("DiffuseMap");
verifyMaterial("AlphaMap");
verifyMaterial("SpecularMap");
verifyMaterial("EmissionMap");
verifyMaterial("BumpMap");
verifyMaterial("NormalMap");
verifyMaterial("Grid");
verifyMaterial("Stripe");
verifyMaterial("Checkerboard");
verifyMaterial("Dot");
verifyMaterial("Water");
verifyMaterial("RimLighting");
verifyMaterial("Fade");
it("draws Image built-in material", function () {
verifyMaterial("Image");
});
it("draws DiffuseMap built-in material", function () {
verifyMaterial("DiffuseMap");
});
it("draws AlphaMap built-in material", function () {
verifyMaterial("AlphaMap");
});
it("draws SpecularMap built-in material", function () {
verifyMaterial("SpecularMap");
});
it("draws EmissionMap built-in material", function () {
verifyMaterial("EmissionMap");
});
it("draws BumpMap built-in material", function () {
verifyMaterial("BumpMap");
});
it("draws NormalMap built-in material", function () {
verifyMaterial("NormalMap");
});
it("draws Grid built-in material", function () {
verifyMaterial("Grid");
});
it("draws Stripe built-in material", function () {
verifyMaterial("Stripe");
});
it("draws Checkerboard built-in material", function () {
verifyMaterial("Checkerboard");
});
it("draws Dot built-in material", function () {
verifyMaterial("Dot");
});
it("draws Water built-in material", function () {
verifyMaterial("Water");
});
it("draws RimLighting built-in material", function () {
verifyMaterial("RimLighting");
});
it("draws Fade built-in material", function () {
verifyMaterial("Fade");
});
it("draws PolylineArrow built-in material", function () {
verifyPolylineMaterial("PolylineArrow");
});
it("draws PolylineDash built-in material", function () {
verifyPolylineMaterial("PolylineDash");
});
it("draws PolylineGlow built-in material", function () {
verifyPolylineMaterial("PolylineGlow");
});
it("draws PolylineOutline built-in material", function () {
verifyPolylineMaterial("PolylineOutline");
});
verifyPolylineMaterial("PolylineArrow");
verifyPolylineMaterial("PolylineDash");
verifyPolylineMaterial("PolylineGlow");
verifyPolylineMaterial("PolylineOutline");
it("gets the material type", function () {
const material = new Material({
@ -630,55 +592,117 @@ describe(
});
});
it("creates material with custom texture filter", function () {
const materialLinear = new Material({
it("creates a material using fromTypeAsync", async function () {
const material = await Material.fromTypeAsync("Color");
renderMaterial(material);
});
it("loads a 2D texture image synchronously when awaiting fromTypeAsync", async function () {
const imageMaterial = await Material.fromTypeAsync("Image", {
image: "./Data/Images/Blue.png",
});
renderMaterial(imageMaterial, false, function (rgba) {
expect(rgba).toEqual([0, 0, 255, 255]);
});
});
it("loads cubemap images synchronously when awaiting fromTypeAsync", async function () {
// First make a material with a cubemap, then use its type to make a second cubemap material asynchronously.
const material = new Material({
strict: true,
fabric: {
type: "DiffuseMap",
uniforms: {
image: "./Data/Images/BlueOverRed.png",
cubeMap: {
positiveX: "./Data/Images/Blue.png",
negativeX: "./Data/Images/Blue.png",
positiveY: "./Data/Images/Blue.png",
negativeY: "./Data/Images/Blue.png",
positiveZ: "./Data/Images/Blue.png",
negativeZ: "./Data/Images/Blue.png",
},
},
source:
"uniform samplerCube cubeMap;\n" +
"czm_material czm_getMaterial(czm_materialInput materialInput)\n" +
"{\n" +
" czm_material material = czm_getDefaultMaterial(materialInput);\n" +
" material.diffuse = czm_textureCube(cubeMap, vec3(1.0)).xyz;\n" +
" return material;\n" +
"}\n",
},
minificationFilter: TextureMinificationFilter.LINEAR,
magnificationFilter: TextureMagnificationFilter.LINEAR,
});
const materialNearest = new Material({
fabric: {
type: "DiffuseMap",
uniforms: {
image: "./Data/Images/BlueOverRed.png",
const materialFromTypeAsync = await Material.fromTypeAsync(
material.type,
{
cubeMap: {
positiveX: "./Data/Images/Green.png",
negativeX: "./Data/Images/Green.png",
positiveY: "./Data/Images/Green.png",
negativeY: "./Data/Images/Green.png",
positiveZ: "./Data/Images/Green.png",
negativeZ: "./Data/Images/Green.png",
},
},
);
renderMaterial(materialFromTypeAsync);
});
it("loads sub-materials synchronously when awaiting fromTypeAsync", async function () {
// First make a material with submaterials, then use its type to make a second material asynchronously.
const material = new Material({
strict: true,
fabric: {
materials: {
greenMaterial: {
type: "Image",
uniforms: {
image: "./Data/Images/Green.png", // Green image
},
},
blueMaterial: {
type: "Image",
uniforms: {
image: "./Data/Images/Blue.png", // Blue image
},
},
},
components: {
diffuse:
"clamp(greenMaterial.diffuse + blueMaterial.diffuse, 0.0, 1.0)",
},
},
minificationFilter: TextureMinificationFilter.NEAREST,
magnificationFilter: TextureMagnificationFilter.NEAREST,
});
const materialFromTypeAsync = await Material.fromTypeAsync(material.type);
renderMaterial(materialFromTypeAsync, false, function (rgba) {
expect(rgba).toEqual([0, 255, 255, 255]); // Expect cyan from green + blue
});
});
it("creates material with custom texture filter", async function () {
const materialLinear = await Material.fromTypeAsync("DiffuseMap", {
image: "./Data/Images/BlueOverRed.png",
});
materialLinear.minificationFilter = TextureMinificationFilter.LINEAR;
materialLinear.magnificationFilter = TextureMagnificationFilter.LINEAR;
const materialNearest = await Material.fromTypeAsync("DiffuseMap", {
image: "./Data/Images/BlueOverRed.png",
});
materialNearest.minificationFilter = TextureMinificationFilter.NEAREST;
materialNearest.magnificationFilter = TextureMagnificationFilter.NEAREST;
const purple = [127, 0, 127, 255];
const ignoreBackground = true;
renderMaterial(materialLinear, ignoreBackground); // Populate the scene with the primitive prior to updating
return pollToPromise(function () {
const imageLoaded = materialLinear._loadedImages.length !== 0;
scene.renderForSpecs();
return imageLoaded;
})
.then(function () {
renderMaterial(materialLinear, ignoreBackground, function (rgba) {
expect(rgba).toEqualEpsilon(purple, 1);
});
})
.then(function () {
renderMaterial(materialNearest, ignoreBackground); // Populate the scene with the primitive prior to updating
return pollToPromise(function () {
const imageLoaded = materialNearest._loadedImages.length !== 0;
scene.renderForSpecs();
return imageLoaded;
}).then(function () {
renderMaterial(materialNearest, ignoreBackground, function (rgba) {
expect(rgba).not.toEqualEpsilon(purple, 1);
});
});
});
renderMaterial(materialLinear, ignoreBackground, function (rgba) {
expect(rgba).toEqualEpsilon(purple, 1);
});
renderMaterial(materialNearest, ignoreBackground, function (rgba) {
expect(rgba).not.toEqualEpsilon(purple, 1);
});
});
it("handles when material image is undefined", function () {
@ -1085,6 +1109,18 @@ describe(
renderMaterial(material);
material.destroy();
});
it("throws when loaded async and image loading fails", async function () {
spyOn(Resource.prototype, "fetchImage").and.callFake(function () {
return Promise.reject(new DeveloperError("Image loading failed"));
});
await expectAsync(
Material.fromTypeAsync("DiffuseMap", {
image: "i_dont_exist.png",
}),
).toBeRejectedWithDeveloperError("Image loading failed");
});
},
"WebGL",
);

View File

@ -1,6 +1,6 @@
{
"name": "@cesium/engine",
"version": "19.0.0",
"version": "20.0.0",
"description": "CesiumJS is a JavaScript library for creating 3D globes and 2D maps in a web browser without a plugin.",
"keywords": [
"3D",
@ -35,7 +35,7 @@
"@cesium/wasm-splats": "^0.1.0-alpha.2",
"@spz-loader/core": "0.3.0",
"@tweenjs/tween.js": "^25.0.0",
"@zip.js/zip.js": "^2.7.70",
"@zip.js/zip.js": "^2.8.1",
"autolinker": "^4.0.0",
"bitmap-sdf": "^1.0.3",
"dompurify": "^3.0.2",
@ -47,7 +47,7 @@
"ktx-parse": "^1.0.0",
"lerc": "^2.0.0",
"mersenne-twister": "^1.1.0",
"meshoptimizer": "^0.24.0",
"meshoptimizer": "^0.25.0",
"pako": "^2.0.4",
"protobufjs": "^7.1.0",
"rbush": "^4.0.1",

View File

@ -13,7 +13,6 @@ dist-ssr
*.local
*.tsbuildinfo
gallery/list.json
gallery/pagefind
public/gallery
templates/Sandcastle.d.ts
templates/Sandcastle.js

View File

@ -0,0 +1,332 @@
/*
* Sourced from https://github.com/Pagefind/pagefind/blob/main/pagefind_web_js/types/index.d.ts
*/
export {};
declare global {
// Copied/extracted from https://github.com/Pagefind/pagefind/blob/main/pagefind_web_js/lib/coupled_search.ts
// and https://github.com/Pagefind/pagefind/blob/main/pagefind_web_js/types/index.d.ts
// Issue: https://github.com/Pagefind/pagefind/issues/767
type Pagefind = {
init: (overrideLanguage?: string) => void;
search: (
term: string,
options?: PagefindSearchOptions,
) => Promise<PagefindIndexesSearchResults>;
preload: (term: string, options?: PagefindSearchOptions) => Promise<void>;
filters: () => Promise<PagefindFilterCounts>;
options: (options: PagefindIndexOptions) => Promise<void>;
};
/** Global index options that can be passed to pagefind.options() */
type PagefindIndexOptions = {
/** Overrides the URL path that Pagefind uses to load its search bundle */
basePath?: string;
/** Appends the given baseURL to all search results. May be a path, or a full domain */
baseUrl?: string;
/** The maximum length of excerpts that Pagefind should generate for search results. Default to 30 */
excerptLength?: number;
/**
* Multiply all rankings for this index by the given weight.
*
* Only applies in multisite setups, where one site should rank higher or lower than others.
*/
indexWeight?: number;
/**
* Merge this filter object into all search queries in this index.
*
* Only applies in multisite setups.
*/
mergeFilter?: object;
/**
* If set, will ass the search term as a query parameter under this key, for use with Pagefind's highlighting script.
*/
highlightParam?: string;
language?: string;
/**
* Whether an instance of Pagefind is the primary index or not (for multisite).
*
* This is set for you automatically, so it is unlikely you should set this directly.
*/
primary?: boolean;
/**
* Provides the ability to fine tune Pagefind's ranking algorithm to better suit your dataset.
*/
ranking?: PagefindRankingWeights;
};
type PagefindRankingWeights = {
/**
Controls page ranking based on similarity of terms to the search query (in length).
Increasing this number means pages rank higher when they contain words very close to the query,
e.g. if searching for `part` then `party` will boost a page higher than one containing `partition`.
Minimum value is 0.0, where `party` and `partition` would be viewed equally.
*/
termSimilarity?: number;
/**
Controls how much effect the average page length has on ranking.
Maximum value is 1.0, where ranking will strongly favour pages that are shorter than the average page on the site.
Minimum value is 0.0, where ranking will exclusively look at term frequency, regardless of how long a document is.
*/
pageLength?: number;
/**
Controls how quickly a term saturates on the page and reduces impact on the ranking.
Maximum value is 2.0, where pages will take a long time to saturate, and pages with very high term frequencies will take over.
As this number trends to 0, it does not take many terms to saturate and allow other paramaters to influence the ranking.
Minimum value is 0.0, where terms will saturate immediately and results will not distinguish between one term and many.
*/
termSaturation?: number;
/**
Controls how much ranking uses term frequency versus raw term count.
Maximum value is 1.0, where term frequency fully applies and is the main ranking factor.
Minimum value is 0.0, where term frequency does not apply, and pages are ranked based on the raw sum of words and weights.
Values between 0.0 and 1.0 will interpolate between the two ranking methods.
Reducing this number is a good way to boost longer documents in your search results, as they no longer get penalized for having a low term frequency.
*/
termFrequency?: number;
};
/** Options that can be passed to pagefind.search() */
type PagefindSearchOptions = {
/** If set, this call will load all assets but return before searching. Prefer using pagefind.preload() instead */
preload?: boolean;
/** Add more verbose console logging for this search query */
verbose?: boolean;
/** The set of filters to execute with this search. Input type is extremely flexible, see the filtering docs for details */
filters?: object;
/** The set of sorts to use for this search, instead of relevancy */
sort?: object;
};
/** Filter counts returned from pagefind.filters(), and alongside results from pagefind.search() */
type PagefindFilterCounts = Record<string, Record<string, number>>;
/** The main results object returned from a call to pagefind.search() */
type PagefindSearchResults = {
/** All pages that match the search query and filters provided */
results: PagefindSearchResult[];
/** How many results would there have been if you had omitted the filters */
unfilteredResultCount: number;
/** Given the query and filters provided, how many remaining results are there under each filter? */
filters: PagefindFilterCounts;
/** If the searched filters were removed, how many total results for each filter are there? */
totalFilters: PagefindFilterCounts;
/** Information on how long it took Pagefind to execute this query */
timings: {
preload: number;
search: number;
total: number;
};
/** Verbose information on stemming returned in the Pagefind Playground */
search_keywords?: string[];
};
/** The main results object returned from a call to pagefind.search() */
type PagefindIndexesSearchResults = {
/** All pages that match the search query and filters provided */
results: PagefindSearchResult[];
/** How many results would there have been if you had omitted the filters */
unfilteredResultCount: number;
/** Given the query and filters provided, how many remaining results are there under each filter? */
filters: PagefindFilterCounts;
/** If the searched filters were removed, how many total results for each filter are there? */
totalFilters: PagefindFilterCounts;
/** Information on how long it took Pagefind to execute this query */
timings: {
preload: number;
search: number;
total: number;
}[];
/** Verbose information on stemming returned in the Pagefind Playground */
search_keywords?: string[];
};
/** A single result from a search query, before actual data has been loaded */
type PagefindSearchResult = {
/** Pagefind's internal ID for this page, unique across the site */
id: string;
/** Pagefind's internal score for your query matching this page, that is used when ranking these results */
score: number;
/** The locations of all matching words in this page */
words: number[];
/** Verbose information returned in the Pagefind playground mode */
params?: PagefindTermParams;
/** Verbose information returned in the Pagefind playground mode */
scores?: PagefindTermScore[];
/**
* Calling data() loads the final data fragment needed to display this result.
*
* Only call this when you need to display the data, rather than all at once.
* (e.g. one page as a time, or in a scroll listener)
* */
data: () => Promise<PagefindSearchFragment>;
};
export type PagefindTermParams = {
/** Length of this result */
document_length: number;
/** Average page length */
average_page_length: number;
/** Total pages */
total_pages: number;
};
type PagefindTermScore = {
/** Term */
search_term: string;
/** Inverse document frequency for term — how common is this word */
idf: number;
/** Term frequency, saturating */
saturating_tf: number;
/** Term frequency, raw counts */
raw_tf: number;
/** Pagefind output term frequency */
pagefind_tf: number;
/** Final score for term */
score: number;
/** Input parameters used for the score */
params: PagefindTermScoreParams;
};
export type PagefindTermScoreParams = {
/** Weighted term frequency */
weighted_term_frequency: number;
/** Pages containing term */
pages_containing_term: number;
/** Length bonus */
length_bonus: number;
};
/** The useful data Pagefind provides for a search result */
type PagefindSearchFragment = {
/** Pagefind's processed URL for this page. Will include the baseUrl if configured */
url: string;
/** Pagefind's unprocessed URL for this page */
raw_url?: string;
/** The full processed content text of this page */
content: string;
/** Internal type — ignore for now */
raw_content?: string;
/** The processed excerpt for this result, with matching terms wrapping in `<mark>` elements */
excerpt: string;
/**
* What regions of the page matched this search query?
*
* Precalculates based on h1->6 tags with IDs, using the text between each.
*/
sub_results: PagefindSubResult[];
/** How many total words are there on this page? */
word_count: number;
/** The locations of all matching words in this page */
locations: number[];
/**
* The locations of all matching words in this page,
* paired with data about their weight and relevance to this query
*/
weighted_locations: PagefindWordLocation[];
/** The filter keys and values this page was tagged with */
filters: Record<string, string[]>;
/** The metadata keys and values this page was tagged with */
meta: Record<string, string>;
/**
* The raw anchor data that Pagefind used to generate sub_results.
*
* Contains _all_ elements that had IDs on the page, so can be used to
* implement your own sub result calculations with different semantics.
*/
anchors: PagefindSearchAnchor[];
};
/** Data for a matched section within a page */
type PagefindSubResult = {
/**
* Title of this sub result derived from the heading content.
*
* If this is a result for the section of the page before any headings with IDs,
* this will be the same as the page's meta.title value.
*/
title: string;
/**
* Direct URL to this sub result, comprised of the page's URL plus the hash string of the heading.
*
* If this is a result for the section of the page before any headings with IDs,
* this will be the same as the page URL.
*/
url: string;
/** The locations of all matching words in this segment */
locations: number[];
/**
* The locations of all matching words in this segment,
* paired with data about their weight and relevance to this query
*/
weighted_locations: PagefindWordLocation[];
/** The processed excerpt for this segment, with matching terms wrapping in `<mark>` elements */
excerpt: string;
/**
* Raw data about the anchor element associated with this sub result.
*
* The omission of this field means this sub result is for text found on the page
* before the first heading that had an ID.
*/
anchor?: PagefindSearchAnchor;
};
/** Information about a matching word on a page */
type PagefindWordLocation = {
/** The weight that this word was originally tagged as */
weight: number;
/**
* An internal score that Pagefind calculated for this word.
*
* The absolute value is somewhat meaningless, but the value can be used
* in comparison to other values in this set of search results to perform custom ranking.
*/
balanced_score: number;
/**
* The index of this word in the result content.
*
* Splitting the content key by whitespacing and indexing by this number
* will yield the correct word.
*/
location: number;
/**
* Verbose word information returned when running Pagefind in playground mode.
*/
verbose?: PagefindVerboseWordLocation;
};
/** Verbose playground information about a matching word on a page */
type PagefindVerboseWordLocation = {
/**
* The indexed string for this word, usually stemmed.
*/
word_string: string;
/**
* The scoring bonus this word received based on length similarity to a search term.
*/
length_bonus: number;
};
/** Raw data about elements with IDs that Pagefind encountered when indexing the page */
type PagefindSearchAnchor = {
/** What element type was this anchor? e.g. `h1`, `div` */
element: string;
/** The raw id="..." attribute contents of the element */
id: string;
/**
* The text content of this element.
*
* In order to prevent repeating most of the page data for every anchor,
* Pagefind will only take top level text nodes, or text nodes nested within
* inline elements such as <a> and <span>.
*/
text?: string;
/**
* The position of this anchor in the result content.
* Splitting the content key by whitespacing and indexing by this number
* will yield the first word indexed after this element's ID was found.
*/
location: number;
};
}

View File

@ -0,0 +1,97 @@
# CesiumJS Sandcastle
This package is the application for Sandcastle.
## Running/Building
- `npm run dev`: run the development server
- `npm run build`: alias for `npm run build-app`
- `npm run build-app`: build to static files in `/Apps/Sandcastle2` for hosting/access from the root cesium dev server
- `npm run build-ci`: build to static files in `/Apps/Sandcastle2` and configure paths as needed for CI deployment
Linting and style is managed under the project root's scripts.
## Gallery structure
The gallery for Sandcastle is located in the `gallery` directory. A "single sandcastle" consists of 4 files which should be contained in a sub-directory that matches the id of the sandcastle.
```text
gallery
├── 3d-models <-- "slug" id
│   ├── index.html <-- Code that goes into the HTML tab
│   ├── main.js <-- Code that goes into the JS tab (the main code of a Sandcastle)
│   ├── sandcastle.yaml <-- Metadata file containing title, description, labels, etc.
│   └── thumbnail.jpg <-- Optional thumbnail file
└── gallery-list.json <-- "entry point" for a gallery, generated with `scripts/buildGallery.js`
```
### `sandcastle.yaml`
Below is a sample metadata yaml file. This data is used in the `scripts/buildGallery.js` file to create the full `gallery-list.json` information. That script also does some validation on these values.
```yaml
# The id of this sandcastle. Should match the sub-directory name and not contain spaces
id: 3d-models-coloring
# Used to map this sandcastle to a legacy html identifier. New sandcastles should NOT include this
legacyId: 3D Models Coloring.html
# Title for this sandcastle
title: 3D Models Coloring
# Description for this sandcastle
description: Change color of 3D models.
# Labels for this Sandcastle to help with filtering
labels:
- Showcases
- Beginner
# Optional thumbnail file. If set the file should be in the same directory
thumbnail: thumbnail.jpg
# Identify this as a development only Sandcastle. Will not be included in production builds if true
development: false
```
## Expanding the ESLint configuration
<!-- TODO: this section was auto-generated, should figure out if we want these suggestions then remove this -->
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default tseslint.config({
extends: [
// Remove ...tseslint.configs.recommended and replace with this
...tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
...tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
...tseslint.configs.stylisticTypeChecked,
],
languageOptions: {
// other options...
parserOptions: {
project: ["./tsconfig.node.json", "./tsconfig.app.json"],
tsconfigRootDir: import.meta.dirname,
},
},
});
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from "eslint-plugin-react-x";
import reactDom from "eslint-plugin-react-dom";
export default tseslint.config({
plugins: {
// Add the react-x and react-dom plugins
"react-x": reactX,
"react-dom": reactDom,
},
rules: {
// other rules...
// Enable its recommended typescript rules
...reactX.configs["recommended-typescript"].rules,
...reactDom.configs.recommended.rules,
},
});
```

View File

@ -0,0 +1,38 @@
<style>
@import url(../templates/bucket.css);
</style>
<div id="cesiumContainer" class="fullSize"></div>
<div id="loadingOverlay"><h1>Loading...</h1></div>
<div id="toolbar">
<div>
Articulation:
<select
class="cesium-button"
data-bind="options: articulations,
optionsText: 'name',
value: selectedArticulation"
></select>
</div>
<table>
<tbody data-bind="foreach: stages">
<tr>
<td data-bind="text: name"></td>
<td>
<input
type="range"
min="-3"
max="3"
step="0.01"
data-bind="value: current,
valueUpdate: 'input',
attr: {
min: minimum,
max: maximum
}"
/>
<input type="text" size="2" data-bind="value: currentText" />
</td>
</tr>
</tbody>
</table>
</div>

View File

@ -0,0 +1,104 @@
import * as Cesium from "cesium";
// this can be changed to any glTF model
const modelUrl = "https://assets.agi.com/models/launchvehicle.glb";
const viewModel = {
articulations: [],
stages: [],
selectedArticulation: undefined,
};
Cesium.knockout.track(viewModel);
Cesium.knockout
.getObservable(viewModel, "selectedArticulation")
.subscribe(function (newArticulation) {
viewModel.stages = newArticulation.stages;
});
const toolbar = document.getElementById("toolbar");
Cesium.knockout.applyBindings(viewModel, toolbar);
const viewer = new Cesium.Viewer("cesiumContainer");
const scene = viewer.scene;
const height = 220000.0;
const origin = Cesium.Cartesian3.fromDegrees(-74.693, 28.243, height);
const modelMatrix = Cesium.Transforms.headingPitchRollToFixedFrame(
origin,
new Cesium.HeadingPitchRoll(),
);
try {
const model = scene.primitives.add(
await Cesium.Model.fromGltfAsync({
url: modelUrl,
modelMatrix: modelMatrix,
minimumPixelSize: 128,
}),
);
model.readyEvent.addEventListener(() => {
const camera = viewer.camera;
// Zoom to model
const controller = scene.screenSpaceCameraController;
const r = 2.0 * Math.max(model.boundingSphere.radius, camera.frustum.near);
controller.minimumZoomDistance = r * 0.2;
const center = Cesium.Matrix4.multiplyByPoint(
model.modelMatrix,
Cesium.Cartesian3.ZERO,
new Cesium.Cartesian3(),
);
const heading = Cesium.Math.toRadians(0.0);
const pitch = Cesium.Math.toRadians(-10.0);
camera.lookAt(
center,
new Cesium.HeadingPitchRange(heading, pitch, r * 0.8),
);
const articulations = model.sceneGraph._runtimeArticulations;
viewModel.articulations = Object.keys(articulations).map(
function (articulationName) {
return {
name: articulationName,
stages: articulations[articulationName]._runtimeStages.map(
function (stage) {
const stageModel = {
name: stage.name,
minimum: stage.minimumValue,
maximum: stage.maximumValue,
current: stage.currentValue,
};
Cesium.knockout.track(stageModel);
Cesium.knockout.defineProperty(stageModel, "currentText", {
get: function () {
return stageModel.current.toString();
},
set: function (value) {
// coerce values to number
stageModel.current = +value;
},
});
Cesium.knockout
.getObservable(stageModel, "current")
.subscribe(function (newValue) {
model.setArticulationStage(
`${articulationName} ${stage.name}`,
+stageModel.current,
);
model.applyArticulations();
});
return stageModel;
},
),
};
},
);
viewModel.selectedArticulation = viewModel.articulations[0];
});
} catch (error) {
console.log(`Error loading model: ${error}`);
}

View File

@ -0,0 +1,8 @@
legacyId: development/3D Models Articulations.html
title: 3D Models Articulations - Dev
description: Explore node transformations of 3D models.
labels:
- Development
- 3D Models
thumbnail: thumbnail.jpg
development: true

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,95 @@
<style>
@import url(../templates/bucket.css);
</style>
<div id="cesiumContainer" class="fullSize"></div>
<div id="loadingOverlay"><h1>Loading...</h1></div>
<div id="toolbar">
<table>
<tbody>
<tr>
<td><b>Model Color</b></td>
</tr>
<tr>
<td>Mode</td>
<td>
<select
data-bind="options: colorBlendModes, value: colorBlendMode"
></select>
</td>
</tr>
<tr>
<td>Color</td>
<td><select data-bind="options: colors, value: color"></select></td>
</tr>
<tr>
<td>Alpha</td>
<td>
<input
type="range"
min="0.0"
max="1.0"
step="0.01"
data-bind="value: alpha, valueUpdate: 'input'"
/>
<input type="text" size="5" data-bind="value: alpha" />
</td>
</tr>
<tr>
<td data-bind="style: { color: colorBlendAmountEnabled ? '' : 'gray'}">
Mix
</td>
<td>
<input
type="range"
min="0.0"
max="1.0"
step="0.01"
data-bind="value: colorBlendAmount, valueUpdate: 'input', enable: colorBlendAmountEnabled"
/>
<input
type="text"
size="5"
data-bind="value: colorBlendAmount, enable: colorBlendAmountEnabled"
/>
</td>
</tr>
<tr>
<td><b>Model Silhouette</b></td>
</tr>
<tr>
<td>Color</td>
<td>
<select
data-bind="options: silhouetteColors, value: silhouetteColor"
></select>
</td>
</tr>
<tr>
<td>Alpha</td>
<td>
<input
type="range"
min="0.0"
max="1.0"
step="0.01"
data-bind="value: silhouetteAlpha, valueUpdate: 'input'"
/>
<input type="text" size="5" data-bind="value: silhouetteAlpha" />
</td>
</tr>
<tr>
<td>Size</td>
<td>
<input
type="range"
min="0.0"
max="10.0"
step="0.01"
data-bind="value: silhouetteSize, valueUpdate: 'input'"
/>
<input type="text" size="5" data-bind="value: silhouetteSize" />
</td>
</tr>
</tbody>
</table>
</div>

View File

@ -0,0 +1,176 @@
import * as Cesium from "cesium";
import Sandcastle from "Sandcastle";
const viewer = new Cesium.Viewer("cesiumContainer", {
infoBox: false,
selectionIndicator: false,
shadows: true,
shouldAnimate: true,
});
let entity;
function getColorBlendMode(colorBlendMode) {
return Cesium.ColorBlendMode[colorBlendMode.toUpperCase()];
}
function getColor(colorName, alpha) {
const color = Cesium.Color[colorName.toUpperCase()];
return Cesium.Color.fromAlpha(color, parseFloat(alpha));
}
// The viewModel tracks the state of our mini application.
const viewModel = {
color: "Red",
colors: ["White", "Red", "Green", "Blue", "Yellow", "Gray"],
alpha: 1.0,
colorBlendMode: "Highlight",
colorBlendModes: ["Highlight", "Replace", "Mix"],
colorBlendAmount: 0.5,
colorBlendAmountEnabled: false,
silhouetteColor: "Red",
silhouetteColors: ["Red", "Green", "Blue", "Yellow", "Gray"],
silhouetteAlpha: 1.0,
silhouetteSize: 2.0,
};
// Convert the viewModel members into knockout observables.
Cesium.knockout.track(viewModel);
// Bind the viewModel to the DOM elements of the UI that call for it.
const toolbar = document.getElementById("toolbar");
Cesium.knockout.applyBindings(viewModel, toolbar);
Cesium.knockout
.getObservable(viewModel, "color")
.subscribe(function (newValue) {
entity.model.color = getColor(newValue, viewModel.alpha);
});
Cesium.knockout
.getObservable(viewModel, "alpha")
.subscribe(function (newValue) {
entity.model.color = getColor(viewModel.color, newValue);
});
Cesium.knockout
.getObservable(viewModel, "colorBlendMode")
.subscribe(function (newValue) {
const colorBlendMode = getColorBlendMode(newValue);
entity.model.colorBlendMode = colorBlendMode;
viewModel.colorBlendAmountEnabled =
colorBlendMode === Cesium.ColorBlendMode.MIX;
});
Cesium.knockout
.getObservable(viewModel, "colorBlendAmount")
.subscribe(function (newValue) {
entity.model.colorBlendAmount = parseFloat(newValue);
});
Cesium.knockout
.getObservable(viewModel, "silhouetteColor")
.subscribe(function (newValue) {
entity.model.silhouetteColor = getColor(
newValue,
viewModel.silhouetteAlpha,
);
});
Cesium.knockout
.getObservable(viewModel, "silhouetteAlpha")
.subscribe(function (newValue) {
entity.model.silhouetteColor = getColor(
viewModel.silhouetteColor,
newValue,
);
});
Cesium.knockout
.getObservable(viewModel, "silhouetteSize")
.subscribe(function (newValue) {
entity.model.silhouetteSize = parseFloat(newValue);
});
function createModel(url, height) {
viewer.entities.removeAll();
const position = Cesium.Cartesian3.fromDegrees(
-123.0744619,
44.0503706,
height,
);
const heading = Cesium.Math.toRadians(135);
const pitch = 0;
const roll = 0;
const hpr = new Cesium.HeadingPitchRoll(heading, pitch, roll);
const orientation = Cesium.Transforms.headingPitchRollQuaternion(
position,
hpr,
);
entity = viewer.entities.add({
name: url,
position: position,
orientation: orientation,
model: {
uri: url,
minimumPixelSize: 128,
maximumScale: 20000,
color: getColor(viewModel.color, viewModel.alpha),
colorBlendMode: getColorBlendMode(viewModel.colorBlendMode),
colorBlendAmount: parseFloat(viewModel.colorBlendAmount),
silhouetteColor: getColor(
viewModel.silhouetteColor,
viewModel.silhouetteAlpha,
),
silhouetteSize: parseFloat(viewModel.silhouetteSize),
},
});
viewer.trackedEntity = entity;
}
const options = [
{
text: "Aircraft",
onselect: function () {
createModel("../../SampleData/models/CesiumAir/Cesium_Air.glb", 5000.0);
},
},
{
text: "Ground Vehicle",
onselect: function () {
createModel("../../SampleData/models/GroundVehicle/GroundVehicle.glb", 0);
},
},
{
text: "Hot Air Balloon",
onselect: function () {
createModel(
"../../SampleData/models/CesiumBalloon/CesiumBalloon.glb",
1000.0,
);
},
},
{
text: "Milk Truck",
onselect: function () {
createModel(
"../../SampleData/models/CesiumMilkTruck/CesiumMilkTruck.glb",
0,
);
},
},
{
text: "Skinned Character",
onselect: function () {
createModel("../../SampleData/models/CesiumMan/Cesium_Man.glb", 0);
},
},
];
Sandcastle.addToolbarMenu(options);
Sandcastle.addToggleButton("Shadows", viewer.shadows, function (checked) {
viewer.shadows = checked;
});

View File

@ -0,0 +1,8 @@
legacyId: 3D Models Coloring.html
title: Coloring and Styling glTF Models
description: Customize the appearance of glTF models in CesiumJS using colors, blend modes, transparency, and silhouettes. Useful for highlighting, theming, and differentiating models in a scene at runtime.
labels:
- 3D Models
- Entities
- Styling
thumbnail: thumbnail.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -0,0 +1,55 @@
<style>
@import url(../templates/bucket.css);
</style>
<div id="cesiumContainer" class="fullSize"></div>
<div id="loadingOverlay"><h1>Loading...</h1></div>
<div id="toolbar">
<table>
<tbody>
<tr>
<td>Mode</td>
<td>
<select
data-bind="options: colorBlendModes, value: colorBlendMode"
></select>
</td>
</tr>
<tr>
<td>Color</td>
<td><select data-bind="options: colors, value: color"></select></td>
</tr>
<tr>
<td>Alpha</td>
<td>
<input
type="range"
min="0.0"
max="1.0"
step="0.01"
data-bind="value: alpha, valueUpdate: 'input'"
/>
<input type="text" size="5" data-bind="value: alpha" />
</td>
</tr>
<tr>
<td data-bind="style: { color: colorBlendAmountEnabled ? '' : 'gray'}">
Mix
</td>
<td>
<input
type="range"
min="0.0"
max="1.0"
step="0.01"
data-bind="value: colorBlendAmount, valueUpdate: 'input', enable: colorBlendAmountEnabled"
/>
<input
type="text"
size="5"
data-bind="value: colorBlendAmount, enable: colorBlendAmountEnabled"
/>
</td>
</tr>
</tbody>
</table>
</div>

View File

@ -0,0 +1,206 @@
import * as Cesium from "cesium";
import Sandcastle from "Sandcastle";
const viewer = new Cesium.Viewer("cesiumContainer", {
shouldAnimate: true,
shadows: true,
});
const scene = viewer.scene;
let model;
function getColorBlendMode(colorBlendMode) {
return Cesium.ColorBlendMode[colorBlendMode.toUpperCase()];
}
function getColor(color) {
return Cesium.Color[color.toUpperCase()];
}
// The viewModel tracks the state of our mini application.
const viewModel = {
color: "White",
colors: ["White", "Red", "Green", "Blue", "Yellow", "Gray"],
alpha: 1.0,
colorBlendMode: "Highlight",
colorBlendModes: ["Highlight", "Replace", "Mix"],
colorBlendAmount: 0.5,
colorBlendAmountEnabled: false,
};
// Convert the viewModel members into knockout observables.
Cesium.knockout.track(viewModel);
// Bind the viewModel to the DOM elements of the UI that call for it.
const toolbar = document.getElementById("toolbar");
Cesium.knockout.applyBindings(viewModel, toolbar);
Cesium.knockout
.getObservable(viewModel, "color")
.subscribe(function (newValue) {
model.color = Cesium.Color.fromAlpha(
getColor(newValue),
Number(viewModel.alpha),
);
});
Cesium.knockout
.getObservable(viewModel, "alpha")
.subscribe(function (newValue) {
model.color = Cesium.Color.fromAlpha(
getColor(viewModel.color),
Number(newValue),
);
});
Cesium.knockout
.getObservable(viewModel, "colorBlendMode")
.subscribe(function (newValue) {
const colorBlendMode = getColorBlendMode(newValue);
model.colorBlendMode = colorBlendMode;
viewModel.colorBlendAmountEnabled =
colorBlendMode === Cesium.ColorBlendMode.MIX;
});
Cesium.knockout
.getObservable(viewModel, "colorBlendAmount")
.subscribe(function (newValue) {
model.colorBlendAmount = Number(newValue);
});
async function createModel(url, height, heading, pitch, roll) {
height = height ?? 0.0;
heading = heading ?? 0.0;
pitch = pitch ?? 0.0;
roll = roll ?? 0.0;
const hpr = new Cesium.HeadingPitchRoll(heading, pitch, roll);
const origin = Cesium.Cartesian3.fromDegrees(
-123.0744619,
44.0503706,
height,
);
const modelMatrix = Cesium.Transforms.headingPitchRollToFixedFrame(
origin,
hpr,
);
scene.primitives.removeAll(); // Remove previous model
try {
model = scene.primitives.add(
await Cesium.Model.fromGltfAsync({
url: url,
modelMatrix: modelMatrix,
minimumPixelSize: 128,
}),
);
model.readyEvent.addEventListener(() => {
model.color = Cesium.Color.fromAlpha(
getColor(viewModel.color),
Number(viewModel.alpha),
);
model.colorBlendMode = getColorBlendMode(viewModel.colorBlendMode);
model.colorBlendAmount = Number(viewModel.colorBlendAmount);
// Play and loop all animations at half-speed
model.activeAnimations.addAll({
multiplier: 0.5,
loop: Cesium.ModelAnimationLoop.REPEAT,
});
const camera = viewer.camera;
// Zoom to model
const controller = scene.screenSpaceCameraController;
const r =
2.0 * Math.max(model.boundingSphere.radius, camera.frustum.near);
controller.minimumZoomDistance = r * 0.5;
const center = model.boundingSphere.center;
const heading = Cesium.Math.toRadians(230.0);
const pitch = Cesium.Math.toRadians(-20.0);
camera.lookAt(
center,
new Cesium.HeadingPitchRange(heading, pitch, r * 2.0),
);
});
} catch (error) {
window.alert(error);
}
}
const handler = new Cesium.ScreenSpaceEventHandler(scene.canvas);
handler.setInputAction(function (movement) {
const pick = scene.pick(movement.endPosition);
if (
Cesium.defined(pick) &&
Cesium.defined(pick.node) &&
Cesium.defined(pick.mesh)
) {
// Output glTF node and mesh under the mouse.
console.log(`node: ${pick.node.name}. mesh: ${pick.mesh.name}`);
}
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
///////////////////////////////////////////////////////////////////////////
const options = [
{
text: "Aircraft",
onselect: function () {
const height = 5000.0;
const heading = 0.0;
const pitch = Cesium.Math.toRadians(10.0);
const roll = Cesium.Math.toRadians(-20.0);
createModel(
"../../SampleData/models/CesiumAir/Cesium_Air.glb",
height,
heading,
pitch,
roll,
);
},
},
{
text: "Drone",
onselect: function () {
const height = 150.0;
const heading = 0.0;
const pitch = Cesium.Math.toRadians(10.0);
const roll = Cesium.Math.toRadians(-20.0);
createModel(
"../../SampleData/models/CesiumDrone/CesiumDrone.glb",
height,
heading,
pitch,
roll,
);
},
},
{
text: "Ground Vehicle",
onselect: function () {
createModel("../../SampleData/models/GroundVehicle/GroundVehicle.glb");
},
},
{
text: "Milk Truck",
onselect: function () {
createModel(
"../../SampleData/models/CesiumMilkTruck/CesiumMilkTruck.glb",
);
},
},
{
text: "Skinned Character",
onselect: function () {
createModel("../../SampleData/models/CesiumMan/Cesium_Man.glb");
},
},
];
Sandcastle.addToolbarMenu(options);
Sandcastle.addToggleButton("Shadows", viewer.shadows, function (checked) {
viewer.shadows = checked;
});

View File

@ -0,0 +1,10 @@
legacyId: development/3D Models.html
title: glTF Models — Development
description: Load and visualize glTF and glb models with entities, including examples with compression (Draco, KTX2), instancing, and animations. Useful for learning how to place, orient, and scale 3D models in the scene for use cases like vehicles, characters, or custom assets.
labels:
- 3D Models
- Entities
- Animation
- Development
thumbnail: thumbnail.jpg
development: true

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -0,0 +1,153 @@
<style>
@import url(../templates/bucket.css);
</style>
<div id="cesiumContainer" class="fullSize"></div>
<div id="loadingOverlay"><h1>Loading...</h1></div>
<div id="toolbar">
<table>
<tbody>
<tr>
<td
colspan="2"
data-bind="click: function() { showTranslation = !showTranslation }"
>
<span data-bind="text: showTranslation ? '-' : '+'">+</span>
Translation
</td>
</tr>
<tr data-bind="visible: showTranslation">
<td>X</td>
<td>
<input
type="range"
min="-3"
max="3"
step="0.01"
data-bind="value: translationX, valueUpdate: 'input'"
/>
<input type="text" size="2" data-bind="value: translationX" />
</td>
</tr>
<tr data-bind="visible: showTranslation">
<td>Y</td>
<td>
<input
type="range"
min="-3"
max="3"
step="0.01"
data-bind="value: translationY, valueUpdate: 'input'"
/>
<input type="text" size="2" data-bind="value: translationY" />
</td>
</tr>
<tr data-bind="visible: showTranslation">
<td>Z</td>
<td>
<input
type="range"
min="-3"
max="3"
step="0.01"
data-bind="value: translationZ, valueUpdate: 'input'"
/>
<input type="text" size="2" data-bind="value: translationZ" />
</td>
</tr>
<tr>
<td
colspan="2"
data-bind="click: function() { showRotation = !showRotation }"
>
<span data-bind="text: showRotation ? '-' : '+'">+</span> Rotation
</td>
</tr>
<tr data-bind="visible: showRotation">
<td>H</td>
<td>
<input
type="range"
min="-3"
max="3"
step="0.01"
data-bind="value: rotationHeading, valueUpdate: 'input'"
/>
<input type="text" size="2" data-bind="value: rotationHeading" />
</td>
</tr>
<tr data-bind="visible: showRotation">
<td>P</td>
<td>
<input
type="range"
min="-3"
max="3"
step="0.01"
data-bind="value: rotationPitch, valueUpdate: 'input'"
/>
<input type="text" size="2" data-bind="value: rotationPitch" />
</td>
</tr>
<tr data-bind="visible: showRotation">
<td>R</td>
<td>
<input
type="range"
min="-3"
max="3"
step="0.01"
data-bind="value: rotationRoll, valueUpdate: 'input'"
/>
<input type="text" size="2" data-bind="value: rotationRoll" />
</td>
</tr>
<tr>
<td
colspan="2"
data-bind="click: function() { showScale = !showScale }"
>
<span data-bind="text: showScale ? '-' : '+'">+</span> Scale
</td>
</tr>
<tr data-bind="visible: showScale">
<td>X</td>
<td>
<input
type="range"
min="0.01"
max="3"
step="0.01"
data-bind="value: scaleX, valueUpdate: 'input'"
/>
<input type="text" size="2" data-bind="value: scaleX" />
</td>
</tr>
<tr data-bind="visible: showScale">
<td>Y</td>
<td>
<input
type="range"
min="0.01"
max="3"
step="0.01"
data-bind="value: scaleY, valueUpdate: 'input'"
/>
<input type="text" size="2" data-bind="value: scaleY" />
</td>
</tr>
<tr data-bind="visible: showScale">
<td>Z</td>
<td>
<input
type="range"
min="0.01"
max="3"
step="0.01"
data-bind="value: scaleZ, valueUpdate: 'input'"
/>
<input type="text" size="2" data-bind="value: scaleZ" />
</td>
</tr>
</tbody>
</table>
</div>

View File

@ -0,0 +1,167 @@
import * as Cesium from "cesium";
import Sandcastle from "Sandcastle";
// this can be changed to any glTF model
const modelUrl = "../../SampleData/models/CesiumMan/Cesium_Man.glb";
const viewModel = {
nodeName: undefined,
showTranslation: false,
showRotation: false,
showScale: false,
transformations: {},
};
Cesium.knockout.track(viewModel);
// transformation is a computed property returning the values storage for the current node name
Cesium.knockout.defineProperty(viewModel, "transformation", function () {
const transformations = viewModel.transformations;
const nodeName = viewModel.nodeName;
if (!Cesium.defined(transformations[nodeName])) {
transformations[nodeName] = {
translationX: 0.0,
translationY: 0.0,
translationZ: 0.0,
rotationHeading: 0.0,
rotationPitch: 0.0,
rotationRoll: 0.0,
scaleX: 1.0,
scaleY: 1.0,
scaleZ: 1.0,
};
Cesium.knockout.track(transformations[nodeName]);
}
return transformations[nodeName];
});
// these writable computed properties produce individual values for use in the UI
[
"translationX",
"translationY",
"translationZ",
"rotationHeading",
"rotationPitch",
"rotationRoll",
"scaleX",
"scaleY",
"scaleZ",
].forEach(function (p) {
Cesium.knockout.defineProperty(viewModel, p, {
get: function () {
return viewModel.transformation[p];
},
set: function (value) {
// coerce values to number
viewModel.transformation[p] = +value;
},
});
});
// these computed properties return each element of the transform
Cesium.knockout.defineProperty(viewModel, "translation", function () {
return new Cesium.Cartesian3(
viewModel.translationX,
viewModel.translationY,
viewModel.translationZ,
);
});
Cesium.knockout.defineProperty(viewModel, "rotation", function () {
const hpr = new Cesium.HeadingPitchRoll(
viewModel.rotationHeading,
viewModel.rotationPitch,
viewModel.rotationRoll,
);
return Cesium.Quaternion.fromHeadingPitchRoll(hpr);
});
Cesium.knockout.defineProperty(viewModel, "scale", function () {
return new Cesium.Cartesian3(
viewModel.scaleX,
viewModel.scaleY,
viewModel.scaleZ,
);
});
// this computed property combines the above properties into a single matrix to be applied to the node
Cesium.knockout.defineProperty(viewModel, "matrix", function () {
return Cesium.Matrix4.fromTranslationQuaternionRotationScale(
viewModel.translation,
viewModel.rotation,
viewModel.scale,
);
});
const toolbar = document.getElementById("toolbar");
Cesium.knockout.applyBindings(viewModel, toolbar);
const viewer = new Cesium.Viewer("cesiumContainer");
const scene = viewer.scene;
const height = 250000.0;
const origin = Cesium.Cartesian3.fromDegrees(-123.0744619, 44.0503706, height);
const modelMatrix = Cesium.Transforms.headingPitchRollToFixedFrame(
origin,
new Cesium.HeadingPitchRoll(),
);
try {
const model = scene.primitives.add(
await Cesium.Model.fromGltfAsync({
url: modelUrl,
modelMatrix: modelMatrix,
minimumPixelSize: 128,
}),
);
model.readyEvent.addEventListener(() => {
const camera = viewer.camera;
// Zoom to model
const controller = scene.screenSpaceCameraController;
const r = 2.0 * Math.max(model.boundingSphere.radius, camera.frustum.near);
controller.minimumZoomDistance = r * 0.5;
const center = model.boundingSphere.center;
const heading = Cesium.Math.toRadians(230.0);
const pitch = Cesium.Math.toRadians(-20.0);
camera.lookAt(
center,
new Cesium.HeadingPitchRange(heading, pitch, r * 2.0),
);
// enumerate nodes and add options
const options = Object.keys(model._nodesByName).map(function (nodeName) {
return {
text: nodeName,
onselect: function () {
viewModel.nodeName = nodeName;
},
};
});
options[0].onselect();
Sandcastle.addToolbarMenu(options);
// This only affects nodes that draw primitives. Setting this value
// for a joint node will have no effect.
Sandcastle.addToggleButton("Show Node", true, function (value) {
const node = model.getNode(viewModel.nodeName);
node.show = value;
});
// respond to viewmodel changes by applying the computed matrix
Cesium.knockout
.getObservable(viewModel, "matrix")
.subscribe(function (newValue) {
const node = model.getNode(viewModel.nodeName);
if (!Cesium.defined(node.originalMatrix)) {
node.originalMatrix = node.matrix.clone();
}
node.matrix = Cesium.Matrix4.multiply(
node.originalMatrix,
newValue,
new Cesium.Matrix4(),
);
});
});
} catch (error) {
window.alert(error);
}

View File

@ -0,0 +1,9 @@
legacyId: development/3D Models Node Explorer.html
title: 3D Models Node Explorer - Dev
description: Explore node transformations of 3D models.
labels:
- Development
- 3D Models
- Transformations
thumbnail: thumbnail.jpg
development: true

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@ -0,0 +1,6 @@
<style>
@import url(../templates/bucket.css);
</style>
<div id="cesiumContainer" class="fullSize"></div>
<div id="loadingOverlay"><h1>Loading...</h1></div>
<div id="toolbar"></div>

View File

@ -0,0 +1,121 @@
import * as Cesium from "cesium";
import Sandcastle from "Sandcastle";
const viewer = new Cesium.Viewer("cesiumContainer", {
infoBox: false,
selectionIndicator: false,
shadows: true,
shouldAnimate: true,
});
function createModel(url, height) {
viewer.entities.removeAll();
const position = Cesium.Cartesian3.fromDegrees(
-123.0744619,
44.0503706,
height,
);
const heading = Cesium.Math.toRadians(135);
const pitch = 0;
const roll = 0;
const hpr = new Cesium.HeadingPitchRoll(heading, pitch, roll);
const orientation = Cesium.Transforms.headingPitchRollQuaternion(
position,
hpr,
);
const entity = viewer.entities.add({
name: url,
position: position,
orientation: orientation,
model: {
uri: url,
minimumPixelSize: 128,
maximumScale: 20000,
},
});
viewer.trackedEntity = entity;
}
const options = [
{
text: "Aircraft",
onselect: function () {
createModel("../../SampleData/models/CesiumAir/Cesium_Air.glb", 5000.0);
},
},
{
text: "Drone",
onselect: function () {
createModel("../../SampleData/models/CesiumDrone/CesiumDrone.glb", 150.0);
},
},
{
text: "Ground Vehicle",
onselect: function () {
createModel("../../SampleData/models/GroundVehicle/GroundVehicle.glb", 0);
},
},
{
text: "Hot Air Balloon",
onselect: function () {
createModel(
"../../SampleData/models/CesiumBalloon/CesiumBalloon.glb",
1000.0,
);
},
},
{
text: "Milk Truck",
onselect: function () {
createModel(
"../../SampleData/models/CesiumMilkTruck/CesiumMilkTruck.glb",
0,
);
},
},
{
text: "Skinned Character",
onselect: function () {
createModel("../../SampleData/models/CesiumMan/Cesium_Man.glb", 0);
},
},
{
text: "Unlit Box",
onselect: function () {
createModel("../../SampleData/models/BoxUnlit/BoxUnlit.gltf", 10.0);
},
},
{
text: "Draco Compressed Model",
onselect: function () {
createModel(
"../../SampleData/models/DracoCompressed/CesiumMilkTruck.gltf",
0,
);
},
},
{
text: "KTX2 Compressed Balloon",
onselect: function () {
if (!Cesium.FeatureDetection.supportsBasis(viewer.scene)) {
window.alert(
"This browser does not support Basis Universal compressed textures",
);
}
createModel(
"../../SampleData/models/CesiumBalloonKTX2/CesiumBalloonKTX2.glb",
1000.0,
);
},
},
{
text: "Instanced Box",
onselect: function () {
createModel("../../SampleData/models/BoxInstanced/BoxInstanced.gltf", 15);
},
},
];
Sandcastle.addToolbarMenu(options);

View File

@ -0,0 +1,8 @@
legacyId: 3D Models.html
title: glTF Models
description: Load and visualize glTF and glb models with entities, including examples with compression (Draco, KTX2), instancing, and animations. Useful for learning how to place, orient, and scale 3D models in the scene for use cases like vehicles, characters, or custom assets.
labels:
- 3D Models
- Entities
- Animation
thumbnail: thumbnail.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -0,0 +1,15 @@
<style>
@import url(../templates/bucket.css);
table,
th,
td {
border: 1px solid white;
border-collapse: collapse;
}
tt {
padding: 8px;
}
</style>
<div id="cesiumContainer" class="fullSize"></div>
<div id="loadingOverlay"><h1>Loading...</h1></div>
<div id="toolbar"></div>

View File

@ -0,0 +1,394 @@
import * as Cesium from "cesium";
import Sandcastle from "Sandcastle";
const viewer = new Cesium.Viewer("cesiumContainer", {});
viewer.clock.currentTime = Cesium.JulianDate.fromIso8601(
"2021-11-09T07:27:37.016064475348684937Z",
);
const scene = viewer.scene;
scene.light.intensity = 7.0;
const cameraTransforms = {
tileset: {
destination: new Cesium.Cartesian3(
4397999.822774582,
4404502.67774069,
1397782.4709840622,
),
direction: new Cesium.Cartesian3(
-0.29335588497705106,
-0.6066709587467911,
0.7388454997917905,
),
up: new Cesium.Cartesian3(
0.6240972421637774,
0.46391380837591956,
0.6287182283994301,
),
},
airport: {
destination: new Cesium.Cartesian3(
4394719.151490939,
4402317.401942875,
1406608.6602404779,
),
direction: new Cesium.Cartesian3(
0.4146699515908668,
-0.8887814163588482,
0.1952342828060377,
),
up: new Cesium.Cartesian3(
0.8415067525520951,
0.4561872920946922,
0.28941240460723,
),
},
crater: {
destination: new Cesium.Cartesian3(
4398179.160380196,
4402518.469409466,
1399161.7612076725,
),
direction: new Cesium.Cartesian3(
-0.2800903637088597,
-0.6348021519070498,
0.7201219452923355,
),
up: new Cesium.Cartesian3(
0.6319189548885261,
0.4427783126727723,
0.6361020360596605,
),
},
port: {
destination: new Cesium.Cartesian3(
4399698.85724341,
4399019.639078034,
1405153.7766045567,
),
direction: new Cesium.Cartesian3(
-0.5651458936543287,
0.17696574231117793,
-0.8057873447342694,
),
up: new Cesium.Cartesian3(
0.4886488937394081,
0.8587605935024302,
-0.15411846642958343,
),
},
};
function flyCameraTo(cameraTransform, duration) {
// Fly to a nice overview of the city.
viewer.camera.flyTo({
duration: duration,
destination: cameraTransform.destination,
orientation: {
direction: cameraTransform.direction,
up: cameraTransform.up,
},
easingFunction: Cesium.EasingFunction.QUADRATIC_IN_OUT,
});
}
// --- Style ---
const buildingStyle = new Cesium.Cesium3DTileStyle({
color: {
conditions: [
["${HGT} !== undefined && ${HGT} < 5", "color('#f5fd2d')"],
[
"${HGT} !== undefined && ${HGT} >= 5 && ${HGT} < 10",
"color('#d3a34a')",
],
[
"${HGT} !== undefined && ${HGT} >= 10 && ${HGT} < 15",
"color('#947e75')",
],
[
"${HGT} !== undefined && ${HGT} >= 15 && ${HGT} < 20",
"color('#565a9f')",
],
["${HGT} !== undefined && ${HGT} > 20", "color('#223bc3')"],
["true", "color('white')"],
],
},
});
const terrainStyle = new Cesium.Cesium3DTileStyle({
color: {
conditions: [
["${name} === 'OCEAN'", "color('#436d9d')"],
["${name} === 'LAKE'", "color('#3987c9')"],
["${name} === 'CALCAREOUS'", "color('#BBB6B1')"],
["${name} === 'GRASS'", "color('#567d46')"],
["${name} === 'FOREST'", "color('green')"],
["${name} === 'CITY'", "color('lightgray')"],
["${name} === 'ASPHALTROAD'", "color('#434343')"],
["${name} === 'ASPHALT'", "color('#463d39')"],
["${name} === 'CONCRETE'", "color('#b9b4ab')"],
["${name} === 'DRYGROUND'", "color('#9B7653')"],
["${name} === 'WETGROUND'", "color('#5a4332')"],
["${name} === 'SAND'", "color('gold')"],
["true", "color('#9B7653')"],
],
},
});
// --- Picking ---
let enablePicking = true;
const handler = new Cesium.ScreenSpaceEventHandler(scene.canvas);
const metadataOverlay = document.createElement("div");
viewer.container.appendChild(metadataOverlay);
metadataOverlay.className = "backdrop";
metadataOverlay.style.display = "none";
metadataOverlay.style.position = "absolute";
metadataOverlay.style.bottom = "0";
metadataOverlay.style.left = "0";
metadataOverlay.style["pointer-events"] = "none";
metadataOverlay.style.padding = "4px";
metadataOverlay.style.backgroundColor = "#303030";
metadataOverlay.style.whiteSpace = "pre-line";
metadataOverlay.style.fontSize = "16px";
metadataOverlay.style.borderRadius = "4px";
let tableHtmlScratch;
let materialsScratch;
let weightsScratch;
let i;
const highlighted = {
feature: undefined,
originalColor: new Cesium.Color(),
};
const selected = {
feature: undefined,
originalColor: new Cesium.Color(),
};
handler.setInputAction(function (movement) {
if (enablePicking) {
// If a feature was previously highlighted, undo the highlight
if (Cesium.defined(highlighted.feature)) {
highlighted.feature.color = highlighted.originalColor;
highlighted.feature = undefined;
}
const feature = scene.pick(movement.endPosition);
const featurePicked = feature instanceof Cesium.Cesium3DTileFeature;
const isTerrainFeature = featurePicked && feature.hasProperty("substrates");
const isBuildingFeature = featurePicked && feature.hasProperty("HGT");
if (isTerrainFeature) {
metadataOverlay.style.display = "block";
metadataOverlay.style.bottom = `${
viewer.canvas.clientHeight - movement.endPosition.y
}px`;
metadataOverlay.style.left = `${movement.endPosition.x}px`;
tableHtmlScratch = `<table><thead><tr><td>Material:</td><th><tt>${feature.getProperty(
"name",
)}</tt></tr></thead><tbody>`;
materialsScratch = feature.getProperty("substrates");
weightsScratch = feature.getProperty("weights");
tableHtmlScratch +=
"<tr><td colspan='2' style='text-align: center;'><b>Substrates</b></td></tr>";
for (i = 0; i < materialsScratch.length; i++) {
tableHtmlScratch += `<tr><td><tt>${materialsScratch[i].slice(
3,
)}</tt></td><td style='text-align: right;'><tt>${
weightsScratch[i]
}%</tt></td></tr>`;
}
tableHtmlScratch += "</tbody></table>";
metadataOverlay.innerHTML = tableHtmlScratch;
} else {
metadataOverlay.style.display = "none";
}
if (isBuildingFeature) {
// Highlight the feature if it's not already selected.
if (feature !== selected.feature) {
highlighted.feature = feature;
Cesium.Color.clone(feature.color, highlighted.originalColor);
feature.color = Cesium.Color.MAGENTA;
}
}
}
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
handler.setInputAction(function (movement) {
// If a feature was previously selected, undo the highlight
if (Cesium.defined(selected.feature)) {
selected.feature.color = selected.originalColor;
selected.feature = undefined;
}
const feature = scene.pick(movement.position);
const featurePicked = feature instanceof Cesium.Cesium3DTileFeature;
const isBuildingFeature = featurePicked && feature.hasProperty("HGT");
if (isBuildingFeature) {
// Select the feature if it's not already selected
if (selected.feature === feature) {
return;
}
selected.feature = feature;
// Save the selected feature's original color
if (feature === highlighted.feature) {
Cesium.Color.clone(highlighted.originalColor, selected.originalColor);
highlighted.feature = undefined;
} else {
Cesium.Color.clone(feature.color, selected.originalColor);
}
feature.color = Cesium.Color.LIME;
tableHtmlScratch = "<table class='cesium-infoBox-defaultTable'>";
tableHtmlScratch +=
"<tr><th>Property Name</th><th>ID</th><th>Type</th><th>Value</th></tr><tbody>";
const metadataClass = feature.content.batchTable._propertyTable.class;
const propertyIds = feature.getPropertyIds();
const length = propertyIds.length;
for (let i = 0; i < length; ++i) {
const propertyId = propertyIds[i];
// Skip these properties, since they are always empty.
if (
propertyId === "APID" ||
propertyId === "FACC" ||
propertyId === "RWID"
) {
continue;
}
const propertyValue = feature.getProperty(propertyId);
const property = metadataClass.properties[propertyId];
const propertyType = property.componentType ?? property.type;
tableHtmlScratch += `<tr style='font-family: monospace;' title='${property.description}'><th>${property.name}</th><th><b>${property.id}</b></th><td>${propertyType}</td><td>${propertyValue}</td></tr>`;
}
tableHtmlScratch +=
"<tr><th colspan='4'><i style='font-size:10px'>Hover on a row for description</i></th></tr></tbody></table>";
viewer.selectedEntity.description = tableHtmlScratch;
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
// Hide the terrain metadata overlay when the mouse is over the info box, to prevent overlaps.
const infoBoxContainer = document
.getElementsByClassName("cesium-viewer-infoBoxContainer")
.item(0);
infoBoxContainer.onmouseover = function (e) {
metadataOverlay.style.display = "none";
};
// --- UI ---
const locations = [
{
text: "Overview",
onselect: function () {
flyCameraTo(cameraTransforms.tileset);
},
},
{
text: "Airport",
onselect: function () {
flyCameraTo(cameraTransforms.airport);
},
},
{
text: "Crater",
onselect: function () {
flyCameraTo(cameraTransforms.crater);
},
},
{
text: "Port",
onselect: function () {
flyCameraTo(cameraTransforms.port);
},
},
];
function resetHighlight() {
if (Cesium.defined(selected.feature)) {
selected.feature.color = selected.originalColor;
selected.feature = undefined;
}
if (Cesium.defined(highlighted.feature)) {
highlighted.feature.color = highlighted.originalColor;
highlighted.feature = undefined;
}
}
try {
// 3D Tiles 1.1 converted from CDB of Aden, Yemen (CDB provided by Presagis)
const terrainTileset = await Cesium.Cesium3DTileset.fromIonAssetId(2389063);
viewer.scene.primitives.add(terrainTileset);
const buildingsTileset = await Cesium.Cesium3DTileset.fromIonAssetId(
2389064,
{
maximumScreenSpaceError: 12,
},
);
viewer.scene.primitives.add(buildingsTileset);
viewer.camera.flyTo({
duration: 0,
destination: cameraTransforms.tileset.destination,
orientation: {
direction: cameraTransforms.tileset.direction,
up: cameraTransforms.tileset.up,
},
});
const modes = [
{
text: "No style",
onselect: function () {
resetHighlight();
buildingsTileset.style = undefined;
terrainTileset.style = undefined;
},
},
{
text: "Style buildings based on height",
onselect: function () {
resetHighlight();
buildingsTileset.style = buildingStyle;
terrainTileset.style = undefined;
},
},
{
text: "Style terrain based on materials",
onselect: function () {
buildingsTileset.style = undefined;
terrainTileset.style = terrainStyle;
},
},
];
Sandcastle.addToolbarMenu(modes);
} catch (error) {
console.log(`Error loading tileset: ${error}`);
}
Sandcastle.addToolbarMenu(locations);
Sandcastle.addToggleButton(
"Enable terrain picking",
enablePicking,
function (checked) {
if (enablePicking) {
metadataOverlay.style.display = "none";
}
enablePicking = checked;
},
);

View File

@ -0,0 +1,9 @@
title: CDB Metadata in 3D Tiles
description: Demonstrates loading a 3D Tiles 1.1 tileset converted from CDB, carrying through detailed metadata. 3D Tiles enables runtime styling, filtering, and querying features using CDB attributes.
labels:
- 3D Tiles
- Metadata
- Styling
- Picking
thumbnail: thumbnail.jpg
legacyId: 3D Tiles 1.1 CDB Yemen.html

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,6 @@
<style>
@import url(../templates/bucket.css);
</style>
<div id="cesiumContainer" class="fullSize"></div>
<div id="loadingOverlay"><h1>Loading...</h1></div>
<div id="toolbar"></div>

View File

@ -0,0 +1,318 @@
import * as Cesium from "cesium";
import Sandcastle from "Sandcastle";
// San Francisco Ferry Building photogrammetry model provided by Aerometrex
const viewer = new Cesium.Viewer("cesiumContainer", {
infoBox: false,
orderIndependentTranslucency: false,
terrain: Cesium.Terrain.fromWorldTerrain(),
});
viewer.clock.currentTime = Cesium.JulianDate.fromIso8601(
"2021-11-09T20:27:37.016064475348684937Z",
);
const scene = viewer.scene;
// Fly to a nice overview of the city.
viewer.camera.flyTo({
destination: new Cesium.Cartesian3(
-2703640.80485846,
-4261161.990345464,
3887439.511104276,
),
orientation: new Cesium.HeadingPitchRoll(
0.22426651143535548,
-0.2624145362506527,
0.000006972977223185239,
),
duration: 0,
});
let tileset;
try {
tileset = await Cesium.Cesium3DTileset.fromIonAssetId(2333904);
const translation = new Cesium.Cartesian3(
-1.398521324920626,
0.7823052871729486,
0.7015244410592609,
);
tileset.modelMatrix = Cesium.Matrix4.fromTranslation(translation);
tileset.maximumScreenSpaceError = 8.0;
scene.pickTranslucentDepth = true;
scene.light.intensity = 7.0;
viewer.scene.primitives.add(tileset);
} catch (error) {
console.log(`Error loading tileset: ${error}`);
}
// Styles =============================================================================
const classificationStyle = new Cesium.Cesium3DTileStyle({
color: "color(${color})",
});
const translucentWindowsStyle = new Cesium.Cesium3DTileStyle({
color: {
conditions: [["${component} === 'Windows'", "color('gray', 0.7)"]],
},
});
// Shaders ============================================================================
// Dummy shader that sets the UNLIT lighting mode. For use with the classification style
const emptyFragmentShader =
"void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material) {}";
const unlitShader = new Cesium.CustomShader({
lightingModel: Cesium.LightingModel.UNLIT,
fragmentShaderText: emptyFragmentShader,
});
const materialShader = new Cesium.CustomShader({
lightingModel: Cesium.LightingModel.PBR,
fragmentShaderText: `
const int WINDOW = 0;
const int FRAME = 1;
const int WALL = 2;
const int ROOF = 3;
const int SKYLIGHT = 4;
const int AIR_CONDITIONER_WHITE = 5;
const int AIR_CONDITIONER_BLACK = 6;
const int AIR_CONDITIONER_TALL = 7;
const int CLOCK = 8;
const int PILLARS = 9;
const int STREET_LIGHT = 10;
const int TRAFFIC_LIGHT = 11;
void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material) {
int featureId = fsInput.featureIds.featureId_0;
if (featureId == CLOCK) {
// Shiny brass
material.specular = vec3(0.98, 0.90, 0.59);
material.roughness = 0.1;
} else if (
featureId == STREET_LIGHT ||
featureId == AIR_CONDITIONER_BLACK ||
featureId == AIR_CONDITIONER_WHITE ||
featureId == AIR_CONDITIONER_TALL ||
featureId == ROOF
) {
// dull aluminum
material.specular = vec3(0.91, 0.92, 0.92);
material.roughness = 0.5;
} else if (featureId == WINDOW || featureId == SKYLIGHT) {
// make translucent, but also set an orange emissive color so it looks like
// it's lit from inside
material.emissive = vec3(1.0, 0.3, 0.0);
material.alpha = 0.5;
} else if (featureId == WALL || featureId == FRAME || featureId == PILLARS) {
// paint the walls and pillars white to contrast the brass clock
material.diffuse = mix(material.diffuse, vec3(1.0), 0.8);
material.roughness = 0.9;
} else {
// brighten everything else
material.diffuse += 0.05;
material.roughness = 0.9;
}
}
`,
});
const NOTHING_SELECTED = 12;
const selectFeatureShader = new Cesium.CustomShader({
uniforms: {
u_selectedFeature: {
type: Cesium.UniformType.INT,
value: NOTHING_SELECTED,
},
},
lightingModel: Cesium.LightingModel.PBR,
fragmentShaderText: `
const int NOTHING_SELECTED = 12;
void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material) {
int featureId = fsInput.featureIds.featureId_0;
if (u_selectedFeature < NOTHING_SELECTED && featureId == u_selectedFeature) {
material.specular = vec3(1.00, 0.85, 0.57);
material.roughness = 0.1;
}
}
`,
});
const multipleFeatureIdsShader = new Cesium.CustomShader({
uniforms: {
u_selectedFeature: {
type: Cesium.UniformType.FLOAT,
value: NOTHING_SELECTED,
},
},
lightingModel: Cesium.LightingModel.UNLIT,
fragmentShaderText: `
const int IDS0_WINDOW = 0;
const int IDS1_FACADE = 2;
const int IDS1_ROOF = 3;
const vec3 PURPLE = vec3(0.5, 0.0, 1.0);
const vec3 YELLOW = vec3(1.0, 1.0, 0.0);
const vec3 NO_TINT = vec3(1.0);
void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material) {
int featureId0 = fsInput.featureIds.featureId_0; // fine features
int featureId1 = fsInput.featureIds.featureId_1; // coarse features
// use both feature ID sets to determine where the features are
float isWindow = float(featureId0 == IDS0_WINDOW);
float isFacade = float(featureId1 == IDS1_FACADE);
float isRoof = float(featureId1 == IDS1_ROOF);
// Tint the roof windows yellow and facade windows purple
vec3 tint = NO_TINT;
tint = mix(tint, YELLOW, isWindow * isRoof);
tint = mix(tint, PURPLE, isWindow * isFacade);
material.diffuse *= tint;
}
`,
});
// Demo Functions =====================================================================
function defaults() {
tileset.style = undefined;
tileset.customShader = unlitShader;
tileset.colorBlendMode = Cesium.Cesium3DTileColorBlendMode.HIGHLIGHT;
tileset.colorBlendAmount = 0.5;
tileset.featureIdLabel = 0;
}
const showPhotogrammetry = defaults;
function showClassification() {
defaults();
tileset.style = classificationStyle;
tileset.colorBlendMode = Cesium.Cesium3DTileColorBlendMode.MIX;
}
function showAlternativeClassification() {
showClassification();
// This dataset has a second feature ID texture.
tileset.featureIdLabel = 1;
}
function translucentWindows() {
defaults();
tileset.style = translucentWindowsStyle;
}
function pbrMaterials() {
defaults();
tileset.customShader = materialShader;
}
function goldenTouch() {
defaults();
tileset.customShader = selectFeatureShader;
}
function multipleFeatureIds() {
defaults();
tileset.customShader = multipleFeatureIdsShader;
}
// Pick Handlers ======================================================================
// HTML overlay for showing feature name on mouseover
const nameOverlay = document.createElement("div");
viewer.container.appendChild(nameOverlay);
nameOverlay.className = "backdrop";
nameOverlay.style.display = "none";
nameOverlay.style.position = "absolute";
nameOverlay.style.bottom = "0";
nameOverlay.style.left = "0";
nameOverlay.style["pointer-events"] = "none";
nameOverlay.style.padding = "4px";
nameOverlay.style.backgroundColor = "black";
nameOverlay.style.whiteSpace = "pre-line";
nameOverlay.style.fontSize = "12px";
let enablePicking = true;
const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
handler.setInputAction(function (movement) {
if (enablePicking) {
const pickedObject = viewer.scene.pick(movement.endPosition);
if (pickedObject instanceof Cesium.Cesium3DTileFeature) {
nameOverlay.style.display = "block";
nameOverlay.style.bottom = `${
viewer.canvas.clientHeight - movement.endPosition.y
}px`;
nameOverlay.style.left = `${movement.endPosition.x}px`;
const component = pickedObject.getProperty("component");
const message = `Component: ${component}\nFeature ID: ${pickedObject.featureId}`;
nameOverlay.textContent = message;
} else {
nameOverlay.style.display = "none";
}
} else {
nameOverlay.style.display = "none";
}
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
const clickHandler = new Cesium.ScreenSpaceEventHandler(scene.canvas);
clickHandler.setInputAction(function (movement) {
if (enablePicking) {
const pickedObject = scene.pick(movement.position);
if (
Cesium.defined(pickedObject) &&
Cesium.defined(pickedObject.featureId)
) {
selectFeatureShader.setUniform(
"u_selectedFeature",
pickedObject.featureId,
);
} else {
selectFeatureShader.setUniform("u_selectedFeature", NOTHING_SELECTED);
}
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
// UI ============================================================================
Sandcastle.addToggleButton("Enable picking", enablePicking, function (checked) {
enablePicking = checked;
});
const demos = [
{
text: "Show Classification",
onselect: showClassification,
},
{
text: "Show Alternative Classification",
onselect: showAlternativeClassification,
},
{
text: "Translucent Windows",
onselect: translucentWindows,
},
{
text: "Stylized PBR Materials",
onselect: pbrMaterials,
},
{
text: "Golden Touch",
onselect: goldenTouch,
},
{
text: "Multiple Feature ID Sets",
onselect: multipleFeatureIds,
},
{
text: "No Classification",
onselect: showPhotogrammetry,
},
];
Sandcastle.addDefaultToolbarMenu(demos);
showClassification();

View File

@ -0,0 +1,11 @@
legacyId: 3D Tiles 1.1 Photogrammetry Classification.html
title: 3D Tiles Photogrammetry Classification
description: Demonstrates a photogrammetry dataset using feature ID textures to encode metadata with EXT_mesh_features. 3D Tiles 1.1 enables classification, styling, and querying individual features in photogrammetry datasets.
labels:
- 3D Tiles
- Photogrammetry
- Classification
- Metadata
- Styling
- Picking
thumbnail: thumbnail.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -0,0 +1,101 @@
<style>
@import url(../templates/bucket.css);
#slider {
position: absolute;
left: 50%;
top: 0px;
background-color: #d3d3d3;
width: 5px;
height: 100%;
z-index: 9999;
}
#slider:hover {
cursor: ew-resize;
}
#statsContainer {
position: absolute;
top: 15%;
width: 100%;
}
.panel {
background-color: rgba(42, 42, 42, 0.8);
padding: 4px;
border-radius: 4px;
font-size: 14px;
}
.statsPane {
position: relative;
z-index: 9998;
}
.statsTitle {
font-size: 20px;
}
#left {
float: left;
text-align: left;
}
#right {
float: right;
text-align: right;
}
.benchmarkNotice {
color: #ffee00;
}
</style>
<div id="cesiumContainer" class="fullSize">
<div id="slider"></div>
<div id="statsContainer">
<div id="left" class="statsPane panel">
<div id="leftTitle" class="statsTitle">3D Tiles 1.0 (JPEG textures)</div>
<div id="leftStats">
Tiles Loaded: <span id="leftTilesLoaded">---</span> /
<span id="leftTilesTotal">---</span>
<br />
GPU Memory: <span id="leftGpuMemoryMB">---</span> MB
<br />
<span id="leftBenchmarkNotice" class="benchmarkNotice"></span>
<br />
Tile Load Time (s): <span id="leftTileLoadTime">---</span>
<br />
</div>
</div>
<div id="right" class="statsPane panel">
<div id="rightTitle" class="statsTitle">3D Tiles 1.1 (KTX2 textures)</div>
<div id="rightStats">
Tiles Loaded: <span id="rightTilesLoaded">---</span> /
<span id="rightTilesTotal">---</span>
<br />
GPU Memory: <span id="rightGpuMemoryMB">---</span> MB
<br />
<span id="rightBenchmarkNotice" class="benchmarkNotice"></span>
<br />
Tile Load Time (s): <span id="rightTileLoadTime">---</span>
<br />
</div>
</div>
</div>
</div>
<div id="loadingOverlay"><h1>Loading...</h1></div>
<div id="toolbar" class="panel">
<div id="toolbarSelect"></div>
<div id="maxSSE">
Maximum Screen Space Error
<input
type="range"
min="0.0"
max="64.0"
step="0.1"
data-bind="value: maximumScreenSpaceError, valueUpdate: 'input'"
/>
<input type="text" size="5" data-bind="value: maximumScreenSpaceError" />
</div>
</div>

View File

@ -0,0 +1,263 @@
import * as Cesium from "cesium";
import Sandcastle from "Sandcastle";
const viewer = new Cesium.Viewer("cesiumContainer", {
geocoder: false,
sceneModePicker: false,
homeButton: false,
navigationHelpButton: false,
baseLayerPicker: false,
});
// Asset Lookup tables ================================================
// Left half of the screen:
// Tilesets produced by the Cesium ion 3D Model Tiler
const leftAssetIds = {
"AGI HQ": 40866,
Melbourne: 69380,
};
// Right half of the screen:
// Tilesets produced by the Cesium ion Reality Tiler
const rightAssetIds = {
"AGI HQ": 2325106,
Melbourne: 2325107,
};
const ellipsoidProvider = new Cesium.EllipsoidTerrainProvider();
// AGI HQ looks better with Cesium World Terrain, but Melbourne looks
// better using the ellipsoid terrain.
const updateTerrainFunc = {
"AGI HQ": (viewer) => {
viewer.scene.setTerrain(Cesium.Terrain.fromWorldTerrain());
},
Melbourne: (viewer) => {
viewer.terrainProvider = ellipsoidProvider;
},
};
// List of tileset names for creating options and indexing into the
// lookup tables above.
const tilesetNames = ["AGI HQ", "Melbourne"];
// Tileset Loading ====================================================
// Create two primitive collections, one for each half of the screen.
// This way we can clear one half of the screen at a time.
const leftCollection = viewer.scene.primitives.add(
new Cesium.PrimitiveCollection(),
);
const rightCollection = viewer.scene.primitives.add(
new Cesium.PrimitiveCollection(),
);
// Load a tileset to one half of the screen, returning the tileset
async function loadTileset(tilesetName, splitDirection) {
const isLeft = splitDirection === Cesium.SplitDirection.LEFT;
const assetIds = isLeft ? leftAssetIds : rightAssetIds;
const collection = isLeft ? leftCollection : rightCollection;
const assetId = assetIds[tilesetName];
if (!Cesium.defined(assetId)) {
collection.removeAll();
return;
}
const side = splitDirection === Cesium.SplitDirection.LEFT ? "left" : "right";
collection.removeAll();
const tileset = await Cesium.Cesium3DTileset.fromIonAssetId(assetId);
tileset.splitDirection = splitDirection;
// Whenever a tile loads/unloads, update the stats about GPU memory
// and tile count. Load time is handled separately for better
// accuracy.
const updateStatsCallback = (tile) => {
updateStatsPanel(side, tileset);
};
tileset.tileLoad.addEventListener(updateStatsCallback);
tileset.tileUnload.addEventListener(updateStatsCallback);
collection.add(tileset);
return tileset;
}
async function viewTileset(tilesetName, splitDirection) {
const tileset = await loadTileset(tilesetName, splitDirection);
viewer.zoomTo(tileset);
}
async function viewTilesets(tilesetName) {
// Load the tilesets simultaneously
viewTileset(tilesetName, Cesium.SplitDirection.LEFT);
viewTileset(tilesetName, Cesium.SplitDirection.RIGHT);
}
async function benchmarkTileset(tilesetName, splitDirection) {
const side = splitDirection === Cesium.SplitDirection.LEFT ? "left" : "right";
clearStatsPanel(side);
const startMilliseconds = performance.now();
const tileset = await loadTileset(tilesetName, splitDirection);
return new Promise((resolve) => {
tileset.initialTilesLoaded.addEventListener(() => {
const endMilliseconds = performance.now();
const deltaSeconds = (endMilliseconds - startMilliseconds) / 1000.0;
updateLoadTime(side, deltaSeconds);
resolve();
});
});
}
async function benchmarkTilesets(tilesetName) {
// Note: For benchmarking, load tilesets one at a time so the loading
// of one tileset doesn't delay the loading of the other.
await benchmarkTileset(tilesetName, Cesium.SplitDirection.LEFT);
await benchmarkTileset(tilesetName, Cesium.SplitDirection.RIGHT);
}
// UI =================================================================
// Tileset dropdown ---------------------------------------------------
// The first tileset in the dropdown will automatically be selected.
let selectedTilesetName = tilesetNames[0];
function createOption(name) {
return {
text: name,
onselect: function () {
selectedTilesetName = name;
viewTilesets(name).catch(console.error);
updateTerrainFunc[name](viewer);
clearStatsPanel("left");
addBenchmarkNotice("left");
clearStatsPanel("right");
addBenchmarkNotice("right");
},
};
}
function createOptions() {
const options = tilesetNames.map(createOption);
return options;
}
Sandcastle.addToolbarMenu(createOptions(), "toolbarSelect");
// Compute load time -------------------------------------------------
// For better accuracy, this button reloads the tilesets one by one
// so the load time of one tileset doesn't affect the other
Sandcastle.addToolbarButton(
"Compute time to load",
async function () {
benchmarkTilesets(selectedTilesetName);
},
"toolbarSelect",
);
// A note to the user that load time requires a button press
function addBenchmarkNotice(side) {
document.getElementById(`${side}BenchmarkNotice`).innerHTML =
"Press 'Compute time to load' to measure load time";
}
// Stats panels -------------------------------------------------------
function clearStatsPanel(side) {
document.getElementById(`${side}TileLoadTime`).innerHTML = "---";
document.getElementById(`${side}BenchmarkNotice`).innerHTML = "";
}
function updateLoadTime(side, tileLoadTimeSeconds) {
document.getElementById(`${side}TileLoadTime`).innerHTML =
tileLoadTimeSeconds.toPrecision(3);
}
function updateStatsPanel(side, tileset) {
const stats = tileset.statistics;
document.getElementById(`${side}TilesLoaded`).innerHTML =
stats.numberOfLoadedTilesTotal;
document.getElementById(`${side}TilesTotal`).innerHTML =
stats.numberOfTilesTotal;
const gpuMemoryBytes = stats.geometryByteLength + stats.texturesByteLength;
const gpuMemoryMB = gpuMemoryBytes / 1024 / 1024;
document.getElementById(`${side}GpuMemoryMB`).innerHTML =
gpuMemoryMB.toPrecision(3);
}
// maximum SSE Slider -------------------------------------------------
const viewModel = {
maximumScreenSpaceError: 16.0,
};
Cesium.knockout.track(viewModel);
const toolbar = document.getElementById("toolbar");
Cesium.knockout.applyBindings(viewModel, toolbar);
Cesium.knockout
.getObservable(viewModel, "maximumScreenSpaceError")
.subscribe((value) => {
const valueFloat = parseFloat(value);
if (leftCollection.length > 0) {
const leftTileset = leftCollection.get(0);
leftTileset.maximumScreenSpaceError = valueFloat;
}
if (rightCollection.length > 0) {
const rightTileset = rightCollection.get(0);
rightTileset.maximumScreenSpaceError = valueFloat;
}
});
// Splitter ----------------------------------------------------------
// This code is the same as in the 3D Tiles Compare Sandcastle.
// Sync the position of the slider with the split position
const slider = document.getElementById("slider");
viewer.scene.splitPosition =
slider.offsetLeft / slider.parentElement.offsetWidth;
const handler = new Cesium.ScreenSpaceEventHandler(slider);
let moveActive = false;
function move(movement) {
if (!moveActive) {
return;
}
const relativeOffset = movement.endPosition.x;
const splitPosition =
(slider.offsetLeft + relativeOffset) / slider.parentElement.offsetWidth;
slider.style.left = `${100.0 * splitPosition}%`;
viewer.scene.splitPosition = splitPosition;
}
handler.setInputAction(function () {
moveActive = true;
}, Cesium.ScreenSpaceEventType.LEFT_DOWN);
handler.setInputAction(function () {
moveActive = true;
}, Cesium.ScreenSpaceEventType.PINCH_START);
handler.setInputAction(move, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
handler.setInputAction(move, Cesium.ScreenSpaceEventType.PINCH_MOVE);
handler.setInputAction(function () {
moveActive = false;
}, Cesium.ScreenSpaceEventType.LEFT_UP);
handler.setInputAction(function () {
moveActive = false;
}, Cesium.ScreenSpaceEventType.PINCH_END);

View File

@ -0,0 +1,8 @@
legacyId: 3D Tiles 1.1 Photogrammetry.html
title: 3D Tiles Photogrammetry with KTX2
description: Compare photogrammetry performance between 3D Tiles 1.0 and 3D Tiles 1.1 with KTX2 texture compression. KTX2 reduces GPU memory usage and improves runtime efficiency when visualizing large-scale photogrammetry datasets.
labels:
- 3D Tiles
- Photogrammetry
- Performance
thumbnail: thumbnail.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -0,0 +1,15 @@
<style>
@import url(../templates/bucket.css);
table,
th,
td {
border: 1px solid white;
border-collapse: collapse;
}
tt {
padding: 8px;
}
</style>
<div id="cesiumContainer" class="fullSize"></div>
<div id="loadingOverlay"><h1>Loading...</h1></div>
<div id="toolbar"></div>

View File

@ -0,0 +1,184 @@
import * as Cesium from "cesium";
import Sandcastle from "Sandcastle";
// One World Terrain Base Globe provided by Maxar
const viewer = new Cesium.Viewer("cesiumContainer", {
globe: false,
});
const scene = viewer.scene;
viewer.camera.flyTo({
duration: 0,
destination: new Cesium.Cartesian3(
762079.3157173397,
-28363749.882652905,
19814354.842565004,
),
orientation: {
direction: new Cesium.Cartesian3(
-0.022007098944236157,
0.819079900508189,
-0.5732571885110153,
),
up: new Cesium.Cartesian3(
-0.015396759850986286,
0.5730503851893346,
0.8193754913471885,
),
},
easingFunction: Cesium.EasingFunction.QUADRATIC_IN_OUT,
});
let tileset;
try {
// MAXAR OWT WFF 1.2 Base Globe
tileset = await Cesium.Cesium3DTileset.fromIonAssetId(1208297, {
maximumScreenSpaceError: 4,
});
scene.primitives.add(tileset);
} catch (error) {
console.log(`Error loading tileset: ${error}`);
}
// --- Style ---
const style = new Cesium.Cesium3DTileStyle({
defines: {
LandCoverColor: "rgb(${color}[0], ${color}[1], ${color}[2])",
},
color:
"${LandCoverColor} === vec4(1.0) ? rgb(254, 254, 254) : ${LandCoverColor}",
});
// --- Custom Shader ---
const customShader = new Cesium.CustomShader({
uniforms: {
u_time: {
type: Cesium.UniformType.FLOAT,
value: 0,
},
},
fragmentShaderText: `
void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material)
{
int featureId = fsInput.featureIds.featureId_0;
// Use cartesian coordinates but scale to be roughly [-1, 1]
vec3 positionWC = fsInput.attributes.positionWC / 6.3e6;
if (featureId == 60)
{
// Something like FM synthesis to make irregularly spaced waves
float wave = sin(14.0 * positionWC.z - u_time);
wave = 0.5 + 0.5 * sin(10.0 * wave * positionWC.z - u_time);
// mix in an over-saturated version of the diffuse to make shimmering bands of color
material.diffuse = mix(material.diffuse, material.diffuse * 3.0, wave);
}
}
`,
});
const startTime = performance.now();
const customShaderUpdate = function () {
const elapsedTimeSeconds = (performance.now() - startTime) / 1000;
customShader.setUniform("u_time", elapsedTimeSeconds);
};
viewer.scene.postUpdate.addEventListener(function () {
customShaderUpdate();
});
// --- Picking ---
let enablePicking = true;
const handler = new Cesium.ScreenSpaceEventHandler(scene.canvas);
const metadataOverlay = document.createElement("div");
viewer.container.appendChild(metadataOverlay);
metadataOverlay.className = "backdrop";
metadataOverlay.style.display = "none";
metadataOverlay.style.position = "absolute";
metadataOverlay.style.bottom = "0";
metadataOverlay.style.left = "0";
metadataOverlay.style["pointer-events"] = "none";
metadataOverlay.style.padding = "4px";
metadataOverlay.style.backgroundColor = "#303030";
metadataOverlay.style.whiteSpace = "pre-line";
metadataOverlay.style.fontSize = "16px";
metadataOverlay.style.borderRadius = "4px";
let tableHtmlScratch;
handler.setInputAction(function (movement) {
if (enablePicking) {
const feature = scene.pick(movement.endPosition);
if (feature instanceof Cesium.Cesium3DTileFeature) {
metadataOverlay.style.display = "block";
metadataOverlay.style.bottom = `${
viewer.canvas.clientHeight - movement.endPosition.y
}px`;
metadataOverlay.style.left = `${movement.endPosition.x}px`;
tableHtmlScratch =
"<table><thead><tr><th><tt>Property</tt></th><th><tt>Value</tt></th></tr></thead><tbody>";
const propertyIds = feature.getPropertyIds();
const length = propertyIds.length;
for (let i = 0; i < length; ++i) {
const propertyId = propertyIds[i];
const propertyValue = feature.getProperty(propertyId);
tableHtmlScratch += `<tr><td><tt>${propertyId}</tt></td><td><tt>${propertyValue}</tt></td></tr>`;
}
tableHtmlScratch += "</tbody></table>";
metadataOverlay.innerHTML = tableHtmlScratch;
} else {
metadataOverlay.style.display = "none";
}
}
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
// --- UI ---
const modes = [
{
text: "Globe View",
onselect: function () {
tileset.customShader = undefined;
tileset.debugShowBoundingVolume = false;
tileset.style = undefined;
},
},
{
text: "Show S2 Bounding Volumes",
onselect: function () {
tileset.customShader = undefined;
tileset.debugShowBoundingVolume = true;
tileset.style = undefined;
},
},
{
text: "Apply Style",
onselect: function () {
tileset.customShader = undefined;
tileset.debugShowBoundingVolume = false;
tileset.style = style;
},
},
{
text: "Apply Custom Shader",
onselect: function () {
tileset.customShader = customShader;
tileset.debugShowBoundingVolume = false;
tileset.style = undefined;
},
},
];
Sandcastle.addToolbarMenu(modes);
Sandcastle.addToggleButton("Enable picking", enablePicking, function (checked) {
if (enablePicking) {
metadataOverlay.style.display = "none";
}
enablePicking = checked;
});

View File

@ -0,0 +1,7 @@
legacyId: 3D Tiles 1.1 S2 Globe.html
title: S2 Globe with 3D Tiles
description: Loads a One World Terrain dataset, courtesy Maxar,—A global scale 3D Tiles 1.1 tileset that uses 3DTILES_bounding_volume_S2, a schema for defining bounding volumes as hierarchical cells composing a unit sphere. This extension is well suited for tilesets that span the whole globe as, opposed to traditional GIS projections such as Web Mercator which can create distortions in scale, divides an ellipsoid into cells of relatively similar size.
labels:
- 3D Tiles
- Terrain
thumbnail: thumbnail.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,16 @@
<style>
@import url(../templates/bucket.css);
</style>
<div id="cesiumContainer" class="fullSize"></div>
<div id="loadingOverlay"><h1>Loading...</h1></div>
<div id="toolbar">
<div>Height</div>
<input
type="range"
min="-100.0"
max="100.0"
step="1"
data-bind="value: height, valueUpdate: 'input'"
/>
<input type="text" size="5" data-bind="value: height" />
</div>

View File

@ -0,0 +1,60 @@
import * as Cesium from "cesium";
const viewer = new Cesium.Viewer("cesiumContainer", {
shadows: true,
});
const viewModel = {
height: 0,
};
Cesium.knockout.track(viewModel);
const toolbar = document.getElementById("toolbar");
Cesium.knockout.applyBindings(viewModel, toolbar);
let tileset;
try {
tileset = await Cesium.Cesium3DTileset.fromUrl(
"../../SampleData/Cesium3DTiles/Tilesets/Tileset/tileset.json",
);
viewer.scene.primitives.add(tileset);
viewer.scene.globe.depthTestAgainstTerrain = true;
viewer.zoomTo(
tileset,
new Cesium.HeadingPitchRange(
0.0,
-0.5,
tileset.boundingSphere.radius * 2.0,
),
);
} catch (error) {
console.log(`Error loading tileset: ${error}`);
}
Cesium.knockout.getObservable(viewModel, "height").subscribe(function (height) {
height = Number(height);
if (isNaN(height) || !Cesium.defined(tileset)) {
return;
}
const cartographic = Cesium.Cartographic.fromCartesian(
tileset.boundingSphere.center,
);
const surface = Cesium.Cartesian3.fromRadians(
cartographic.longitude,
cartographic.latitude,
0.0,
);
const offset = Cesium.Cartesian3.fromRadians(
cartographic.longitude,
cartographic.latitude,
height,
);
const translation = Cesium.Cartesian3.subtract(
offset,
surface,
new Cesium.Cartesian3(),
);
tileset.modelMatrix = Cesium.Matrix4.fromTranslation(translation);
});

View File

@ -0,0 +1,7 @@
legacyId: 3D Tiles Adjust Height.html
title: Offset 3D Tileset Height
description: Change the vertical position of a 3D Tileset by applying a translation matrix. Useful for aligning datasets with terrain.
labels:
- 3D Tiles
- Transformations
thumbnail: thumbnail.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,9 @@
<style>
@import url(../templates/bucket.css);
#toolbar button {
display: block;
}
</style>
<div id="cesiumContainer" class="fullSize"></div>
<div id="loadingOverlay"><h1>Loading...</h1></div>
<div id="toolbar"></div>

View File

@ -0,0 +1,190 @@
import * as Cesium from "cesium";
import Sandcastle from "Sandcastle";
// Doorknobs, doors, roofs, and walls are styled with the batch table hierarchy.
// Since buildings and zones are not backed by geometry they are not styled directly. However
// styles may be written that take building and zone properties into account.
//
// Hierarchy layout (doorknobs are children of doors):
//
// zone0
// building0
// roof0
// wall0
// door0 - doorknob0
// door1 - doorknob1
// door2 - doorknob2
// door3 - doorknob3
// building1
// roof1
// wall1
// door4 - doorknob4
// door5 - doorknob5
// door6 - doorknob6
// door7 - doorknob7
// building2
// roof2
// wall2
// door8 - doorknob8
// door9 - doorknob9
// door10 - doorknob10
// door11 - doorknob11
//
// Class properties:
//
// zone:
// * zone_building
// * zone_name
// building:
// * building_area
// * building_name
// wall:
// * wall_paint
// * wall_windows
// * wall_name
// roof:
// * roof_paint
// * roof_name
// door:
// * door_mass
// * door_width
// * door_name
// doorknob:
// * doorknob_size
// * doorknob_name
const viewer = new Cesium.Viewer("cesiumContainer");
viewer.clock.currentTime = new Cesium.JulianDate(2457522.154792);
let tileset;
try {
tileset = await Cesium.Cesium3DTileset.fromUrl(
"../../SampleData/Cesium3DTiles/Hierarchy/BatchTableHierarchy/tileset.json",
);
viewer.scene.primitives.add(tileset);
viewer.zoomTo(tileset, new Cesium.HeadingPitchRange(0.0, -0.3, 0.0));
tileset.style = new Cesium.Cesium3DTileStyle({
color: {
conditions: [
["isExactClass('door')", "color('orange')"],
["true", "color('white')"],
],
},
});
} catch (error) {
console.log(`Error loading tileset: ${error}`);
}
function setStyle(style) {
return function () {
if (!Cesium.defined(tileset)) {
return;
}
tileset.style = new Cesium.Cesium3DTileStyle(style);
};
}
const styles = [];
function addStyle(name, style) {
styles.push({
name: name,
style: style,
});
}
addStyle("Color all doors", {
color: {
conditions: [
["isExactClass('door')", "color('orange')"],
["true", "color('white')"],
],
},
});
addStyle("Color all features derived from door", {
color: {
conditions: [
["isClass('door')", "color('orange')"],
["true", "color('white')"],
],
},
});
addStyle("Color by building", {
color: {
conditions: [
["${building_name} === 'building0'", "color('purple')"],
["${building_name} === 'building1'", "color('red')"],
["${building_name} === 'building2'", "color('orange')"],
["true", "color('blue')"],
],
},
});
addStyle("Color features by class name", {
defines: {
suffix: "regExp('door(.*)').exec(getExactClassName())",
},
color: {
conditions: [
["${suffix} === 'knob'", "color('yellow')"],
["${suffix} === ''", "color('lime')"],
["${suffix} === null", "color('gray')"],
["true", "color('blue')"],
],
},
});
addStyle("Style by height", {
color: {
conditions: [
["${height} >= 10", "color('purple')"],
["${height} >= 6", "color('red')"],
["${height} >= 5", "color('orange')"],
["true", "color('blue')"],
],
},
});
addStyle("No style", {});
const styleOptions = [];
for (let i = 0; i < styles.length; ++i) {
const style = styles[i];
styleOptions.push({
text: style.name,
onselect: setStyle(style.style),
});
}
Sandcastle.addToolbarMenu(styleOptions);
const handler = new Cesium.ScreenSpaceEventHandler(viewer.canvas);
// When a feature is left clicked, print its class name and properties
handler.setInputAction(function (movement) {
const feature = viewer.scene.pick(movement.position);
if (!Cesium.defined(feature)) {
return;
}
console.log(`Class: ${feature.getExactClassName()}`);
console.log("Properties:");
const propertyIds = feature.getPropertyIds();
const length = propertyIds.length;
for (let i = 0; i < length; ++i) {
const propertyId = propertyIds[i];
const value = feature.getProperty(propertyId);
console.log(` ${propertyId}: ${value}`);
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
// When a feature is middle clicked, hide it
handler.setInputAction(function (movement) {
const feature = viewer.scene.pick(movement.position);
if (!Cesium.defined(feature)) {
return;
}
feature.show = false;
}, Cesium.ScreenSpaceEventType.MIDDLE_CLICK);

View File

@ -0,0 +1,8 @@
legacyId: 3D Tiles Batch Table Hierarchy.html
title: 3D Tiles Styling
description: Shows how to style 3D Tiles by hierarchical class and properties (e.g., buildings → doors → doorknobs). Demonstrates 3D Tiles Style predicates like `isClass` and `isExactClass` and coloring or filtering by element property data.
labels:
- 3D Tiles
- Metadata
- Styling
thumbnail: thumbnail.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,6 @@
<style>
@import url(../templates/bucket.css);
</style>
<div id="cesiumContainer" class="fullSize"></div>
<div id="loadingOverlay"><h1>Loading...</h1></div>
<div id="toolbar"></div>

View File

@ -0,0 +1,146 @@
import * as Cesium from "cesium";
import Sandcastle from "Sandcastle";
// Power Plant design model provided by Bentley Systems
const viewer = new Cesium.Viewer("cesiumContainer");
const scene = viewer.scene;
viewer.clock.currentTime = Cesium.JulianDate.fromIso8601(
"2022-08-01T00:00:00Z",
);
let selectedFeature;
let picking = false;
Sandcastle.addToggleButton("Per-feature selection", false, function (checked) {
picking = checked;
if (!picking) {
unselectFeature(selectedFeature);
}
});
function selectFeature(feature) {
const element = feature.getProperty("element");
setElementColor(element, Cesium.Color.YELLOW);
selectedFeature = feature;
}
function unselectFeature(feature) {
if (!Cesium.defined(feature)) {
return;
}
const element = feature.getProperty("element");
setElementColor(element, Cesium.Color.WHITE);
if (feature === selectedFeature) {
selectedFeature = undefined;
}
}
const handler = new Cesium.ScreenSpaceEventHandler(scene.canvas);
handler.setInputAction(function (movement) {
if (!picking) {
return;
}
const feature = scene.pick(movement.endPosition);
unselectFeature(selectedFeature);
if (feature instanceof Cesium.Cesium3DTileFeature) {
selectFeature(feature);
}
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
// In this tileset every feature has an "element" property which is a global ID.
// This property is used to associate features across different tiles and LODs.
// Workaround until 3D Tiles has the concept of global batch ids: https://github.com/CesiumGS/3d-tiles/issues/265
const elementMap = {}; // Build a map of elements to features.
const hiddenElements = [
112001, 113180, 131136, 113167, 71309, 109652, 111178, 113156, 113170, 124846,
114076, 131122, 113179, 114325, 131134, 113164, 113153, 113179, 109656,
114095, 114093, 39225, 39267, 113149, 113071, 112003, 39229, 113160, 39227,
39234, 113985, 39230, 112004, 39223,
];
function getElement(feature) {
return parseInt(feature.getProperty("element"), 10);
}
function setElementColor(element, color) {
const featuresToColor = elementMap[element];
const length = featuresToColor.length;
for (let i = 0; i < length; ++i) {
const feature = featuresToColor[i];
feature.color = Cesium.Color.clone(color, feature.color);
}
}
function unloadFeature(feature) {
unselectFeature(feature);
const element = getElement(feature);
const features = elementMap[element];
const index = features.indexOf(feature);
if (index > -1) {
features.splice(index, 1);
}
}
function loadFeature(feature) {
const element = getElement(feature);
let features = elementMap[element];
if (!Cesium.defined(features)) {
features = [];
elementMap[element] = features;
}
features.push(feature);
if (hiddenElements.indexOf(element) > -1) {
feature.show = false;
}
}
function processContentFeatures(content, callback) {
const featuresLength = content.featuresLength;
for (let i = 0; i < featuresLength; ++i) {
const feature = content.getFeature(i);
callback(feature);
}
}
function processTileFeatures(tile, callback) {
const content = tile.content;
const innerContents = content.innerContents;
if (Cesium.defined(innerContents)) {
const length = innerContents.length;
for (let i = 0; i < length; ++i) {
processContentFeatures(innerContents[i], callback);
}
} else {
processContentFeatures(content, callback);
}
}
try {
const tileset = await Cesium.Cesium3DTileset.fromIonAssetId(2464651);
scene.primitives.add(tileset);
viewer.zoomTo(
tileset,
new Cesium.HeadingPitchRange(
0.5,
-0.2,
tileset.boundingSphere.radius * 4.0,
),
);
tileset.colorBlendMode = Cesium.Cesium3DTileColorBlendMode.REPLACE;
tileset.tileLoad.addEventListener(function (tile) {
processTileFeatures(tile, loadFeature);
});
tileset.tileUnload.addEventListener(function (tile) {
processTileFeatures(tile, unloadFeature);
});
} catch (error) {
console.log(`Error loading tileset: ${error}`);
}

View File

@ -0,0 +1,8 @@
legacyId: 3D Tiles BIM.html
title: BIM Design Model with 3D Tiles
description: Render and interact with a Building Information Modeling (BIM) design model dataset using 3D Tiles. Highlights per-feature selection, global element IDs, and visibility control—useful for inspecting and managing complex engineering or infrastructure models.
labels:
- 3D Tiles
- Metadata
- Picking
thumbnail: thumbnail.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -0,0 +1,20 @@
<style>
@import url(../templates/bucket.css);
</style>
<div id="cesiumContainer" class="fullSize"></div>
<div id="loadingOverlay"><h1>Loading...</h1></div>
<div id="toolbar">
<select data-bind="options: exampleTypes, value: currentExampleType"></select>
<input
type="checkbox"
value="false"
data-bind="checked: debugBoundingVolumesEnabled, valueUpdate: 'input'"
/>
Show bounding volume
<input
type="checkbox"
value="true"
data-bind="checked: edgeStylingEnabled, valueUpdate: 'input'"
/>
Enable edge styling
</div>

View File

@ -0,0 +1,271 @@
import * as Cesium from "cesium";
// Add a clipping plane, a plane geometry to show the representation of the
// plane, and control the magnitude of the plane distance with the mouse.
const viewer = new Cesium.Viewer("cesiumContainer", {
infoBox: false,
selectionIndicator: false,
});
const scene = viewer.scene;
viewer.clock.currentTime = Cesium.JulianDate.fromIso8601(
"2022-08-01T00:00:00Z",
);
const clipObjects = ["BIM", "Point Cloud", "Instanced", "Model"];
const viewModel = {
debugBoundingVolumesEnabled: false,
edgeStylingEnabled: true,
exampleTypes: clipObjects,
currentExampleType: clipObjects[0],
};
let targetY = 0.0;
let planeEntities = [];
let selectedPlane;
let clippingPlanes;
// Select plane when mouse down
const downHandler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
downHandler.setInputAction(function (movement) {
const pickedObject = scene.pick(movement.position);
if (
Cesium.defined(pickedObject) &&
Cesium.defined(pickedObject.id) &&
Cesium.defined(pickedObject.id.plane)
) {
selectedPlane = pickedObject.id.plane;
selectedPlane.material = Cesium.Color.WHITE.withAlpha(0.05);
selectedPlane.outlineColor = Cesium.Color.WHITE;
scene.screenSpaceCameraController.enableInputs = false;
}
}, Cesium.ScreenSpaceEventType.LEFT_DOWN);
// Release plane on mouse up
const upHandler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
upHandler.setInputAction(function () {
if (Cesium.defined(selectedPlane)) {
selectedPlane.material = Cesium.Color.WHITE.withAlpha(0.1);
selectedPlane.outlineColor = Cesium.Color.WHITE;
selectedPlane = undefined;
}
scene.screenSpaceCameraController.enableInputs = true;
}, Cesium.ScreenSpaceEventType.LEFT_UP);
// Update plane on mouse move
const moveHandler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
moveHandler.setInputAction(function (movement) {
if (Cesium.defined(selectedPlane)) {
const deltaY = movement.startPosition.y - movement.endPosition.y;
targetY += deltaY;
}
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
function createPlaneUpdateFunction(plane) {
return function () {
plane.distance = targetY;
return plane;
};
}
let tileset;
async function loadTileset(resource, modelMatrix) {
const currentExampleType = viewModel.currentExampleType;
clippingPlanes = new Cesium.ClippingPlaneCollection({
planes: [
new Cesium.ClippingPlane(new Cesium.Cartesian3(0.0, 0.0, -1.0), 0.0),
],
edgeWidth: viewModel.edgeStylingEnabled ? 1.0 : 0.0,
});
try {
const url = await Promise.resolve(resource);
tileset = await Cesium.Cesium3DTileset.fromUrl(url, {
clippingPlanes: clippingPlanes,
});
if (currentExampleType !== viewModel.currentExampleType) {
// Another tileset was loaded, discard the current result
return;
}
if (Cesium.defined(modelMatrix)) {
tileset.modelMatrix = modelMatrix;
}
viewer.scene.primitives.add(tileset);
tileset.debugShowBoundingVolume = viewModel.debugBoundingVolumesEnabled;
const boundingSphere = tileset.boundingSphere;
const radius = boundingSphere.radius;
viewer.zoomTo(
tileset,
new Cesium.HeadingPitchRange(0.5, -0.2, radius * 4.0),
);
if (
!Cesium.Matrix4.equals(tileset.root.transform, Cesium.Matrix4.IDENTITY)
) {
// The clipping plane is initially positioned at the tileset's root transform.
// Apply an additional matrix to center the clipping plane on the bounding sphere center.
const transformCenter = Cesium.Matrix4.getTranslation(
tileset.root.transform,
new Cesium.Cartesian3(),
);
const transformCartographic =
Cesium.Cartographic.fromCartesian(transformCenter);
const boundingSphereCartographic = Cesium.Cartographic.fromCartesian(
tileset.boundingSphere.center,
);
const height =
boundingSphereCartographic.height - transformCartographic.height;
clippingPlanes.modelMatrix = Cesium.Matrix4.fromTranslation(
new Cesium.Cartesian3(0.0, 0.0, height),
);
}
for (let i = 0; i < clippingPlanes.length; ++i) {
const plane = clippingPlanes.get(i);
const planeEntity = viewer.entities.add({
position: boundingSphere.center,
plane: {
dimensions: new Cesium.Cartesian2(radius * 2.5, radius * 2.5),
material: Cesium.Color.WHITE.withAlpha(0.1),
plane: new Cesium.CallbackProperty(
createPlaneUpdateFunction(plane),
false,
),
outline: true,
outlineColor: Cesium.Color.WHITE,
},
});
planeEntities.push(planeEntity);
}
return tileset;
} catch (error) {
console.log(`Error loading tileset: ${error}`);
}
}
function loadModel(url) {
clippingPlanes = new Cesium.ClippingPlaneCollection({
planes: [
new Cesium.ClippingPlane(new Cesium.Cartesian3(0.0, 0.0, -1.0), 0.0),
],
edgeWidth: viewModel.edgeStylingEnabled ? 1.0 : 0.0,
});
const position = Cesium.Cartesian3.fromDegrees(
-123.0744619,
44.0503706,
300.0,
);
const heading = Cesium.Math.toRadians(135.0);
const pitch = 0.0;
const roll = 0.0;
const hpr = new Cesium.HeadingPitchRoll(heading, pitch, roll);
const orientation = Cesium.Transforms.headingPitchRollQuaternion(
position,
hpr,
);
const entity = viewer.entities.add({
name: url,
position: position,
orientation: orientation,
model: {
uri: url,
scale: 8,
minimumPixelSize: 100.0,
clippingPlanes: clippingPlanes,
},
});
viewer.trackedEntity = entity;
for (let i = 0; i < clippingPlanes.length; ++i) {
const plane = clippingPlanes.get(i);
const planeEntity = viewer.entities.add({
position: position,
plane: {
dimensions: new Cesium.Cartesian2(300.0, 300.0),
material: Cesium.Color.WHITE.withAlpha(0.1),
plane: new Cesium.CallbackProperty(
createPlaneUpdateFunction(plane),
false,
),
outline: true,
outlineColor: Cesium.Color.WHITE,
},
});
planeEntities.push(planeEntity);
}
}
// Power Plant design model provided by Bentley Systems
const bimUrl = Cesium.IonResource.fromAssetId(2464651);
const pointCloudUrl = Cesium.IonResource.fromAssetId(16421);
const instancedUrl =
"../../SampleData/Cesium3DTiles/Instanced/InstancedOrientation/tileset.json";
const modelUrl = "../../SampleData/models/CesiumAir/Cesium_Air.glb";
loadTileset(bimUrl);
// Track and create the bindings for the view model
const toolbar = document.getElementById("toolbar");
Cesium.knockout.track(viewModel);
Cesium.knockout.applyBindings(viewModel, toolbar);
Cesium.knockout
.getObservable(viewModel, "currentExampleType")
.subscribe(function (newValue) {
reset();
if (newValue === clipObjects[0]) {
loadTileset(bimUrl);
} else if (newValue === clipObjects[1]) {
loadTileset(pointCloudUrl);
} else if (newValue === clipObjects[2]) {
// Position the instanced tileset above terrain
loadTileset(
instancedUrl,
Cesium.Matrix4.fromTranslation(
new Cesium.Cartesian3(15.0, -58.6, 50.825),
),
);
} else {
loadModel(modelUrl);
}
});
Cesium.knockout
.getObservable(viewModel, "debugBoundingVolumesEnabled")
.subscribe(function (value) {
if (Cesium.defined(tileset)) {
tileset.debugShowBoundingVolume = value;
}
});
Cesium.knockout
.getObservable(viewModel, "edgeStylingEnabled")
.subscribe(function (value) {
const edgeWidth = value ? 1.0 : 0.0;
clippingPlanes.edgeWidth = edgeWidth;
});
function reset() {
viewer.entities.removeAll();
if (Cesium.defined(tileset)) {
viewer.scene.primitives.remove(tileset);
}
planeEntities = [];
targetY = 0.0;
tileset = undefined;
}

Some files were not shown because too many files have changed in this diff Show More