vcti-shader-material 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vcti_shader_material-1.0.0/LICENSE +8 -0
- vcti_shader_material-1.0.0/PKG-INFO +316 -0
- vcti_shader_material-1.0.0/README.md +287 -0
- vcti_shader_material-1.0.0/pyproject.toml +106 -0
- vcti_shader_material-1.0.0/setup.cfg +4 -0
- vcti_shader_material-1.0.0/src/vcti/shader/material/__init__.py +287 -0
- vcti_shader_material-1.0.0/src/vcti/shader/material/brdf.py +579 -0
- vcti_shader_material-1.0.0/src/vcti/shader/material/py.typed +0 -0
- vcti_shader_material-1.0.0/src/vcti/shader/material/slang/material.slang +364 -0
- vcti_shader_material-1.0.0/src/vcti/shader/material/slang/material_surface_finish.slang +142 -0
- vcti_shader_material-1.0.0/src/vcti/shader/material/slang/material_table.slang +77 -0
- vcti_shader_material-1.0.0/src/vcti/shader/material/slang/material_texture.slang +91 -0
- vcti_shader_material-1.0.0/src/vcti/shader/material/specs.py +416 -0
- vcti_shader_material-1.0.0/src/vcti/shader/material/surface_finish.py +179 -0
- vcti_shader_material-1.0.0/src/vcti/shader/material/table.py +172 -0
- vcti_shader_material-1.0.0/src/vcti/shader/material/texture.py +124 -0
- vcti_shader_material-1.0.0/src/vcti_shader_material.egg-info/PKG-INFO +316 -0
- vcti_shader_material-1.0.0/src/vcti_shader_material.egg-info/SOURCES.txt +30 -0
- vcti_shader_material-1.0.0/src/vcti_shader_material.egg-info/dependency_links.txt +1 -0
- vcti_shader_material-1.0.0/src/vcti_shader_material.egg-info/requires.txt +16 -0
- vcti_shader_material-1.0.0/src/vcti_shader_material.egg-info/top_level.txt +1 -0
- vcti_shader_material-1.0.0/tests/test_brdf.py +696 -0
- vcti_shader_material-1.0.0/tests/test_definition.py +393 -0
- vcti_shader_material-1.0.0/tests/test_docs.py +238 -0
- vcti_shader_material-1.0.0/tests/test_examples.py +268 -0
- vcti_shader_material-1.0.0/tests/test_interface.py +297 -0
- vcti_shader_material-1.0.0/tests/test_material_math.py +601 -0
- vcti_shader_material-1.0.0/tests/test_specs.py +175 -0
- vcti_shader_material-1.0.0/tests/test_surface_finish.py +185 -0
- vcti_shader_material-1.0.0/tests/test_table.py +297 -0
- vcti_shader_material-1.0.0/tests/test_texture.py +360 -0
- vcti_shader_material-1.0.0/tests/test_version.py +15 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Copyright (c) 2018-2026 Visual Collaboration Technologies Inc.
|
|
2
|
+
All Rights Reserved.
|
|
3
|
+
|
|
4
|
+
This software is proprietary and confidential. Unauthorized copying,
|
|
5
|
+
distribution, or use of this software, via any medium, is strictly
|
|
6
|
+
prohibited. Access is granted only to authorized VCollab developers
|
|
7
|
+
and individuals explicitly authorized by Visual Collaboration
|
|
8
|
+
Technologies Inc.
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vcti-shader-material
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: The material shader feature: the fragment-stage lighting that shades a surface by a metallic-roughness material.
|
|
5
|
+
Author: Visual Collaboration Technologies Inc.
|
|
6
|
+
License-Expression: LicenseRef-Proprietary
|
|
7
|
+
Project-URL: Repository, https://github.com/vcollab/vcti-python-shader-material
|
|
8
|
+
Project-URL: Changelog, https://github.com/vcollab/vcti-python-shader-material/blob/main/CHANGELOG.md
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
13
|
+
Requires-Python: <3.15,>=3.12
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: vcti-shader-base>=2.2.0
|
|
17
|
+
Provides-Extra: gl
|
|
18
|
+
Requires-Dist: vcti-shader-compiler[gl]>=4.2.0; extra == "gl"
|
|
19
|
+
Provides-Extra: test
|
|
20
|
+
Requires-Dist: pytest; extra == "test"
|
|
21
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
22
|
+
Requires-Dist: vcti-shader-compiler>=4.2.0; extra == "test"
|
|
23
|
+
Requires-Dist: numpy; extra == "test"
|
|
24
|
+
Provides-Extra: lint
|
|
25
|
+
Requires-Dist: ruff; extra == "lint"
|
|
26
|
+
Provides-Extra: typecheck
|
|
27
|
+
Requires-Dist: mypy; extra == "typecheck"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# vcti-shader-material
|
|
31
|
+
|
|
32
|
+
The material shader feature: the fragment-stage lighting that shades a surface by a metallic-roughness material.
|
|
33
|
+
|
|
34
|
+
## Overview
|
|
35
|
+
|
|
36
|
+
A viewer wants a mesh to look like the object it represents: a painted casting,
|
|
37
|
+
a brushed steel bracket, a rubber seal. This feature is the lighting that does
|
|
38
|
+
that. It shades each fragment by a **metallic-roughness material** — a base colour,
|
|
39
|
+
a metallic factor, a roughness, an occlusion factor and an emissive — a subset
|
|
40
|
+
of glTF 2.0's metallic-roughness material — under a sky/ground environment and up to four
|
|
41
|
+
directional or point lights, with the Cook-Torrance specular model: the GGX
|
|
42
|
+
distribution, the Smith geometry term and the Schlick Fresnel approximation.
|
|
43
|
+
|
|
44
|
+
The lighting takes those values as one resolved `MaterialValues` and does not
|
|
45
|
+
care where they came from. The source shipped today is **uniforms**: one material for the whole
|
|
46
|
+
draw. A texture, a materials table indexed by a per-vertex id, or the fringe
|
|
47
|
+
colour of a result plot are other sources, each a capability of its own, and
|
|
48
|
+
none of them changes the BRDF. Tone mapping and gamma are a separate finishing
|
|
49
|
+
step the composition applies to a lit colour and never to a fringe colour, which
|
|
50
|
+
is a legend colour and must reach the screen exactly.
|
|
51
|
+
|
|
52
|
+
`vcti-shader-material` is the shader half of that arrangement. It ships the
|
|
53
|
+
Slang that computes the lighting, a Python mirror of every function in it so a
|
|
54
|
+
caller can predict a shaded colour and the tests have an oracle, the specs
|
|
55
|
+
saying what the lighting needs bound, and the `ShaderDefinition` saying what the
|
|
56
|
+
feature is.
|
|
57
|
+
|
|
58
|
+
Everything here is a declaration or fixed shader source. Nothing compiles or
|
|
59
|
+
runs a shader; a build step does that, using what this package declares.
|
|
60
|
+
|
|
61
|
+
## Installation
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install vcti-shader-material
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Requires Python 3.12, 3.13, or 3.14, matching `vcti-shader-base`. Nothing native
|
|
68
|
+
is built on that path, and nothing native is built by `[test]` either — only the
|
|
69
|
+
`[gl]` extra pulls a GL binding, and only on 3.14 does that compile from source
|
|
70
|
+
for want of a cp314 wheel.
|
|
71
|
+
|
|
72
|
+
### In `requirements.txt`
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
vcti-shader-material>=1.0.0
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### In `pyproject.toml` dependencies
|
|
79
|
+
|
|
80
|
+
```toml
|
|
81
|
+
dependencies = [
|
|
82
|
+
"vcti-shader-material>=1.0.0",
|
|
83
|
+
]
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Quick Start
|
|
87
|
+
|
|
88
|
+
### Describe a material and some lights
|
|
89
|
+
|
|
90
|
+
A material is a handful of values. The base colour is a linear colour in
|
|
91
|
+
`[0, 1]` — a reflectance, so it has a ceiling; the emissive is a linear radiance
|
|
92
|
+
and has none; the three factors are in `[0, 1]`. A light is directional, with the unit direction from the surface
|
|
93
|
+
toward it, or a point, with a position and inverse-square falloff:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from vcti.shader.material import Light, MaterialValues
|
|
97
|
+
|
|
98
|
+
steel = MaterialValues(base_color=(0.56, 0.57, 0.58), metallic=1.0, roughness=0.35)
|
|
99
|
+
paint = MaterialValues(base_color=(0.8, 0.1, 0.1), metallic=0.0, roughness=0.5)
|
|
100
|
+
|
|
101
|
+
key = Light.directional((0.3, 0.4, 1.0), (1.0, 1.0, 1.0))
|
|
102
|
+
lamp = Light.point((1.0, 1.0, 3.0), (4.0, 4.0, 4.0))
|
|
103
|
+
assert key.is_directional and not lamp.is_directional
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Predict what the shader produces
|
|
107
|
+
|
|
108
|
+
The same math the shader runs, in Python. `shade` returns linear light; `finish`
|
|
109
|
+
is the tone mapping and gamma step:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from vcti.shader.material import EnvironmentLight, finish, shade
|
|
113
|
+
|
|
114
|
+
surface = (0.0, 0.0, 0.0)
|
|
115
|
+
normal = (0.0, 0.0, 1.0)
|
|
116
|
+
eye = (0.0, 0.0, 5.0)
|
|
117
|
+
|
|
118
|
+
# The environment is a sky/ground gradient. `uniform` is the flat case a single
|
|
119
|
+
# ambient colour used to be; `studio` is a bright ceiling over a darker floor.
|
|
120
|
+
environment = EnvironmentLight.studio()
|
|
121
|
+
|
|
122
|
+
lit = shade(surface, normal, eye, paint, environment, [key, lamp])
|
|
123
|
+
assert all(channel > 0.0 for channel in lit)
|
|
124
|
+
|
|
125
|
+
shown = finish(lit)
|
|
126
|
+
assert all(0.0 <= channel < 1.0 for channel in shown)
|
|
127
|
+
assert shade(surface, normal, eye, steel, environment, [key]) != shade(
|
|
128
|
+
surface, normal, eye, paint, environment, [key]
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# A metal is almost entirely a reflection of its surroundings, so which way it
|
|
132
|
+
# faces changes what it shows. A flat ambient could not express that.
|
|
133
|
+
up = shade(surface, (0.0, 0.0, 1.0), eye, steel, environment, [])
|
|
134
|
+
down = shade(surface, (0.0, 0.0, -1.0), (0.0, 0.0, -5.0), steel, environment, [])
|
|
135
|
+
assert up[0] > down[0]
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
A light behind the surface contributes nothing, a metal has no diffuse term,
|
|
139
|
+
and a roughness below the floor shades as the floor. The material refuses a
|
|
140
|
+
factor outside `[0, 1]`:
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
try:
|
|
144
|
+
MaterialValues(roughness=1.5)
|
|
145
|
+
except ValueError as error:
|
|
146
|
+
assert "roughness" in str(error)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Set the uniforms
|
|
150
|
+
|
|
151
|
+
The material and the lights are uniforms, one set per draw. The helpers write
|
|
152
|
+
them by the names the specs declare, with the light arrays padded to the size the
|
|
153
|
+
shader is built for:
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
from vcti.shader.material import MAX_LIGHTS, light_uniforms, material_uniforms
|
|
157
|
+
|
|
158
|
+
uniforms = material_uniforms(paint) | light_uniforms([key, lamp])
|
|
159
|
+
uniforms |= {
|
|
160
|
+
"u_viewPosition": eye,
|
|
161
|
+
"u_environmentSky": environment.sky_color,
|
|
162
|
+
"u_environmentGround": environment.ground_color,
|
|
163
|
+
"u_environmentUp": environment.up,
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
assert uniforms["u_materialBaseColor"] == (0.8, 0.1, 0.1)
|
|
167
|
+
assert uniforms["u_lightCount"] == 2
|
|
168
|
+
assert len(uniforms["u_lightPositions"]) == MAX_LIGHTS == 4
|
|
169
|
+
assert uniforms["u_lightPositions"][1] == (1.0, 1.0, 3.0, 1.0) # w = 1: a point light
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### What a build step binds
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
from vcti.shader.material import fragment_inputs, fragment_outputs, fragment_uniforms
|
|
176
|
+
|
|
177
|
+
(normal_attribute,) = fragment_inputs()
|
|
178
|
+
assert (normal_attribute.name, normal_attribute.type, normal_attribute.semantic) == (
|
|
179
|
+
"a_normal", "vec3", "normal"
|
|
180
|
+
)
|
|
181
|
+
assert [u.name for u in fragment_uniforms()] == [
|
|
182
|
+
"u_materialBaseColor", "u_materialMetallic", "u_materialRoughness",
|
|
183
|
+
"u_materialOcclusion", "u_materialEmissive",
|
|
184
|
+
"u_viewPosition", "u_environmentSky", "u_environmentGround", "u_environmentUp",
|
|
185
|
+
"u_lightPositions", "u_lightColors", "u_lightCount",
|
|
186
|
+
]
|
|
187
|
+
assert fragment_outputs()[0].name == "fragColor"
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
The vertex stage rotates the normal into world space, through whatever
|
|
191
|
+
placement it applies to the position, and forwards it with the world position
|
|
192
|
+
as two varyings. Those varyings are contract rather than spec; see
|
|
193
|
+
[docs/design.md](docs/design.md).
|
|
194
|
+
|
|
195
|
+
### Select a pipeline
|
|
196
|
+
|
|
197
|
+
One tag for the lighting and one naming where the values come from:
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
from vcti.shader.material import (
|
|
201
|
+
DEFINITION,
|
|
202
|
+
DEFINITIONS,
|
|
203
|
+
TABLE_DEFINITION,
|
|
204
|
+
TEXTURE_DEFINITION,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
assert DEFINITION.id == "material"
|
|
208
|
+
assert DEFINITION.role.value == "fragment"
|
|
209
|
+
assert set(DEFINITION.capabilities) == {"material", "material-uniform"}
|
|
210
|
+
assert DEFINITION.slang_modules == (
|
|
211
|
+
"material.slang",
|
|
212
|
+
"material_table.slang",
|
|
213
|
+
"material_texture.slang",
|
|
214
|
+
"material_surface_finish.slang",
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# One definition per source. Same lighting tag, different source tag, so a
|
|
218
|
+
# pipeline asking for one never matches another.
|
|
219
|
+
assert set(TABLE_DEFINITION.capabilities) == {"material", "material-table"}
|
|
220
|
+
assert set(TEXTURE_DEFINITION.capabilities) == {"material", "material-texture"}
|
|
221
|
+
assert DEFINITIONS == (DEFINITION, TABLE_DEFINITION, TEXTURE_DEFINITION)
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Vary a surface without authoring anything
|
|
225
|
+
|
|
226
|
+
A **layer** takes a resolved material and returns another, so layers compose
|
|
227
|
+
where sources cannot: this is how a painted casting can also carry a label. The
|
|
228
|
+
surface finish is the first, and it needs no UVs, no tangents and no images —
|
|
229
|
+
just world position:
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
from vcti.shader.material import SurfaceFinish, apply_surface_finish
|
|
233
|
+
|
|
234
|
+
cast_iron = MaterialValues(base_color=(0.35, 0.34, 0.33), roughness=0.7)
|
|
235
|
+
finish = SurfaceFinish.cast()
|
|
236
|
+
|
|
237
|
+
here = apply_surface_finish(cast_iron, (0.0, 0.0, 0.0), finish)
|
|
238
|
+
there = apply_surface_finish(cast_iron, (0.12, 0.04, -0.08), finish)
|
|
239
|
+
assert here.roughness != there.roughness # the surface varies from point to point
|
|
240
|
+
|
|
241
|
+
# The default finish is the identity, which is what "no finish" means.
|
|
242
|
+
assert apply_surface_finish(cast_iron, (1.0, 2.0, 3.0), SurfaceFinish()) == cast_iron
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
*The* finish (`finish`, `tone_map`, `gamma_encode`) is tone mapping and gamma on
|
|
246
|
+
the way to the screen. A *surface* finish is what a surface looks like. They are
|
|
247
|
+
unrelated.
|
|
248
|
+
|
|
249
|
+
### Many materials in one draw
|
|
250
|
+
|
|
251
|
+
The table source replaces the five material uniforms with an id per vertex and
|
|
252
|
+
a table of rows. A client uploads `table_texel_rows(...)` as a three-row
|
|
253
|
+
`rgba32f` texture and binds the ids; the lighting is unchanged:
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
from vcti.shader.material import MaterialValues, pack_row, row_material, table_texel_rows
|
|
257
|
+
|
|
258
|
+
materials = [steel, paint, MaterialValues(base_color=(0.1, 0.1, 0.1), emissive=(0.0, 0.9, 0.2))]
|
|
259
|
+
rows = table_texel_rows(materials)
|
|
260
|
+
|
|
261
|
+
assert len(rows) == 3 # three texels per material
|
|
262
|
+
assert len(rows[0]) == len(materials) * 4 # one column per material
|
|
263
|
+
assert row_material(pack_row(steel)) == steel
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## The lighting
|
|
267
|
+
|
|
268
|
+
| Term | Model |
|
|
269
|
+
|---|---|
|
|
270
|
+
| diffuse | Lambert, `base_color / π`, scaled by `(1 − F)(1 − metallic)` |
|
|
271
|
+
| distribution | GGX / Trowbridge-Reitz on `roughness²` |
|
|
272
|
+
| geometry | Smith, Schlick-GGX form, `k = (roughness + 1)² / 8` |
|
|
273
|
+
| Fresnel | Schlick, `F0 = mix(0.04, base_color, metallic)` |
|
|
274
|
+
| environment | a sky/ground gradient: diffuse along `N`, specular along `reflect(-V, N)`, weighted by roughness-aware Fresnel |
|
|
275
|
+
| tone map | Reinhard `c / (c + 1)` |
|
|
276
|
+
| gamma | `c ^ (1 / 2.2)` — skipped on an sRGB framebuffer |
|
|
277
|
+
| finish | the two composed |
|
|
278
|
+
|
|
279
|
+
Roughness is floored at 0.04. Lights are a fixed array of four; `w` in a
|
|
280
|
+
light's position selects directional (0) or point (1). Everything is in linear
|
|
281
|
+
light until the finish.
|
|
282
|
+
|
|
283
|
+
## API surface
|
|
284
|
+
|
|
285
|
+
| Name | What it is |
|
|
286
|
+
|---|---|
|
|
287
|
+
| `DEFINITION` | the `ShaderDefinition` a build step imports to compose this feature |
|
|
288
|
+
| `SLANG_DIR`, `SLANG_MODULES` | the installed Slang directory, and the module in it |
|
|
289
|
+
| `MaterialValues`, `Light`, `EnvironmentLight` | the resolved material a source produces, a directional or point light, and the sky/ground environment |
|
|
290
|
+
| `shade`, `light_contribution`, `ambient`, `view_direction` | the mirror of the shader's lighting |
|
|
291
|
+
| `tone_map`, `gamma_encode`, `finish` | the output transform, in two independently callable steps |
|
|
292
|
+
| `fresnel_schlick`, `distribution_ggx`, `geometry_smith`, `geometry_schlick_ggx`, `f0` | the BRDF's terms |
|
|
293
|
+
| `material_uniforms`, `light_uniforms` | the uniforms a caller sets, by the declared names |
|
|
294
|
+
| `MAX_LIGHTS`, `MIN_ROUGHNESS`, `DIELECTRIC_F0` | the constants the Slang shares |
|
|
295
|
+
| `fragment_inputs`, `fragment_uniforms`, `fragment_outputs` | what a build step binds |
|
|
296
|
+
| `CAPABILITY`, `SOURCE_CAPABILITY` | the tags |
|
|
297
|
+
|
|
298
|
+
## Dependencies
|
|
299
|
+
|
|
300
|
+
`vcti-shader-base>=2.1.0` is the only runtime dependency — declaring a feature
|
|
301
|
+
is pure data. `vcti-shader-compiler>=4.0.0` and `numpy` are test-only, and a separate
|
|
302
|
+
`gl` extra adds the GL binding the shader tests need to execute rather than
|
|
303
|
+
skip.
|
|
304
|
+
|
|
305
|
+
## Documentation
|
|
306
|
+
|
|
307
|
+
| If you want to… | Read |
|
|
308
|
+
|---|---|
|
|
309
|
+
| Get started using the package | Quick Start above |
|
|
310
|
+
| Understand the lighting, the contract and the decisions behind them | [docs/design.md](docs/design.md) |
|
|
311
|
+
| Know what a material is, what an authored one loses on the way in, and what is planned | [docs/material-model.md](docs/material-model.md) |
|
|
312
|
+
| Understand how authored textures and composed shaders meet, and what was decided | `vcti-shader-base`, `docs/authored-textures.md` — the decisions there settle the texture and sampler declarations, which ship from that package |
|
|
313
|
+
| Navigate or modify the source, including the Slang | [docs/source-guide.md](docs/source-guide.md) |
|
|
314
|
+
|
|
315
|
+
The full API reference is generated from the source docstrings and published in
|
|
316
|
+
the unified VCollab docs.
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# vcti-shader-material
|
|
2
|
+
|
|
3
|
+
The material shader feature: the fragment-stage lighting that shades a surface by a metallic-roughness material.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
A viewer wants a mesh to look like the object it represents: a painted casting,
|
|
8
|
+
a brushed steel bracket, a rubber seal. This feature is the lighting that does
|
|
9
|
+
that. It shades each fragment by a **metallic-roughness material** — a base colour,
|
|
10
|
+
a metallic factor, a roughness, an occlusion factor and an emissive — a subset
|
|
11
|
+
of glTF 2.0's metallic-roughness material — under a sky/ground environment and up to four
|
|
12
|
+
directional or point lights, with the Cook-Torrance specular model: the GGX
|
|
13
|
+
distribution, the Smith geometry term and the Schlick Fresnel approximation.
|
|
14
|
+
|
|
15
|
+
The lighting takes those values as one resolved `MaterialValues` and does not
|
|
16
|
+
care where they came from. The source shipped today is **uniforms**: one material for the whole
|
|
17
|
+
draw. A texture, a materials table indexed by a per-vertex id, or the fringe
|
|
18
|
+
colour of a result plot are other sources, each a capability of its own, and
|
|
19
|
+
none of them changes the BRDF. Tone mapping and gamma are a separate finishing
|
|
20
|
+
step the composition applies to a lit colour and never to a fringe colour, which
|
|
21
|
+
is a legend colour and must reach the screen exactly.
|
|
22
|
+
|
|
23
|
+
`vcti-shader-material` is the shader half of that arrangement. It ships the
|
|
24
|
+
Slang that computes the lighting, a Python mirror of every function in it so a
|
|
25
|
+
caller can predict a shaded colour and the tests have an oracle, the specs
|
|
26
|
+
saying what the lighting needs bound, and the `ShaderDefinition` saying what the
|
|
27
|
+
feature is.
|
|
28
|
+
|
|
29
|
+
Everything here is a declaration or fixed shader source. Nothing compiles or
|
|
30
|
+
runs a shader; a build step does that, using what this package declares.
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install vcti-shader-material
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Requires Python 3.12, 3.13, or 3.14, matching `vcti-shader-base`. Nothing native
|
|
39
|
+
is built on that path, and nothing native is built by `[test]` either — only the
|
|
40
|
+
`[gl]` extra pulls a GL binding, and only on 3.14 does that compile from source
|
|
41
|
+
for want of a cp314 wheel.
|
|
42
|
+
|
|
43
|
+
### In `requirements.txt`
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
vcti-shader-material>=1.0.0
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### In `pyproject.toml` dependencies
|
|
50
|
+
|
|
51
|
+
```toml
|
|
52
|
+
dependencies = [
|
|
53
|
+
"vcti-shader-material>=1.0.0",
|
|
54
|
+
]
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Quick Start
|
|
58
|
+
|
|
59
|
+
### Describe a material and some lights
|
|
60
|
+
|
|
61
|
+
A material is a handful of values. The base colour is a linear colour in
|
|
62
|
+
`[0, 1]` — a reflectance, so it has a ceiling; the emissive is a linear radiance
|
|
63
|
+
and has none; the three factors are in `[0, 1]`. A light is directional, with the unit direction from the surface
|
|
64
|
+
toward it, or a point, with a position and inverse-square falloff:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from vcti.shader.material import Light, MaterialValues
|
|
68
|
+
|
|
69
|
+
steel = MaterialValues(base_color=(0.56, 0.57, 0.58), metallic=1.0, roughness=0.35)
|
|
70
|
+
paint = MaterialValues(base_color=(0.8, 0.1, 0.1), metallic=0.0, roughness=0.5)
|
|
71
|
+
|
|
72
|
+
key = Light.directional((0.3, 0.4, 1.0), (1.0, 1.0, 1.0))
|
|
73
|
+
lamp = Light.point((1.0, 1.0, 3.0), (4.0, 4.0, 4.0))
|
|
74
|
+
assert key.is_directional and not lamp.is_directional
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Predict what the shader produces
|
|
78
|
+
|
|
79
|
+
The same math the shader runs, in Python. `shade` returns linear light; `finish`
|
|
80
|
+
is the tone mapping and gamma step:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from vcti.shader.material import EnvironmentLight, finish, shade
|
|
84
|
+
|
|
85
|
+
surface = (0.0, 0.0, 0.0)
|
|
86
|
+
normal = (0.0, 0.0, 1.0)
|
|
87
|
+
eye = (0.0, 0.0, 5.0)
|
|
88
|
+
|
|
89
|
+
# The environment is a sky/ground gradient. `uniform` is the flat case a single
|
|
90
|
+
# ambient colour used to be; `studio` is a bright ceiling over a darker floor.
|
|
91
|
+
environment = EnvironmentLight.studio()
|
|
92
|
+
|
|
93
|
+
lit = shade(surface, normal, eye, paint, environment, [key, lamp])
|
|
94
|
+
assert all(channel > 0.0 for channel in lit)
|
|
95
|
+
|
|
96
|
+
shown = finish(lit)
|
|
97
|
+
assert all(0.0 <= channel < 1.0 for channel in shown)
|
|
98
|
+
assert shade(surface, normal, eye, steel, environment, [key]) != shade(
|
|
99
|
+
surface, normal, eye, paint, environment, [key]
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# A metal is almost entirely a reflection of its surroundings, so which way it
|
|
103
|
+
# faces changes what it shows. A flat ambient could not express that.
|
|
104
|
+
up = shade(surface, (0.0, 0.0, 1.0), eye, steel, environment, [])
|
|
105
|
+
down = shade(surface, (0.0, 0.0, -1.0), (0.0, 0.0, -5.0), steel, environment, [])
|
|
106
|
+
assert up[0] > down[0]
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
A light behind the surface contributes nothing, a metal has no diffuse term,
|
|
110
|
+
and a roughness below the floor shades as the floor. The material refuses a
|
|
111
|
+
factor outside `[0, 1]`:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
try:
|
|
115
|
+
MaterialValues(roughness=1.5)
|
|
116
|
+
except ValueError as error:
|
|
117
|
+
assert "roughness" in str(error)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Set the uniforms
|
|
121
|
+
|
|
122
|
+
The material and the lights are uniforms, one set per draw. The helpers write
|
|
123
|
+
them by the names the specs declare, with the light arrays padded to the size the
|
|
124
|
+
shader is built for:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from vcti.shader.material import MAX_LIGHTS, light_uniforms, material_uniforms
|
|
128
|
+
|
|
129
|
+
uniforms = material_uniforms(paint) | light_uniforms([key, lamp])
|
|
130
|
+
uniforms |= {
|
|
131
|
+
"u_viewPosition": eye,
|
|
132
|
+
"u_environmentSky": environment.sky_color,
|
|
133
|
+
"u_environmentGround": environment.ground_color,
|
|
134
|
+
"u_environmentUp": environment.up,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
assert uniforms["u_materialBaseColor"] == (0.8, 0.1, 0.1)
|
|
138
|
+
assert uniforms["u_lightCount"] == 2
|
|
139
|
+
assert len(uniforms["u_lightPositions"]) == MAX_LIGHTS == 4
|
|
140
|
+
assert uniforms["u_lightPositions"][1] == (1.0, 1.0, 3.0, 1.0) # w = 1: a point light
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### What a build step binds
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from vcti.shader.material import fragment_inputs, fragment_outputs, fragment_uniforms
|
|
147
|
+
|
|
148
|
+
(normal_attribute,) = fragment_inputs()
|
|
149
|
+
assert (normal_attribute.name, normal_attribute.type, normal_attribute.semantic) == (
|
|
150
|
+
"a_normal", "vec3", "normal"
|
|
151
|
+
)
|
|
152
|
+
assert [u.name for u in fragment_uniforms()] == [
|
|
153
|
+
"u_materialBaseColor", "u_materialMetallic", "u_materialRoughness",
|
|
154
|
+
"u_materialOcclusion", "u_materialEmissive",
|
|
155
|
+
"u_viewPosition", "u_environmentSky", "u_environmentGround", "u_environmentUp",
|
|
156
|
+
"u_lightPositions", "u_lightColors", "u_lightCount",
|
|
157
|
+
]
|
|
158
|
+
assert fragment_outputs()[0].name == "fragColor"
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The vertex stage rotates the normal into world space, through whatever
|
|
162
|
+
placement it applies to the position, and forwards it with the world position
|
|
163
|
+
as two varyings. Those varyings are contract rather than spec; see
|
|
164
|
+
[docs/design.md](docs/design.md).
|
|
165
|
+
|
|
166
|
+
### Select a pipeline
|
|
167
|
+
|
|
168
|
+
One tag for the lighting and one naming where the values come from:
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
from vcti.shader.material import (
|
|
172
|
+
DEFINITION,
|
|
173
|
+
DEFINITIONS,
|
|
174
|
+
TABLE_DEFINITION,
|
|
175
|
+
TEXTURE_DEFINITION,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
assert DEFINITION.id == "material"
|
|
179
|
+
assert DEFINITION.role.value == "fragment"
|
|
180
|
+
assert set(DEFINITION.capabilities) == {"material", "material-uniform"}
|
|
181
|
+
assert DEFINITION.slang_modules == (
|
|
182
|
+
"material.slang",
|
|
183
|
+
"material_table.slang",
|
|
184
|
+
"material_texture.slang",
|
|
185
|
+
"material_surface_finish.slang",
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# One definition per source. Same lighting tag, different source tag, so a
|
|
189
|
+
# pipeline asking for one never matches another.
|
|
190
|
+
assert set(TABLE_DEFINITION.capabilities) == {"material", "material-table"}
|
|
191
|
+
assert set(TEXTURE_DEFINITION.capabilities) == {"material", "material-texture"}
|
|
192
|
+
assert DEFINITIONS == (DEFINITION, TABLE_DEFINITION, TEXTURE_DEFINITION)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Vary a surface without authoring anything
|
|
196
|
+
|
|
197
|
+
A **layer** takes a resolved material and returns another, so layers compose
|
|
198
|
+
where sources cannot: this is how a painted casting can also carry a label. The
|
|
199
|
+
surface finish is the first, and it needs no UVs, no tangents and no images —
|
|
200
|
+
just world position:
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
from vcti.shader.material import SurfaceFinish, apply_surface_finish
|
|
204
|
+
|
|
205
|
+
cast_iron = MaterialValues(base_color=(0.35, 0.34, 0.33), roughness=0.7)
|
|
206
|
+
finish = SurfaceFinish.cast()
|
|
207
|
+
|
|
208
|
+
here = apply_surface_finish(cast_iron, (0.0, 0.0, 0.0), finish)
|
|
209
|
+
there = apply_surface_finish(cast_iron, (0.12, 0.04, -0.08), finish)
|
|
210
|
+
assert here.roughness != there.roughness # the surface varies from point to point
|
|
211
|
+
|
|
212
|
+
# The default finish is the identity, which is what "no finish" means.
|
|
213
|
+
assert apply_surface_finish(cast_iron, (1.0, 2.0, 3.0), SurfaceFinish()) == cast_iron
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
*The* finish (`finish`, `tone_map`, `gamma_encode`) is tone mapping and gamma on
|
|
217
|
+
the way to the screen. A *surface* finish is what a surface looks like. They are
|
|
218
|
+
unrelated.
|
|
219
|
+
|
|
220
|
+
### Many materials in one draw
|
|
221
|
+
|
|
222
|
+
The table source replaces the five material uniforms with an id per vertex and
|
|
223
|
+
a table of rows. A client uploads `table_texel_rows(...)` as a three-row
|
|
224
|
+
`rgba32f` texture and binds the ids; the lighting is unchanged:
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
from vcti.shader.material import MaterialValues, pack_row, row_material, table_texel_rows
|
|
228
|
+
|
|
229
|
+
materials = [steel, paint, MaterialValues(base_color=(0.1, 0.1, 0.1), emissive=(0.0, 0.9, 0.2))]
|
|
230
|
+
rows = table_texel_rows(materials)
|
|
231
|
+
|
|
232
|
+
assert len(rows) == 3 # three texels per material
|
|
233
|
+
assert len(rows[0]) == len(materials) * 4 # one column per material
|
|
234
|
+
assert row_material(pack_row(steel)) == steel
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## The lighting
|
|
238
|
+
|
|
239
|
+
| Term | Model |
|
|
240
|
+
|---|---|
|
|
241
|
+
| diffuse | Lambert, `base_color / π`, scaled by `(1 − F)(1 − metallic)` |
|
|
242
|
+
| distribution | GGX / Trowbridge-Reitz on `roughness²` |
|
|
243
|
+
| geometry | Smith, Schlick-GGX form, `k = (roughness + 1)² / 8` |
|
|
244
|
+
| Fresnel | Schlick, `F0 = mix(0.04, base_color, metallic)` |
|
|
245
|
+
| environment | a sky/ground gradient: diffuse along `N`, specular along `reflect(-V, N)`, weighted by roughness-aware Fresnel |
|
|
246
|
+
| tone map | Reinhard `c / (c + 1)` |
|
|
247
|
+
| gamma | `c ^ (1 / 2.2)` — skipped on an sRGB framebuffer |
|
|
248
|
+
| finish | the two composed |
|
|
249
|
+
|
|
250
|
+
Roughness is floored at 0.04. Lights are a fixed array of four; `w` in a
|
|
251
|
+
light's position selects directional (0) or point (1). Everything is in linear
|
|
252
|
+
light until the finish.
|
|
253
|
+
|
|
254
|
+
## API surface
|
|
255
|
+
|
|
256
|
+
| Name | What it is |
|
|
257
|
+
|---|---|
|
|
258
|
+
| `DEFINITION` | the `ShaderDefinition` a build step imports to compose this feature |
|
|
259
|
+
| `SLANG_DIR`, `SLANG_MODULES` | the installed Slang directory, and the module in it |
|
|
260
|
+
| `MaterialValues`, `Light`, `EnvironmentLight` | the resolved material a source produces, a directional or point light, and the sky/ground environment |
|
|
261
|
+
| `shade`, `light_contribution`, `ambient`, `view_direction` | the mirror of the shader's lighting |
|
|
262
|
+
| `tone_map`, `gamma_encode`, `finish` | the output transform, in two independently callable steps |
|
|
263
|
+
| `fresnel_schlick`, `distribution_ggx`, `geometry_smith`, `geometry_schlick_ggx`, `f0` | the BRDF's terms |
|
|
264
|
+
| `material_uniforms`, `light_uniforms` | the uniforms a caller sets, by the declared names |
|
|
265
|
+
| `MAX_LIGHTS`, `MIN_ROUGHNESS`, `DIELECTRIC_F0` | the constants the Slang shares |
|
|
266
|
+
| `fragment_inputs`, `fragment_uniforms`, `fragment_outputs` | what a build step binds |
|
|
267
|
+
| `CAPABILITY`, `SOURCE_CAPABILITY` | the tags |
|
|
268
|
+
|
|
269
|
+
## Dependencies
|
|
270
|
+
|
|
271
|
+
`vcti-shader-base>=2.1.0` is the only runtime dependency — declaring a feature
|
|
272
|
+
is pure data. `vcti-shader-compiler>=4.0.0` and `numpy` are test-only, and a separate
|
|
273
|
+
`gl` extra adds the GL binding the shader tests need to execute rather than
|
|
274
|
+
skip.
|
|
275
|
+
|
|
276
|
+
## Documentation
|
|
277
|
+
|
|
278
|
+
| If you want to… | Read |
|
|
279
|
+
|---|---|
|
|
280
|
+
| Get started using the package | Quick Start above |
|
|
281
|
+
| Understand the lighting, the contract and the decisions behind them | [docs/design.md](docs/design.md) |
|
|
282
|
+
| Know what a material is, what an authored one loses on the way in, and what is planned | [docs/material-model.md](docs/material-model.md) |
|
|
283
|
+
| Understand how authored textures and composed shaders meet, and what was decided | `vcti-shader-base`, `docs/authored-textures.md` — the decisions there settle the texture and sampler declarations, which ship from that package |
|
|
284
|
+
| Navigate or modify the source, including the Slang | [docs/source-guide.md](docs/source-guide.md) |
|
|
285
|
+
|
|
286
|
+
The full API reference is generated from the source docstrings and published in
|
|
287
|
+
the unified VCollab docs.
|