# Functional Grading Guide This guide is a practical reference for defining **spatial gradients** in OpenVCAD. Gradients are the core mechanism for creating functionally graded designs — parts whose material properties, colors, or compositions change continuously through space. ## Prerequisites This guide assumes you have completed the [Getting Started Guide](getting-started.md), in particular: - **Lesson 4** (Attributes / `FloatAttribute`) - **Lesson 5** (Colors / `Vec4Attribute`) - **Lesson 6** (Volume Fractions / `VolumeFractionsAttribute`) If you have not worked through those lessons, please do so first. They introduce the attribute system and show how constant and simple gradient expressions are attached to geometry. ## Setup All scripts in this guide use **matplotlib** and **numpy** to plot the gradient functions alongside the 3D renders. Install them if you haven't already: ```bash pip install matplotlib numpy ``` ## Running the Examples Scripts are located in `examples/2_gradients/`. Run any script directly: ```bash python 01_linear_gradients.py ``` Each script will: 1. Open the interactive OpenVCAD render preview (close the window to continue). 2. Display a matplotlib plot of the gradient expressions used. > **Note:** To view an attribute in the render window, select it from **Object -> Selected Attribute**. --- ## Building Parametric Expressions with Python f-Strings All gradient expressions in OpenVCAD are **strings** that get compiled at runtime. To make your designs parametric, you can use Python f-strings to inject variable values into expression strings: ```python slope = 0.18 offset = 5.5 expr = f'{slope} * z + {offset}' # becomes: '0.18 * z + 5.5' ``` A few tips: - **Numeric formatting**: Use `f'{val:.4f}'` to control decimal places when precision matters. - **Curly braces**: The `{variable}` syntax is replaced by the variable's value. Everything outside braces is literal. - **Mixing variables**: You can combine Python parameters with OpenVCAD coordinate variables freely: `f'{amp} * sin({freq} * x)'`. For the full list of available math operators, functions (`sin`, `exp`, `clamp`, etc.), and coordinate variables, see [Math expressions in OpenVCAD](math-expressions.md). --- ## Coordinate Systems at a Glance Every expression can use any combination of these spatial variables: | System | Variables | Description | |---|---|---| | Cartesian | `x`, `y`, `z` | Standard axes | | Cylindrical | `rho`, `phic`, `z` | Distance from Z axis, azimuthal angle, height | | Spherical | `r`, `theta`, `phis` | Distance from origin, azimuthal angle, polar angle | | Signed Distance | `d` | Distance to the object surface (negative = inside, 0 = surface, positive = outside) | You can mix variables from different systems in a single expression. See [Math expressions in OpenVCAD](math-expressions.md) for complete definitions and all available operators. --- ## Lesson 1: Linear Gradients The simplest gradient is a linear function of a single spatial variable. The general form is: **f(x) = slope * x + offset** This maps any Cartesian axis to a linearly varying attribute value. A **power-law** gradient uses the form **(normalized_position)^n** to create a non-uniform ramp where the exponent `n` controls how quickly the value grows. In this example, both gradients are attached to the same rectangular prism along the X axis: - **Modulus** (linear): maps x in \[-20, +20\] to \[1, 10\] MPa - **Density** (power law with n=3): same range, but the value stays low for most of the span and rises sharply near the end ```python import os import pyvcad as pv import pyvcad_rendering as viz import matplotlib.pyplot as plt import numpy as np length = 40.0 width = 10.0 height = 10.0 half_len = length / 2.0 # Linear: maps x in [-20, +20] to [1, 10] mod_min = 1.0 mod_max = 10.0 slope = (mod_max - mod_min) / length offset = (mod_max + mod_min) / 2.0 modulus_expr = f'{slope}*x + {offset}' # Power law: same range, exponent n=3 n = 3.0 power_law_expr = f'({mod_min} + ({mod_max} - {mod_min}) * ((x + {half_len}) / {length}) ^ {n})' prism = pv.RectPrism(pv.Vec3(0, 0, 0), pv.Vec3(length, width, height)) prism.set_attribute(pv.DefaultAttributes.MODULUS, pv.FloatAttribute(modulus_expr)) prism.set_attribute(pv.DefaultAttributes.DENSITY, pv.FloatAttribute(power_law_expr)) root = prism viz.Render(root) ```

Plot

Linear vs Power Law

Render (modulus)

Linear Modulus

Render (density)

