Custom Shaders API

Overview

HOOPS Communicator allows you to replace the built-in rendering shaders with your own custom GLSL programs on a per-node basis. This gives you full control over how specific geometry is rendered, enabling effects like custom lighting models, procedural textures, and more.

texture2D uniforms are currently experimental: the API shape is not final yet, and texture binding is not working in the current release. A follow-up release will finalize and enable this feature.

Custom shaders are applied through the Model API:

  • Model.setNodesShader() — Compiles and applies custom vertex and fragment shaders to nodes.
  • Model.setNodesShaderUniforms() — Updates uniform values on previously applied custom shaders.

API Reference

Model.setNodesShader

async setNodesShader(
  ids: NodeId[],
  vertexSource: string,
  fragmentSource: string,
  options?: SetShaderOptions,
): Promise<void>

Applies custom GLSL shader programs to the specified nodes.

Parameter Type Description
ids NodeId[] Array of node identifiers to apply the shader to
vertexSource string GLSL vertex shader source code
fragmentSource string GLSL fragment shader source code
options SetShaderOptions Optional configuration, including initial uniform values.

SetShaderOptions

interface SetShaderOptions {
    uniforms?: Record<string, UniformDescription>;
}

Model.setNodesShaderUniforms

setNodesShaderUniforms(
  ids: NodeId[],
  uniforms: Record<string, UniformDescription>,
): void

Updates uniform values for custom shaders previously applied via setNodesShader. This call is synchronous and takes effect on the next draw.


UniformDescription

Discriminated union type describing a shader uniform with its type and value(s):

type UniformDescription =
    | { type: "bool"; values: boolean; isArray?: false }
    | { type: "bool"; values: boolean[]; isArray: true }
    | { type: "bvec2" | "bvec3" | "bvec4"; values: boolean[]; isArray?: boolean }
    | { type: "int" | "uint" | "float"; values: number; isArray?: false }
    | { type: "int" | "uint" | "float"; values: number[]; isArray: true }
    | {
          type:
              | "vec2"
              | "vec3"
              | "vec4"
              | "ivec2"
              | "ivec3"
              | "ivec4"
              | "uvec2"
              | "uvec3"
              | "uvec4"
              | "mat2"
              | "mat3"
              | "mat4";
          values: number[];
          isArray?: boolean;
      }
    | { type: "texture2D"; values: ImageId; isArray?: false };

Supported uniform types: bool, int, uint, float, bvec2, bvec3, bvec4, ivec2, ivec3, ivec4, uvec2, uvec3, uvec4, vec2, vec3, vec4, mat2, mat3, mat4, texture2D (experimental, non-functional in the current release).

All types except texture2D support arrays via isArray: true.


Writing Custom Shaders

GLSL Version

The recommended shader language for custom shaders is GLSL ES 3.00. The engine targets WebGL 2 (GLSL ES 3.00) when available, falling back to WebGL 1 (GLSL ES 1.00).

Precision

  • Vertex shaders use precision highp float;
  • Fragment shaders use precision mediump float;

These are set by the engine. You can override them in your source if needed.


Engine-Provided Attributes

The engine binds vertex attributes using the tc_ prefix. All attribute names use the convention tc_a{type}_{name}.

Which attributes are actually populated for a given mesh depends on the mesh data. You can inspect this at runtime using Model.getNodeMeshData() (see Determining Available Attributes from MeshDataCopy).

GLSL Name Type Description Availability
tc_av4_vertex vec4 Vertex position (model space) Always present
tc_av3_normal vec3 Vertex normal (model space) When mesh has normals (MeshDataCopyElementGroup.hasNormals)
tc_av4_base_color vec4 Per-vertex color (RGBA) When mesh has per-vertex colors (MeshDataCopyElementGroup.hasRGBAs)
tc_av2_texture_coords vec2 Texture coordinates When mesh has UVs (MeshDataCopyElementGroup.hasUVs)
tc_af_batch_index float Batch index (non-instanced mode) Non-instanced rendering (standard path)
tc_av4_matrix_col1 vec4 Instancing matrix column 1 Instanced rendering only
tc_av4_matrix_col2 vec4 Instancing matrix column 2 Instanced rendering only
tc_av4_matrix_col3 vec4 Instancing matrix column 3 Instanced rendering only
tc_af_line_pattern_offset float Line pattern offset along line Line geometry with patterns
tc_af_line_jitter_offset float Line jitter offset Jitter rendering mode

