Skip to content

18 - Spatial textures and property fields

Use UV images, georeferenced rasters, measured spectra, RGB imagery, temperature, and emissivity as spatially varying Property inputs.


πŸ“‹ What a texture means in LESS

A LESS texture is a spatial source for one named physical parameter. It is not a complete material by itself. Assign the texture to a Property parameter, then assign that property to a terrain or object component:

roof = less.Lambertian(
    reflectance=less.RGBTexture("roof_albedo.png"))
building.set_property("roof", roof)

This keeps the physical meaning explicit. The same image declaration can be a reflectance source, but it cannot silently become temperature, emissivity, or transmittance.

Input Public type Typical parameter
One physical value per pixel ScalarTexture temperature
Measured spectral samples SpectralTexture reflectance, transmittance, emissivity
Three-channel image RGBTexture empirical visible reflectance reconstruction

Constants and textures use the same Property definitions. A component can therefore change from a constant reflectance to a spatial reflectance without changing the sensor or simulation workflow.

πŸ“ Choose the spatial mapping

Mapping Use it for Required information
mapping="uv" OBJ models, buildings, vehicles, photogrammetry meshes OBJ vt coordinates
mapping=less.WorldXY() GeoTIFF, DEM-aligned products, land-cover and LST rasters raster CRS/affine and scene.georeference

UV textures default to wrap="repeat". World-XY textures default to wrap="clamp" and require an explicit policy when the scene extends outside the raster.

⚠️ Do not use mapping="uv" for terrain. Terrain has no imported OBJ UV atlas. LESS reports this as an error instead of sampling an arbitrary point.

πŸ“š Use measured hyperspectral reflectance

The raster stores linear surface reflectance with shape (height, width, bands). Wavelength samples may be irregularly spaced.

import less

leaf = less.Lambertian(
    reflectance=0.10,
    back_reflectance=0.12,
    transmittance=0.05,
).with_fields(
    reflectance=less.SpectralTexture(
        "leaf_front.tif",
        wavelengths="metadata",
        mapping="uv",
        filter="trilinear",
        nodata="component_default",
    ),
    back_reflectance=less.SpectralTexture(
        "leaf_back.tif",
        wavelengths="metadata",
        mapping="uv",
    ),
    transmittance=less.SpectralTexture(
        "leaf_transmittance.tif",
        wavelengths="metadata",
        mapping="uv",
    ),
)
tree.set_property("leaves", leaf)

GeoTIFF wavelength metadata can come from per-band wavelength tags, band descriptions, or ENVI wavelength tags. For .npy, pass wavelengths explicitly; for .npz, store arrays named data and wavelengths.

soil = less.SpectralTexture(
    "soil_reflectance.npy",
    wavelengths=(450.0, 550.0, 670.0, 865.0, 1610.0),
    mapping="uv",
)

Values with units="fraction" must be linear physical fractions in [0, 1]. Do not apply sRGB gamma to measured reflectance.

πŸ“š Use an RGB image

RGBTexture converts sRGB to linear RGB and reconstructs an empirical smooth visible spectrum with spectral_reconstruction="jh19":

facade = less.Lambertian(
    reflectance=less.RGBTexture(
        "facade.png",
        color_space="srgb",
        spectral_reconstruction="jh19",
        mapping="uv",
    ))
building.set_property("facade", facade)

⚠️ RGB reconstruction is an estimate, not measured hyperspectral reflectance. A photograph or satellite RGB composite may already contain illumination, shadows, atmosphere, tone mapping, and compression. Use a surface-reflectance product for quantitative simulation whenever possible.

The JH19 reconstruction is defined only in the visible domain used by the implementation. A sensor wavelength outside that domain raises an error unless you configure an explicit fractional outside_spectral_domain value. An RGB texture should therefore not be treated as a 905 nm or 1064 nm LiDAR spectrum.

πŸ“ Place a georeferenced raster

For world-XY mapping, the scene and raster must use the same projected CRS in metres. The scene origin is the world coordinate of its local (0, 0) corner.

import less

scene = less.Scene()
scene.size = (1000.0, 1000.0)
scene.georeference = {
    "crs": "EPSG:32650",
    "origin": (500000.0, 4000000.0),
    "units": "m",
}

terrain_reflectance = less.SpectralTexture(
    "surface_reflectance.tif",
    wavelengths="metadata",
    mapping=less.WorldXY(outside="error"),
    filter="bilinear",
    nodata="component_default",
)
terrain_property = less.Lambertian(reflectance=0.15).with_field(
    "reflectance", terrain_reflectance)
scene.terrain = less.Terrain(property=terrain_property)

LESS reads GeoTIFF band-first storage, applies valid scale/offset metadata, uses raster masks as nodata. A CRS mismatch fails closed; LESS does not reproject a scientific raster implicitly.

Land-cover products contain class codes rather than reflectance. Use class_values to map each class explicitly to a physical scalar or spectrum; unlisted classes follow the selected nodata policy:

