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.
texture2Duniforms 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_normalon 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 usegl_FragData[0]/gl_FragData[1]syntax via compatibility headers. For custom shaders which are compiled directly, prefer the explicitlayout(location = N) out vec4syntax, 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 intotc_uv4_model_matrices[]and indexed viatc_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
Prefix convention: Uniforms prefixed with
tc_are reserved for the engine. Use a different prefix for your custom uniforms (e.g.,u_).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.
Batch vs single matrix mode: By default, the engine batches multiple instances into draw calls using
tc_uv4_model_matrices[]indexed bytc_af_batch_index. For simple custom shaders, usetc_um4_model_matrixandtc_um3_normal_matrix(single-matrix mode). The engine will detect which mode your shader uses based on the declared uniforms.Cutting sections: If you want your custom shader to respect cutting planes, you must implement the discard logic yourself using
tc_ui_cutting_sectionsandtc_uv4_cutting_planes.Premultiplied alpha: The compositing pipeline expects premultiplied alpha output. Multiply RGB by alpha in your fragment shader.
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.
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.
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))