cesium/Apps/Sandcastle/gallery/VolumeCloud.html

659 lines
20 KiB
HTML

<!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="Rendering Volume Cloud with Texture3D and Custom GLSL. Transplanted from Three.js"
/>
<meta name="cesium-sandcastle-labels" content="Tutorials,Showcases" />
<title>Cesium Demo</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);
</style>
<div id="cesiumContainer" class="fullSize"></div>
<div id="loadingOverlay">
<h1>Loading...</h1>
</div>
<div id="toolbar"></div>
<script id="cesium_sandcastle_script">
window.startup = async function (Cesium) {
"use strict";
//Sandcastle_Begin
// ImprovedNoise from Three.js
// https://github.com/mrdoob/three.js/blob/dev/examples/jsm/math/ImprovedNoise.js
const lerp = Cesium.Math.lerp;
const _p = [
151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36,
103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, 247, 120, 234, 75, 0,
26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33, 88, 237, 149, 56,
87, 174, 20, 125, 136, 171, 168, 68, 175, 74, 165, 71, 134, 139, 48, 27, 166,
77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41, 55,
46, 245, 40, 244, 102, 143, 54, 65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132,
187, 208, 89, 18, 169, 200, 196, 135, 130, 116, 188, 159, 86, 164, 100, 109,
198, 173, 186, 3, 64, 52, 217, 226, 250, 124, 123, 5, 202, 38, 147, 118, 126,
255, 82, 85, 212, 207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42, 223, 183,
170, 213, 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172,
9, 129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104,
218, 246, 97, 228, 251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241, 81,
51, 145, 235, 249, 14, 239, 107, 49, 192, 214, 31, 181, 199, 106, 157, 184, 84,
204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236, 205, 93, 222, 114, 67,
29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180,
];
for (let i = 0; i < 256; i++) {
_p[256 + i] = _p[i];
}
function fade(t) {
return t * t * t * (t * (t * 6 - 15) + 10);
}
function grad(hash, x, y, z) {
const h = hash & 15;
const u = h < 8 ? x : y,
v = h < 4 ? y : h === 12 || h === 14 ? x : z;
return ((h & 1) === 0 ? u : -u) + ((h & 2) === 0 ? v : -v);
}
/**
* A utility class providing a 3D noise function.
*
* The code is based on [IMPROVED NOISE]{@link https://cs.nyu.edu/~perlin/noise/}
* by Ken Perlin, 2002.
*
* @three_import import { ImprovedNoise } from 'three/addons/math/ImprovedNoise.js';
*/
class ImprovedNoise {
/**
* Returns a noise value for the given parameters.
*
* @param {number} x - The x coordinate.
* @param {number} y - The y coordinate.
* @param {number} z - The z coordinate.
* @return {number} The noise value.
*/
noise(x, y, z) {
const floorX = Math.floor(x),
floorY = Math.floor(y),
floorZ = Math.floor(z);
const X = floorX & 255,
Y = floorY & 255,
Z = floorZ & 255;
x -= floorX;
y -= floorY;
z -= floorZ;
const xMinus1 = x - 1,
yMinus1 = y - 1,
zMinus1 = z - 1;
const u = fade(x),
v = fade(y),
w = fade(z);
const A = _p[X] + Y,
AA = _p[A] + Z,
AB = _p[A + 1] + Z,
B = _p[X + 1] + Y,
BA = _p[B] + Z,
BB = _p[B + 1] + Z;
return lerp(
lerp(
lerp(grad(_p[AA], x, y, z), grad(_p[BA], xMinus1, y, z), u),
lerp(grad(_p[AB], x, yMinus1, z), grad(_p[BB], xMinus1, yMinus1, z), u),
v,
),
lerp(
lerp(
grad(_p[AA + 1], x, y, zMinus1),
grad(_p[BA + 1], xMinus1, y, zMinus1),
u,
),
lerp(
grad(_p[AB + 1], x, yMinus1, zMinus1),
grad(_p[BB + 1], xMinus1, yMinus1, zMinus1),
u,
),
v,
),
w,
);
}
}
// End ImprovedNoise from Three.js
// GeometryPrimitive
const {
Cartesian3,
destroyObject,
DrawCommand,
VertexArray,
GeometryPipeline,
Matrix4,
} = Cesium;
/**
* Custom Primitive for Geometry
*/
class GeometryPrimitive {
/**
*
* @param {*} options
* @param {*} options.modelMatrix
* @param {*} options.vertexShaderSource
* @param {*} options.fragmentShaderSource
* @param {*} options.uniformMap
* @param {*} options.renderState
* @param {*} options.pass
*/
constructor(geometry, options) {
this.options = options;
this.geometry = geometry;
}
/**
*
* @param {*} frameState
*/
update(frameState) {
if (Cesium.defined(this._drawCommand)) {
frameState.commandList.push(this._drawCommand);
return;
}
if (this.geometry.constructor.createGeometry) {
this.geometry = this.geometry.constructor.createGeometry(this.geometry);
}
const context = frameState.context;
const attributeLocations = GeometryPipeline.createAttributeLocations(
this.geometry,
);
const vertexArray = VertexArray.fromGeometry({
context: context,
geometry: this.geometry,
attributeLocations,
});
// calculate boundingSphere
const boundingSphere = this.geometry.boundingSphere;
boundingSphere.center = Matrix4.multiplyByPoint(
this.options.modelMatrix,
boundingSphere.center,
new Cartesian3(),
);
boundingSphere.radius = 1000000;
this._boundingSphereWC = [boundingSphere];
const shaderProgram = Cesium.ShaderProgram.fromCache({
context: context,
attributeLocations,
vertexShaderSource: this.options.vertexShaderSource,
fragmentShaderSource: this.options.fragmentShaderSource,
});
this._drawCommand = new DrawCommand({
owner: this,
boundingVolume: boundingSphere,
primitiveType: this.geometry.primitiveType,
vertexArray: vertexArray,
shaderProgram,
...this.options,
});
}
destroy() {
return destroyObject(this);
}
isDestroyed() {
return false;
}
}
// GeometryPrimitive end
function makeTexture3D(context) {
const size = 100;
const dataLength = size * size * size;
const data = new Uint8Array(dataLength);
let i = 0;
const scale = 0.05;
const perlin = new ImprovedNoise();
let vector = new Cesium.Cartesian3();
const halfSize = Cesium.Cartesian3.fromElements(
size / 2,
size / 2,
size / 2,
new Cesium.Cartesian3(),
);
for (let z = 0; z < size; z++) {
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
vector = Cesium.Cartesian3.fromElements(x, y, z, vector);
vector = Cesium.Cartesian3.subtract(vector, halfSize, vector);
vector = Cesium.Cartesian3.divideByScalar(vector, size, vector);
const d = 1.0 - Cesium.Cartesian3.magnitude(vector);
const tv =
(128 +
128 * perlin.noise((x * scale) / 1.5, y * scale, (z * scale) / 1.5)) *
d *
d;
data[i] = tv;
i++;
}
}
}
return new Cesium.Texture3D({
context: context,
width: size,
height: size,
depth: size,
flipY: false,
pixelFormat: Cesium.PixelFormat.RED,
pixelDatatype: Cesium.PixelDatatype.UNSIGNED_BYTE,
source: {
arrayBufferView: data,
width: size,
height: size,
depth: size,
},
sampler: new Cesium.Sampler({
minificationFilter: Cesium.TextureMinificationFilter.LINEAR,
magnificationFilter: Cesium.TextureMagnificationFilter.LINEAR,
}),
});
}
const vertexShader = /* glsl */ `
in vec3 position3DHigh;
in vec3 position3DLow;
in vec3 normal;
in vec2 st;
in float batchId;
out vec3 vOrigin;
out vec3 vDirection;
out vec3 vPosition;
vec4 translateRelativeToEye(vec3 high, vec3 low) {
vec3 highDifference = high - czm_encodedCameraPositionMCHigh;
if(length(highDifference) == 0.0f) {
highDifference = vec3(0);
}
vec3 lowDifference = low - czm_encodedCameraPositionMCLow;
return vec4(highDifference + lowDifference, 1.0f);
}
void main()
{
vec4 p = translateRelativeToEye(position3DHigh, position3DLow);
vOrigin = czm_encodedCameraPositionMCHigh + czm_encodedCameraPositionMCLow;
vec3 modelPosition = position3DHigh + position3DLow;
vPosition = modelPosition;
vDirection = modelPosition - vOrigin;
gl_Position = czm_modelViewProjectionRelativeToEye * p;
}`;
const fragmentShader = /* glsl */ `
precision highp float;
precision highp sampler3D;
in vec3 vOrigin;
in vec3 vDirection;
// https://github.com/mrdoob/three.js/blob/dev/examples/webgl_volume_cloud.html
uniform vec3 base;
uniform sampler3D map;
uniform float threshold;
uniform float range;
uniform float opacity;
uniform float steps;
uniform float frame;
uint wang_hash(uint seed)
{
seed = (seed ^ 61u) ^ (seed >> 16u);
seed *= 9u;
seed = seed ^ (seed >> 4u);
seed *= 0x27d4eb2du;
seed = seed ^ (seed >> 15u);
return seed;
}
float randomFloat(inout uint seed)
{
return float(wang_hash(seed)) / 4294967296.;
}
vec2 hitBox( vec3 orig, vec3 dir ) {
const vec3 box_min = vec3( - 0.5 );
const vec3 box_max = vec3( 0.5 );
vec3 inv_dir = 1.0 / dir;
vec3 tmin_tmp = ( box_min - orig ) * inv_dir;
vec3 tmax_tmp = ( box_max - orig ) * inv_dir;
vec3 tmin = min( tmin_tmp, tmax_tmp );
vec3 tmax = max( tmin_tmp, tmax_tmp );
float t0 = max( tmin.x, max( tmin.y, tmin.z ) );
float t1 = min( tmax.x, min( tmax.y, tmax.z ) );
return vec2( t0, t1 );
}
float sample1( vec3 p ) {
return texture( map, p ).r;
}
float shading( vec3 coord ) {
float step = 0.01;
return sample1( coord + vec3( - step ) ) - sample1( coord + vec3( step ) );
}
vec4 linearToSRGB( in vec4 value ) {
return vec4( mix( pow( value.rgb, vec3( 0.41666 ) ) * 1.055 - vec3( 0.055 ), value.rgb * 12.92, vec3( lessThanEqual( value.rgb, vec3( 0.0031308 ) ) ) ), value.a );
}
void main() {
vec3 rayDir = normalize( vDirection );
vec2 bounds = hitBox( vOrigin, rayDir );
if ( bounds.x > bounds.y ) discard;
bounds.x = max( bounds.x, 0.0 );
vec3 p = vOrigin + bounds.x * rayDir;
vec3 inc = 1.0 / abs( rayDir );
float delta = min( inc.x, min( inc.y, inc.z ) );
delta /= steps;
// Nice little seed from
// https://blog.demofox.org/2020/05/25/casual-shadertoy-path-tracing-1-basic-camera-diffuse-emissive/
uint seed = uint( gl_FragCoord.x ) * uint( 1973 ) + uint( gl_FragCoord.y ) * uint( 9277 ) + uint( frame ) * uint( 26699 );
vec3 size = vec3( textureSize( map, 0 ) );
float randNum = randomFloat( seed ) * 2.0 - 1.0;
p += rayDir * randNum * ( 1.0 / size );
vec4 ac = vec4( base, 0.0 );
for ( float t = bounds.x; t < bounds.y; t += delta ) {
float d = sample1( p + 0.5 );
d = smoothstep( threshold - range, threshold + range, d ) * opacity;
float col = shading( p + 0.5 ) * 3.0 + ( ( p.x + p.y ) * 0.25 ) + 0.2;
ac.rgb += ( 1.0 - ac.a ) * d * col;
ac.a += ( 1.0 - ac.a ) * d;
if ( ac.a >= 0.95 ) break;
p += rayDir * delta;
}
vec4 color = linearToSRGB( ac );
color = czm_gammaCorrect( color );
if ( color.a == 0.0 ) discard;
out_FragColor = color;
}
`;
const viewer = new Cesium.Viewer("cesiumContainer", {
orderIndependentTranslucency: true,
});
const texture3D = makeTexture3D(viewer.scene.context);
texture3D.generateMipmap();
const boxSideLength = 1.0;
const zoomScale = 10000;
const centerPoint = Cesium.Cartesian3.fromDegrees(
113,
33,
(boxSideLength / 0.5) * zoomScale,
);
const modelMatrix = Cesium.Transforms.eastNorthUpToFixedFrame(centerPoint);
const boxSize = new Cesium.Cartesian3(
boxSideLength,
boxSideLength,
boxSideLength,
);
const renderState = Cesium.RenderState.fromCache({
depthMask: false,
blending: {
enabled: true,
color: {
red: 0.0,
green: 0.0,
blue: 0.0,
alpha: 0.0,
},
},
depthTest: {
enabled: true,
func: Cesium.DepthFunction.LESS_OR_EQUAL,
},
cull: {
enabled: true,
face: Cesium.CullFace.FRONT,
},
});
const zoomMat = Cesium.Matrix4.fromScale(
new Cesium.Cartesian3(zoomScale, zoomScale, zoomScale),
new Cesium.Matrix4(),
);
Cesium.Matrix4.multiply(modelMatrix, zoomMat, modelMatrix);
const halfBox = Cesium.Cartesian3.multiplyByScalar(
boxSize,
0.5,
new Cesium.Cartesian3(),
);
const negHalfBox = Cesium.Cartesian3.negate(halfBox, new Cesium.Cartesian3());
const boxGeometry = new Cesium.BoxGeometry({
minimum: negHalfBox,
maximum: halfBox,
});
const uniforms = {
base: new Cesium.Color(0.1912, 0.2542, 0.3515, 0),
map: texture3D,
opacity: 0.25,
range: 0.1,
steps: 100,
frame: 0,
threshold: 0.25,
};
window.uniforms = uniforms;
const cmdUniforms = {};
for (const key in uniforms) {
if (key) {
cmdUniforms[key] = function () {
return uniforms[key];
};
}
}
const primitive = new GeometryPrimitive(boxGeometry, {
uniformMap: cmdUniforms,
vertexShaderSource: vertexShader,
fragmentShaderSource: fragmentShader,
renderState: renderState,
modelMatrix,
pass: Cesium.Pass.TRANSLUCENT,
});
viewer.scene.primitives.add(primitive);
const cameraState = {
destination: centerPoint,
orientation: {
heading: 4.159717744111784,
pitch: -0.4648127266675117,
roll: boxSideLength * zoomScale * 2,
},
duration: 0,
};
// viewer.camera.flyTo(camState);
const hpr = new Cesium.HeadingPitchRange(
cameraState.orientation.heading,
cameraState.orientation.pitch,
boxSideLength * zoomScale * 2,
);
viewer.camera.lookAt(cameraState.destination, hpr);
// create params control UI
function createSlider(
toolbar,
{ labelText, min, max, step, defaultValue, callback },
) {
const container = document.createElement("div");
container.style.cssText = `
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
`;
const label = document.createElement("label");
label.textContent = labelText;
label.style.cssText = `
color: #ffffff;
font-family: Arial, sans-serif;
min-width: 60px;
font-size: 14px;
`;
const slider = document.createElement("input");
slider.type = "range";
slider.min = min;
slider.max = max;
slider.step = step;
slider.value = defaultValue;
slider.style.cssText = `
width: 100px;
cursor: pointer;
accent-color: #4CAF50;
`;
slider.addEventListener("input", (e) => callback(e.target.value));
const valueDisplay = document.createElement("span");
valueDisplay.textContent = defaultValue;
valueDisplay.style.cssText = `
color: #ffffff;
font-family: monospace;
width: 30px;
text-align: right;
`;
slider.addEventListener("input", (e) => {
valueDisplay.textContent = parseFloat(e.target.value).toFixed(2);
});
container.appendChild(label);
container.appendChild(slider);
container.appendChild(valueDisplay);
toolbar.appendChild(container);
return slider;
}
function createUi() {
const toolbar = document.getElementById("toolbar");
toolbar.style.cssText = `
position: fixed;
top: 10px;
left: 10px;
padding: 15px 20px 5px 20px;
background: rgba(40, 40, 40, 0.85);
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(5px);
z-index: 1000;
`;
createSlider(toolbar, {
labelText: "opacity",
min: 0,
max: 1,
step: 0.01,
defaultValue: uniforms.opacity,
callback: (val) => {
uniforms.opacity = parseFloat(val);
},
});
createSlider(toolbar, {
labelText: "range",
min: 0,
max: 1,
step: 0.01,
defaultValue: uniforms.range,
callback: (val) => {
uniforms.range = parseFloat(val);
},
});
createSlider(toolbar, {
labelText: "steps",
min: 20,
max: 200,
step: 1,
defaultValue: uniforms.steps,
callback: (val) => {
uniforms.steps = parseFloat(val);
},
});
createSlider(toolbar, {
labelText: "threshold",
min: 0,
max: 1,
step: 0.01,
defaultValue: uniforms.threshold,
callback: (val) => {
uniforms.threshold = parseFloat(val);
},
});
}
createUi();
//Sandcastle_End
};
if (typeof Cesium !== "undefined") {
window.startupCalled = true;
window.startup(Cesium).catch((error) => {
"use strict";
console.error(error);
});
Sandcastle.finishedLoading();
}
</script>
</body>
</html>