simvx.core.graphics.texture¶
Texture – an image the engine owns, with an identity, a version and an update path.
Module Contents¶
Classes¶
An image resource: an identity, a version, and an explicit way to change it. |
Functions¶
Whether source is a node that publishes a live offscreen image. |
Data¶
API¶
- simvx.core.graphics.texture.__all__¶
[‘Texture’, ‘is_live_target’]
- simvx.core.graphics.texture.is_live_target(source: Any) bool[source]¶
Whether source is a node that publishes a live offscreen image.
The one question every consumer that ROUTES a live feed asks: a sprite resolving its bindless slot each draw, a material binding an albedo, a backend skipping the lazy path/bytes load. Both kinds answer it – a
- Class:
~simvx.core.SubViewport, which renders its own subtree, and a- Class:
~simvx.core.RenderView, which renders the main scene through a second camera – so no consumer has to name one kind and mean both.
- class simvx.core.graphics.texture.Texture(source: Any, *, filter: str | None = None, colour_space: str | None = None, premultiply_alpha: bool | None = None, mipmaps: bool | None = None)[source]¶
An image resource: an identity, a version, and an explicit way to change it.
A raw texture source (a path, encoded bytes, an RGBA ndarray) answers neither “is this the same image as before” nor “has it changed”. A
Textureanswers both: its identity is stable across reassignment, and- Meth:
updatebumps :attr:version, which is what drives a re-upload.
Wrapping is optional. Every node property that takes a texture still accepts a plain path, bytes or ndarray, and always will – that is the short spelling and it is not going away. Reach for
Texturewhen the image CHANGES after it is first shown::heightmap = Texture(pixels) # RGBA uint8 (H, W, 4) sprite.texture = heightmap ... pixels[:] = erode(pixels) heightmap.update() # re-uploads in place, same slot icon = Texture("ui/icon.png", filter="nearest") sprite.texture = icon- Meth:
updatemay also swap the pixels outright (heightmap.update(new_pixels)), which is the cheaper route when the replacement is a fresh array rather than an in-place edit. What it cannot do is change the SIZE: that is fixed when the texture is constructed, and a differently sized image is a different texture. Build one and assign it over the old, which re-resolves every consumer.
The sampling settings are fixed at construction: they select which backend slot the image lands in, so changing one is a different texture, not an update to this one. Each one you leave out stays unset, and an unset setting defers to whatever the consumer asks for – the sprite’s
filter, the material map role’s colour space and mip chain. Only a setting you state here overrides that, and it overrides it everywhere the texture is used::sprite.texture = Texture(pixels) # follows sprite.filter sprite.texture = Texture(pixels, filter="nearest") # crisp, wherever it is drawn
One resource owns one backend slot, so it carries one sampling. Two uses that need different ones cannot both be served: the first wins and the second is reported. Give each its own
Textureover the same source.A scene file can carry a texture whose source is a FILE: it is written as the constructor call that rebuilds it, with exactly the sampling settings that were stated, and one held by several nodes is written once into a local so the reloaded scene shares it the same way. Pixels in memory have no source form – a scene is code, not an image container – so a texture over an array or over encoded bytes stops the save and is named in the report. Build that one in
on_ready()and assign it there, which is where code belongs anyway.A texture owns its image on the backend outright. Two resources over the same file do not share it, nor does one share with the same file assigned as a plain path, so :meth:
updatecan never change an image something else asked for; the price is one extra copy on the GPU when a file is used both ways. Ownership runs the other way too: the backend slot is handed back when the resource is collected, so building a texture per level or per entity does not exhaust the texture table.Initialization
Wrap source as an owned texture.
Every sampling setting is optional, and omitting one is not the same as choosing its usual default: an omitted setting follows the consumer’s own request, so the same texture is sampled the way a plain path or array would have been in that position. State one only to override that everywhere the texture is used.
Args: source: File path, encoded image bytes, or an RGBA
uint8ndarray of shape(H, W, 4)– the same set every texture property accepts. A live offscreen target (aSubViewport, aRenderView) is refused: it is a texture source already, so assign the node itself where a live feed is sampled – either kind tosprite.textureor to analbedo_map. filter: Sampler filter,"linear"or"nearest". Unset follows the consumer (a sprite’sfilterproperty). colour_space:"srgb"(perceptual colour) or"linear"(data maps: normals, metallic-roughness, AO). Unset follows the consumer, which is what keeps a texture used as anormal_maplinear without being told twice. premultiply_alpha: Multiply RGB by alpha before upload. Unset follows the consumer. mipmaps: Generate a runtime mip chain. Unset follows the consumer, which is what keeps analbedo_map’s chain.- __slots__¶
(‘_uid’, ‘_source’, ‘_version’, ‘_size’, ‘_filter’, ‘_colour_space’, ‘_premultiply_alpha’, ‘_mipmaps…
- property filter: str | None[source]¶
Sampler filter, or
Nonewhen it follows the consumer’s request.
- property colour_space: str | None[source]¶
Colour space, or
Nonewhen it follows the consumer’s request.
- property premultiply_alpha: bool | None[source]¶
Premultiplication, or
Nonewhen it follows the consumer’s request.
- property mipmaps: bool | None[source]¶
Mip-chain request, or
Nonewhen it follows the consumer’s request.
- property size: tuple[int, int][source]¶
Native pixel
(width, height), or(0, 0)when it cannot be read.Once known, the size never changes: :meth:
updaterefuses pixels of a different size, so anything that measures a texture once – a sprite drawing at native size, a nine-patch normalising its border UVs – stays correct without watching for changes.Resolved without the GPU (an ndarray’s shape, or the image’s header), so it answers identically on every backend and before the first upload. Only a source no header reader here can measure answers
(0, 0), and only until a backend decodes it and reports the dimensions back – which on a runtime with no imaging library means the first upload.
- update(pixels: Any = None) None[source]¶
Replace this texture’s pixels, keeping its size, its slot and its identity.
This is the change-the-image path and only that. Call it after mutating the pixel array in place, or pass pixels to swap the array outright::
pixels[:] = erode(pixels) heightmap.update() # or: heightmap.update(new_pixels)
Either way :attr:
versionmoves and every backend holding a slot for this resource writes the new pixels into that SAME slot, so anything already drawing the texture keeps its handle and shows the new image on the next frame. Only consumers of this resource are affected: the slot is the resource’s own, never one shared with a plain path or array.Resizing is a different operation, and this is not it. Consumers measure a texture when they resolve it – a sprite sizes its quad to the native pixels, a :class:
~simvx.core.NinePatchRectnormalises every border UV by them – so a slot that changed shape underneath them would draw the new image at the old scale, or sample the wrong region, with nothing to say so. A differently sized image is a different texture::sprite.texture = Texture(bigger_pixels)
Reassignment re-resolves every consumer, which is what makes it the right answer rather than a workaround.
Args: pixels: Replacement source of the same size and pixel format, or
Noneto re-upload the current one after an in-place edit.Raises: ValueError: If the pixels differ in size or format from this texture’s. Nothing is mutated and no backend is told, so a refused update leaves every slot exactly as it was. TypeError: If pixels is a live offscreen target, refused here for the same reason the constructor refuses one – and equally without mutating anything. A live target measures as a real size, so a size check cannot be trusted to catch one: when the sizes match it passes straight through, and when they do not it is reported as a resize.
- __copy__() simvx.core.graphics.texture.Texture[source]¶
- __deepcopy__(memo: dict) simvx.core.graphics.texture.Texture[source]¶
- __getstate__() tuple[dict | None, dict][source]¶
Pickle everything but the process-local state.
_managersholds weak references to live backend managers, which no pickle can carry;_uidis only meaningful in the process that minted it. Both are rebuilt by :meth:__setstate__.