worldcover = less.SpectralTexture(
    "worldcover.tif",
    wavelengths=(550.0, 850.0),
    class_values={
        10: (0.08, 0.45),  # example only; use your study's spectral library
        20: (0.12, 0.38),
        80: (0.04, 0.02),
    },
    mapping=less.WorldXY(),
    filter="nearest",
    nodata="component_default",
)

Categorical lookup requires explicit wavelengths and cannot also apply a GeoTIFF scale/offset. LESS does not ship an β€œauthoritative WorldCover spectrum”: reflectance for one class varies with location, season, and water content.

Statistical TurbidBoundary leaves have no OBJ UV coordinates. They may use a world-XY Field when that workflow is available, but cannot use a surface UV atlas.

πŸ“š Map temperature and emissivity

Temperature is a scalar field in kelvin. Emissivity may be constant or spectral:

thermal = less.ThermalProperty(
    temperature=300.0,
    emissivity=0.95,
).with_fields(
    temperature=less.ScalarTexture(
        "lst.tif",
        units="K",
        mapping=less.WorldXY(outside="error"),
        nodata="component_default",
    ),
    emissivity=less.SpectralTexture(
        "emissivity.tif",
        wavelengths=(10600.0, 12000.0),
        mapping=less.WorldXY(outside="error"),
        nodata="component_default",
    ),
)
scene.terrain.set_property(thermal)

Use land-surface temperature, not top-of-atmosphere brightness temperature. Missing pixels with nodata="component_default" use the constant value on the Property. Keep the constant in the Property constructor and attach the texture with with_field() or with_fields(), as shown above.

πŸ”„ Reuse one asset with multiple sensors

Texture wavelengths describe the material data, not output bands. Each sensor selects its own wavelengths or SRFs; LESS evaluates the property at the required spectral samples and integrates the requested response.

rgb = scene.simulate(less.OpticalImager(
    less.Orthographic(image_size=512),
    bands=[450.0, 550.0, 650.0],
))

hyperspectral = scene.simulate(less.OpticalImager(
    less.Orthographic(image_size=512),
    bands=list(range(400, 1001, 5)),
))

The scene is persistent: unchanged geometry and texture resources are reused. Changing sensor bands does not require editing the OBJ or duplicating the texture.

πŸ’Ύ Control filtering, nodata, and residency

Option Values Meaning
filter nearest, bilinear, trilinear Spatial reconstruction and MIP filtering
wrap clamp, repeat Sampling beyond the UV extent
nodata error, component_default, number Missing-pixel policy
missing_uv error, component_default Missing mesh-UV policy

trilinear is currently supported for optical and thermal imaging, where LESS can select a texture level from the area covered by each image pixel. Use nearest or bilinear for BRF, RadiationField, irradiance, LiDAR, energy-balance, photosynthesis, and SIF workflows. LESS reports an unsupported combination before starting the simulation and does not silently choose a different filter.

You can query the complete scene/request combination before simulation:

if not scene.can_use(sensor, explain=True):
    raise RuntimeError("This simulation and spatial-texture combination is unavailable")
image = scene.simulate(sensor)

Unresolved turbid boundaries have no surface UV coordinate. Their spatial Fields must use WorldXY; ordinary mesh surfaces in the same scene may still use UV textures. | outside_spectral_domain | error, fraction | Wavelength-domain policy | | raster_bands | all, one-based band indices | Select GeoTIFF bands | | scale, offset | metadata, scalar, or per-band values | Convert integer remote-sensing DN to physical values | | class_values | {class code: scalar or spectrum} | Convert a categorical raster to a physical Field |

Large world-XY GeoTIFFs use a scene-footprint window. Set a hard resident budget and tile alignment on the scene:

scene.texture_memory_budget_bytes = 1024 * 1024 * 1024
scene.texture_tile_size = 256

If the complete raster area needed by the scene cannot fit the budget, LESS raises MemoryError; it does not silently downsample the raster.

πŸ’Ύ Save and move a textured scene

Reference saves store absolute texture paths. Packed saves copy textures into the .less archive with content-derived names, so the archive remains valid after the original raster is moved or deleted:

scene.save("survey.less", pack=True)
restored = less.Scene.load("survey.less")

Packing also distinguishes different source files with the same filename and deduplicates repeated references to the same source.

πŸ”§ Troubleshooting

spectral texture wavelength metadata is missing

The file has no readable wavelength tags. Pass an explicit increasing wavelengths=(...) sequence, or add per-band/ENVI wavelength metadata.

world_xy geospatial textures require scene.georeference

Set scene.georeference with matching projected crs, origin, and metre units before building the scene.

texture contains nodata while nodata='error'

Use nodata="component_default" when the Property has a scientifically valid constant default, or provide an explicit finite fallback value.

requested wavelength is outside the texture spectral domain

Use measured data covering the sensor domain. Configure a numeric fallback only when that value has a defensible physical meaning.

  • less.Texture2D
  • less.SpectralTexture
  • less.ScalarTexture
  • less.UVMapping
  • less.WorldXYMapping
  • less.Property.with_field() and less.Property.with_fields()