Power Law Density
--- ## Lesson 2: Non-Linear Gradients Non-linear gradients provide smooth transitions, bell-shaped profiles, and exponential falloffs that are common in engineering applications. This example attaches three different non-linear gradients to the same prism: | Gradient | Expression | Attribute | |---|---|---| | **Sigmoid** | `min + (max - min) / (1 + exp(-k * x))` | Modulus | | **Gaussian** | `A * exp(-(x^2) / (2 * sigma^2))` | Temperature | | **Exponential Decay** | `A * exp(-rate * (x + half_len))` | Density | The **sigmoid** creates a smooth step transition — useful for transitioning between two material regions without a hard boundary. The steepness parameter `k` controls how sharp the transition is. The **Gaussian** creates a bell-curve peak centered at the origin — useful for localized reinforcement or heating profiles. The **exponential decay** starts high and drops off — useful for modeling diffusion or attenuation from a surface. ```python import os import pyvcad as pv import pyvcad_rendering as viz import matplotlib.pyplot as plt import numpy as np length = 50.0 half_len = length / 2.0 # Sigmoid: smooth step along X k = 0.2 sig_min, sig_max = 1.0, 10.0 sigmoid_expr = f'{sig_min} + ({sig_max} - {sig_min}) / (1 + exp(-{k} * x))' # Gaussian: bell curve centered at origin amplitude, sigma = 10.0, 10.0 gaussian_expr = f'{amplitude} * exp(-(x^2) / (2 * {sigma}^2))' # Exponential decay from left edge decay_rate, exp_amplitude = 0.06, 10.0 exponential_expr = f'{exp_amplitude} * exp(-{decay_rate} * (x + {half_len}))' prism = pv.RectPrism(pv.Vec3(0, 0, 0), pv.Vec3(length, 10, 10)) prism.set_attribute(pv.DefaultAttributes.MODULUS, pv.FloatAttribute(sigmoid_expr)) prism.set_attribute(pv.DefaultAttributes.TEMPERATURE, pv.FloatAttribute(gaussian_expr)) prism.set_attribute(pv.DefaultAttributes.DENSITY, pv.FloatAttribute(exponential_expr)) root = prism viz.Render(root) ```

Plot

Non-Linear Gradients

Render (modulus — sigmoid)

Sigmoid Modulus

Render (temperature — Gaussian)

Gaussian Temperature

Render (density — exponential)

Exponential Density
--- ## Lesson 3: Periodic Gradients Periodic gradients use trigonometric functions to create oscillating patterns. They are useful for alternating material layers, wave-like stiffness profiles, and decorative color bands. This example applies two periodic gradients to a rectangular prism: - **Sinusoidal modulus**: oscillates along X with a base value and amplitude - **Color bands**: uses phase-shifted sine waves on the red and blue channels to create alternating stripes ```python import os import pyvcad as pv import pyvcad_rendering as viz import matplotlib.pyplot as plt import numpy as np length = 50.0 half_len = length / 2.0 # Sinusoidal modulus frequency = 0.5 mod_base, mod_amplitude = 5.0, 4.0 sin_expr = f'{mod_base} + {mod_amplitude} * sin({frequency} * x)' # Color bands using phase-shifted sine color_freq = 0.8 r_expr = f'0.5 * sin({color_freq} * x) + 0.5' g_expr = '0.2' b_expr = f'0.5 * sin({color_freq} * x + 3.14159) + 0.5' a_expr = '1.0' prism = pv.RectPrism(pv.Vec3(0, 0, 0), pv.Vec3(length, 10, 10)) prism.set_attribute(pv.DefaultAttributes.MODULUS, pv.FloatAttribute(sin_expr)) color_attr = pv.Vec4Attribute(r_expr, g_expr, b_expr, a_expr) prism.set_attribute(pv.DefaultAttributes.COLOR_RGBA, color_attr) root = prism viz.Render(root) ```

Plot

Periodic Gradients

Render (modulus)

Sinusoidal Modulus

Render (color)