Declaring Attributes

You must declare any engine attributes you want to use in your vertex shader:

attribute vec4 tc_av4_vertex;
attribute vec3 tc_av3_normal;
attribute vec4 tc_av4_base_color;
attribute vec2 tc_av2_texture_coords;

Attribute locations are not fixed — the engine queries them by name after linking your shader.


Engine-Provided Uniforms

The engine automatically sets built-in uniforms if your shader declares them. Any uniform whose name starts with tc_ is matched and populated by the engine. Uniforms not prefixed with tc_ are treated as custom user uniforms (controlled via SetShaderOptions.uniforms or setNodesShaderUniforms).

Transform Uniforms

GLSL Name Type Description
tc_um4_projection_matrix mat4 Projection matrix
tc_um4_view_matrix mat4 View matrix (camera)
tc_um4_model_matrix mat4 Model matrix (single-matrix mode)
tc_um3_normal_matrix mat3 Normal matrix (inverse transpose of model matrix; apply tc_um4_view_matrix separately)
tc_uv4_model_matrices vec4[] Batched model matrices (batch mode, TC_BATCH_SIZE * 3 entries)
tc_uiv2_matrix_offsets ivec2 Matrix offsets for batch mode
tc_uv3_explode_translation vec3 Explode translation offset

Important: The translation part of the view matrix (camera) is directly applied to the model matrix at CPU side before data is pushed to the GPU.

That means this translation part is in both view and model matrix in the GPU.

To avoid applying the translation twice, it’s necessary to remove this translation part from the view matrix with the following code before multiplying the model matrix:

mat4(mat3(tc_um4_view_matrix))

Viewport & Rendering

GLSL Name Type Description
tc_uv2_viewport_size vec2 Viewport dimensions in pixels
tc_ub_projection_is_ortho bool true if using orthographic projection
tc_ub_flat bool true if flat shading is active
tc_ub_auto_flip bool true if back-face auto-flipping is active
tc_uf_point_size float Point size for point rendering

Material & Color

GLSL Name Type Description Condition
tc_uv4_base_color vec4 Material base color (RGBA) Always
tc_uf_opacity float Material opacity Always
tc_uv2_specular_mix_and_gloss vec2 Specular intensity (x) and gloss (y) Phong shading
tc_uv3_emissive_color vec3 Emissive color Always
tc_uv3_ambient_color vec3 Ambient color Phong shading
tc_uf_ambient_mix float Ambient mix factor Phong shading
tc_uf_light_mix float Light contribution mix factor Always
tc_uf_mirror float Mirror/reflection factor Texture mapping

Lighting

GLSL Name Type Description
tc_uv3_ambient_light_color vec3 Scene ambient light color
tc_uv3_dir_light_position[i] vec3[] Directional light positions (eye-space, up to 3)
tc_uv3_dir_light_color[i] vec3[] Directional light colors
tc_uv3_point_light_position[i] vec3[] Point light positions (eye-space, up to 8)
tc_uv3_point_light_color[i] vec3[] Point light colors
tc_uf_point_light_power[i] float[] Point light power
tc_uf_point_light_decay[i] float[] Point light decay

Texture Samplers

