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
~~~~~~~~~~~~~~~~~~~~

.. code:: typescript

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

Applies custom GLSL shader programs to the specified nodes.

.. csv-table::
    :header: "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
^^^^^^^^^^^^^^^^

.. code:: typescript

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

--------------

Model.setNodesShaderUniforms
~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. code:: typescript

   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):

.. code:: typescript

   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 <#determining-available-attributes-from-meshdatacopy>`__).

.. csv-table::
    :header: "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:

.. code:: glsl

   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
~~~~~~~~~~~~~~~~~~

.. csv-table::
    :header: "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:

    .. code:: glsl

       mat4(mat3(tc_um4_view_matrix))


Viewport & Rendering
~~~~~~~~~~~~~~~~~~~~

.. csv-table::
    :header: "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
~~~~~~~~~~~~~~~~

.. csv-table::
    :header: "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
~~~~~~~~

.. csv-table::
    :header: "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
~~~~~~~~~~~~~~~~

.. csv-table::
    :header: "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
~~~~~~~~~~~~

.. csv-table::
    :header: "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)
~~~~~~~~~~~~~~~~~~~~~~~~~~

.. csv-table::
    :header: "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
~~~~~~~~~~~~~~~~

.. csv-table::
    :header: "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
~~~~~~~~~~~~~

.. csv-table::
    :header: "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:

.. csv-table::
    :header: "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.

.. code:: typescript

   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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. csv-table::
    :header: "``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.

.. code:: typescript

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


Mapping IMaterial to Uniforms
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. csv-table::
    :header: "``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:

.. csv-table::
    :header: "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
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. code:: glsl

   #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:

.. code:: glsl

   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:

.. code:: glsl

   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:

    .. code:: glsl

       mat4(mat3(tc_um4_view_matrix))


Standard Vertex Transform
~~~~~~~~~~~~~~~~~~~~~~~~~

.. code:: glsl

   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:

.. code:: typescript

   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:

.. code:: typescript

   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:

.. code:: typescript

   // 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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. code:: typescript

   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:

.. code:: glsl

   mat4(mat3(tc_um4_view_matrix))