Periodic Color Bands
--- ## Lesson 4: Multi-Axis Gradients Gradients can be functions of two or more spatial variables simultaneously. This creates 2D patterns across the surface of an object. This example uses a flat square slab and applies: - **Diagonal color gradient**: `f(x, y) = (x + y)` normalized to \[0, 1\], producing a corner-to-corner color sweep - **Radial modulus**: `sqrt(x^2 + y^2)` normalized, producing a bull's-eye pattern from center to edges - **Radial volume fractions**: two-material blend that transitions from one material at the center to another at the edges ```python import os import pyvcad as pv import pyvcad_rendering as viz import numpy as np side = 40.0 half_side = side / 2.0 max_rho = half_side * np.sqrt(2) materials = pv.default_materials # Diagonal color: (x + y) normalized diag_r = f'clamp((x + y + {side}) / ({2 * side}), 0, 1)' diag_g = f'1.0 - clamp((x + y + {side}) / ({2 * side}), 0, 1)' # Radial modulus from center radial_expr = f'clamp(sqrt(x^2 + y^2) / {max_rho}, 0, 1)' # Volume fractions: radial blend vf_a = f'clamp(sqrt(x^2 + y^2) / {half_side}, 0, 1)' vf_b = f'1.0 - clamp(sqrt(x^2 + y^2) / {half_side}, 0, 1)' slab = pv.RectPrism(pv.Vec3(0, 0, 0), pv.Vec3(side, side, 10)) slab.set_attribute(pv.DefaultAttributes.COLOR_RGBA, pv.Vec4Attribute(diag_r, diag_g, '0.3', '1.0')) slab.set_attribute(pv.DefaultAttributes.MODULUS, pv.FloatAttribute(radial_expr)) slab.set_attribute(pv.DefaultAttributes.VOLUME_FRACTIONS, pv.VolumeFractionsAttribute([ (vf_a, materials.id("red")), (vf_b, materials.id("blue")) ])) root = slab viz.Render(root, materials) ```

Plot (2D heatmaps)

Multi-Axis Gradients

Render (color — diagonal)

Diagonal Color

Render (modulus — radial)

Radial Modulus

Render (volume fractions — radial)

Radial Volume Fractions
--- ## Lesson 5: Signed-Distance Gradients The special variable `d` represents the **signed distance** to the object's implicit surface: - **d < 0**: inside the object - **d = 0**: on the surface - **d > 0**: outside the object (not sampled during rendering) This makes `d` ideal for creating **surface-following** gradients — stiff skins, colored shells, or material transitions that conform to the object's shape regardless of its geometry. This example applies three `d`-based gradients to a sphere: - **Skin modulus**: exponential decay from surface inward — high stiffness at the shell, soft in the core - **Shell color**: red on the surface transitioning to blue in the interior - **Volume fractions**: stiff material at the skin, soft material in the core > **Note:** The renders below show a clipped cross-section to reveal the internal gradient. ```python import os import pyvcad as pv import pyvcad_rendering as viz radius = 15.0 materials = pv.default_materials # Skin modulus: high at surface, decays inward skin_thickness = 3.0 skin_min, skin_max = 1.0, 10.0 skin_expr = f'{skin_min} + ({skin_max} - {skin_min}) * exp(d / {skin_thickness})' # Shell color: red at surface, blue in core shell_t = 2.0 shell_r = f'clamp(1.0 + d / {shell_t}, 0, 1)' shell_b = f'clamp(-d / {shell_t}, 0, 1)' # Volume fractions: skin vs core vf_skin = f'clamp(1.0 + d / {skin_thickness}, 0, 1)' vf_core = f'1.0 - clamp(1.0 + d / {skin_thickness}, 0, 1)' sphere = pv.Sphere(pv.Vec3(0, 0, 0), radius) sphere.set_attribute(pv.DefaultAttributes.MODULUS, pv.FloatAttribute(skin_expr)) sphere.set_attribute(pv.DefaultAttributes.COLOR_RGBA, pv.Vec4Attribute(shell_r, '0.1', shell_b, '1.0')) sphere.set_attribute(pv.DefaultAttributes.VOLUME_FRACTIONS, pv.VolumeFractionsAttribute([ (vf_skin, materials.id("red")), (vf_core, materials.id("blue")) ])) root = sphere viz.Render(root, materials) ```

Plot

Signed Distance Gradients

Render (modulus — clipped)

Skin Modulus

Render (color — clipped)

Shell Color

Render (volume fractions — clipped)