GLSL Name Type Description Condition
tc_us2_texture sampler2D Diffuse/color texture Material has colorMap
tc_us2_normal_map sampler2D Normal map Material has normalMap
tc_us2_emissive_map sampler2D Emissive texture Material has emissiveMap
tc_us2_metallic_roughness_map sampler2D Metallic/roughness texture (PBR) Material has metallicRoughnessMap
tc_us2_occlusion_map sampler2D Occlusion texture Material has occlusionMap
tc_us2_sphere_map sampler2D Sphere environment map Sphere mapping enabled
tc_us2_line_pattern sampler2D Line pattern texture Line geometry with patterns
tc_us2_clearcoat_intensity_map sampler2D Clearcoat intensity texture PBR clearcoat
tc_us2_clearcoat_roughness_map sampler2D Clearcoat roughness texture PBR clearcoat
tc_us2_clearcoat_normal_map sampler2D Clearcoat normal map PBR clearcoat
tc_us2_specular_map sampler2D Specular factor texture PBR specular extension
tc_us2_specular_color_map sampler2D Specular color texture PBR specular extension
tc_us2_glossiness_diffuse_map sampler2D Glossiness diffuse texture PBR glossiness
tc_us2_glossiness_specular_map sampler2D Glossiness specular texture PBR glossiness

PBR Uniforms

GLSL Name Type Description Condition
tc_uv2_metallic_roughness vec2 Metallic (x) and roughness (y) factors Material isPbr
tc_uf_occlusion_strength float Occulsion strength Material has occlusionMap
tc_uf_alpha_cutoff float Alpha mask cutoff threshold Alpha mask mode
tc_uv2_clearcoat_intensity_roughness vec2 Clearcoat intensity (x) and roughness (y) PBR clearcoat
tc_uf_specular_factor float Specular factor PBR specular extension
tc_uv3_specular_color_factor vec3 Specular color factor PBR specular extension
tc_uf_glossiness_factor float Glossiness factor PBR glossiness
tc_uv4_glossiness_diffuse_factor vec4 Glossiness diffuse factor PBR glossiness
tc_uv3_glossiness_specular_factor vec3 Glossiness specular factor PBR glossiness

Image-Based Lighting (IBL)

GLSL Name Type Description
tc_uv3_ibl_diffuse_sh[9] vec3[9] Diffuse spherical harmonics
tc_us2_ibl_specular_brdf sampler2D Specular BRDF lookup texture
tc_usc_ibl_specular_map samplerCube Specular environment cube map
tc_uf_ibl_specular_map_levels float Mipmap levels of specular map
tc_uf_ibl_intensity float IBL intensity factor
tc_um3_ibl_matrix mat3 IBL orientation matrix

Cutting Sections

GLSL Name Type Description
tc_ui_cutting_sections int Number of active cutting section planes
tc_uv4_cutting_planes[N] vec4[] Cutting plane equations (eye-space)

Miscellaneous

GLSL Name Type Description
tc_uv4_ground_plane vec4 Ground plane equation
tc_uv2_reflection_attenuation vec2 Reflection near/far attenuation
tc_uv3_texture_matrix_row0 vec3 Texture matrix row 0
tc_uv3_texture_matrix_row1 vec3 Texture matrix row 1
tc_uf_line_pattern_inverse_length float Inverse of line pattern length
tc_uv2_jitter_diameter_and_frequency vec2 Line jitter parameters

Varyings (Vertex → Fragment)

The engine’s built-in shaders pass the following interpolated values between vertex and fragment shaders. In a custom shader, you are free to define your own varyings, but if you want to reuse any of the engine’s patterns, these are the conventions:

GLSL Name Type Description
_vv4_base_color vec4 Interpolated per-vertex color
_vv3_normal vec3 View-space normal
_vv3_eye_pos vec3 View-space position
_vv2_texture_coords vec2 Interpolated texture coordinates
_vf_line_pattern_offset float Interpolated line pattern offset

These are not required in custom shaders. They are listed here for reference if you want to follow the same data flow pattern as the built-in shaders.


Determining Available Attributes from MeshDataCopy

Before writing a custom shader, you can inspect what data a mesh provides using Model.getNodeMeshData(). The returned MeshDataCopy object tells you which vertex attributes will be populated by the engine.

const meshData: MeshDataCopy = await hwv.model.getNodeMeshData(nodeId);

// Check face geometry capabilities
const faces = meshData.faces;
console.log("Has face normals:", faces.hasNormals); // → tc_av3_normal available
console.log("Has face UVs:", faces.hasUVs); // → tc_av2_texture_coords available
console.log("Has face colors:", faces.hasRGBAs); // → tc_av4_base_color available

