--- name: cesiumjs-custom-shader description: "CustomShader authoring — vertexShaderText and fragmentShaderText against VertexInput, FragmentInput, FeatureIds, Metadata, czm_modelMaterial. Use when reading EXT_mesh_features or EXT_structural_metadata property textures/tables, vertex displacement, or shading VoxelPrimitive." --- # CesiumJS CustomShader Version baseline: CesiumJS 1.144. All imports use ES module style. `CustomShader` injects user GLSL into the `Model` / `Cesium3DTileset` / `VoxelPrimitive` rendering pipeline. It exposes glTF attributes, feature IDs, and `EXT_structural_metadata` to per-vertex and per-fragment code, and returns values through the built-in `czm_modelVertexOutput` and `czm_modelMaterial` structs. Use this skill for **writing the shader body**. Use: - `cesiumjs-materials-shaders` — for Fabric `Material`, `ImageBasedLighting`, `PostProcessStage` (bloom, SSAO, FXAA, tonemapping). - `cesiumjs-3d-tiles` — for declarative per-feature coloring via `Cesium3DTileStyle`, and for `VoxelPrimitive` setup/configuration. - `cesiumjs-models-particles` — for `Model.fromGltfAsync`, animations, `ModelFeature.getProperty()`. ## Out of scope - **Fabric `Material`** for entity polylines/polygons/walls — see `cesiumjs-materials-shaders`. - **`PostProcessStage`** screen-space effects — see `cesiumjs-materials-shaders`. - **`ImageBasedLighting`** — see `cesiumjs-materials-shaders`. - **`Cesium3DTileStyle`** declarative JSON styling — see `cesiumjs-3d-tiles`. **Do not combine with CustomShader on the same tileset.** - **Authoring `EXT_structural_metadata` / `EXT_mesh_features` in glTF** — tooling concern, not runtime. ## Minimal example ```js import { CustomShader, Model } from "cesium"; const shader = new CustomShader({ fragmentShaderText: ` void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material) { material.diffuse = vec3(1.0, 0.0, 0.0); } `, }); const model = await Model.fromGltfAsync({ url: "./aircraft.glb", customShader: shader }); viewer.scene.primitives.add(model); ``` > **Note:** Writing `material.alpha` requires `translucencyMode: CustomShaderTranslucencyMode.TRANSLUCENT` — see "Translucency" below. On opaque models with the default `INHERIT` mode, alpha writes are silently ignored. > **Visual eval framing:** after adding a shadered `Model`, wait until the model > is ready enough to render, then frame a known target point explicitly. Do not > rely on the first post-load frame or on a tiny default-scale model. For public > model evals, set `minimumPixelSize` (usually 256-400), use a moderate `scale`, > and add a silhouette or strong color tint when the shader effect is the thing > being judged. ## Applying a CustomShader **Model** — constructor option or mutable property: ```js const model = await Model.fromGltfAsync({ url, customShader }); model.customShader = newShader; // hot-swap model.customShader = undefined; // clear ``` **Cesium3DTileset** — constructor option or mutable property. Only `Model`-backed tile content is affected (not native I3S or other formats): ```js const tileset = await Cesium3DTileset.fromUrl(url, { customShader }); tileset.customShader = newShader; ``` > Per the `Cesium3DTileset.customShader` JSDoc: *"Using custom shaders with a `Cesium3DTileStyle` may lead to undefined behavior."* The property is also marked `@experimental` — it uses 3D Tiles spec surface that is not final and may change without Cesium's standard deprecation policy. **VoxelPrimitive** — fragment-only subset (see "VoxelPrimitive shader subset" below): ```js const voxelPrimitive = new VoxelPrimitive({ provider, customShader }); ``` The engine calls `customShader.update(frameState)` automatically each frame. When finished with a CustomShader, call `customShader.destroy()` to release GPU texture resources owned by its `TextureManager`. ## Constructor reference ```js new CustomShader({ mode, // CustomShaderMode — default MODIFY_MATERIAL lightingModel, // LightingModel — if omitted, model's default is preserved translucencyMode, // CustomShaderTranslucencyMode — default INHERIT uniforms, // { [name]: { type: UniformType, value } } — default {} varyings, // { [name]: VaryingType } — default {} vertexShaderText, // string or undefined fragmentShaderText, // string or undefined }); ``` Either `vertexShaderText` or `fragmentShaderText` is typically required. See `REFERENCE.md` for exhaustive enum values. ## Shader function signatures The runtime calls these from generated pipeline stages. Parameter names are part of the contract — renaming them breaks the shader. ```glsl void vertexMain(VertexInput vsInput, inout czm_modelVertexOutput vsOutput) { ... } void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material) { ... } ``` ## Uniforms Declare uniforms with `{ type, value }`. The type is a `UniformType` value; the JS value type must match (e.g. `VEC3` → `Cartesian3`). Uniforms are accessible in GLSL by their declared name. ```js import { CustomShader, UniformType, TextureUniform, Cartesian3 } from "cesium"; const shader = new CustomShader({ uniforms: { u_tint: { type: UniformType.VEC3, value: new Cartesian3(1.0, 0.5, 0.2) }, u_time: { type: UniformType.FLOAT, value: 0.0 }, u_detail: { type: UniformType.SAMPLER_2D, value: new TextureUniform({ url: "./detail.png", repeat: true }) }, }, fragmentShaderText: ` void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material) { vec3 d = texture(u_detail, fsInput.attributes.texCoord_0).rgb; material.diffuse = mix(material.diffuse, u_tint, d.r + 0.1 * sin(u_time)); } `, }); // Update at runtime. For Cartesian/Matrix values, setUniform clones into existing storage. shader.setUniform("u_time", performance.now() / 1000); ``` `TextureUniform` accepts either `url` (string or `Resource`) or `typedArray` + `width` + `height` — **exactly one** (constructor throws otherwise). Other options: `repeat` (default `true`), `pixelFormat`, `pixelDatatype`, `minificationFilter`, `magnificationFilter`, `maximumAnisotropy`. **`SAMPLER_CUBE` is declared on `UniformType` but rejected at construction** — throws `DeveloperError("CustomShader does not support samplerCube uniforms")`. Only `SAMPLER_2D` is supported. ## Varyings Declared varyings are emitted as `out ` in the vertex shader and `in ` in the fragment shader. Write in vertex, read in fragment. ```js import { CustomShader, VaryingType } from "cesium"; const shader = new CustomShader({ varyings: { v_worldHeight: VaryingType.FLOAT }, vertexShaderText: ` void vertexMain(VertexInput vsInput, inout czm_modelVertexOutput vsOutput) { v_worldHeight = vsInput.attributes.positionMC.z; } `, fragmentShaderText: ` void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material) { float t = clamp(v_worldHeight / 100.0, 0.0, 1.0); material.diffuse = mix(vec3(0.2,0.4,0.8), vec3(1.0,0.8,0.2), t); } `, }); ``` `VaryingType` supports `FLOAT`, `VEC2`–`VEC4`, `MAT2`–`MAT4`. No `INT`/`BOOL`/`SAMPLER` variants. ## Modes & lighting **`CustomShaderMode`:** - `MODIFY_MATERIAL` (default) — runs after the material stage, before lighting. `czm_modelMaterial` is populated with PBR/texture results; the shader refines them. - `REPLACE_MATERIAL` — skips the material stage entirely. Shader sets every field procedurally. Cheaper when the source material is not needed. **`LightingModel`:** - `UNLIT` — skip lighting; `material.diffuse` becomes the final color (alpha still applied). Flat-shaded. - `PBR` — physically-based with IBL when available. - *Omitted* — preserves the model's default lighting. Omit unless overriding intentionally. Pair `REPLACE_MATERIAL` + `UNLIT` for pure procedural flat shading (no material sampling, no lighting). ## Translucency `CustomShaderTranslucencyMode` governs how alpha writes interact with the render pass: - `INHERIT` (default) — alpha is only honored if the source material is translucent. - `OPAQUE` — force opaque pass. - `TRANSLUCENT` — force translucent pass. **Pitfall:** writing `material.alpha` on an opaque model with `INHERIT` silently does nothing. Set `translucencyMode: CustomShaderTranslucencyMode.TRANSLUCENT` to make alpha writes effective. ```js import { CustomShader, CustomShaderTranslucencyMode } from "cesium"; const shader = new CustomShader({ translucencyMode: CustomShaderTranslucencyMode.TRANSLUCENT, fragmentShaderText: ` void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material) { material.diffuse = vec3(0.2, 0.6, 1.0); material.alpha = 0.5; // honored because translucencyMode is TRANSLUCENT } `, }); ``` See `examples/04-translucent-override.js`. ## Attributes `vsInput.attributes` and `fsInput.attributes` expose glTF vertex attributes. Names are case-sensitive and coordinate-space suffixes are required — the constructor rejects bare `position`/`normal`/`tangent`/`bitangent`. Common fields (full table in `REFERENCE.md`): - `positionMC` — model coords, valid in VS and FS - `positionWC` — world (ECEF) coords, **fragment only**, low-precision - `positionEC` — eye coords, **fragment only** - `normalMC` / `normalEC` — vertex / fragment - `tangentMC` / `tangentEC`, `bitangentMC` / `bitangentEC` - `texCoord_N`, `color_N`, `joints_N`, `weights_N` > **Coordinate-space validation.** The constructor scans shader text and throws `DeveloperError(" is not available in the shader. Did you mean instead?")` for invalid combinations. Examples: `positionEC` in vertex, `normalMC` in fragment. Custom underscore-prefixed glTF attributes (`_FEATURE_ID_0`, `_SURFACE_TEMP`) are lowercased and un-prefixed: `fsInput.attributes.surface_temp`. ## FeatureIds `vsInput.featureIds` / `fsInput.featureIds` unify three glTF sources into one struct: - `featureId_N` — feature ID attributes and implicit attributes from `EXT_mesh_features` (N is the array index in the primitive's `featureIds` array). Also covers feature ID **textures**, which are fragment-shader-only. - `instanceFeatureId_N` — per-instance feature IDs from `EXT_instance_features` + `EXT_mesh_gpu_instancing`. - Named aliases — if glTF specifies `"label": "perVertex"`, then `featureIds.perVertex` is also available. - Legacy 3D Tiles 1.0 `BATCH_ID` / `_BATCHID` → transparently renamed to `featureId_0`. GLSL type is always `int`. WebGL 1 loses precision above 2^24. ```glsl void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material) { int id = fsInput.featureIds.featureId_0; if (id == 0) material.diffuse = vec3(1.0, 0.2, 0.2); // roof else if (id == 1) material.diffuse = vec3(0.2, 0.8, 0.2); // wall } ``` See `examples/03-feature-id-tileset.js`. ## Metadata `EXT_structural_metadata` surfaces three source types (all addressable from shaders as of 1.139): - **Property attributes** — per-vertex. Vertex and fragment shaders. - **Property textures** — per-texel. **Fragment only.** - **Property tables** — per-feature, keyed by feature ID. **Added in 1.139 (#13124).** ```glsl void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material) { float t = fsInput.metadata.temperature; float tMin = fsInput.metadataStatistics.temperature.minValue; float tMax = fsInput.metadataStatistics.temperature.maxValue; float v = (t - tMin) / (tMax - tMin); material.diffuse = vec3(v, 0.0, 1.0 - v); } ``` **Property ID sanitization** (GLSL identifier rules): - Non-alphanumeric runs → single `_` (`temperature ℃` → `temperature_`) - Leading `gl_` stripped (`gl_custom` → `custom`) - Leading digit prefixed with `_` (`12345` → `_12345`) - Post-sanitization collisions → undefined behavior **Sibling structs:** `metadataClass..noData | defaultValue | minValue | maxValue` (class-schema bounds) and `metadataStatistics..minValue | maxValue | mean | median | standardDeviation | variance | sum` (populated only when `tileset.json` declares `statistics`). > **1.139 breaking change (#13135):** unsigned integer metadata is no longer cast to signed int. Assigning a `UINT` property to a GLSL `int` (`int x = fsInput.metadata.myUint;`) no longer compiles. Use the matching unsigned type. **Public assets without `EXT_structural_metadata` on a `.glb` are scarce** — most real-world metadata lives on 3D Tiles. See `examples/06-metadata-ramp.js` (Cesium3DTileset target). ## czm_modelVertexOutput & czm_modelMaterial **`czm_modelVertexOutput`** (vertex shader's `inout vsOutput`): ```glsl struct czm_modelVertexOutput { vec3 positionMC; // initialized to vsInput.attributes.positionMC float pointSize; // overrides gl_PointSize and Cesium3DTileStyle point sizing }; ``` > **Gotcha:** mutating `positionMC` displaces vertices but does **not** update the primitive's bounding sphere. Heavily displaced vertices can be frustum-culled. **`czm_modelMaterial`** (fragment shader's `inout material`). All colors are linear RGB. Conditional fields `specularWeight` / `anisotropic*` / `clearcoatFactor`... appear only when `KHR_materials_specular` / `_anisotropy` / `_clearcoat` is active on the primitive (see `REFERENCE.md` for the full `#ifdef`-guarded struct): ```glsl struct czm_modelMaterial { vec4 baseColor; vec3 diffuse; float alpha; vec3 specular; float roughness; vec3 normalEC; float occlusion; vec3 emissive; // + conditional PBR extension fields }; ``` ## Built-in `czm_*` automatic uniforms Available without declaration. Most useful for CustomShader: `czm_frameNumber`, `czm_pi`, `czm_model`, `czm_view`, `czm_projection`, `czm_modelView`, `czm_normal`, `czm_lightDirectionEC`, `czm_sunDirectionWC`, `czm_eyeHeight`, `czm_sceneMode`, `czm_viewerPositionWC`, `czm_splitPosition`. Full catalog in `REFERENCE.md`. ## VoxelPrimitive shader subset Fragment-only. Executes at every raymarching step along the view ray; the final pixel composites all steps. Supplying `vertexShaderText` is silently ignored. Reduced struct availability: - `fsInput.attributes` — only `positionEC` and `normalEC`. - `fsInput.featureIds` — **not present**. - `fsInput.metadata` — fully supported. - `fsInput.metadataClass` — **not present**. - `fsInput.metadataStatistics` — only `minValue` and `maxValue`. Assigning `customShader = undefined` falls back to `VoxelPrimitive.DefaultCustomShader`. See `examples/07-voxel-shader.js`. For `VoxelPrimitive` setup (provider, shape, modelMatrix, nearestSampling), see `cesiumjs-3d-tiles`. > **1.130 breaking change (#12636):** `fsInput.voxel.positionUv | positionShapeUv | positionLocal` were removed. Use `fsInput.attributes.positionEC` instead. `fsInput.voxel.surfaceNormal` → `fsInput.attributes.normalEC`. > **1.142 fix (#13517):** the built-in default voxel shader handles common metadata types more robustly. Keep a custom shader only when you need explicit classification, coloring, filtering, or raymarch-step logic. ## Common patterns | File | Target | Demonstrates | |---|---|---| | `examples/01-diffuse-tint.js` | Model | Time uniform driving `material.diffuse` | | `examples/02-texture-swap.js` | Model | `TextureUniform`, `SAMPLER_2D`, `setUniform` | | `examples/03-feature-id-tileset.js` | Cesium3DTileset | `fsInput.featureIds.featureId_0` classification coloring | | `examples/04-translucent-override.js` | Model (opaque source) | `CustomShaderTranslucencyMode.TRANSLUCENT` | | `examples/05-vertex-displacement.js` | Model | `vsOutput.positionMC` + normal offset | | `examples/06-metadata-ramp.js` | Cesium3DTileset | `fsInput.metadata.` + `metadataStatistics` normalization | | `examples/07-voxel-shader.js` | VoxelPrimitive | FS-only subset, per-voxel metadata | ## CesiumJS 1.139-1.142 version notes Verbatim from upstream `CHANGES.md`: **Breaking (1.139, #13135):** > Custom Shaders that rely on metadata derived from the `EXT_structural_metadata` extension no longer cast unsigned integer metadata types to signed integers. Any existing custom shaders that assign UINT-type metadata to local integers (e.g. `int myMetadata = vsInput.metadata.myUintMetadata`) will no longer compile. **Breaking (1.139, #13170):** Fixed precision of point cloud attributes when accessed in a custom fragment shader. **Addition (1.139, #13124):** Access to metadata from property tables (previously only attributes/textures). **Addition (1.139, #13135):** More metadata types via property textures. **Fix (1.139, #13231):** Metadata variable regex extended to `metadataClass` and `metadataStatistics`. **Fix (1.139.1, #13247):** NGA-GPM local extension + custom shader regression fix. **Fix (1.140, #13258):** Custom shaders are no longer disabled for primitives with missing metadata when the metadata exists on the class definition. **Addition (1.140, #13323):** Limited double-precision metadata support via downcasting. **Fix (1.142, #13517):** Improved default voxel shader for common metadata types. **Breaking (1.130, #12636):** Voxel `FragmentInput` restructured (see VoxelPrimitive section). ## Gotchas & pitfalls 1. **Coordinate-space suffix required.** `positionEC` in vertex, `normalMC` in fragment, or any bare `position`/`normal`/`tangent`/`bitangent` throws `DeveloperError` at construction with a suggested alternative. 2. **`positionMC` is valid in fragment shader** — despite the `MC` suffix. Only `normalMC`/`tangentMC`/`bitangentMC` are FS-rejected. 3. **Vertex displacement does not update bounding sphere.** Displaced vertices may be frustum-culled unexpectedly. 4. **`Cesium3DTileStyle` + CustomShader = undefined behavior.** Per upstream JSDoc. Choose one per tileset. 5. **`SAMPLER_CUBE` rejected at construction.** Use `SAMPLER_2D` only. 6. **Parameter-name contract.** `vsInput`, `vsOutput`, `fsInput`, `material` are scanned by regex — renaming breaks codegen. 7. **`TextureUniform` URL-vs-typedArray XOR.** Supplying both or neither throws. `typedArray` requires `width` + `height`. 8. **Alpha writes on opaque models are silently ignored under `INHERIT`.** Set `translucencyMode: CustomShaderTranslucencyMode.TRANSLUCENT` — do not just write `material.alpha` and expect it to work. 9. **`customShader.destroy()` required.** Call when disposing of a shader that holds texture uniforms — otherwise its `TextureManager` leaks GPU resources. 10. **`vsOutput.pointSize` overrides `Cesium3DTileStyle` point sizing.** Don't set it unless intended. 11. **Metadata property IDs are sanitized.** Non-alphanumeric → `_`; leading `gl_` stripped; collisions are undefined behavior. 12. **UINT metadata now preserves signedness (1.139+, #13135).** Use `uint x = fsInput.metadata.prop;`, not `int x = ...`. 13. **`customShader` on `Cesium3DTileset` is `@experimental`.** May change without Cesium's standard deprecation policy. 14. **Tilesets: only `Model`-backed tile content uses CustomShader.** Native I3S and other formats are unaffected. ## Performance tips - `REPLACE_MATERIAL` skips the material stage (textures, PBR inputs not sampled). - `LightingModel.UNLIT` skips lighting math — combine with `REPLACE_MATERIAL` for flat procedural shading. - Write to varyings in vertex rather than recomputing per-fragment. - Avoid per-frame `setUniform` of `SAMPLER_2D` — it triggers async texture reload. Use `url` once and hold the reference. - Call `customShader.destroy()` on teardown to release GPU texture resources. ## See also - **`REFERENCE.md`** — full struct tables (`Attributes`, `FeatureIds`, `Metadata`/`Class`/`Statistics`), enum value tables, built-in `czm_*` uniform catalog, coordinate-space validation error reference. - **`examples/`** — seven compile-tested snippets. `examples/_sandcastle-template.html` is the internal scaffold; `examples/README.md` documents the layout. - **`cesiumjs-materials-shaders`** — Fabric `Material`, `ImageBasedLighting`, `PostProcessStage`. - **`cesiumjs-3d-tiles`** — `Cesium3DTileStyle`, `Cesium3DTileset` setup, `VoxelPrimitive` instantiation. - **`cesiumjs-models-particles`** — `Model.fromGltfAsync`, `ModelFeature.getProperty()`, animations. - **`cesiumjs-primitives`** — Fabric on Appearances for classic Primitive geometry. - **CesiumJS Custom Shader Guide** — `Documentation/CustomShaderGuide/README.md` on `CesiumGS/cesium` `main`.