@cesium/engine
Version:
CesiumJS is a JavaScript library for creating 3D globes and 2D maps in a web browser without a plugin.
709 lines (628 loc) • 20.9 kB
JavaScript
import { MetadataComponentType } from "@cesium/engine";
import defined from "../Core/defined.js";
import DrawCommand from "../Renderer/DrawCommand.js";
import RenderState from "../Renderer/RenderState.js";
import ShaderSource from "../Renderer/ShaderSource.js";
import MetadataType from "./MetadataType.js";
import MetadataPickingPipelineStage from "./Model/MetadataPickingPipelineStage.js";
/**
* @private
*/
function DerivedCommand() {}
const fragDepthRegex = /\bgl_FragDepth\b/;
const discardRegex = /\bdiscard\b/;
function getDepthOnlyShaderProgram(context, shaderProgram) {
const cachedShader = context.shaderCache.getDerivedShaderProgram(
shaderProgram,
"depthOnly",
);
if (defined(cachedShader)) {
return cachedShader;
}
let fs = shaderProgram.fragmentShaderSource;
let writesDepthOrDiscards = false;
const sources = fs.sources;
for (let i = 0; i < sources.length; ++i) {
if (fragDepthRegex.test(sources[i]) || discardRegex.test(sources[i])) {
writesDepthOrDiscards = true;
break;
}
}
const usesLogDepth = fs.defines.indexOf("LOG_DEPTH") >= 0;
if (!writesDepthOrDiscards && !usesLogDepth) {
const source = `void main()
{
out_FragColor = vec4(1.0);
}
`;
fs = new ShaderSource({
sources: [source],
});
} else if (!writesDepthOrDiscards && usesLogDepth) {
const source = `void main()
{
out_FragColor = vec4(1.0);
czm_writeLogDepth();
}
`;
fs = new ShaderSource({
defines: ["LOG_DEPTH"],
sources: [source],
});
}
return context.shaderCache.createDerivedShaderProgram(
shaderProgram,
"depthOnly",
{
vertexShaderSource: shaderProgram.vertexShaderSource,
fragmentShaderSource: fs,
attributeLocations: shaderProgram._attributeLocations,
},
);
}
function getDepthOnlyRenderState(scene, renderState) {
const cache = scene._depthOnlyRenderStateCache;
const cachedDepthOnlyState = cache[renderState.id];
if (defined(cachedDepthOnlyState)) {
return cachedDepthOnlyState;
}
const rs = RenderState.getState(renderState);
rs.depthMask = true;
rs.colorMask = {
red: false,
green: false,
blue: false,
alpha: false,
};
const depthOnlyState = RenderState.fromCache(rs);
cache[renderState.id] = depthOnlyState;
return depthOnlyState;
}
DerivedCommand.createDepthOnlyDerivedCommand = function (
scene,
command,
context,
result,
) {
// For a depth only pass, we bind a framebuffer with only a depth attachment (no color attachments),
// do not write color, and write depth. If the fragment shader doesn't modify the fragment depth
// or discard, the driver can replace the fragment shader with a pass-through shader. We're unsure if this
// actually happens so we modify the shader to use a pass-through fragment shader.
if (!defined(result)) {
result = {};
}
const shader = result.depthOnlyCommand?.shaderProgram;
const renderState = result.depthOnlyCommand?.renderState;
result.depthOnlyCommand = DrawCommand.shallowClone(
command,
result.depthOnlyCommand,
);
if (!defined(shader) || result.shaderProgramId !== command.shaderProgram.id) {
result.depthOnlyCommand.shaderProgram = getDepthOnlyShaderProgram(
context,
command.shaderProgram,
);
result.depthOnlyCommand.renderState = getDepthOnlyRenderState(
scene,
command.renderState,
);
result.shaderProgramId = command.shaderProgram.id;
} else {
result.depthOnlyCommand.shaderProgram = shader;
result.depthOnlyCommand.renderState = renderState;
}
return result;
};
const writeLogDepthRegex = /\s+czm_writeLogDepth\(/;
const vertexlogDepthRegex = /\s+czm_vertexLogDepth\(/;
function getLogDepthShaderProgram(context, shaderProgram) {
const disableLogDepthWrite =
shaderProgram.fragmentShaderSource.defines.indexOf("LOG_DEPTH_READ_ONLY") >=
0;
if (disableLogDepthWrite) {
return shaderProgram;
}
const cachedShader = context.shaderCache.getDerivedShaderProgram(
shaderProgram,
"logDepth",
);
if (defined(cachedShader)) {
return cachedShader;
}
const attributeLocations = shaderProgram._attributeLocations;
const vs = shaderProgram.vertexShaderSource.clone();
const fs = shaderProgram.fragmentShaderSource.clone();
vs.defines = defined(vs.defines) ? vs.defines.slice(0) : [];
vs.defines.push("LOG_DEPTH");
fs.defines = defined(fs.defines) ? fs.defines.slice(0) : [];
fs.defines.push("LOG_DEPTH");
let writesLogDepth = false;
let sources = vs.sources;
for (let i = 0; i < sources.length; ++i) {
if (vertexlogDepthRegex.test(sources[i])) {
writesLogDepth = true;
break;
}
}
if (!writesLogDepth) {
for (let i = 0; i < sources.length; ++i) {
sources[i] = ShaderSource.replaceMain(sources[i], "czm_log_depth_main");
}
const logMain = `
void main()
{
czm_log_depth_main();
czm_vertexLogDepth();
}
`;
sources.push(logMain);
}
sources = fs.sources;
writesLogDepth = false;
for (let i = 0; i < sources.length; ++i) {
if (writeLogDepthRegex.test(sources[i])) {
writesLogDepth = true;
}
}
// This define indicates that a log depth value is written by the shader but doesn't use czm_writeLogDepth.
if (fs.defines.indexOf("LOG_DEPTH_WRITE") !== -1) {
writesLogDepth = true;
}
let logSource = "";
if (!writesLogDepth) {
for (let i = 0; i < sources.length; i++) {
sources[i] = ShaderSource.replaceMain(sources[i], "czm_log_depth_main");
}
logSource = `
void main()
{
czm_log_depth_main();
czm_writeLogDepth();
}
`;
}
sources.push(logSource);
return context.shaderCache.createDerivedShaderProgram(
shaderProgram,
"logDepth",
{
vertexShaderSource: vs,
fragmentShaderSource: fs,
attributeLocations: attributeLocations,
},
);
}
DerivedCommand.createLogDepthCommand = function (command, context, result) {
if (!defined(result)) {
result = {};
}
const shader = result.command?.shaderProgram;
result.command = DrawCommand.shallowClone(command, result.command);
if (!defined(shader) || result.shaderProgramId !== command.shaderProgram.id) {
result.command.shaderProgram = getLogDepthShaderProgram(
context,
command.shaderProgram,
);
result.shaderProgramId = command.shaderProgram.id;
} else {
result.command.shaderProgram = shader;
}
return result;
};
function getPickShaderProgram(context, shaderProgram, pickId) {
const cachedShader = context.shaderCache.getDerivedShaderProgram(
shaderProgram,
"pick",
);
if (defined(cachedShader)) {
return cachedShader;
}
const attributeLocations = shaderProgram._attributeLocations;
const { sources, defines } = shaderProgram.fragmentShaderSource;
const hasFragData = sources.some((source) => source.includes("out_FragData"));
const outputColorVariable = hasFragData ? "out_FragData_0" : "out_FragColor";
const newMain = `void main ()
{
czm_non_pick_main();
if (${outputColorVariable}.a == 0.0) {
discard;
}
${outputColorVariable} = ${pickId};
} `;
const length = sources.length;
const newSources = new Array(length + 1);
for (let i = 0; i < length; ++i) {
newSources[i] = ShaderSource.replaceMain(sources[i], "czm_non_pick_main");
}
newSources[length] = newMain;
const fragmentShaderSource = new ShaderSource({
sources: newSources,
defines: defines,
});
return context.shaderCache.createDerivedShaderProgram(shaderProgram, "pick", {
vertexShaderSource: shaderProgram.vertexShaderSource,
fragmentShaderSource: fragmentShaderSource,
attributeLocations: attributeLocations,
});
}
function getPickRenderState(scene, renderState) {
const cache = scene.picking.pickRenderStateCache;
const cachedPickState = cache[renderState.id];
if (defined(cachedPickState)) {
return cachedPickState;
}
const rs = RenderState.getState(renderState);
rs.blending.enabled = false;
// Turns on depth writing for opaque and translucent passes
// Overlapping translucent geometry on the globe surface may exhibit z-fighting
// during the pick pass which may not match the rendered scene. Once
// terrain is on by default and ground primitives are used instead
// this will become less of a problem.
rs.depthMask = true;
const pickState = RenderState.fromCache(rs);
cache[renderState.id] = pickState;
return pickState;
}
DerivedCommand.createPickDerivedCommand = function (
scene,
command,
context,
result,
) {
if (!defined(result)) {
result = {};
}
const shader = result.pickCommand?.shaderProgram;
const renderState = result.pickCommand?.renderState;
result.pickCommand = DrawCommand.shallowClone(command, result.pickCommand);
if (!defined(shader) || result.shaderProgramId !== command.shaderProgram.id) {
result.pickCommand.shaderProgram = getPickShaderProgram(
context,
command.shaderProgram,
command.pickId,
);
result.pickCommand.renderState = getPickRenderState(
scene,
command.renderState,
);
result.shaderProgramId = command.shaderProgram.id;
} else {
result.pickCommand.shaderProgram = shader;
result.pickCommand.renderState = renderState;
}
return result;
};
/**
* Replaces the value of the specified 'define' directive identifier
* with the given value.
*
* The given defines are the parts of the define directives that are
* stored in the `ShaderSource`. For example, the defines may be
* `["EXAMPLE", "EXAMPLE_VALUE 123"]`
*
* Calling `replaceDefine(defines, "EXAMPLE", 999)` will result in
* the defines being
* `["EXAMPLE 999", "EXAMPLE_VALUE 123"]`
*
* @param {string[]} defines The define directive identifiers
* @param {string} defineName The name (identifier) of the define directive
* @param {any} newDefineValue The new value whose string representation
* will become the token string for the define directive
* @private
*/
function replaceDefine(defines, defineName, newDefineValue) {
const n = defines.length;
for (let i = 0; i < n; i++) {
const define = defines[i];
const tokens = define.trimStart().split(/\s+/);
if (tokens[0] === defineName) {
defines[i] = `${defineName} ${newDefineValue}`;
}
}
}
/**
* Returns the component count for the given class property, or
* its array length if it is an array.
*
* This will be
* `[1, 2, 3, 4]` for `[SCALAR, VEC2, VEC3, VEC4`] types,
* or the array length if it is an array.
*
* @param {MetadataClassProperty} classProperty The class property
* @returns {number} The component count
* @private
*/
function getComponentCount(classProperty) {
if (!classProperty.isArray) {
return MetadataType.getComponentCount(classProperty.type);
}
return classProperty.arrayLength;
}
/**
* Returns the type that the given class property has in a GLSL shader.
*
* It returns the same string as `PropertyTextureProperty.prototype.getGlslType`
* for a property texture property with the given class property
*
* @param {MetadataClassProperty} classProperty The class property
* @returns {string} The GLSL shader type string for the property
*/
function getGlslType(classProperty) {
const componentCount = getComponentCount(classProperty);
if (classProperty.normalized) {
if (componentCount === 1) {
return "float";
}
return `vec${componentCount}`;
}
if (componentCount === 1) {
return "int";
}
return `ivec${componentCount}`;
}
/**
* Returns a shader statement that applies the inverse of the
* value transform to the given value, based on the given offset
* and scale.
*
* @param {string} input The input value
* @param {string} offset The offset
* @param {string} scale The scale
* @returns {string} The statement
*/
function unapplyValueTransform(input, offset, scale) {
return `((${input} - float(${offset})) / float(${scale}))`;
}
/**
* Returns a shader statement that applies the inverse of the
* normalization, based on the given component type
*
* @param {string} input The input value
* @param {string} componentType The component type
* @returns {string} The statement
*/
function unnormalize(input, componentType) {
const max = MetadataComponentType.getMaximum(componentType);
return `(${input}) / float(${max})`;
}
/**
* Creates a shader statement that returns the value of the specified
* property, normalized to the range [0, 1].
*
* @param {MetadataClassProperty} classProperty The class property
* @param {object} metadataProperty The metadata property, either
* a `PropertyTextureProperty` or a `PropertyAttributeProperty`
* @returns {string} The string
*/
function getSourceValueStringScalar(classProperty, metadataProperty) {
let result = `float(value)`;
// The 'hasValueTransform' indicates whether the property
// (or its class property) did define an 'offset' or 'scale'.
// Even when they had not been defined in the JSON, they are
// defined in the object, with default values.
if (metadataProperty.hasValueTransform) {
const offset = metadataProperty.offset;
const scale = metadataProperty.scale;
result = unapplyValueTransform(result, offset, scale);
}
if (!classProperty.normalized) {
result = unnormalize(result, classProperty.componentType);
}
return result;
}
/**
* Creates a shader statement that returns the value of the specified
* component of the given property, normalized to the range [0, 1].
*
* @param {MetadataClassProperty} classProperty The class property
* @param {object} metadataProperty The metadata property, either
* a `PropertyTextureProperty` or a `PropertyAttributeProperty`
* @param {string} componentName The name, in ["x", "y", "z", "w"]
* @returns {string} The string
*/
function getSourceValueStringComponent(
classProperty,
metadataProperty,
componentName,
) {
const valueString = `value.${componentName}`;
let result = `float(${valueString})`;
// The 'hasValueTransform' indicates whether the property
// (or its class property) did define an 'offset' or 'scale'.
// Even when they had not been defined in the JSON, they are
// defined in the object, with default values
// Note that in the 'PropertyTextureProperty' and the
// 'PropertyAttributeProperty', these values are
// stored as "object types" (like 'Cartesian2'), whereas
// in the 'MetadataClassProperty', they are stored as
// "array types", e.g. a `[number, number]`
if (metadataProperty.hasValueTransform) {
const offset = metadataProperty.offset[componentName];
const scale = metadataProperty.scale[componentName];
result = unapplyValueTransform(result, offset, scale);
}
if (!classProperty.normalized) {
result = unnormalize(result, classProperty.componentType);
}
return result;
}
/**
* Creates a new `ShaderProgram` from the given input that renders metadata
* values into the frame buffer, according to the given picked metadata info.
*
* This will update the `defines` of the fragment shader of the given shader
* program, by setting `METADATA_PICKING_ENABLED`, and updating the
* `METADATA_PICKING_VALUE_*` defines so that they reflect the components
* of the metadata that should be written into the RGBA (vec4) that
* ends up as the 'color' in the frame buffer.
*
* The RGBA values will eventually be converted back into an actual metadata
* value in `Picking.js`, by calling `MetadataPicking.decodeMetadataValues`.
*
* @param {Context} context The context
* @param {ShaderProgram} shaderProgram The shader program
* @param {PickedMetadataInfo} pickedMetadataInfo The picked metadata info
* @returns {ShaderProgram} The new shader program
* @private
*/
function getPickMetadataShaderProgram(
context,
shaderProgram,
pickedMetadataInfo,
) {
const schemaId = pickedMetadataInfo.schemaId;
const className = pickedMetadataInfo.className;
const propertyName = pickedMetadataInfo.propertyName;
const keyword = `pickMetadata-${schemaId}-${className}-${propertyName}`;
const shader = context.shaderCache.getDerivedShaderProgram(
shaderProgram,
keyword,
);
if (defined(shader)) {
return shader;
}
const metadataProperty = pickedMetadataInfo.metadataProperty;
const classProperty = pickedMetadataInfo.classProperty;
const glslType = getGlslType(classProperty);
// Define the components that will go into the output `metadataValues`.
// This will be the 'color' that is written into the frame buffer,
// meaning that the values should be in [0.0, 1.0], and will become
// values in [0, 255] in the frame buffer.
// By default, all of them are 0.0.
const sourceValueStrings = ["0.0", "0.0", "0.0", "0.0"];
const componentCount = getComponentCount(classProperty);
if (componentCount === 1) {
// When the property is a scalar, store the source value
// string directly in `metadataValues.x`
sourceValueStrings[0] = getSourceValueStringScalar(
classProperty,
metadataProperty,
);
} else {
// When the property is an array, store the array elements
// in `metadataValues.x/y/z/w`
const componentNames = ["x", "y", "z", "w"];
for (let i = 0; i < componentCount; i++) {
sourceValueStrings[i] = getSourceValueStringComponent(
classProperty,
metadataProperty,
componentNames[i],
);
}
}
const newDefines = shaderProgram.fragmentShaderSource.defines.slice();
newDefines.push(MetadataPickingPipelineStage.METADATA_PICKING_ENABLED);
// Replace the defines of the shader, using the type, property
// access, and value components that have been determined
replaceDefine(
newDefines,
MetadataPickingPipelineStage.METADATA_PICKING_VALUE_TYPE,
glslType,
);
replaceDefine(
newDefines,
MetadataPickingPipelineStage.METADATA_PICKING_VALUE_STRING,
`metadata.${propertyName}`,
);
replaceDefine(
newDefines,
MetadataPickingPipelineStage.METADATA_PICKING_VALUE_COMPONENT_X,
sourceValueStrings[0],
);
replaceDefine(
newDefines,
MetadataPickingPipelineStage.METADATA_PICKING_VALUE_COMPONENT_Y,
sourceValueStrings[1],
);
replaceDefine(
newDefines,
MetadataPickingPipelineStage.METADATA_PICKING_VALUE_COMPONENT_Z,
sourceValueStrings[2],
);
replaceDefine(
newDefines,
MetadataPickingPipelineStage.METADATA_PICKING_VALUE_COMPONENT_W,
sourceValueStrings[3],
);
const newFragmentShaderSource = new ShaderSource({
sources: shaderProgram.fragmentShaderSource.sources,
defines: newDefines,
});
const newShader = context.shaderCache.createDerivedShaderProgram(
shaderProgram,
keyword,
{
vertexShaderSource: shaderProgram.vertexShaderSource,
fragmentShaderSource: newFragmentShaderSource,
attributeLocations: shaderProgram._attributeLocations,
},
);
return newShader;
}
/**
* @private
*/
DerivedCommand.createPickMetadataDerivedCommand = function (
scene,
command,
context,
result,
) {
if (!defined(result)) {
result = {};
}
result.pickMetadataCommand = DrawCommand.shallowClone(
command,
result.pickMetadataCommand,
);
result.pickMetadataCommand.shaderProgram = getPickMetadataShaderProgram(
context,
command.shaderProgram,
command.pickedMetadataInfo,
);
result.pickMetadataCommand.renderState = getPickRenderState(
scene,
command.renderState,
);
result.shaderProgramId = command.shaderProgram.id;
return result;
};
function getHdrShaderProgram(context, shaderProgram) {
const cachedShader = context.shaderCache.getDerivedShaderProgram(
shaderProgram,
"HDR",
);
if (defined(cachedShader)) {
return cachedShader;
}
const attributeLocations = shaderProgram._attributeLocations;
const vs = shaderProgram.vertexShaderSource.clone();
const fs = shaderProgram.fragmentShaderSource.clone();
vs.defines = defined(vs.defines) ? vs.defines.slice(0) : [];
vs.defines.push("HDR");
fs.defines = defined(fs.defines) ? fs.defines.slice(0) : [];
fs.defines.push("HDR");
return context.shaderCache.createDerivedShaderProgram(shaderProgram, "HDR", {
vertexShaderSource: vs,
fragmentShaderSource: fs,
attributeLocations: attributeLocations,
});
}
DerivedCommand.createHdrCommand = function (command, context, result) {
if (!defined(result)) {
result = {};
}
const shader = result.command?.shaderProgram;
result.command = DrawCommand.shallowClone(command, result.command);
if (!defined(shader) || result.shaderProgramId !== command.shaderProgram.id) {
result.command.shaderProgram = getHdrShaderProgram(
context,
command.shaderProgram,
);
result.shaderProgramId = command.shaderProgram.id;
} else {
result.command.shaderProgram = shader;
}
return result;
};
export default DerivedCommand;