// Check line geometry capabilities
const lines = meshData.lines;
console.log("Has line normals:", lines.hasNormals);
console.log("Has line UVs:", lines.hasUVs);
console.log("Has line colors:", lines.hasRGBAs);

// Check point geometry capabilities
const points = meshData.points;
console.log("Has point normals:", points.hasNormals);
console.log("Has point UVs:", points.hasUVs);
console.log("Has point colors:", points.hasRGBAs);

// Mesh properties
console.log("Is two-sided:", meshData.isTwoSided);
console.log("Is manifold:", meshData.isManifold);
console.log("Winding:", meshData.winding); // 'clockwise' | 'counterClockwise' | undefined

Mapping MeshDataCopy to Attributes

MeshDataCopyElementGroup property Attribute available Notes
(always) tc_av4_vertex Vertex position is always present
hasNormals === true tc_av3_normal Per-vertex normals
hasUVs === true tc_av2_texture_coords Texture coordinates
hasRGBAs === true tc_av4_base_color Per-vertex RGBA colors
Important: If your custom shader declares an attribute that is not present in the mesh data (e.g., tc_av3_normal on a mesh without normals), the attribute will receive default values (typically zero). Your shader should handle this gracefully.

Determining Active Uniforms from IMaterial

You can inspect the material properties of a node using Model.getNodesMaterial(). The returned IMaterial tells you which engine uniforms will be populated.

const materials: IMaterial[] = await hwv.model.getNodesMaterial([nodeId]);
const mat = materials[0];

Mapping IMaterial to Uniforms

IMaterial property Uniforms populated
faceColor tc_uv4_base_color
lineColor tc_uv4_base_color (for line geometry)
pointColor tc_uv4_base_color`` (for point geometry)
specularColor tc_uv2_specular_mix_and_gloss
specularIntensity tc_uv2_specular_mix_and_gloss
ambientColor tc_uv3_ambient_color
emissiveColor tc_uv3_emissive_color
opacity tc_uf_opacity
isPbr === true tc_uv2_metallic_roughness
metallic tc_uv2_metallic_roughness.x
roughness tc_uv2_metallic_roughness.y
colorMap tc_us2_texture`` (sampler bound)
normalMap tc_us2_normal_map (sampler bound)
emissiveMap tc_us2_emissive_map (sampler bound)
metallicRoughnessMap tc_us2_metallic_roughness_map (sampler bound)
occlusionMap tc_us2_occlusion_map (sampler bound)
textureMatrix tc_uv3_texture_matrix_row0, tc_uv3_texture_matrix_row1
linePattern tc_us2_line_pattern, tc_uf_line_pattern_inverse_length

Fragment Shader Output

Multiple Render Targets (MRT)

On most WebGL 2 platforms, the engine uses Multiple Render Targets (MRT). The opaque rendering pass renders into a framebuffer with two color attachments:

Attachments Content
0 Color output (RGBA, premultiplied alpha)
1 View-space normal buffer (encoded as normal * 0.5 + 0.5, alpha = 1.0)

The normal buffer is used by post-processing effects such as ambient occlusion (SSAO) and edge detection / silhouettes.

Because the framebuffer has two attachments, your custom fragment shader must declare two output variables in WebGL 2 (GLSL ES 3.00). If only one output is declared, the GPU will emit warnings or errors about the missing second output.

Required Fragment Shader Structure

#version 300 es
precision mediump float;

// Declare both outputs — the layout qualifiers match framebuffer attachment indices
layout(location = 0) out vec4 out_color;
layout(location = 1) out vec4 out_normal;

// Your varyings
in vec3 v_normal;

void main() {
    vec4 color = vec4(1.0, 0.0, 0.0, 1.0);

    // Output 0: premultiplied-alpha color
    out_color = vec4(color.rgb * color.a, color.a);

    // Output 1: view-space normal (encoded), or vec4(0) if you don't have a normal
    out_normal = vec4(normalize(v_normal) * 0.5 + 0.5, 1.0);
}