Skin Volume Fractions
--- ## Lesson 6: Cylindrical Coordinate Gradients Cylindrical coordinates are natural for objects with rotational symmetry about the Z axis. The key variables are: - `rho` — radial distance from the Z axis - `phic` — azimuthal angle (radians, -pi to +pi) - `z` — height (same as Cartesian z) This example applies gradients to a cylinder: - **Radial modulus**: stiffness increases linearly from center (`rho=0`) to edge (`rho=R`) - **Angular color**: a color sweep around the circumference using `phic` - **Radial volume fractions**: two-material blend from core to edge > **Note:** The modulus and volume fraction renders below show a clipped cross-section to reveal the internal radial gradient. ```python import os import pyvcad as pv import pyvcad_rendering as viz cyl_radius = 15.0 cyl_height = 30.0 materials = pv.default_materials # Radial modulus mod_min, mod_max = 1.0, 10.0 radial_expr = f'{mod_min} + ({mod_max} - {mod_min}) * (rho / {cyl_radius})' # Angular color sweep r_expr = f'clamp((phic + 3.14159) / (2 * 3.14159), 0, 1)' g_expr = f'clamp(1.0 - (phic + 3.14159) / (2 * 3.14159), 0, 1)' # Volume fractions: radial blend vf_edge = f'clamp(rho / {cyl_radius}, 0, 1)' vf_core = f'1.0 - clamp(rho / {cyl_radius}, 0, 1)' cylinder = pv.Cylinder(pv.Vec3(0, 0, 0), cyl_radius, cyl_height) cylinder.set_attribute(pv.DefaultAttributes.MODULUS, pv.FloatAttribute(radial_expr)) cylinder.set_attribute(pv.DefaultAttributes.COLOR_RGBA, pv.Vec4Attribute(r_expr, g_expr, '0.3', '1.0')) cylinder.set_attribute(pv.DefaultAttributes.VOLUME_FRACTIONS, pv.VolumeFractionsAttribute([ (vf_edge, materials.id("red")), (vf_core, materials.id("blue")) ])) root = cylinder viz.Render(root, materials) ```

Plot

Cylindrical Gradients

Render (modulus — clipped)

Radial Modulus

Render (color — angular)

Angular Color

Render (volume fractions — clipped)

Radial Volume Fractions
--- ## Lesson 7: Spherical Coordinate Gradients Spherical coordinates are ideal for objects with radial symmetry from a central point. The key variables are: - `r` — radial distance from the origin - `theta` — azimuthal angle in the XY plane (radians, -pi to +pi) - `phis` — polar angle from the +Z axis (radians, 0 to pi) This example applies gradients to a sphere: - **Radial modulus**: stiffness increases from center (`r=0`) to surface (`r=R`) - **Polar color**: color varies from the north pole (`phis=0`) to the south pole (`phis=pi`) - **Radial volume fractions**: two-material blend from center to surface > **Note:** The modulus and volume fraction renders below show a clipped cross-section to reveal the internal radial gradient. ```python import os import pyvcad as pv import pyvcad_rendering as viz sph_radius = 15.0 materials = pv.default_materials # Radial modulus mod_min, mod_max = 1.0, 10.0 radial_expr = f'{mod_min} + ({mod_max} - {mod_min}) * (r / {sph_radius})' # Polar angle color: north=blue, south=red r_expr = f'clamp(phis / 3.14159, 0, 1)' b_expr = f'clamp(1.0 - phis / 3.14159, 0, 1)' # Volume fractions: center vs surface vf_outer = f'clamp(r / {sph_radius}, 0, 1)' vf_inner = f'1.0 - clamp(r / {sph_radius}, 0, 1)' sphere = pv.Sphere(pv.Vec3(0, 0, 0), sph_radius) sphere.set_attribute(pv.DefaultAttributes.MODULUS, pv.FloatAttribute(radial_expr)) sphere.set_attribute(pv.DefaultAttributes.COLOR_RGBA, pv.Vec4Attribute(r_expr, '0.2', b_expr, '1.0')) sphere.set_attribute(pv.DefaultAttributes.VOLUME_FRACTIONS, pv.VolumeFractionsAttribute([ (vf_outer, materials.id("red")), (vf_inner, materials.id("blue")) ])) root = sphere viz.Render(root, materials) ```

Plot

Spherical Gradients

Render (modulus — clipped)

Radial Modulus

Render (color — polar)

Polar Color

Render (volume fractions — clipped)

Radial Volume Fractions
--- ## Summary: Gradient Toolbox | Gradient Type | Expression Pattern | Best For | |---|---|---| | **Linear** | `m * x + b` | Uniform property ramps | | **Power Law** | `((x - min) / range) ^ n` | Tunable non-uniform ramps | | **Sigmoid** | `1 / (1 + exp(-k * x))` | Smooth step transitions | | **Gaussian** | `exp(-(x^2) / (2*s^2))` | Localized peaks | | **Exponential** | `exp(-rate * x)` | Decay / attenuation | | **Sinusoidal** | `sin(freq * x)` | Periodic layers / bands | | **Multi-axis** | `f(x, y)` | 2D patterns, diagonal sweeps | | **Signed distance** | `f(d)` | Surface-following skins | | **Cylindrical** | `f(rho)`, `f(phic)` | Radial and angular patterns | | **Spherical** | `f(r)`, `f(phis)` | Radial and polar patterns | For the complete list of available operators, functions, and coordinate variables, see [Math expressions in OpenVCAD](math-expressions.md).