If your shader does not have access to a view-space normal (e.g., for line or point geometry), simply write a zero vector:

out_normal = vec4(0.0);
Note: The built-in shaders use gl_FragData[0] / gl_FragData[1] syntax via compatibility headers. For custom shaders which are compiled directly, prefer the explicit layout(location = N) out vec4 syntax, which is clearer and avoids relying on engine-internal aliases.

Premultiplied Alpha

The compositing pipeline expects premultiplied alpha output. Multiply your RGB components by alpha before writing to the color output:

out_color = vec4(color.rgb * color.a, color.a);

Coordinate Spaces

The engine follows this transform pipeline:

Model Space  →  View Space (Eye Space)  →  Clip Space
 (vertex)      (view_matrix × model)      (projection × view × model)
  • ``tc_av4_vertex``: Model-space position.
  • ``tc_um4_model_matrix``: Transforms from model space to world space (available in single-matrix mode).
  • ``tc_um4_view_matrix``: Transforms from world space to view/eye space.
  • ``tc_um4_projection_matrix``: Transforms from view space to clip space.
  • ``tc_um3_normal_matrix``: Transforms normals from model space to view space (inverse transpose).

Important: The translation part of the view matrix (camera) is directly applied to the model matrix at CPU side before data is pushed to the GPU.

That means this translation part is in both view and model matrix in the GPU.

To avoid applying the translation twice, it’s necessary to remove this translation part from the view matrix with the following code before multiplying the model matrix:

mat4(mat3(tc_um4_view_matrix))

Standard Vertex Transform

attribute vec4 tc_av4_vertex;
uniform mat4 tc_um4_projection_matrix;
uniform mat4 tc_um4_view_matrix;
uniform mat4 tc_um4_model_matrix;

void main() {
    vec4 eye_pos = mat4(mat3(tc_um4_view_matrix)) * tc_um4_model_matrix * tc_av4_vertex;
    gl_Position = tc_um4_projection_matrix * eye_pos;
}
Note on batching: In the default rendering mode, model matrices are batched into tc_uv4_model_matrices[] and indexed via tc_af_batch_index. The single-matrix mode (tc_um4_model_matrix) is used when the engine detects it is appropriate. Custom shaders should generally use single-matrix mode for simplicity.

Examples

Minimal Custom Shader

A basic shader that renders geometry with a flat color:

const vertexShaderSource = `#version 300 es
   precision highp float;

   in vec4 tc_av4_vertex;
   uniform mat4 tc_um4_projection_matrix;
   uniform mat4 tc_um4_view_matrix;
   uniform mat4 tc_um4_model_matrix;

   void main() {
       vec4 world_position = tc_um4_model_matrix * tc_av4_vertex;
       gl_Position = tc_um4_projection_matrix * mat4(mat3(tc_um4_view_matrix)) * world_position;
   }
 `;

const fragmentShaderSource = `#version 300 es
 precision mediump float;

 uniform vec4 u_color;

 layout(location = 0) out vec4 out_color;
 layout(location = 1) out vec4 out_normal;

 void main() {
     out_color = vec4(u_color.rgb * u_color.a, u_color.a);
     out_normal = vec4(0.0);
 }
 `;

await hwv.model.setNodesShader([nodeId], vertexShader, fragmentShader, {
    uniforms: {
        u_color: { type: "vec4", values: [1.0, 0.0, 0.0, 1.0] },
    },
});

Custom Shader with Normals and Lighting

A shader that uses the mesh normals for simple diffuse lighting:

const vertexShader = `
  attribute vec4 tc_av4_vertex;
  attribute vec3 tc_av3_normal;

  uniform mat4 tc_um4_projection_matrix;
  uniform mat4 tc_um4_view_matrix;
  uniform mat4 tc_um4_model_matrix;
  uniform mat3 tc_um3_normal_matrix;

  varying vec3 v_normal;
  varying vec3 v_eye_pos;

  void main() {
      vec4 eye_pos = tc_um4_view_matrix * tc_um4_model_matrix * tc_av4_vertex;
      v_eye_pos = eye_pos.xyz;
      v_normal = normalize(mat3(tc_um4_view_matrix) * tc_um3_normal_matrix * tc_av3_normal);
      gl_Position = tc_um4_projection_matrix * eye_pos;
  }
`;

const fragmentShader = `
  uniform vec4 u_color;
  uniform vec3 tc_uv3_ambient_light_color;

  varying vec3 v_normal;
  varying vec3 v_eye_pos;

  void main() {
      vec3 normal = normalize(v_normal);
      vec3 light_dir = normalize(-v_eye_pos);
      float diffuse = max(dot(normal, light_dir), 0.0);

      vec3 color = u_color.rgb * (tc_uv3_ambient_light_color + diffuse);
      float alpha = u_color.a;
      gl_FragColor = vec4(color * alpha, alpha);
  }
`;

await hwv.model.setNodesShader([nodeId], vertexShader, fragmentShader, {
    uniforms: {
        u_color: { type: "vec4", values: [0.2, 0.6, 1.0, 1.0] },
    },
});

Updating Uniforms at Runtime

After applying a custom shader, you can update its uniform values without recompiling:

// Animate color over time
function animate(time: number) {
    const r = Math.sin(time * 0.001) * 0.5 + 0.5;
    const g = Math.cos(time * 0.002) * 0.5 + 0.5;

    hwv.model.setNodesShaderUniforms([nodeId], {
        u_color: { type: "vec4", values: [r, g, 0.5, 1.0] },
    });

    requestAnimationFrame(animate);
}
requestAnimationFrame(animate);

Inspecting Mesh Data Before Writing a Shader

async function inspectNode(nodeId: NodeId) {
    // Check mesh capabilities
    const meshData = await hwv.model.getNodeMeshData(nodeId);
    console.log("Face vertex count:", meshData.faces.vertexCount);
    console.log("Has normals:", meshData.faces.hasNormals);
    console.log("Has UVs:", meshData.faces.hasUVs);
    console.log("Has vertex colors:", meshData.faces.hasRGBAs);
    console.log("Is two-sided:", meshData.isTwoSided);

    // Check material properties
    const materials = await hwv.model.getNodesMaterial([nodeId]);
    const mat = materials[0];
    console.log("Is PBR:", mat.isPbr);
    console.log("Has color map:", mat.colorMap !== undefined);
    console.log("Has normal map:", mat.normalMap !== undefined);
    console.log("Opacity:", mat.opacity);
}

Tips and Caveats

  1. Prefix convention: Uniforms prefixed with tc_ are reserved for the engine. Use a different prefix for your custom uniforms (e.g., u_).

  2. Unused uniforms: The GLSL compiler may optimize out uniforms that are not used in the shader. The engine will not report an error if a built-in uniform is declared but optimized away.

  3. Batch vs single matrix mode: By default, the engine batches multiple instances into draw calls using tc_uv4_model_matrices[] indexed by tc_af_batch_index. For simple custom shaders, use tc_um4_model_matrix and tc_um3_normal_matrix (single-matrix mode). The engine will detect which mode your shader uses based on the declared uniforms.

  4. Cutting sections: If you want your custom shader to respect cutting planes, you must implement the discard logic yourself using tc_ui_cutting_sections and tc_uv4_cutting_planes.

  5. Premultiplied alpha: The compositing pipeline expects premultiplied alpha output. Multiply RGB by alpha in your fragment shader.

  6. Per-element-type shaders: Custom shaders are applied per node and affect face geometry. The current APIdoes not support customization of line or point geometry.

  7. Performance: Custom shaders bypass the engine’s shader caching and optimization. Each unique shader source results in a separate compile and link step. Reuse the same source strings across nodes when possible.

  8. The translation part of the view matrix (camera) is directly applied to the model matrix at CPU side before data is pushed to the GPU.

    That means this translation part is in both view and model matrix in the GPU.

    To avoid applying the translation twice, it’s necessary to remove this translation part from the view matrix with the following code before multiplying the model matrix:

mat4(mat3(tc_um4_view_matrix))