Every function the engine has, one page each, grouped by module.
Nothing here goes by that name.
The prose half of the docs - read one front to back, or take just the part you need. Then go and make something.
Nothing here goes by that name.
NatinyAudioListener natiny_audio_create_listener(void);
Creates a listener entity and makes it the active listener.
| Type | Description |
|---|---|
NatinyAudioListener |
Listener handle, also usable as a NatinyEntity; 0 on failure |
NatinyAudioListener listener = natiny_audio_create_listener();
natiny_entity_*. Its
forward direction is local -Z; scale does not affect listening direction.natiny.audio.create_listener() -> listener
Creates a listener entity and makes it the active listener.
| Type | Description |
|---|---|
number |
Listener handle, also usable as an entity handle |
local listener = natiny.audio.create_listener()
natiny.entity. Its
forward direction is local -Z; scale does not affect listening direction.NatinyAudioSource natiny_audio_create_source(NatinyClip clip);
Creates a stopped sound-source entity that plays a clip.
| Name | Type | Description |
|---|---|---|
clip |
NatinyClip |
Clip whose samples this source plays |
| Type | Description |
|---|---|
NatinyAudioSource |
Source handle, also usable as a NatinyEntity; 0 on failure |
NatinyAudioSource source = natiny_audio_create_source(clip);
1.0f.
Its attenuation defaults to NATINY_AUDIO_ATTENUATION_INVERSE, distances
1.0f and 100.0f, and rolloff 1.0f.natiny_entity_*.0 when the voice limit is exhausted or allocation fails.natiny.audio.create_source(clip) -> source
Creates a stopped sound-source entity that plays a clip.
| Name | Type | Description |
|---|---|---|
clip |
number |
Clip handle whose samples this source plays |
| Type | Description |
|---|---|
number |
Source handle, also usable as an entity handle |
local source = natiny.audio.create_source(clip)
1.0.
Its attenuation defaults to ATTENUATION_INVERSE, distances 1.0 and
100.0, and rolloff 1.0.natiny.entity.0
as a clip handle.int natiny_audio_init(void);
Opens the audio output device and starts the mixer.
| Type | Description |
|---|---|
int |
1 when audio output opened, otherwise 0 |
int audio_available = natiny_audio_init();
natiny_backend_init, because backend initialization resets
all engine state. Repeated successful calls do nothing and return 1.natiny_backend_loop frame callback.natiny.audio.init() -> ok
Opens the audio output device and starts the mixer.
| Type | Description |
|---|---|
boolean |
true when audio output opened, otherwise false |
local audio_available = natiny.audio.init()
natiny.backend.init, because backend initialization resets
all engine state. Repeated successful calls do nothing and return true.natiny.backend.loop frame callback.void natiny_audio_listener_destroy(NatinyAudioListener listener);
Destroys a listener and invalidates its entity handle.
| Name | Type | Description |
|---|---|---|
listener |
NatinyAudioListener |
Listener handle |
natiny_audio_listener_destroy(listener);
natiny_audio_listener_set_active to say.natiny_audio_source_destroy.natiny_entity_set_parent(child, 0) leaves its
local transform as its world transform, and the destroy passes it by.natiny.audio.listener_destroy(listener)
Destroys a listener and invalidates its entity handle.
| Name | Type | Description |
|---|---|---|
listener |
number |
Listener handle |
natiny.audio.listener_destroy(listener)
natiny.audio.listener_set_active to say.natiny.audio.source_destroy.natiny.entity.set_parent(child, 0) leaves its
local transform as its world transform, and the destroy passes it by.void natiny_audio_listener_set_active(NatinyAudioListener listener, bool active);
Activates or deactivates a listener.
| Name | Type | Description |
|---|---|---|
listener |
NatinyAudioListener |
Listener handle |
active |
bool |
Whether this listener should be active |
natiny_audio_listener_set_active(listener, true);
false deactivates the listener only if it is currently active.
With no active listener, spatial sources are silent.natiny.audio.listener_set_active(listener, active)
Activates or deactivates a listener.
| Name | Type | Description |
|---|---|---|
listener |
number |
Listener handle |
active |
boolean |
Whether this listener should be active |
natiny.audio.listener_set_active(listener, true)
false deactivates the listener only if it is currently active.
With no active listener, spatial sources are silent.listener is 0.void natiny_audio_set_hrtf(bool enabled);
Selects binaural or stereo rendering for spatial sources.
| Name | Type | Description |
|---|---|---|
enabled |
bool |
true for binaural HRTF, false for stereo decoding |
natiny_audio_set_hrtf(false);
natiny.audio.set_hrtf(enabled)
Selects binaural or stereo rendering for spatial sources.
| Name | Type | Description |
|---|---|---|
enabled |
boolean |
true for binaural HRTF, false for stereo decoding |
natiny.audio.set_hrtf(false)
void natiny_audio_set_master_volume(float volume);
Sets the final gain applied to the complete audio mix.
| Name | Type | Description |
|---|---|---|
volume |
float |
Linear gain; 1.0f is unchanged and 0.0f is silent |
natiny_audio_set_master_volume(0.5f);
1.0f. Negative values are clamped to 0.0f;
values above 1.0f are allowed.-1.0f .. 1.0f, so high gain can distort.natiny.audio.set_master_volume(volume)
Sets the final gain applied to the complete audio mix.
| Name | Type | Description |
|---|---|---|
volume |
number |
Linear gain; 1.0 is unchanged and 0.0 is silent |
natiny.audio.set_master_volume(0.5)
1.0. Negative values are clamped to 0.0;
values above 1.0 are allowed.-1.0 .. 1.0, so high gain can distort.void natiny_audio_shutdown(void);
Stops audio output and clears all audio sources and listeners.
natiny_audio_shutdown();
natiny_audio_init and create new sources and listeners.natiny_clip_destroy.natiny_audio_source_destroy and
natiny_audio_listener_destroy before shutdown if their entity handles must
also be invalidated.natiny_backend_shutdown already performs audio shutdown and also destroys
all clips.natiny.audio.shutdown()
Stops audio output and clears all audio sources and listeners.
natiny.audio.shutdown()
natiny.audio.init and create new sources and listeners.natiny.clip.destroy.natiny.audio.source_destroy and
natiny.audio.listener_destroy before shutdown if their entity handles must
also be invalidated.natiny.backend.shutdown already performs audio shutdown and also destroys
all clips.void natiny_audio_source_destroy(NatinyAudioSource source);
Destroys a source and invalidates its entity handle.
| Name | Type | Description |
|---|---|---|
source |
NatinyAudioSource |
Source handle |
natiny_audio_source_destroy(source);
natiny_clip_destroy.natiny_audio_listener_destroy.natiny_entity_set_parent(child, 0) leaves its
local transform as its world transform, and the destroy passes it by.natiny.audio.source_destroy(source)
Destroys a source and invalidates its entity handle.
| Name | Type | Description |
|---|---|---|
source |
number |
Source handle |
natiny.audio.source_destroy(source)
natiny.clip.destroy.natiny.audio.listener_destroy.natiny.entity.set_parent(child, 0) leaves its
local transform as its world transform, and the destroy passes it by.NatinyClip natiny_audio_source_get_clip(NatinyAudioSource source);
Returns the clip a source plays.
| Name | Type | Description |
|---|---|---|
source |
NatinyAudioSource |
Source handle |
| Type | Description |
|---|---|
NatinyClip |
Clip handle, or 0 when the source has none |
NatinyClip clip = natiny_audio_source_get_clip(source);
0.natiny.audio.source_get_clip(source) -> clip
Returns the clip a source plays.
| Name | Type | Description |
|---|---|---|
source |
number |
Source handle |
| Type | Description |
|---|---|
number |
Clip handle, or 0 when the source has none |
local clip = natiny.audio.source_get_clip(source)
0.bool natiny_audio_source_get_playing(NatinyAudioSource source);
Reports whether a source is playing.
| Name | Type | Description |
|---|---|---|
source |
NatinyAudioSource |
Source handle |
| Type | Description |
|---|---|
bool |
true while the source is playing |
bool playing = natiny_audio_source_get_playing(source);
false while paused or stopped and after a non-looping source
reaches its end.false.natiny.audio.source_get_playing(source) -> playing
Reports whether a source is playing.
| Name | Type | Description |
|---|---|---|
source |
number |
Source handle |
| Type | Description |
|---|---|
boolean |
true while the source is playing |
local playing = natiny.audio.source_get_playing(source)
false while paused or stopped and after a non-looping source
reaches its end.false.void natiny_audio_source_pause(NatinyAudioSource source);
Pauses a source at its current playback position.
| Name | Type | Description |
|---|---|---|
source |
NatinyAudioSource |
Source handle |
natiny_audio_source_pause(source);
natiny_audio_source_play resumes from the paused position;
natiny_audio_source_stop rewinds to the beginning.natiny.audio.source_pause(source)
Pauses a source at its current playback position.
| Name | Type | Description |
|---|---|---|
source |
number |
Source handle |
natiny.audio.source_pause(source)
natiny.audio.source_play resumes from the paused position;
natiny.audio.source_stop rewinds to the beginning.void natiny_audio_source_play(NatinyAudioSource source);
Starts a stopped source or resumes a paused source.
| Name | Type | Description |
|---|---|---|
source |
NatinyAudioSource |
Source handle |
natiny_audio_source_play(source);
natiny_audio_source_get_playing returns false for it.natiny.audio.source_play(source)
Starts a stopped source or resumes a paused source.
| Name | Type | Description |
|---|---|---|
source |
number |
Source handle |
natiny.audio.source_play(source)
natiny.audio.source_get_playing returns false for it.void natiny_audio_source_seek(NatinyAudioSource source, float seconds);
Sets a source's playback position.
| Name | Type | Description |
|---|---|---|
source |
NatinyAudioSource |
Source handle |
seconds |
float |
Position in seconds from the beginning |
natiny_audio_source_seek(source, 30.0f);
0.0f .. natiny_clip_get_duration(clip) of the
clip the source plays. A source with no clip cannot be seeked.natiny.audio.source_seek(source, seconds)
Sets a source's playback position.
| Name | Type | Description |
|---|---|---|
source |
number |
Source handle |
seconds |
number |
Position in seconds from the beginning |
natiny.audio.source_seek(source, 30.0)
0.0 .. natiny.clip.get_duration(clip) of the
clip the source plays. A source with no clip cannot be seeked.void natiny_audio_source_set_attenuation(
NatinyAudioSource source,
uint32_t model,
float min_distance,
float max_distance,
float rolloff
);
Sets distance attenuation for a spatial source.
| Name | Type | Description |
|---|---|---|
source |
NatinyAudioSource |
Source handle |
model |
uint32_t |
NATINY_AUDIO_ATTENUATION_* constant |
min_distance |
float |
Distance at which attenuation begins |
max_distance |
float |
Distance beyond which gain stops changing |
rolloff |
float |
Attenuation strength |
natiny_audio_source_set_attenuation(source, NATINY_AUDIO_ATTENUATION_LINEAR,
1.0f, 8.0f, 1.0f);
d clamped between min_distance and max_distance, the
models calculate gain as follows:| Constant | Gain |
|---|---|
NATINY_AUDIO_ATTENUATION_NONE |
1 |
NATINY_AUDIO_ATTENUATION_INVERSE |
min / (min + rolloff * (d - min)) |
NATINY_AUDIO_ATTENUATION_LINEAR |
max(0, 1 - rolloff * (d - min) / (max - min)) |
NATINY_AUDIO_ATTENUATION_EXPONENTIAL |
(d / min) ^ -rolloff |
min_distance is clamped to at least 0.0001f, max_distance to at least
min_distance, and rolloff to at least 0.0f.natiny.audio.source_set_attenuation(source, model [, min_distance [, max_distance [, rolloff]]])
Sets distance attenuation for a spatial source.
| Name | Type | Default | Description |
|---|---|---|---|
source |
number |
Source handle | |
model |
integer |
natiny.audio.ATTENUATION_* constant |
|
min_distance |
number |
1.0 |
Distance at which attenuation begins |
max_distance |
number |
100.0 |
Distance beyond which gain stops changing |
rolloff |
number |
1.0 |
Attenuation strength |
natiny.audio.source_set_attenuation(source, natiny.audio.ATTENUATION_LINEAR,
1.0, 8.0, 1.0)
d clamped between min_distance and max_distance, the
models calculate gain as follows:| Constant | Gain |
|---|---|
ATTENUATION_NONE |
1 |
ATTENUATION_INVERSE |
min / (min + rolloff * (d - min)) |
ATTENUATION_LINEAR |
max(0, 1 - rolloff * (d - min) / (max - min)) |
ATTENUATION_EXPONENTIAL |
(d / min) ^ -rolloff |
min_distance is clamped to at least 0.0001, max_distance to at least
min_distance, and rolloff to at least 0.0.void natiny_audio_source_set_clip(NatinyAudioSource source, NatinyClip clip);
Sets which clip a source plays.
| Name | Type | Description |
|---|---|---|
source |
NatinyAudioSource |
Source handle |
clip |
NatinyClip |
Clip handle |
natiny_audio_source_set_clip(source, footstep_gravel);
natiny_audio_source_play to start the new clip.0 clears the clip.natiny.audio.source_set_clip(source, clip)
Sets which clip a source plays.
| Name | Type | Description |
|---|---|---|
source |
number |
Source handle |
clip |
number |
Clip handle |
natiny.audio.source_set_clip(source, footstep_gravel)
natiny.audio.source_play to start the new clip.clip is 0.void natiny_audio_source_set_loop(NatinyAudioSource source, bool loop);
Sets whether a source repeats after reaching its end.
| Name | Type | Description |
|---|---|---|
source |
NatinyAudioSource |
Source handle |
loop |
bool |
Whether playback should repeat |
natiny_audio_source_set_loop(source, true);
natiny.audio.source_set_loop(source, loop)
Sets whether a source repeats after reaching its end.
| Name | Type | Description |
|---|---|---|
source |
number |
Source handle |
loop |
boolean |
Whether playback should repeat |
natiny.audio.source_set_loop(source, true)
void natiny_audio_source_set_pitch(NatinyAudioSource source, float pitch);
Sets a source's playback-speed and pitch multiplier.
| Name | Type | Description |
|---|---|---|
source |
NatinyAudioSource |
Source handle |
pitch |
float |
Multiplier; 1.0f uses the recorded speed |
natiny_audio_source_set_pitch(source, 1.25f);
2.0f plays twice as fast and one octave higher; 0.5f plays half as fast
and one octave lower.0.05f .. 16.0f. Changing pitch does not restart the
source.natiny.audio.source_set_pitch(source, pitch)
Sets a source's playback-speed and pitch multiplier.
| Name | Type | Description |
|---|---|---|
source |
number |
Source handle |
pitch |
number |
Multiplier; 1.0 uses the recorded speed |
natiny.audio.source_set_pitch(source, 1.25)
2.0 plays twice as fast and one octave higher; 0.5 plays half as fast
and one octave lower.0.05 .. 16.0. Changing pitch does not restart the
source.void natiny_audio_source_set_spatial(NatinyAudioSource source, bool spatial);
Sets whether a source is positioned in the 3D world.
| Name | Type | Description |
|---|---|---|
source |
NatinyAudioSource |
Source handle |
spatial |
bool |
Whether to use spatial rendering |
natiny_audio_source_set_spatial(source, false);
natiny.audio.source_set_spatial(source, spatial)
Sets whether a source is positioned in the 3D world.
| Name | Type | Description |
|---|---|---|
source |
number |
Source handle |
spatial |
boolean |
Whether to use spatial rendering |
natiny.audio.source_set_spatial(source, false)
void natiny_audio_source_set_volume(NatinyAudioSource source, float volume);
Sets a source's linear gain before master volume is applied.
| Name | Type | Description |
|---|---|---|
source |
NatinyAudioSource |
Source handle |
volume |
float |
Linear gain; 1.0f is unchanged and 0.0f is silent |
natiny_audio_source_set_volume(source, 0.6f);
1.0f. Negative values are clamped to 0.0f;
values above 1.0f are allowed and can cause clipping.natiny.audio.source_set_volume(source, volume)
Sets a source's linear gain before master volume is applied.
| Name | Type | Description |
|---|---|---|
source |
number |
Source handle |
volume |
number |
Linear gain; 1.0 is unchanged and 0.0 is silent |
natiny.audio.source_set_volume(source, 0.6)
1.0. Negative values are clamped to 0.0;
values above 1.0 are allowed and can cause clipping.void natiny_audio_source_stop(NatinyAudioSource source);
Stops a source and resets its playback position to the beginning.
| Name | Type | Description |
|---|---|---|
source |
NatinyAudioSource |
Source handle |
natiny_audio_source_stop(source);
natiny_audio_source_play begins at 0.0f seconds.natiny.audio.source_stop(source)
Stops a source and resets its playback position to the beginning.
| Name | Type | Description |
|---|---|---|
source |
number |
Source handle |
natiny.audio.source_stop(source)
natiny.audio.source_play begins at 0.0 seconds.int natiny_backend_get_available(int backend);
Returns whether natiny_backend_init accepts the requested backend on this platform.
| Name | Type | Description |
|---|---|---|
backend |
int |
One of the NATINY_BACKEND_* constants |
int supported = natiny_backend_get_available(NATINY_BACKEND_VULKAN);
natiny_backend_init().NATINY_BACKEND_AUTO is available wherever any backend is.natiny.backend.get_available(backend) -> boolean
Returns whether init accepts the requested backend on this platform.
| Name | Type | Description |
|---|---|---|
backend |
number or string |
A natiny.backend.* constant, or its name |
local supported = natiny.backend.get_available("vulkan")
"auto", "d3d12", "vulkan", "opengl", "metal" and "webgpu".false, not an error.AUTO is available wherever any backend is.int natiny_backend_get_current(void);
The backend that is actually drawing, as one of the NATINY_BACKEND_*
constants. NATINY_BACKEND_AUTO never comes back out of this: by the time
natiny_backend_init has returned non-zero, it has been resolved to a
real one.
| Type | Description |
|---|---|
int |
Active NATINY_BACKEND_* constant; NATINY_BACKEND_AUTO if initialization has not succeeded |
natiny_backend_init(NATINY_BACKEND_AUTO);
printf("drawing with %s\n",
natiny_backend_get_name(natiny_backend_get_current()));
natiny_backend_init() has returned non-zero.natiny.backend.get_current() -> backend, name
The backend that is actually drawing - the constant, and its name for a
message. AUTO never comes back out of this: by the time
init has returned, it has been resolved to a real one.
| Name | Type | Description |
|---|---|---|
backend |
number |
Active constant, or natiny.backend.AUTO before successful initialization |
name |
string |
Its display name, e.g. "Direct3D 12" |
natiny.backend.init("auto")
local backend, name = natiny.backend.get_current()
print("drawing with " .. name)
true.const char* natiny_backend_get_name(int backend);
Returns the display name for a backend constant, such as "Direct3D 12".
| Name | Type | Description |
|---|---|---|
backend |
int |
One of the NATINY_BACKEND_* constants |
A static string owned by the engine. Nothing to free, and it outlives natiny_backend_shutdown.
printf("%s\n", natiny_backend_get_name(NATINY_BACKEND_D3D12)); // Direct3D 12
// AUTO names the one it would resolve to on this platform
printf("%s\n", natiny_backend_get_name(NATINY_BACKEND_AUTO));
natiny.backend.get_name(backend) -> string
Returns the display name for a backend constant, such as "Direct3D 12".
| Name | Type | Description |
|---|---|---|
backend |
number or string |
A natiny.backend.* constant, or its name |
print(natiny.backend.get_name(natiny.backend.D3D12)) --> "Direct3D 12"
print(natiny.backend.get_name("vulkan")) --> "Vulkan"
-- AUTO names the one it would resolve to on this platform
print(natiny.backend.get_name(natiny.backend.AUTO)) --> "Direct3D 12"
int natiny_backend_init(int backend);
Chooses the graphics backend and prepares the engine to draw with it. Call it before APIs that require an initialized engine. Returns 0 when this build cannot run what was asked for, after printing what it does have.
| constant | platforms |
|---|---|
NATINY_BACKEND_AUTO |
the platform's first choice |
NATINY_BACKEND_D3D12 |
Windows |
NATINY_BACKEND_VULKAN |
Windows, Linux |
NATINY_BACKEND_OPENGL |
Windows, Linux |
NATINY_BACKEND_METAL |
macOS |
NATINY_BACKEND_WEBGPU |
web |
The library carries every backend enabled for its platform; this call chooses which one to use.
int main(void) {
if (!natiny_backend_init(NATINY_BACKEND_AUTO))
return 1;
printf("%s\n", natiny_backend_get_name(natiny_backend_get_current()));
// ... your code ...
natiny_backend_shutdown();
return 0;
}
natiny_window_create() then fails too.natiny.backend.init([backend]) -> ok
Chooses the graphics backend and prepares the engine to draw with it. Call it before APIs that require an initialized engine.
backend is one of the constants below, the same thing spelled out as a
string, or nothing at all - which means AUTO. It returns true when the
engine can run what was asked for, and false, after printing what this build
does have, when it cannot.
| constant | string | platforms |
|---|---|---|
natiny.backend.AUTO |
"auto" |
the platform's first choice |
natiny.backend.D3D12 |
"d3d12" |
Windows |
natiny.backend.VULKAN |
"vulkan" |
Windows, Linux |
natiny.backend.OPENGL |
"opengl" |
Windows, Linux |
natiny.backend.METAL |
"metal" |
macOS |
natiny.backend.WEBGPU |
"webgpu" |
web |
One executable carries every backend its platform can run, so this is the only place the choice is made.
if not natiny.backend.init(natiny.backend.AUTO) then
os.exit(1)
end
local backend = natiny.backend.get_current()
print(natiny.backend.get_name(backend))
-- ... your code ...
natiny.backend.shutdown()
false, and natiny.window.create then fails too.void natiny_backend_loop(natiny_loop_fn fn, void* userdata);
Starts the main application loop. The callback is called once per frame.
natiny_loop_fn is declared in natiny.h as
typedef void (*natiny_loop_fn)(float dt, void* userdata);
| Name | Type | Description |
|---|---|---|
fn |
natiny_loop_fn |
Function called every frame with delta time in seconds |
userdata |
void* |
User pointer passed to the callback |
void frame(float dt, void* ud) {
update_scene(dt, ud);
}
natiny_backend_loop(frame, &app_state);
requestAnimationFrame.natiny_window_destroy - leaves
the others running, in whatever order they go. The same on every backend.natiny.backend.loop(callback)
Starts the main application loop. The callback is called once per frame.
| Name | Type | Description |
|---|---|---|
callback |
function |
Function called every frame with delta time in seconds |
natiny.backend.loop(function(dt)
update(dt)
end)
requestAnimationFrame.natiny.window.destroy - leaves
the others running, in whatever order they go. The same on every backend.void natiny_backend_shutdown(void);
Shuts down the Natiny engine and releases its internal resources. In a web
build, a call made after natiny_backend_loop has scheduled the browser loop
is a no-op because those resources must remain alive for its callbacks.
int main(void) {
natiny_backend_init(NATINY_BACKEND_AUTO);
// ...
natiny_backend_shutdown();
return 0;
}
natiny.backend.shutdown()
Shuts down the Natiny engine and releases its internal resources. In a web
build, a call made after natiny.backend.loop has scheduled the browser loop
is a no-op because those resources must remain alive for its callbacks.
natiny.backend.init()
-- ...
natiny.backend.shutdown()
void natiny_camera_bind(NatinyCamera cam);
Binds a camera. All subsequent 3D draw calls use this camera until it is unbound or another one is bound. Zero means no camera.
| Name | Type | Description |
|---|---|---|
cam |
NatinyCamera |
Camera handle from natiny_camera_create, or 0 for none |
natiny_camera_bind(cam);
draw_scene();
natiny_camera_unbind();
natiny_camera_unbind
is equivalent to binding 0; it does not restore the previous camera.natiny.camera.bind(cam [, fn])
Binds a camera. All subsequent 3D draw calls use this camera until it is unbound or another one is bound.
If an optional function fn is provided, the bind ends when it returns.
| Name | Type | Default | Description |
|---|---|---|---|
cam |
number |
Camera handle from natiny.camera.create |
|
fn |
function |
nil |
Callback executed while the camera is bound |
natiny.camera.bind(cam)
natiny.camera.unbind()
natiny.camera.bind(cam, function()
draw_scene()
end)
natiny.camera.unbind
selects the default 2D projection; it does not restore the previous camera.fn, Lua unbinds the camera even if the callback raises an error, then
propagates the error.NatinyCamera natiny_camera_create(void);
Creates a new 3D camera.
| Type | Description |
|---|---|
NatinyCamera |
Camera handle. 0 on failure. |
NatinyCamera cam = natiny_camera_create();
0.1f and far clip
100.0f.natiny_camera_destroy when it is no longer needed.local cam = natiny.camera.create()
Creates a new 3D camera.
| Type | Description |
|---|---|
number |
Camera handle |
local cam = natiny.camera.create()
0.1
and far clip 100.natiny.camera.destroy when it is no longer needed.void natiny_camera_destroy(NatinyCamera cam);
Destroys a camera and frees its resources.
| Name | Type | Description |
|---|---|---|
cam |
NatinyCamera |
Camera handle from natiny_camera_create |
NatinyCamera cam = natiny_camera_create();
/* use cam... */
natiny_camera_destroy(cam);
natiny_entity_set_parent(child, 0) leaves its
local transform as its world transform, and the destroy passes it by.natiny.camera.destroy(cam)
Destroys a camera and frees its resources.
| Name | Type | Description |
|---|---|---|
cam |
number |
Camera handle from natiny.camera.create |
local cam = natiny.camera.create()
-- use cam...
natiny.camera.destroy(cam)
natiny.entity.set_parent(child, 0) leaves its
local transform as its world transform, and the destroy passes it by.void natiny_camera_set_clip(NatinyCamera cam, float near_z, float far_z);
Sets the near and far planes of a camera, in world units. Nothing closer than
near_z or further than far_z is drawn, and the depth buffer spreads its
precision between them.
| Name | Type | Description |
|---|---|---|
cam |
NatinyCamera |
Camera handle |
near_z |
float |
Distance to the near plane; must be above 0 |
far_z |
float |
Distance to the far plane; must be beyond near_z |
natiny_camera_set_clip(cam, 0.2f, 2000.0f);
0.1f and 100.0f.near_z is clamped to at least 0.0001f; far_z is clamped to at least
near_z * 1.001f.far_z / near_z ratio reduces depth-buffer precision.natiny.camera.set_clip(cam, near, far)
Sets the near and far planes of a camera, in world units. Nothing closer than
near or further than far is drawn, and the depth buffer spreads its
precision between them.
| Name | Type | Description |
|---|---|---|
cam |
number |
Camera handle |
near |
number |
Distance to the near plane; must be above 0 |
far |
number |
Distance to the far plane; must be beyond near |
natiny.camera.set_clip(cam, 0.2, 2000.0)
0.1 and 100.near is clamped to at least 0.0001; far is clamped to at least
near * 1.001.far / near ratio reduces depth-buffer precision.void natiny_camera_set_fov(NatinyCamera cam, float fov);
Sets the vertical field of view for a camera.
| Name | Type | Description |
|---|---|---|
cam |
NatinyCamera |
Camera handle from natiny_camera_create |
fov |
float |
Vertical field of view in radians - the Lua binding is the one that takes degrees |
NatinyCamera cam = natiny_camera_create();
natiny_camera_set_fov(cam, 1.13f); /* 65 degrees */
natiny.camera.set_fov(cam, fov)
Sets the vertical field of view for a camera.
| Name | Type | Description |
|---|---|---|
cam |
number |
Camera handle from natiny.camera.create |
fov |
number |
Vertical field of view in degrees |
local cam = natiny.camera.create()
natiny.camera.set_fov(cam, 90)
void natiny_camera_unbind(void);
Ends the camera bind: drawing goes back to the target's default 2D
projection. The same thing as natiny_camera_bind(0).
natiny_camera_unbind();
natiny_camera_bind(0); it does not restore
the previously bound camera.natiny.camera.unbind()
Ends the camera bind: drawing goes back to the target's default 2D projection.
natiny.camera.unbind()
void natiny_clip_destroy(NatinyClip clip);
Frees a clip's decoded audio.
| Name | Type | Description |
|---|---|---|
clip |
NatinyClip |
Clip handle |
natiny_clip_destroy(clip);
natiny_audio_source_set_clip.natiny_audio_shutdown leaves clips alone; natiny_backend_shutdown
destroys them along with the rest of the engine state.natiny.clip.destroy(clip)
Frees a clip's decoded audio.
| Name | Type | Description |
|---|---|---|
clip |
number |
Clip handle |
natiny.clip.destroy(clip)
natiny.audio.source_set_clip.natiny.audio.shutdown leaves clips alone; natiny.backend.shutdown
destroys them along with the rest of the engine state.uint32_t natiny_clip_get_channels(NatinyClip clip);
Returns how many channels a clip was decoded with.
| Name | Type | Description |
|---|---|---|
clip |
NatinyClip |
Clip handle |
| Type | Description |
|---|---|
uint32_t |
Channel count as the file carries it; 0 for an invalid clip |
uint32_t channels = natiny_clip_get_channels(clip);
0.natiny.clip.get_channels(clip) -> channels
Returns how many channels a clip was decoded with.
| Name | Type | Description |
|---|---|---|
clip |
number |
Clip handle |
| Type | Description |
|---|---|
integer |
Channel count as the file carries it; 0 for an invalid clip |
local channels = natiny.clip.get_channels(clip)
0.float natiny_clip_get_duration(NatinyClip clip);
Returns how long a clip is, in seconds.
| Name | Type | Description |
|---|---|---|
clip |
NatinyClip |
Clip handle |
| Type | Description |
|---|---|
float |
Length in seconds, or 0.0f for an invalid clip |
float seconds = natiny_clip_get_duration(clip);
1.0f takes correspondingly more or less time to get through it.0.0f.natiny.clip.get_duration(clip) -> seconds
Returns how long a clip is, in seconds.
| Name | Type | Description |
|---|---|---|
clip |
number |
Clip handle |
| Type | Description |
|---|---|
number |
Length in seconds, or 0.0 for an invalid clip |
local seconds = natiny.clip.get_duration(clip)
1.0 takes correspondingly more or less time to get through it.0.NatinyClip natiny_clip_load(NatinyResource ogg);
Decodes an Ogg Vorbis resource into a clip.
| Name | Type | Description |
|---|---|---|
ogg |
NatinyResource |
Resource containing Ogg Vorbis data |
| Type | Description |
|---|---|
NatinyClip |
Clip handle; 0 on failure |
NatinyClip clip = natiny_clip_load(ogg);
0 for an invalid resource, invalid Ogg data or allocation
failure.natiny.clip.load(ogg) -> clip
Decodes an Ogg Vorbis resource into a clip.
| Name | Type | Description |
|---|---|---|
ogg |
number |
Resource handle containing Ogg Vorbis data |
| Type | Description |
|---|---|
number |
Clip handle |
local clip = natiny.clip.load(ogg)
int natiny_clip_reload(NatinyClip clip, NatinyResource ogg);
Replaces the samples inside an existing clip. The handle does not change.
| Name | Type | Description |
|---|---|---|
clip |
NatinyClip |
Live clip handle from natiny_clip_load |
ogg |
NatinyResource |
Resource handle containing Ogg Vorbis data |
| Type | Description |
|---|---|
int |
1 on success, 0 on failure |
NatinyClip clip = natiny_clip_load(resource);
NatinyAudioSource source = natiny_audio_create_source(clip);
NatinyResource replacement = natiny_resource_load("sounds/rain.ogg");
/* Every source on this clip picks up the new sound. */
if (!natiny_clip_reload(clip, replacement)) {
/* The clip still contains its previous samples. */
}
natiny_resource_destroy(replacement);
0 if clip is not live or ogg cannot be decoded as Ogg Vorbis.
A failure leaves the old samples unchanged.natiny.clip.reload(clip, ogg)
Replaces the samples inside an existing clip. The handle does not change.
| Name | Type | Description |
|---|---|---|
clip |
number |
Live clip handle from natiny.clip.load |
ogg |
number |
Resource handle containing Ogg Vorbis data |
local clip = natiny.clip.load(resource)
local source = natiny.audio.create_source(clip)
local replacement = natiny.resource.load("sounds/rain.ogg")
-- every source on this clip picks up the new sound
natiny.clip.reload(clip, replacement)
natiny.resource.destroy(replacement)
clip is not live or ogg cannot be decoded as Ogg
Vorbis. A failure leaves the old samples unchanged.NatinyEntity natiny_entity_get_parent(NatinyEntity node);
Returns the node this one hangs off, or 0 when it sits at the root.
| Name | Type | Description |
|---|---|---|
node |
NatinyEntity |
Entity handle |
| Type | Description |
|---|---|
NatinyEntity |
Parent entity handle, or 0 when the node has no parent |
NatinyEntity root = hand;
while (natiny_entity_get_parent(root) != 0)
root = natiny_entity_get_parent(root);
0 is the same value natiny_entity_set_parent takes to detach a node, so
a value read here can be written straight back.0. That is what the
transform already does with it: a released slot is treated as no parent, and
the child's local transform is its world one.0.natiny.entity.get_parent(node) -> parent
Returns the node this one hangs off, or 0 when it sits at the root.
| Name | Type | Description |
|---|---|---|
node |
number |
Entity handle (e.g. model, pivot, body, sound source or camera) |
| Type | Description |
|---|---|
number |
Parent entity handle, or 0 when the node has no parent |
local root = hand
while natiny.entity.get_parent(root) ~= 0 do
root = natiny.entity.get_parent(root)
end
0 is the same value natiny.entity.set_parent takes to
detach a node, so a value read here can be written straight back.0. That is what the
transform already does with it: a released slot is treated as no parent, and
the child's local transform is its world one.0.void natiny_entity_get_position(NatinyEntity node, uint32_t space, float* x, float* y, float* z);
Writes the position of a node in the given coordinate space into three output parameters.
| Name | Type | Description |
|---|---|---|
node |
NatinyEntity |
Entity handle |
space |
uint32_t |
NATINY_SPACE_LOCAL or NATINY_SPACE_WORLD |
x |
float* |
Receives position X; may be NULL |
y |
float* |
Receives position Y; may be NULL |
z |
float* |
Receives position Z; may be NULL |
float x, y, z;
natiny_entity_get_position(player, NATINY_SPACE_WORLD, &x, &y, &z);
natiny_entity_set_position(camera, NATINY_SPACE_WORLD, x, y + 1.7f, z);
NATINY_SPACE_LOCAL returns the stored parent-relative position, which is
exactly what natiny_entity_set_position wrote in the same
space. NATINY_SPACE_WORLD returns the absolute position the parent chain
places the node at.natiny_physics_set_interpolation
is enabled for that body.0 to every non-NULL output.natiny.entity.get_position(node [, space]) -> x, y, z
Returns the position of a node in the given coordinate space, as three separate numbers.
| Name | Type | Default | Description |
|---|---|---|---|
node |
number |
Entity handle (e.g. model, body, sound source or camera) | |
space |
integer |
natiny.entity.SPACE_WORLD |
natiny.entity.SPACE_LOCAL or natiny.entity.SPACE_WORLD |
| Type | Description |
|---|---|
number |
Position X |
number |
Position Y |
number |
Position Z |
local x, y, z = natiny.entity.get_position(player, natiny.entity.SPACE_WORLD)
natiny.entity.set_position(camera, natiny.entity.SPACE_WORLD, x, y + 1.7, z)
SPACE_LOCAL returns the stored parent-relative position, which is exactly
what natiny.entity.set_position wrote in the same space.
SPACE_WORLD returns the absolute position the parent chain places the node
at.natiny.physics.set_interpolation
is enabled for that body.0, 0, 0.void natiny_entity_get_rotation(NatinyEntity node, uint32_t space, float* rx, float* ry, float* rz);
Writes the orientation of a node, in degrees, into three output parameters.
| Name | Type | Description |
|---|---|---|
node |
NatinyEntity |
Entity handle |
space |
uint32_t |
NATINY_SPACE_LOCAL or NATINY_SPACE_WORLD |
rx |
float* |
Receives rotation around X, in degrees; may be NULL |
ry |
float* |
Receives rotation around Y, in degrees; may be NULL |
rz |
float* |
Receives rotation around Z, in degrees; may be NULL |
float yaw;
natiny_entity_get_rotation(turret, NATINY_SPACE_WORLD, NULL, &yaw, NULL);
natiny_entity_set_rotation(barrel, NATINY_SPACE_WORLD, 0.0f, yaw, 0.0f);
natiny_entity_set_rotation and
natiny_entity_rotate take.natiny_entity_set_rotation reads them in.NATINY_SPACE_LOCAL returns the stored parent-relative angles, which is
exactly what natiny_entity_set_rotation wrote in the same space.
NATINY_SPACE_WORLD composes them with every parent's and decomposes the
result.0 to every non-NULL output.natiny.entity.get_rotation(node [, space]) -> rx, ry, rz
Returns the orientation of a node in degrees, as three separate numbers.
| Name | Type | Default | Description |
|---|---|---|---|
node |
number |
Entity handle (e.g. model, pivot, body, sound source or camera) | |
space |
integer |
natiny.entity.SPACE_WORLD |
natiny.entity.SPACE_LOCAL or natiny.entity.SPACE_WORLD |
| Type | Description |
|---|---|
number |
Rotation around X, in degrees |
number |
Rotation around Y, in degrees |
number |
Rotation around Z, in degrees |
local _, yaw = natiny.entity.get_rotation(turret, natiny.entity.SPACE_WORLD)
natiny.entity.set_rotation(barrel, natiny.entity.SPACE_WORLD, 0, yaw, 0)
natiny.entity.set_rotation and
natiny.entity.rotate take.set_rotation reads them in.SPACE_LOCAL returns the stored parent-relative angles, which is exactly
what set_rotation wrote in the same space. SPACE_WORLD composes them with
every parent's and decomposes the result.0, 0, 0.void natiny_entity_get_scale(NatinyEntity node, float* sx, float* sy, float* sz);
Writes the scale factors of a node into three output parameters.
| Name | Type | Description |
|---|---|---|
node |
NatinyEntity |
Entity handle |
sx |
float* |
Receives the scale factor along X; may be NULL |
sy |
float* |
Receives the scale factor along Y; may be NULL |
sz |
float* |
Receives the scale factor along Z; may be NULL |
float sx, sy, sz;
natiny_entity_get_scale(cube, &sx, &sy, &sz);
natiny_entity_set_scale(cube, sx * 1.1f, sy * 1.1f, sz * 1.1f);
space argument because natiny_entity_set_scale has none:
this returns exactly the three numbers that were written. A node under a
scaled parent is drawn scaled without its own three changing.1.0f to every non-NULL output, and
so does a handle that is not a live node.natiny.entity.get_scale(node) -> sx, sy, sz
Returns the scale factors of a node, as three separate numbers.
| Name | Type | Description |
|---|---|---|
node |
number |
Entity handle (e.g. model, pivot, body, sound source or camera) |
| Type | Description |
|---|---|
number |
Scale factor along X |
number |
Scale factor along Y |
number |
Scale factor along Z |
local sx, sy, sz = natiny.entity.get_scale(cube)
natiny.entity.set_scale(cube, sx * 1.1, sy * 1.1, sz * 1.1)
space argument because
natiny.entity.set_scale has none: this returns exactly the
three numbers that were written. A node under a scaled parent is drawn
scaled without its own three changing.1, 1, 1, and so does a handle that is
not a live node.void natiny_entity_look_at(NatinyEntity node, float tx, float ty, float tz, float upx, float upy, float upz);
Orients a node to face a target point.
| Name | Type | Description |
|---|---|---|
node |
NatinyEntity |
Entity handle |
tx |
float |
Target position X |
ty |
float |
Target position Y |
tz |
float |
Target position Z |
upx |
float |
Up vector X |
upy |
float |
Up vector Y |
upz |
float |
Up vector Z |
natiny_entity_look_at(node, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f);
natiny.entity.look_at(node, tx, ty, tz [, upx, upy, upz])
Orients a node to face a target point.
| Name | Type | Default | Description |
|---|---|---|---|
node |
number |
Entity handle (e.g. model, body, sound source or camera) | |
tx |
number |
Target position X | |
ty |
number |
Target position Y | |
tz |
number |
Target position Z | |
upx |
number |
0 |
Up vector X |
upy |
number |
1 |
Up vector Y |
upz |
number |
0 |
Up vector Z |
natiny.entity.look_at(cam, 0.0, 0.0, 0.0)
(0, 1, 0) (Y-up) when omitted.void natiny_entity_move(NatinyEntity node, uint32_t space, float dx, float dy, float dz);
Moves a node by a delta offset in the given coordinate space.
| Name | Type | Description |
|---|---|---|
node |
NatinyEntity |
Entity handle |
space |
uint32_t |
NATINY_SPACE_LOCAL or NATINY_SPACE_WORLD |
dx |
float |
Offset along X axis |
dy |
float |
Offset along Y axis |
dz |
float |
Offset along Z axis |
natiny_entity_move(node, NATINY_SPACE_LOCAL, 0.0f, 0.0f, -1.0f);
NATINY_SPACE_LOCAL rotates the delta by the node's local orientation
before adding it. NATINY_SPACE_WORLD interprets the delta along world axes.natiny.entity.move(node, space, dx, dy, dz)
Moves a node by a delta offset in the given coordinate space.
| Name | Type | Description |
|---|---|---|
node |
number |
Entity handle (e.g. model, body, sound source or camera) |
space |
integer |
natiny.entity.SPACE_LOCAL or natiny.entity.SPACE_WORLD |
dx |
number |
Offset along X axis |
dy |
number |
Offset along Y axis |
dz |
number |
Offset along Z axis |
natiny.entity.move(node, natiny.entity.SPACE_LOCAL, 0.0, 0.0, -1.0)
SPACE_LOCAL rotates the delta by the node's local orientation before
adding it. SPACE_WORLD interprets the delta along world axes.void natiny_entity_rotate(NatinyEntity node, uint32_t space, float drx, float dry, float drz);
Rotates a node by a delta in the given coordinate space.
| Name | Type | Description |
|---|---|---|
node |
NatinyEntity |
Entity handle |
space |
uint32_t |
NATINY_SPACE_LOCAL or NATINY_SPACE_WORLD |
drx |
float |
Rotation delta around X axis in degrees |
dry |
float |
Rotation delta around Y axis in degrees |
drz |
float |
Rotation delta around Z axis in degrees |
NatinyModel cube = natiny_model_create();
natiny_model_set_mesh(cube, natiny_mesh_cube(1.0f, 1.0f, 1.0f, 1));
/* Spin the cube 1 degree per frame around Y */
natiny_entity_rotate((NatinyEntity)cube, NATINY_SPACE_LOCAL, 0.0f, 1.0f, 0.0f);
natiny_entity_set_rotation, this adds to the current rotation rather than replacing it.NATINY_SPACE_LOCAL to rotate around the node's own axes.natiny.entity.rotate(node, space, drx, dry, drz)
Rotates a node by a delta in the given coordinate space.
| Name | Type | Description |
|---|---|---|
node |
number |
Entity handle (e.g. model, body, sound source or camera) |
space |
integer |
natiny.entity.SPACE_LOCAL or natiny.entity.SPACE_WORLD |
drx |
number |
Rotation delta around X axis in degrees |
dry |
number |
Rotation delta around Y axis in degrees |
drz |
number |
Rotation delta around Z axis in degrees |
local cube = natiny.model.create(natiny.mesh.cube(1))
-- Spin the cube 1 degree per frame around Y
natiny.entity.rotate(cube, natiny.entity.SPACE_LOCAL, 0.0, 1.0, 0.0)
set_rotation, this adds to the current rotation rather than replacing it.SPACE_LOCAL to rotate around the node's own axes.void natiny_entity_set_parent(NatinyEntity child, NatinyEntity parent);
Sets the parent of a node, creating a parent-child hierarchy. The child node's transform becomes relative to its parent.
| Name | Type | Description |
|---|---|---|
child |
NatinyEntity |
Child Entity handle |
parent |
NatinyEntity |
Parent entity handle, or 0 to detach |
natiny_entity_set_parent(hand, arm);
0 as parent to detach the child. Its local transform is preserved
and becomes its world transform.destroy: every
destroy that owns an entity takes the whole subtree under it with it.natiny_entity_get_parent reads the parent back, and answers 0 for a node
at the root.natiny.entity.set_parent(child, parent)
Sets the parent of a node, creating a parent-child hierarchy. The child node's transform becomes relative to its parent.
| Name | Type | Description |
|---|---|---|
child |
number |
Child Entity handle |
parent |
number |
Parent entity handle, or 0 to detach |
natiny.entity.set_parent(hand, arm)
0 as parent to detach the child. Its local transform is preserved
and becomes its world transform.destroy: every
destroy that owns an entity takes the whole subtree under it with it.natiny.entity.get_parent reads the parent back, and
answers 0 for a node at the root.void natiny_entity_set_position(NatinyEntity node, uint32_t space, float x, float y, float z);
Sets the position of a node in the given coordinate space.
| Name | Type | Description |
|---|---|---|
node |
NatinyEntity |
Entity handle |
space |
uint32_t |
NATINY_SPACE_LOCAL or NATINY_SPACE_WORLD |
x |
float |
Position X |
y |
float |
Position Y |
z |
float |
Position Z |
natiny_entity_set_position(node, NATINY_SPACE_WORLD, 2.0f, 0.0f, -5.0f);
NATINY_SPACE_LOCAL sets the stored parent-relative position.
NATINY_SPACE_WORLD converts an absolute world position into parent space.natiny.entity.set_position(node, space, x, y, z)
Sets the position of a node in the given coordinate space.
| Name | Type | Description |
|---|---|---|
node |
number |
Entity handle (e.g. model, body, sound source or camera) |
space |
integer |
natiny.entity.SPACE_LOCAL or natiny.entity.SPACE_WORLD |
x |
number |
Position X |
y |
number |
Position Y |
z |
number |
Position Z |
natiny.entity.set_position(node, natiny.entity.SPACE_WORLD, 2.0, 0.0, -5.0)
SPACE_LOCAL sets the stored parent-relative position. SPACE_WORLD
converts an absolute world position into parent space.void natiny_entity_set_rotation(NatinyEntity node, uint32_t space, float rx, float ry, float rz);
Sets the rotation of a node in the given coordinate space.
| Name | Type | Description |
|---|---|---|
node |
NatinyEntity |
Entity handle |
space |
uint32_t |
NATINY_SPACE_LOCAL or NATINY_SPACE_WORLD |
rx |
float |
Rotation around X axis in degrees |
ry |
float |
Rotation around Y axis in degrees |
rz |
float |
Rotation around Z axis in degrees |
NatinyModel cube = natiny_model_create();
natiny_model_set_mesh(cube, natiny_mesh_cube(1.0f, 1.0f, 1.0f, 1));
natiny_entity_set_rotation((NatinyEntity)cube, NATINY_SPACE_LOCAL, 0.0f, 45.0f, 0.0f);
NATINY_SPACE_LOCAL for parent-relative rotation, NATINY_SPACE_WORLD for absolute rotation.natiny.entity.set_rotation(node, space, rx, ry, rz)
Sets the rotation of a node in the given coordinate space.
| Name | Type | Description |
|---|---|---|
node |
number |
Entity handle (e.g. model, body, sound source or camera) |
space |
integer |
natiny.entity.SPACE_LOCAL or natiny.entity.SPACE_WORLD |
rx |
number |
Rotation around X axis in degrees |
ry |
number |
Rotation around Y axis in degrees |
rz |
number |
Rotation around Z axis in degrees |
local cube = natiny.model.create(natiny.mesh.cube(1))
natiny.entity.set_rotation(cube, natiny.entity.SPACE_LOCAL, 0.0, 45.0, 0.0)
SPACE_LOCAL for parent-relative rotation, SPACE_WORLD for absolute rotation.void natiny_entity_set_scale(NatinyEntity node, float sx, float sy, float sz);
Sets the scale of a node.
| Name | Type | Description |
|---|---|---|
node |
NatinyEntity |
Entity handle |
sx |
float |
Scale factor along X axis |
sy |
float |
Scale factor along Y axis |
sz |
float |
Scale factor along Z axis |
NatinyModel cube = natiny_model_create();
natiny_model_set_mesh(cube, natiny_mesh_cube(1.0f, 1.0f, 1.0f, 1));
natiny_entity_set_scale((NatinyEntity)cube, 2.0f, 2.0f, 2.0f);
1.0f on all axes is the default (no scaling).natiny.entity.set_scale(node, sx, sy, sz)
Sets the scale of a node.
| Name | Type | Description |
|---|---|---|
node |
number |
Entity handle (e.g. model, body, sound source or camera) |
sx |
number |
Scale factor along X axis |
sy |
number |
Scale factor along Y axis |
sz |
number |
Scale factor along Z axis |
local cube = natiny.model.create(natiny.mesh.cube(1))
natiny.entity.set_scale(cube, 2.0, 2.0, 2.0)
1.0 on all axes is the default (no scaling).NatinyFont natiny_font_create(NatinyResource resource, float size);
Creates a font from loaded font data.
| Name | Type | Description |
|---|---|---|
resource |
NatinyResource |
Resource handle containing TrueType font data |
size |
float |
Font size in drawing units; values at or below 0.0f use 16.0f |
| Type | Description |
|---|---|
NatinyFont |
Font handle, or 0 if the resource is invalid, the data is not a supported font, or allocation fails |
NatinyResource res = natiny_resource_load("data/fonts/roboto.ttf");
NatinyFont font = natiny_font_create(res, 24.0f);
resource with natiny_resource_load. The font
copies its data, so the resource may be destroyed after this call.NATINY_SCALE_MODE_HIDPI, glyphs are rasterized at the display scale
while font metrics remain in drawing units.natiny_font_destroy.natiny.font.create(resource, size) -> font
Creates a font from loaded font data.
| Name | Type | Description |
|---|---|---|
resource |
number |
Resource handle containing TrueType font data |
size |
number |
Font size in drawing units; values at or below 0 use 16 |
| Type | Description |
|---|---|
number |
Font handle; raises an error if the font cannot be created |
local res = natiny.resource.load("data/fonts/roboto.ttf")
local font = natiny.font.create(res, 24)
resource with natiny.resource.load. The font
copies its data, so the resource may be destroyed after this call.natiny.window.SCALE_MODE_HIDPI, glyphs are rasterized at the display
scale while font metrics remain in drawing units.natiny.font.destroy.void natiny_font_destroy(NatinyFont font);
Destroys a font and releases its internal resources.
| Name | Type | Description |
|---|---|---|
font |
NatinyFont |
Font handle from natiny_font_create |
natiny_font_destroy(font);
natiny.font.destroy(font)
Destroys a font and releases its internal resources.
| Name | Type | Description |
|---|---|---|
font |
number |
Font handle from natiny.font.create |
natiny.font.destroy(font)
float natiny_font_get_ascent(NatinyFont font);
Returns how far the font reaches above the baseline, in drawing units.
| Name | Type | Description |
|---|---|---|
font |
NatinyFont |
Font handle from natiny_font_create |
| Type | Description |
|---|---|
float |
Non-negative distance above the baseline in drawing units, or 0.0f for an invalid handle |
float ascent = natiny_font_get_ascent(big);
natiny_render_text(big, x, y, "A");
natiny_render_text(small, x + 30.0f,
y + ascent - natiny_font_get_ascent(small), "a");
natiny_render_text places the top of the line at y, so the baseline
sits at y + ascent. Two fonts of different sizes line up on the baseline
and not on the top edge, which is what this is for.natiny_font_set_line_spacing.natiny.font.get_ascent(font) -> ascent
Returns how far the font reaches above the baseline, in drawing units.
| Name | Type | Description |
|---|---|---|
font |
number |
Font handle from natiny.font.create |
| Type | Description |
|---|---|
number |
Non-negative distance above the baseline in drawing units, or 0 for an invalid handle |
local ascent = natiny.font.get_ascent(big)
natiny.render.text(big, x, y, "A")
natiny.render.text(small, x + 30, y + ascent - natiny.font.get_ascent(small), "a")
natiny.render.text places the top of the line at y, so the baseline
sits at y + ascent. Two fonts of different sizes line up on the baseline
and not on the top edge, which is what this is for.natiny.font.set_line_spacing.NatinyTexture natiny_font_get_atlas(NatinyFont font);
Returns the texture the font's glyphs are baked into.
| Name | Type | Description |
|---|---|---|
font |
NatinyFont |
Font handle from natiny_font_create |
| Type | Description |
|---|---|
NatinyTexture |
Texture handle, or 0 for an invalid font |
NatinyTexture atlas = natiny_font_get_atlas(font);
natiny_render_texture(atlas, 0.0f, 0.0f, 256.0f, 256.0f);
natiny_texture_destroy or modify it with
natiny_texture_update.natiny_font_get_glyph.natiny.font.get_atlas(font) -> texture
Returns the texture the font's glyphs are baked into.
| Name | Type | Description |
|---|---|---|
font |
number |
Font handle from natiny.font.create |
| Type | Description |
|---|---|
number or nil |
Atlas texture handle, or nil for an invalid font |
local atlas = natiny.font.get_atlas(font)
natiny.render.texture(atlas, 0, 0, 256, 256)
natiny.texture.destroy or modify it with
natiny.texture.update.natiny.font.get_glyph.float natiny_font_get_descent(NatinyFont font);
Returns how far the font reaches below the baseline, in drawing units.
| Name | Type | Description |
|---|---|---|
font |
NatinyFont |
Font handle from natiny_font_create |
| Type | Description |
|---|---|
float |
Non-negative distance below the baseline in drawing units, or 0.0f for an invalid handle |
float ascent = natiny_font_get_ascent(font);
float descent = natiny_font_get_descent(font);
float width = 0.0f;
natiny_font_measure_text(font, text, &width, NULL);
natiny_render_line(x, y + ascent + descent,
x + width, y + ascent + descent, 1.0f);
5 means the tails reach
five units below the baseline. TrueType reports it as a negative offset;
natiny turns the sign once, here.ascent + descent is the font's extent around the baseline. The value from
natiny_font_get_line_height also includes the line gap.natiny.font.get_descent(font) -> descent
Returns how far the font reaches below the baseline, in drawing units.
| Name | Type | Description |
|---|---|---|
font |
number |
Font handle from natiny.font.create |
| Type | Description |
|---|---|
number |
Non-negative distance below the baseline in drawing units, or 0 for an invalid handle |
local ascent = natiny.font.get_ascent(font)
local descent = natiny.font.get_descent(font)
local w = natiny.font.measure_text(font, text)
natiny.render.line(x, y + ascent + descent, x + w, y + ascent + descent, 1)
5 means the tails reach
five units below the baseline.ascent + descent is the font's extent around the baseline. The value from
natiny.font.get_line_height also includes the line gap.NatinyFont natiny_font_get_fallback(NatinyFont font);
Returns the font asked for the glyphs this one lacks.
| Name | Type | Description |
|---|---|---|
font |
NatinyFont |
Font handle from natiny_font_create |
| Type | Description |
|---|---|
NatinyFont |
Direct fallback font handle, or 0 if none is active or font is invalid |
NatinyFont fallback = natiny_font_get_fallback(font);
0 if the configured fallback was destroyed.natiny.font.get_fallback(font) -> fallback
Returns the font asked for the glyphs this one lacks.
| Name | Type | Description |
|---|---|---|
font |
number |
Font handle from natiny.font.create |
| Type | Description |
|---|---|
number or nil |
Direct fallback font handle, or nil if none is active |
local fallback = natiny.font.get_fallback(font)
nil if the configured fallback was destroyed.bool natiny_font_get_glyph(NatinyFont font, uint32_t codepoint,
NatinyGlyph* glyph);
Fills in one glyph's metrics and its rectangle in the atlas, for drawing it by hand.
| Name | Type | Description |
|---|---|---|
font |
NatinyFont |
Font handle from natiny_font_create |
codepoint |
uint32_t |
Unicode code point |
glyph |
NatinyGlyph* |
Receives metrics and atlas coordinates; must not be NULL and is untouched on failure |
| Type | Description |
|---|---|
bool |
true when glyph was filled; false for an invalid handle or output pointer, or when the atlas is full |
| Field | Type | Description |
|---|---|---|
advance |
float |
Pen advance in drawing units |
x, y |
float |
Glyph offset from the pen and baseline in drawing units |
width, height |
float |
Glyph image size in drawing units |
u0, v0, u1, v1 |
float |
Normalized rectangle in atlas |
atlas |
NatinyTexture |
Atlas containing the glyph, including a fallback font's atlas |
NatinyGlyph glyph;
if (natiny_font_get_glyph(font, 0x41, &glyph)) {
float baseline = y + natiny_font_get_ascent(font);
float left = pen + glyph.x;
float top = baseline + glyph.y;
/* glyph.u0, glyph.v0, glyph.u1, and glyph.v1 select the atlas region. */
pen += glyph.advance + natiny_font_get_letter_spacing(font);
}
y is measured from the baseline and is normally negative - the picture
sits above it. natiny_font_get_ascent turns a
top-of-line coordinate into a baseline.advance includes neither kerning nor the font's letter spacing. Add the
current letter spacing explicitly; text placed this way remains unkerned.glyph.atlas says which font's atlas the
glyph actually came from.natiny.font.get_glyph(font, codepoint)
-> advance, x, y, w, h, u0, v0, u1, v1, atlas
Returns one glyph's metrics and its rectangle in the atlas, for drawing it by hand.
| Name | Type | Description |
|---|---|---|
font |
number |
Font handle from natiny.font.create |
codepoint |
number or string |
Unicode code point, or a UTF-8 string whose first character is used |
| Type | Description |
|---|---|
number |
advance: pen advance in drawing units |
number |
x: glyph left edge relative to the pen, in drawing units |
number |
y: glyph top edge relative to the baseline, in drawing units |
number |
w: glyph width in drawing units |
number |
h: glyph height in drawing units |
number |
u0: normalized atlas left edge |
number |
v0: normalized atlas top edge |
number |
u1: normalized atlas right edge |
number |
v1: normalized atlas bottom edge |
number |
atlas: texture handle containing the glyph |
Returns only nil if the font is invalid or the glyph cannot be resolved or
packed into the atlas.
local pen = 20
for _, code in utf8.codes(message) do
local advance = natiny.font.get_glyph(font, code)
if not advance then break end
local bob = math.sin(time * 4 + pen * 0.05) * 6
natiny.render.text(font, pen, 100 + bob, utf8.char(code))
pen = pen + advance + natiny.font.get_letter_spacing(font)
end
y is normally negative because the glyph image sits above the baseline.
natiny.font.get_ascent converts a line-top
coordinate to a baseline.advance includes neither kerning nor the font's letter spacing. Add the
current letter spacing explicitly; text placed this way remains unkerned.atlas says which font's atlas the glyph
actually came from.nil.float natiny_font_get_letter_spacing(NatinyFont font);
Returns the extra gap added after every glyph, in drawing units.
| Name | Type | Description |
|---|---|---|
font |
NatinyFont |
Font handle from natiny_font_create |
| Type | Description |
|---|---|
float |
Drawing units added after each glyph; 0.0f by default and for an invalid handle |
float spacing = natiny_font_get_letter_spacing(font);
natiny.font.get_letter_spacing(font) -> spacing
Returns the extra gap added after every glyph, in drawing units.
| Name | Type | Description |
|---|---|---|
font |
number |
Font handle from natiny.font.create |
| Type | Description |
|---|---|
number |
Drawing units added after each glyph; 0 by default and for an invalid handle |
local spacing = natiny.font.get_letter_spacing(font)
float natiny_font_get_line_height(NatinyFont font);
Returns the vertical distance between consecutive baselines.
| Name | Type | Description |
|---|---|---|
font |
NatinyFont |
Font handle from natiny_font_create |
| Type | Description |
|---|---|
float |
Line height in drawing units, or 0.0f for an invalid handle |
float lh = natiny_font_get_line_height(font);
natiny_font_set_line_spacing.natiny_font_measure_text, the wrapping in
natiny_font_measure_wrapped and the lines natiny_render_text_box draws
all move together with it.natiny_font_get_ascent and
natiny_font_get_descent report distances around the
baseline; their sum does not include the font's line gap.natiny.font.get_line_height(font) -> line_height
Returns the vertical distance between consecutive baselines.
| Name | Type | Description |
|---|---|---|
font |
number |
Font handle from natiny.font.create |
| Type | Description |
|---|---|
number |
Line height in drawing units, or 0 for an invalid handle |
local lh = natiny.font.get_line_height(font)
natiny.font.set_line_spacing.natiny.font.measure_text, the wrapping in
natiny.font.measure_wrapped and the lines natiny.render.text_box draws
all move together with it.natiny.font.get_ascent and
natiny.font.get_descent report distances around the
baseline; their sum does not include the font's line gap.float natiny_font_get_line_spacing(NatinyFont font);
Returns the line spacing multiplier set on this font.
| Name | Type | Description |
|---|---|---|
font |
NatinyFont |
Font handle from natiny_font_create |
| Type | Description |
|---|---|
float |
Line-height multiplier; 1.0f by default, or 0.0f for an invalid handle |
float factor = natiny_font_get_line_spacing(font);
0.0f if that factor was set explicitly.natiny.font.get_line_spacing(font) -> factor
Returns the line spacing multiplier set on this font.
| Name | Type | Description |
|---|---|---|
font |
number |
Font handle from natiny.font.create |
| Type | Description |
|---|---|
number |
Line-height multiplier; 1 by default, or 0 for an invalid handle |
local factor = natiny.font.get_line_spacing(font)
0 if that factor was set explicitly.float natiny_font_get_size(NatinyFont font);
Returns the logical font size requested at creation.
| Name | Type | Description |
|---|---|---|
font |
NatinyFont |
Font handle from natiny_font_create |
| Type | Description |
|---|---|
float |
Font size in drawing units, or 0.0f for an invalid handle |
float size = natiny_font_get_size(font);
natiny.font.get_size(font) -> size
Returns the logical font size requested at creation.
| Name | Type | Description |
|---|---|---|
font |
number |
Font handle from natiny.font.create |
| Type | Description |
|---|---|
number |
Font size in drawing units, or 0 for an invalid handle |
local size = natiny.font.get_size(font)
bool natiny_font_has_glyph(NatinyFont font, uint32_t codepoint);
Answers whether this font can draw a code point, following its fallback chain.
| Name | Type | Description |
|---|---|---|
font |
NatinyFont |
Font handle from natiny_font_create |
codepoint |
uint32_t |
Unicode code point |
| Type | Description |
|---|---|
bool |
true if the font or one of its fallbacks provides the glyph |
bool can_draw = natiny_font_has_glyph(font, 0x4E2D);
false means the code point would be drawn as the missing-glyph box.false.natiny.font.has_glyph(font, codepoint) -> boolean
Answers whether this font can draw a code point, following its fallback chain.
| Name | Type | Description |
|---|---|---|
font |
number |
Font handle from natiny.font.create |
codepoint |
number or string |
Unicode code point, or a UTF-8 string whose first character is tested |
| Type | Description |
|---|---|
boolean |
true if the font or one of its fallbacks provides the glyph |
local can_draw = natiny.font.has_glyph(font, "中")
false means the code point would be drawn as the missing-glyph box.void natiny_font_measure_text(NatinyFont font, const char* text,
float* width, float* height);
Writes the width and height of an unwrapped text block to optional output pointers.
| Name | Type | Description |
|---|---|---|
font |
NatinyFont |
Font handle from natiny_font_create |
text |
const char* |
Null-terminated UTF-8 text |
width |
float* |
Receives the widest-line width in drawing units; may be NULL |
height |
float* |
Receives the block height in drawing units; may be NULL |
float width = 0.0f;
float height = 0.0f;
natiny_font_measure_text(font, "Hello!", &width, &height);
natiny_render_text(font,
(800.0f - width) * 0.5f,
(600.0f - height) * 0.5f,
"Hello!");
\n starts a new line; the widest line determines the result.natiny_font_measure_wrapped to add word wrapping.natiny_render_text.0.0f wide and one line tall. Invalid handles or
NULL text produce zero width and height.natiny.font.measure_text(font, text) -> width, height
Returns the width and height of an unwrapped text block.
| Name | Type | Description |
|---|---|---|
font |
number |
Font handle from natiny.font.create |
text |
string |
UTF-8 text to measure |
| Type | Description |
|---|---|
number |
Width of the widest line in drawing units |
number |
Height of the text block in drawing units |
local text = "Hello!"
local w, h = natiny.font.measure_text(font, text)
natiny.render.text(font, (800 - w) / 2, (600 - h) / 2, text)
\n starts a new line. Use
natiny.font.measure_wrapped to add word wrapping.natiny.render.text.0 wide and one line tall.void natiny_font_measure_wrapped(NatinyFont font, const char* text,
float max_width, float* width,
float* height, uint32_t* lines);
Lays text out and reports the size of the block it makes, without drawing it.
| Name | Type | Description |
|---|---|---|
font |
NatinyFont |
Font handle from natiny_font_create |
text |
const char* |
Null-terminated UTF-8 text |
max_width |
float |
Wrap width in drawing units; values at or below 0.0f do not wrap |
width |
float* |
Receives the widest-line width in drawing units; may be NULL |
height |
float* |
Receives the block height in drawing units; may be NULL |
lines |
uint32_t* |
Receives the line count; may be NULL |
float w = 0.0f, h = 0.0f;
uint32_t lines = 0;
natiny_font_measure_wrapped(font, message, 300.0f, &w, &h, &lines);
natiny_render_rectangle(20.0f, 20.0f, w + 16.0f, h + 16.0f);
natiny_render_text_box does, so a box sized
from this holds what that draws into it.\n, and between Han, kana and Hangul
characters, which are written without spaces. A word longer than
max_width is cut where it stops fitting rather than allowed to overflow.lines times natiny_font_get_line_height, so it follows
natiny_font_set_line_spacing.0, the height is one
line, and lines is 1.NULL text produce zero width, height, and lines.natiny.font.measure_wrapped(font, text [, max_width]) -> width, height, lines
Lays text out and reports the size of the block it makes, without drawing it.
| Name | Type | Default | Description |
|---|---|---|---|
font |
number |
Font handle from natiny.font.create |
|
text |
string |
UTF-8 text to lay out | |
max_width |
number |
0 |
Wrap width in drawing units; values at or below 0 do not wrap |
| Type | Description |
|---|---|
number |
Width of the widest line in drawing units |
number |
Height of the block in drawing units |
number |
Number of lines in the block |
local w, h, lines = natiny.font.measure_wrapped(font, message, 300)
-- a panel that fits its text
natiny.render.set_color(0.1, 0.1, 0.12, 1)
natiny.render.rectangle(20, 20, w + 16, h + 16)
natiny.render.set_color(1, 1, 1, 1)
natiny.render.text_box(font, 28, 28, 300, h, message)
natiny.render.text_box does, so a box sized
from this holds what that draws into it.\n, and between Han, kana and Hangul
characters, which are written without spaces. A word longer than
max_width is cut where it stops fitting rather than allowed to overflow.lines times natiny.font.get_line_height, so it follows
natiny.font.set_line_spacing.0, the height is one
line, and lines is 1.uint32_t natiny_font_offset_at(NatinyFont font, const char* text, float x);
Returns the byte offset in text nearest to horizontal position x.
| Name | Type | Description |
|---|---|---|
font |
NatinyFont |
Font handle from natiny_font_create |
text |
const char* |
Null-terminated UTF-8 text of one line |
x |
float |
Distance from the start of the line, in drawing units |
| Type | Description |
|---|---|
uint32_t |
Byte offset from 0 through strlen(text); 0 for an invalid handle or NULL text |
uint32_t caret = natiny_font_offset_at(font, line, mouse_x - text_x);
/* the text up to the caret */
fwrite(line, 1, caret, stdout);
text and always lands on a UTF-8
boundary, never inside a character.\n ends the search and its byte offset is returned.natiny_font_measure_text.natiny.font.offset_at(font, text, x) -> offset
Returns the byte offset in text nearest to horizontal position x.
| Name | Type | Description |
|---|---|---|
font |
number |
Font handle from natiny.font.create |
text |
string |
UTF-8 text of one line |
x |
number |
Distance from the start of the line, in drawing units |
| Type | Description |
|---|---|
number |
Byte offset from 0 through #text |
-- where a click puts the caret
local caret = natiny.font.offset_at(font, line, mouse_x - text_x)
local before = line:sub(1, caret)
local after = line:sub(caret + 1)
natiny.render.text(font, text_x, y, before .. "|" .. after)
text:sub(1, offset) is the part before
the caret and text:sub(offset + 1) the part after it. It always lands on
a UTF-8 boundary, never inside a character.\n ends the search and its byte offset is returned.
Slice the line out first for multi-line text.natiny.font.measure_text.void natiny_font_preload(NatinyFont font, const char* text);
Rasterizes every glyph the text uses now, instead of on the frame that first draws it.
| Name | Type | Description |
|---|---|---|
font |
NatinyFont |
Font handle from natiny_font_create |
text |
const char* |
Null-terminated UTF-8 text whose glyphs to bake |
NatinyFont font = natiny_font_create(res, 24.0f);
natiny_font_preload(font, "Play Settings Quit");
text are skipped; they have no glyph.NULL text are ignored.natiny.font.preload(font, text)
Rasterizes every glyph the text uses now, instead of on the frame that first draws it.
| Name | Type | Description |
|---|---|---|
font |
number |
Font handle from natiny.font.create |
text |
string |
UTF-8 text whose glyphs to bake |
local font = natiny.font.create(res, 24)
natiny.font.preload(font, "Играть Настройки Выход")
text are skipped; they have no glyph.void natiny_font_preload_range(NatinyFont font, uint32_t first, uint32_t last);
Rasterizes every glyph in a range of code points.
| Name | Type | Description |
|---|---|---|
font |
NatinyFont |
Font handle from natiny_font_create |
first |
uint32_t |
First code point |
last |
uint32_t |
Last code point, inclusive |
natiny_font_preload_range(font, 0x0410, 0x044F); /* Cyrillic */
last is less than
first; the upper endpoint is clamped to U+10FFFF.natiny_font_preload when the strings are known.natiny.font.preload_range(font, first, last)
Rasterizes every glyph in a range of code points.
| Name | Type | Description |
|---|---|---|
font |
number |
Font handle from natiny.font.create |
first |
number or string |
First Unicode code point, or a UTF-8 string whose first character is used |
last |
number or string |
Last Unicode code point, inclusive, or a UTF-8 string whose first character is used |
natiny.font.preload_range(font, 0x0410, 0x044F)
last is less than first.natiny.font.preload when the strings are known.void natiny_font_set_fallback(NatinyFont font, NatinyFont fallback);
Names another font to draw the code points this one has no outline for.
| Name | Type | Description |
|---|---|---|
font |
NatinyFont |
Font handle from natiny_font_create |
fallback |
NatinyFont |
Font used for missing glyphs; 0 clears the fallback |
NatinyFont latin = natiny_font_create(roboto, 24.0f);
NatinyFont emoji = natiny_font_create(noto_emoji, 24.0f);
natiny_font_set_fallback(latin, emoji);
natiny_render_text(latin, 10.0f, 10.0f, "Ready \xF0\x9F\x9A\x80");
natiny.font.set_fallback(font, fallback)
Names another font to draw the code points this one has no outline for.
| Name | Type | Description |
|---|---|---|
font |
number |
Font handle from natiny.font.create |
fallback |
number or nil |
Font used for missing glyphs; nil clears the fallback |
local latin = natiny.font.create(roboto, 24)
local emoji = natiny.font.create(noto_emoji, 24)
natiny.font.set_fallback(latin, emoji)
natiny.render.text(latin, 10, 10, "Ready 🚀")
void natiny_font_set_filter(NatinyFont font, uint32_t min_filter,
uint32_t mag_filter);
Sets how the font's atlas is sampled.
| Name | Type | Description |
|---|---|---|
font |
NatinyFont |
Font handle from natiny_font_create |
min_filter |
uint32_t |
NATINY_FILTER_LINEAR or NATINY_FILTER_NEAREST |
mag_filter |
uint32_t |
NATINY_FILTER_LINEAR or NATINY_FILTER_NEAREST |
natiny_font_set_filter(pixel_font,
NATINY_FILTER_NEAREST, NATINY_FILTER_NEAREST);
NATINY_FILTER_LINEAR is the default.NATINY_FILTER_NEAREST is taken as
NATINY_FILTER_LINEAR.min_filter matters only where text is
drawn smaller than it was baked.natiny.font.set_filter(font, min_filter [, mag_filter])
Sets how the font's atlas is sampled.
| Name | Type | Default | Description |
|---|---|---|---|
font |
number |
Font handle from natiny.font.create |
|
min_filter |
number |
natiny.font.FILTER_LINEAR or natiny.font.FILTER_NEAREST |
|
mag_filter |
number |
min_filter |
natiny.font.FILTER_LINEAR or natiny.font.FILTER_NEAREST |
natiny.font.set_filter(pixel_font, natiny.font.FILTER_NEAREST)
natiny.font.FILTER_LINEAR is the default.mag_filter applies min_filter to both operations.natiny.font.FILTER_NEAREST select linear filtering.min_filter matters only where text is
drawn smaller than it was baked.void natiny_font_set_letter_spacing(NatinyFont font, float spacing);
Sets an extra gap added after every glyph, in drawing units.
| Name | Type | Description |
|---|---|---|
font |
NatinyFont |
Font handle from natiny_font_create |
spacing |
float |
Drawing units added after each glyph; must satisfy -1e9f < spacing < 1e9f |
natiny_font_set_letter_spacing(font, 2.0f);
natiny.font.set_letter_spacing(font, spacing)
Sets an extra gap added after every glyph, in drawing units.
| Name | Type | Description |
|---|---|---|
font |
number |
Font handle from natiny.font.create |
spacing |
number |
Drawing units added after each glyph; must satisfy -1e9 < spacing < 1e9 |
natiny.font.set_letter_spacing(font, 2)
void natiny_font_set_line_spacing(NatinyFont font, float factor);
Sets how far apart the lines of this font sit, as a multiple of its natural line height.
| Name | Type | Description |
|---|---|---|
font |
NatinyFont |
Font handle from natiny_font_create |
factor |
float |
Non-negative multiplier applied to the natural line height; 1.0f restores it |
natiny_font_set_line_spacing(font, 1.4f);
natiny_font_get_line_height, measuring,
wrapping, and drawing all use the adjusted line height.0.0f is allowed and stacks the lines.natiny.font.set_line_spacing(font, factor)
Sets how far apart the lines of this font sit, as a multiple of its natural line height.
| Name | Type | Description |
|---|---|---|
font |
number |
Font handle from natiny.font.create |
factor |
number |
Non-negative multiplier applied to the natural line height; 1 restores it |
natiny.font.set_line_spacing(font, 1.4)
natiny.font.get_line_height, measuring,
wrapping, and drawing all use the adjusted line height.0 is allowed and stacks the lines.uint32_t natiny_input_get_char_at(int index);
Returns the Unicode code point from the current frame text-input buffer at the specified index.
| Name | Type | Description |
|---|---|---|
index |
int |
Zero-based character index |
| Type | Description |
|---|---|
uint32_t |
Unicode code point, or 0 for an invalid index |
uint32_t cp = natiny_input_get_char_at(0);
if (cp != 0) {
/* handle first typed character */
}
0.natiny.input.get_char_at(index) -> codepoint
Returns the Unicode code point from the current frame text-input buffer at the specified index.
| Name | Type | Description |
|---|---|---|
index |
number |
Zero-based character index |
| Type | Description |
|---|---|
number |
Unicode code point, or 0 for an invalid index |
local cp = natiny.input.get_char_at(0)
if cp ~= 0 then
print("first code point:", cp)
end
0.int natiny_input_get_char_count(void);
Returns how many Unicode characters were captured in the current frame text-input buffer.
| Type | Description |
|---|---|
int |
Number of available input characters |
int n = natiny_input_get_char_count();
natiny.input.get_char_count() -> count
Returns how many Unicode characters were captured in the current frame text-input buffer.
| Type | Description |
|---|---|
number |
Number of available input characters |
local n = natiny.input.get_char_count()
bool natiny_input_get_key_down(uint32_t key);
Returns true if the specified keyboard key is currently held down.
| Name | Type | Description |
|---|---|---|
key |
uint32_t |
Key code - a NATINY_INPUT_KEY_* constant |
| Type | Description |
|---|---|
bool |
true if the key is held, false otherwise |
if (natiny_input_get_key_down(NATINY_INPUT_KEY_W))
move_forward();
true every frame while the key remains held.natiny.input.get_key_down(key) -> boolean
Returns true if the specified keyboard key is currently held down.
| Name | Type | Description |
|---|---|---|
key |
number |
Key code - a natiny.input.KEY_* constant, e.g. natiny.input.KEY_W |
| Type | Description |
|---|---|
boolean |
true if the key is held, false otherwise |
if natiny.input.get_key_down(natiny.input.KEY_W) then
move_forward()
end
true every frame as long as the key is held.bool natiny_input_get_key_pressed(uint32_t key);
Returns true only on the frame when the specified key was first pressed down.
| Name | Type | Description |
|---|---|---|
key |
uint32_t |
Key code - a NATINY_INPUT_KEY_* constant |
| Type | Description |
|---|---|
bool |
true on the frame the key was pressed, false otherwise |
if (natiny_input_get_key_pressed(NATINY_INPUT_KEY_SPACE))
jump();
true for the frame that consumes a released-to-pressed transition.natiny.input.get_key_pressed(key) -> boolean
Returns true only on the frame when the specified key was first pressed down.
| Name | Type | Description |
|---|---|---|
key |
number |
Key code - a natiny.input.KEY_* constant, e.g. natiny.input.KEY_W |
| Type | Description |
|---|---|
boolean |
true on the frame the key was pressed, false otherwise |
if natiny.input.get_key_pressed(natiny.input.KEY_SPACE) then
jump()
end
true for the frame that consumes a released-to-pressed transition.bool natiny_input_get_key_released(uint32_t key);
Returns true only on the frame when the specified key was released.
| Name | Type | Description |
|---|---|---|
key |
uint32_t |
Key code - a NATINY_INPUT_KEY_* constant |
| Type | Description |
|---|---|
bool |
true on the frame the key was released, false otherwise |
if (natiny_input_get_key_released(NATINY_INPUT_KEY_ESCAPE))
close_menu();
true for the frame that consumes a pressed-to-released transition.natiny.input.get_key_released(key) -> boolean
Returns true only on the frame when the specified key was released.
| Name | Type | Description |
|---|---|---|
key |
number |
Key code - a natiny.input.KEY_* constant, e.g. natiny.input.KEY_W |
| Type | Description |
|---|---|
boolean |
true on the frame the key was released, false otherwise |
if natiny.input.get_key_released(natiny.input.KEY_ESCAPE) then
close_menu()
end
true for the frame that consumes a pressed-to-released transition.bool natiny_input_get_mouse_button_down(uint32_t button);
Returns true if the specified mouse button is currently held down.
| Name | Type | Description |
|---|---|---|
button |
uint32_t |
Mouse button - a NATINY_INPUT_MOUSE_BUTTON_* constant |
| Type | Description |
|---|---|
bool |
true if the button is held, false otherwise |
if (natiny_input_get_mouse_button_down(NATINY_INPUT_MOUSE_BUTTON_LEFT))
drag();
true every frame while the button remains held.natiny.input.get_mouse_button_down(button) -> boolean
Returns true if the specified mouse button is currently held down.
| Name | Type | Description |
|---|---|---|
button |
number |
Mouse button - a natiny.input.MOUSE_BUTTON_* constant, e.g. natiny.input.MOUSE_BUTTON_LEFT |
| Type | Description |
|---|---|
boolean |
true if the button is held, false otherwise |
if natiny.input.get_mouse_button_down(natiny.input.MOUSE_BUTTON_LEFT) then
drag()
end
true every frame as long as the button is held.mouse_button_pressed to detect the moment the button is first pressed.bool natiny_input_get_mouse_button_pressed(uint32_t button);
Returns true only on the frame when the specified mouse button was first pressed down.
| Name | Type | Description |
|---|---|---|
button |
uint32_t |
Mouse button - a NATINY_INPUT_MOUSE_BUTTON_* constant |
| Type | Description |
|---|---|
bool |
true on the frame the button was pressed, false otherwise |
if (natiny_input_get_mouse_button_pressed(NATINY_INPUT_MOUSE_BUTTON_LEFT))
select_item();
true for the frame that consumes a released-to-pressed transition.natiny.input.get_mouse_button_pressed(button) -> boolean
Returns true only on the frame when the specified mouse button was first pressed down.
| Name | Type | Description |
|---|---|---|
button |
number |
Mouse button - a natiny.input.MOUSE_BUTTON_* constant, e.g. natiny.input.MOUSE_BUTTON_LEFT |
| Type | Description |
|---|---|
boolean |
true on the frame the button was pressed, false otherwise |
if natiny.input.get_mouse_button_pressed(natiny.input.MOUSE_BUTTON_LEFT) then
select_item()
end
true for the frame that consumes a released-to-pressed transition.bool natiny_input_get_mouse_button_released(uint32_t button);
Returns true only on the frame when the specified mouse button was released.
| Name | Type | Description |
|---|---|---|
button |
uint32_t |
Mouse button - a NATINY_INPUT_MOUSE_BUTTON_* constant |
| Type | Description |
|---|---|
bool |
true on the frame the button was released, false otherwise |
if (natiny_input_get_mouse_button_released(NATINY_INPUT_MOUSE_BUTTON_LEFT))
finish_drag();
true for the frame that consumes a pressed-to-released transition.natiny.input.get_mouse_button_released(button) -> boolean
Returns true only on the frame when the specified mouse button was released.
| Name | Type | Description |
|---|---|---|
button |
number |
Mouse button - a natiny.input.MOUSE_BUTTON_* constant, e.g. natiny.input.MOUSE_BUTTON_LEFT |
| Type | Description |
|---|---|
boolean |
true on the frame the button was released, false otherwise |
if natiny.input.get_mouse_button_released(natiny.input.MOUSE_BUTTON_LEFT) then
finish_drag()
end
true for the frame that consumes a pressed-to-released transition.bool natiny_input_get_mouse_captured(void);
Returns whether mouse capture is enabled.
| Type | Description |
|---|---|
bool |
true when the mouse is captured, false otherwise |
bool captured = natiny_input_get_mouse_captured();
natiny_input_set_mouse_capture to change the
state.natiny.input.get_mouse_captured() -> boolean
Returns whether mouse capture is enabled.
| Type | Description |
|---|---|
boolean |
true when the mouse is captured, false otherwise |
local captured = natiny.input.get_mouse_captured()
natiny.input.set_mouse_capture to change the
state.int natiny_input_get_mouse_x(void);
Returns the mouse cursor's X coordinate relative to the window's left edge.
| Type | Description |
|---|---|
int |
Mouse cursor X coordinate in drawing units |
int x = natiny_input_get_mouse_x();
NATINY_SCALE_MODE_PIXELS, and display-scaled drawing units in
NATINY_SCALE_MODE_STRETCH and NATINY_SCALE_MODE_HIDPI.natiny.input.get_mouse_x() -> x
Returns the mouse cursor's X coordinate relative to the window's left edge.
| Type | Description |
|---|---|
number |
Mouse cursor X coordinate in drawing units |
local x = natiny.input.get_mouse_x()
natiny.window.SCALE_MODE_PIXELS, and display-scaled drawing units in
SCALE_MODE_STRETCH and SCALE_MODE_HIDPI.int natiny_input_get_mouse_x_speed(void);
Returns the horizontal movement delta of the mouse cursor since the previous frame.
| Type | Description |
|---|---|
int |
Horizontal movement in drawing units since the previous frame |
int dx = natiny_input_get_mouse_x_speed();
0
when the cursor position does not change.natiny.input.get_mouse_x_speed() -> delta_x
Returns the horizontal movement delta of the mouse cursor since the previous frame.
| Type | Description |
|---|---|
number |
Horizontal movement in drawing units since the previous frame |
local dx = natiny.input.get_mouse_x_speed()
0
when the cursor position does not change.int natiny_input_get_mouse_y(void);
Returns the mouse cursor's Y coordinate relative to the window's top edge.
| Type | Description |
|---|---|
int |
Mouse cursor Y coordinate in drawing units |
int y = natiny_input_get_mouse_y();
NATINY_SCALE_MODE_PIXELS, and display-scaled drawing units in
NATINY_SCALE_MODE_STRETCH and NATINY_SCALE_MODE_HIDPI.natiny.input.get_mouse_y() -> y
Returns the mouse cursor's Y coordinate relative to the window's top edge.
| Type | Description |
|---|---|
number |
Mouse cursor Y coordinate in drawing units |
local y = natiny.input.get_mouse_y()
natiny.window.SCALE_MODE_PIXELS, and display-scaled drawing units in
SCALE_MODE_STRETCH and SCALE_MODE_HIDPI.int natiny_input_get_mouse_y_speed(void);
Returns the vertical movement delta of the mouse cursor since the previous frame.
| Type | Description |
|---|---|
int |
Vertical movement in drawing units since the previous frame |
int dy = natiny_input_get_mouse_y_speed();
0
when the cursor position does not change.natiny.input.get_mouse_y_speed() -> delta_y
Returns the vertical movement delta of the mouse cursor since the previous frame.
| Type | Description |
|---|---|
number |
Vertical movement in drawing units since the previous frame |
local dy = natiny.input.get_mouse_y_speed()
0
when the cursor position does not change.uint32_t natiny_input_get_touch_at(int index);
Returns the stable touch identifier at an index in the current frame's touch list.
| Name | Type | Description |
|---|---|---|
index |
int |
Zero-based index from 0 to natiny_input_get_touch_count() minus one |
| Type | Description |
|---|---|
uint32_t |
Touch identifier, or 0 when index is outside the current list |
for (int index = 0; index < natiny_input_get_touch_count(); index++) {
uint32_t touch = natiny_input_get_touch_at(index);
if (natiny_input_get_touch_pressed(touch))
begin_drag(touch);
}
0 or false after the identifier expires.0 is reserved as the invalid touch identifier.0.natiny.input.get_touch_at(index) -> touch
Returns the stable touch identifier at an index in the current frame's touch list.
| Name | Type | Description |
|---|---|---|
index |
number |
Zero-based index from 0 to natiny.input.get_touch_count() minus one |
| Type | Description |
|---|---|
number |
Touch identifier, or 0 when index is outside the current list |
for index = 0, natiny.input.get_touch_count() - 1 do
local touch = natiny.input.get_touch_at(index)
if natiny.input.get_touch_pressed(touch) then
begin_drag(touch)
end
end
0 or false after the identifier expires.0 is reserved as the invalid touch identifier.0.int natiny_input_get_touch_count(void);
Returns the number of touch identifiers available in the current frame. The count includes touches released during this frame.
| Type | Description |
|---|---|
int |
Number of available touch identifiers, from 0 to 24 |
for (int i = 0; i < natiny_input_get_touch_count(); i++) {
uint32_t touch = natiny_input_get_touch_at(i);
natiny_render_circle((float)natiny_input_get_touch_x(touch),
(float)natiny_input_get_touch_y(touch), 24.0f);
}
natiny_input_get_touch_released.natiny_input_get_touch_at. The other
touch functions take the returned identifier, not the index.0.natiny.input.get_touch_count() -> count
Returns the number of touch identifiers available in the current frame. The count includes touches released during this frame.
| Type | Description |
|---|---|
number |
Number of available touch identifiers, from 0 to 24 |
for i = 0, natiny.input.get_touch_count() - 1 do
local touch = natiny.input.get_touch_at(i)
natiny.render.circle(natiny.input.get_touch_x(touch),
natiny.input.get_touch_y(touch), 24)
end
natiny.input.get_touch_released.natiny.input.get_touch_at. The other
touch functions take the returned identifier, not the index.0.bool natiny_input_get_touch_down(uint32_t touch);
Returns whether the specified touch is currently in contact with the screen.
| Name | Type | Description |
|---|---|---|
touch |
uint32_t |
Touch identifier from natiny_input_get_touch_at |
| Type | Description |
|---|---|
bool |
true while the touch is in contact with the screen; false on its release frame or for an invalid identifier |
if (drag && natiny_input_get_touch_down(drag)) {
camera_x -= (float)natiny_input_get_touch_x_speed(drag);
}
false on the same frame that
natiny_input_get_touch_released returns true.natiny.input.get_touch_down(touch) -> down
Returns whether the specified touch is currently in contact with the screen.
| Name | Type | Description |
|---|---|---|
touch |
number |
Touch identifier from natiny.input.get_touch_at |
| Type | Description |
|---|---|
boolean |
true while the touch is in contact with the screen; false on its release frame or for an invalid identifier |
if drag and natiny.input.get_touch_down(drag) then
camera_x = camera_x - natiny.input.get_touch_x_speed(drag)
end
false on the same frame that
natiny.input.get_touch_released returns true.bool natiny_input_get_touch_pressed(uint32_t touch);
Returns true only on the frame when the specified touch first contacts the
screen.
| Name | Type | Description |
|---|---|---|
touch |
uint32_t |
Touch identifier from natiny_input_get_touch_at |
| Type | Description |
|---|---|
bool |
true on the touch's first frame; false on later frames or for an invalid identifier |
for (int i = 0; i < natiny_input_get_touch_count(); i++) {
uint32_t touch = natiny_input_get_touch_at(i);
if (natiny_input_get_touch_pressed(touch))
fire(natiny_input_get_touch_x(touch), natiny_input_get_touch_y(touch));
}
true here and from
natiny_input_get_touch_released in the same frame.natiny.input.get_touch_pressed(touch) -> pressed
Returns true only on the frame when the specified touch first contacts the
screen.
| Name | Type | Description |
|---|---|---|
touch |
number |
Touch identifier from natiny.input.get_touch_at |
| Type | Description |
|---|---|
boolean |
true on the touch's first frame; false on later frames or for an invalid identifier |
for i = 0, natiny.input.get_touch_count() - 1 do
local touch = natiny.input.get_touch_at(i)
if natiny.input.get_touch_pressed(touch) then
fire(natiny.input.get_touch_x(touch), natiny.input.get_touch_y(touch))
end
end
true here and from
natiny.input.get_touch_released in the same frame.bool natiny_input_get_touch_released(uint32_t touch);
Returns true only on the frame when the specified touch leaves the screen or
is cancelled by the platform.
| Name | Type | Description |
|---|---|---|
touch |
uint32_t |
Touch identifier from natiny_input_get_touch_at |
| Type | Description |
|---|---|
bool |
true on the touch's release frame; false on other frames or for an invalid identifier |
for (int i = 0; i < natiny_input_get_touch_count(); i++) {
uint32_t touch = natiny_input_get_touch_at(i);
if (natiny_input_get_touch_released(touch))
drop(natiny_input_get_touch_x(touch), natiny_input_get_touch_y(touch));
}
natiny.input.get_touch_released(touch) -> released
Returns true only on the frame when the specified touch leaves the screen or
is cancelled by the platform.
| Name | Type | Description |
|---|---|---|
touch |
number |
Touch identifier from natiny.input.get_touch_at |
| Type | Description |
|---|---|
boolean |
true on the touch's release frame; false on other frames or for an invalid identifier |
for i = 0, natiny.input.get_touch_count() - 1 do
local touch = natiny.input.get_touch_at(i)
if natiny.input.get_touch_released(touch) then
drop(natiny.input.get_touch_x(touch), natiny.input.get_touch_y(touch))
end
end
int natiny_input_get_touch_x(uint32_t touch);
Returns the touch position on the x axis in the window's current drawing units.
| Name | Type | Description |
|---|---|---|
touch |
uint32_t |
Touch identifier from natiny_input_get_touch_at |
| Type | Description |
|---|---|
int |
Position in drawing units from the window's left edge, or 0 for an invalid identifier |
for (int index = 0; index < natiny_input_get_touch_count(); index++) {
uint32_t touch = natiny_input_get_touch_at(index);
natiny_render_circle((float)natiny_input_get_touch_x(touch),
(float)natiny_input_get_touch_y(touch), 24.0f);
}
natiny_input_get_mouse_x and 2D drawing calls.natiny_input_get_touch_x_speed.natiny.input.get_touch_x(touch) -> x
Returns the touch position on the x axis in the window's current drawing units.
| Name | Type | Description |
|---|---|---|
touch |
number |
Touch identifier from natiny.input.get_touch_at |
| Type | Description |
|---|---|
number |
Position in drawing units from the window's left edge, or 0 for an invalid identifier |
for index = 0, natiny.input.get_touch_count() - 1 do
local touch = natiny.input.get_touch_at(index)
natiny.render.circle(natiny.input.get_touch_x(touch),
natiny.input.get_touch_y(touch), 24)
end
natiny.input.get_mouse_x and 2D drawing calls.natiny.input.get_touch_x_speed.int natiny_input_get_touch_x_speed(uint32_t touch);
Returns the touch movement along the x axis since the previous frame, in the window's current drawing units.
| Name | Type | Description |
|---|---|---|
touch |
uint32_t |
Touch identifier from natiny_input_get_touch_at |
| Type | Description |
|---|---|
int |
Signed horizontal movement in drawing units; positive to the right, negative to the left, or 0 for a new or invalid touch |
for (int index = 0; index < natiny_input_get_touch_count(); index++) {
uint32_t touch = natiny_input_get_touch_at(index);
if (natiny_input_get_touch_down(touch))
camera_x -= (float)natiny_input_get_touch_x_speed(touch);
}
natiny_input_get_mouse_x_speed.0 because no previous touch position exists.natiny_input_get_touch_x.natiny.input.get_touch_x_speed(touch) -> speed
Returns the touch movement along the x axis since the previous frame, in the window's current drawing units.
| Name | Type | Description |
|---|---|---|
touch |
number |
Touch identifier from natiny.input.get_touch_at |
| Type | Description |
|---|---|
number |
Signed horizontal movement in drawing units; positive to the right, negative to the left, or 0 for a new or invalid touch |
for index = 0, natiny.input.get_touch_count() - 1 do
local touch = natiny.input.get_touch_at(index)
if natiny.input.get_touch_down(touch) then
camera_x = camera_x - natiny.input.get_touch_x_speed(touch)
end
end
natiny.input.get_mouse_x_speed.0 because no previous touch position exists.natiny.input.get_touch_x.int natiny_input_get_touch_y(uint32_t touch);
Returns the touch position on the y axis in the window's current drawing units.
| Name | Type | Description |
|---|---|---|
touch |
uint32_t |
Touch identifier from natiny_input_get_touch_at |
| Type | Description |
|---|---|
int |
Position in drawing units from the window's top edge, or 0 for an invalid identifier |
for (int index = 0; index < natiny_input_get_touch_count(); index++) {
uint32_t touch = natiny_input_get_touch_at(index);
natiny_render_circle((float)natiny_input_get_touch_x(touch),
(float)natiny_input_get_touch_y(touch), 24.0f);
}
natiny_input_get_mouse_y and 2D drawing calls.natiny_input_get_touch_y_speed.natiny.input.get_touch_y(touch) -> y
Returns the touch position on the y axis in the window's current drawing units.
| Name | Type | Description |
|---|---|---|
touch |
number |
Touch identifier from natiny.input.get_touch_at |
| Type | Description |
|---|---|
number |
Position in drawing units from the window's top edge, or 0 for an invalid identifier |
for index = 0, natiny.input.get_touch_count() - 1 do
local touch = natiny.input.get_touch_at(index)
natiny.render.circle(natiny.input.get_touch_x(touch),
natiny.input.get_touch_y(touch), 24)
end
natiny.input.get_mouse_y and 2D drawing calls.natiny.input.get_touch_y_speed.int natiny_input_get_touch_y_speed(uint32_t touch);
Returns the touch movement along the y axis since the previous frame, in the window's current drawing units.
| Name | Type | Description |
|---|---|---|
touch |
uint32_t |
Touch identifier from natiny_input_get_touch_at |
| Type | Description |
|---|---|
int |
Signed vertical movement in drawing units; positive downward, negative upward, or 0 for a new or invalid touch |
for (int index = 0; index < natiny_input_get_touch_count(); index++) {
uint32_t touch = natiny_input_get_touch_at(index);
if (natiny_input_get_touch_down(touch))
camera_y -= (float)natiny_input_get_touch_y_speed(touch);
}
natiny_input_get_mouse_y_speed.0 because no previous touch position exists.natiny_input_get_touch_y.natiny.input.get_touch_y_speed(touch) -> speed
Returns the touch movement along the y axis since the previous frame, in the window's current drawing units.
| Name | Type | Description |
|---|---|---|
touch |
number |
Touch identifier from natiny.input.get_touch_at |
| Type | Description |
|---|---|
number |
Signed vertical movement in drawing units; positive downward, negative upward, or 0 for a new or invalid touch |
for index = 0, natiny.input.get_touch_count() - 1 do
local touch = natiny.input.get_touch_at(index)
if natiny.input.get_touch_down(touch) then
camera_y = camera_y - natiny.input.get_touch_y_speed(touch)
end
end
natiny.input.get_mouse_y_speed.0 because no previous touch position exists.natiny.input.get_touch_y.void natiny_input_set_mouse_capture(bool enabled);
Enables or disables mouse capture. While captured, the cursor is hidden and mouse movement continues past the window edges.
| Name | Type | Description |
|---|---|---|
enabled |
bool |
true to capture the mouse, false to release it |
if (natiny_input_get_mouse_button_pressed(NATINY_INPUT_MOUSE_BUTTON_LEFT))
natiny_input_set_mouse_capture(true);
natiny_input_get_mouse_captured to read the state.0 on the frame capture changes and on the following
frame.natiny.input.set_mouse_capture(enabled)
Enables or disables mouse capture. While captured, the cursor is hidden and mouse movement continues past the window edges.
| Name | Type | Description |
|---|---|---|
enabled |
boolean |
true to capture the mouse, false to release it |
if natiny.input.get_mouse_button_pressed(natiny.input.MOUSE_BUTTON_LEFT) then
natiny.input.set_mouse_capture(true)
end
natiny.input.get_mouse_captured to read the state.0 on the frame capture changes and on the following
frame.void natiny_material_add_tag(NatinyMaterial material, const char* tag);
Adds a tag used by tagged draw passes to select models.
| Name | Type | Description |
|---|---|---|
material |
NatinyMaterial |
Material handle |
tag |
const char* |
Tag name |
natiny_material_add_tag(glass, "world");
natiny.material.add_tag(material, tag)
Adds a tag used by tagged draw passes to select models.
| Name | Type | Description |
|---|---|---|
material |
number |
Material handle |
tag |
string |
Tag name |
natiny.material.add_tag(glass, "world")
void natiny_material_bind(NatinyMaterial material);
Uses this material for subsequent draws until it is unbound or replaced. A
value of 0 selects the built-in material.
Binding flushes the current batch, so queued geometry keeps its previous material.
| Name | Type | Description |
|---|---|---|
material |
NatinyMaterial |
Material handle from natiny_material_create, or 0 for none |
natiny_material_bind(material);
draw_group();
natiny_material_unbind();
natiny_material_unbind
is equivalent to binding 0; it does not restore the previous material.natiny.material.bind(material [, fn])
Uses this material for subsequent draws until it is unbound or replaced. With a callback, the material applies to draws made inside the callback.
Binding flushes the current batch, so queued geometry keeps its previous material.
| Name | Type | Default | Description |
|---|---|---|---|
material |
number |
Material handle from natiny.material.create |
|
fn |
function |
nil |
Callback whose draws use the material |
natiny.material.bind(material)
natiny.material.unbind
removes the override; it does not restore the previous material.fn, Lua unbinds the material even if the callback raises an error, then
propagates the error.NatinyMaterial natiny_material_create(NatinyShader shader);
Builds a material from a shader's reflected constants, textures and vertex layout. Render pipelines are created lazily for the render states and target layouts used when drawing.
| Name | Type | Description |
|---|---|---|
shader |
NatinyShader |
Shader handle from natiny_shader_load |
| Type | Description |
|---|---|
NatinyMaterial |
Material handle, or 0 on failure |
NatinyMaterial material = natiny_material_create(shader);
natiny.material.create(shader [, tags]) -> material
Builds a material from a shader's reflected constants, textures and vertex layout. Render pipelines are created lazily for the render states and target layouts used when drawing.
| Name | Type | Default | Description |
|---|---|---|---|
shader |
number |
Shader handle from natiny.shader.load | |
tags |
string or table |
nil |
Tag name or list of tag names |
| Type | Description |
|---|---|
number |
Material handle; raises an error if creation fails |
local material = natiny.material.create(shader, "world")
void natiny_material_destroy(NatinyMaterial material);
Releases a material's pipelines, layouts and binding data. If the material is currently bound, its binding is cleared.
| Name | Type | Description |
|---|---|---|
material |
NatinyMaterial |
Material handle |
natiny_material_destroy(material);
natiny_backend_shutdown().natiny.material.destroy(material)
Releases a material's pipelines, layouts and binding data. If the material is currently bound, its binding is cleared.
| Name | Type | Description |
|---|---|---|
material |
number |
Material handle |
natiny.material.destroy(material)
bool natiny_material_has_tag(NatinyMaterial material, const char* tag);
Whether the material carries this tag.
| Name | Type | Description |
|---|---|---|
material |
NatinyMaterial |
Material handle |
tag |
const char* |
Tag name |
| Type | Description |
|---|---|
bool |
true when the material carries the tag |
bool transparent = natiny_material_has_tag(material, "transparent");
false rather than being
created here; natiny_material_add_tag is what creates one.false.natiny.material.has_tag(material, tag) -> has
Whether the material carries this tag.
| Name | Type | Description |
|---|---|---|
material |
number |
Material handle |
tag |
string |
Tag name |
| Type | Description |
|---|---|
boolean |
true when the material carries the tag |
local transparent = natiny.material.has_tag(material, "transparent")
false rather than being
created here; add_tag is what creates one.false.void natiny_material_set_constant(NatinyMaterial material, const char* name,
float x, float y, float z, float w);
Sets a scalar or vector constant by its shader member name.
| Name | Type | Description |
|---|---|---|
material |
NatinyMaterial |
Material handle |
name |
const char* |
Member name inside one of the shader's uniform blocks |
x, y, z, w |
float |
Components; anything past the member's size is dropped |
natiny_material_set_constant(material, "tint", 1.0f, 0.5f, 0.5f, 1.0f);
mat4 use natiny_material_set_matrix.natiny.material.set_constant(material, name, x [, y [, z [, w]]])
Sets a scalar or vector constant by its shader member name.
| Name | Type | Default | Description |
|---|---|---|---|
material |
number |
Material handle | |
name |
string |
Member name inside one of the shader's uniform blocks | |
x |
number |
First component | |
y |
number |
0 |
Second component |
z |
number |
0 |
Third component |
w |
number |
0 |
Fourth component |
natiny.material.set_constant(material, "tint", 1, 0.5, 0.5, 1)
mat4 use natiny.material.set_matrix.void natiny_material_set_matrix(NatinyMaterial material, const char* name,
const float* values16);
Sets a mat4 constant by name. values16 is 16 floats in column-major order, the order GLSL uses.
| Name | Type | Description |
|---|---|---|
material |
NatinyMaterial |
Material handle |
name |
const char* |
Member name inside one of the shader's uniform blocks |
values16 |
const float* |
16 floats, column-major |
float m[16] = { 1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1 };
natiny_material_set_matrix(material, "mtx_custom", m);
values16 must point to 16 column-major floats and the named member must be
a mat4.mat4 members are reported and left unchanged.natiny.material.set_matrix(material, name, values)
Sets a mat4 constant by name. values is a table of 16 numbers in column-major order, the order GLSL uses.
| Name | Type | Description |
|---|---|---|
material |
number |
Material handle |
name |
string |
Member name inside one of the shader's uniform blocks |
values |
table |
16 numbers, column-major |
natiny.material.set_matrix(material, "mtx_custom", {
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
})
values must contain 16 numbers in column-major order and the named member
must be a mat4.mat4 members are reported and left unchanged.typedef struct {
uint32_t blend; /* NATINY_BLEND_* */
uint32_t cull; /* NATINY_CULL_* */
uint32_t depth; /* NATINY_DEPTH_* */
} NatinyMaterialState;
void natiny_material_set_state(NatinyMaterial material, const NatinyMaterialState* state);
Sets the material's blend, face-culling and depth-test state.
| Name | Type | Description |
|---|---|---|
material |
NatinyMaterial |
Material handle |
state |
const NatinyMaterialState* |
The whole state; every field is read |
| Define | Value | Meaning |
|---|---|---|
NATINY_BLEND_ALPHA |
0 | straight alpha - the default |
NATINY_BLEND_NONE |
1 | overwrite |
NATINY_BLEND_ADD |
2 | additive blending |
| Define | Value | Meaning |
|---|---|---|
NATINY_CULL_DEFAULT |
0 | 2D keeps both faces, 3D drops the back |
NATINY_CULL_NONE |
1 | keep both |
NATINY_CULL_BACK |
2 | drop back faces |
NATINY_CULL_FRONT |
3 | drop front faces |
| Define | Value | Meaning |
|---|---|---|
NATINY_DEPTH_DEFAULT |
0 | 2D draws ignore depth, 3D draws use it |
NATINY_DEPTH_OFF |
1 | no test, no write |
NATINY_DEPTH_ON |
2 | test and write |
NATINY_DEPTH_TEST |
3 | test, but leave the buffer alone |
NatinyMaterialState state = {
NATINY_BLEND_NONE, NATINY_CULL_DEFAULT, NATINY_DEPTH_OFF
};
natiny_material_set_state(material, &state);
NATINY_BLEND_ALPHA, NATINY_CULL_DEFAULT and
NATINY_DEPTH_DEFAULT.natiny.material.set_state(material, { blend = ..., cull = ..., depth = ... })
Sets the material's blend, face-culling and depth-test state. Omitted fields are reset to their defaults.
| Name | Type | Description |
|---|---|---|
material |
number |
Material handle |
state |
table |
Any of blend, cull, depth |
| Constant | Meaning |
|---|---|
natiny.material.BLEND_ALPHA |
straight alpha - the default |
natiny.material.BLEND_NONE |
overwrite |
natiny.material.BLEND_ADD |
additive blending |
| Constant | Meaning |
|---|---|
natiny.material.CULL_DEFAULT |
2D keeps both faces, 3D drops the back |
natiny.material.CULL_NONE |
keep both |
natiny.material.CULL_BACK |
drop back faces |
natiny.material.CULL_FRONT |
drop front faces |
| Constant | Meaning |
|---|---|
natiny.material.DEPTH_DEFAULT |
2D draws ignore depth, 3D draws use it |
natiny.material.DEPTH_OFF |
no test, no write |
natiny.material.DEPTH_ON |
test and write |
natiny.material.DEPTH_TEST |
test, but leave the buffer alone |
natiny.material.set_state(material, {
blend = natiny.material.BLEND_NONE,
depth = natiny.material.DEPTH_OFF,
})
BLEND_ALPHA, CULL_DEFAULT and
DEPTH_DEFAULT.void natiny_material_set_texture(NatinyMaterial material, const char* texture_name, NatinyTexture texture, uint32_t min_filter, uint32_t mag_filter);
Assigns a texture to a named texture slot and configures its filtering. The assigned texture overrides one supplied by a model or draw for the same slot.
| Name | Type | Description |
|---|---|---|
material |
NatinyMaterial |
Material handle |
texture_name |
const char* |
Texture name declared in the shader |
texture |
NatinyTexture |
Texture handle; 0 selects the built-in white texture |
min_filter |
uint32_t |
Filter used when the texture is minified |
mag_filter |
uint32_t |
Filter used when the texture is magnified |
| Constant | Description |
|---|---|
NATINY_FILTER_LINEAR |
Blends neighbouring texels; minification also blends mip levels |
NATINY_FILTER_NEAREST |
Selects the nearest texel and mip level |
natiny_material_set_texture(material, "albedo", texture, NATINY_FILTER_LINEAR, NATINY_FILTER_LINEAR);
0 still assigns the slot; it does not restore texture inheritance.texture_name is reported to stderr and ignored.NATINY_FILTER_NEAREST is treated as
NATINY_FILTER_LINEAR.natiny.material.set_texture(material, texture_name [, texture [, min_filter [, mag_filter]]])
Assigns a texture to a named texture slot and configures its filtering. The assigned texture overrides one supplied by a model or draw for the same slot.
| Name | Type | Default | Description |
|---|---|---|---|
material |
number |
Material handle | |
texture_name |
string |
Texture name declared in the shader | |
texture |
number |
0 |
Texture handle; 0 selects the built-in white texture |
min_filter |
number |
FILTER_LINEAR |
Filter used when the texture is minified |
mag_filter |
number |
min_filter |
Filter used when the texture is magnified |
| Constant | Description |
|---|---|
natiny.material.FILTER_LINEAR |
Blends neighbouring texels; minification also blends mip levels |
natiny.material.FILTER_NEAREST |
Selects the nearest texel and mip level |
natiny.material.set_texture(material, "albedo", texture)
0 still assigns the slot; it does not restore texture inheritance.texture_name is reported and ignored.void natiny_material_set_vertex_constant(NatinyMaterial material, const char* name,
uint32_t constant_type);
Declares that the engine owns a constant: before every draw it is refilled with an engine-provided value instead of keeping whatever you set.
Members named mtx_world, mtx_view, mtx_proj, mtx_worldview, mtx_viewproj, mtx_worldviewproj and mtx_normal are declared this way for you. Use this call when your shader names them differently, or to turn one back into a plain user constant.
| Name | Type | Description |
|---|---|---|
material |
NatinyMaterial |
Material handle |
name |
const char* |
Member name inside one of the shader's uniform blocks |
constant_type |
uint32_t |
One of the defines below |
| Define | Value | Meaning |
|---|---|---|
NATINY_CONSTANT_USER |
0 | whatever you set |
NATINY_CONSTANT_WORLD |
1 | the drawn object's world transform |
NATINY_CONSTANT_VIEW |
2 | camera view |
NATINY_CONSTANT_PROJECTION |
3 | projection |
NATINY_CONSTANT_WORLDVIEW |
4 | view × world |
NATINY_CONSTANT_VIEWPROJ |
5 | projection × view |
NATINY_CONSTANT_WORLDVIEWPROJ |
6 | projection × view × world |
NATINY_CONSTANT_NORMAL |
7 | inverse transpose of worldview |
NATINY_CONSTANT_INV_VIEW |
8 | inverse of the camera view |
NATINY_CONSTANT_INV_PROJECTION |
9 | inverse projection |
NATINY_CONSTANT_INV_VIEWPROJ |
10 | inverse of projection × view |
NATINY_CONSTANT_CAMERA_POS |
11 | vec4: the eye in world space |
NATINY_CONSTANT_TARGET_SIZE |
12 | vec4: width, height, 1/width, 1/height |
natiny_material_set_vertex_constant(material, "u_mvp", NATINY_CONSTANT_WORLDVIEWPROJ);
NATINY_CONSTANT_WORLD through NATINY_CONSTANT_INV_VIEWPROJ) require a mat4 member. NATINY_CONSTANT_CAMERA_POS and NATINY_CONSTANT_TARGET_SIZE require a vec4 member. Members too small for the selected value are skipped.natiny.material.set_vertex_constant(material, name, constant_type)
Declares that the engine owns a constant: before every draw it is refilled with an engine-provided value instead of keeping whatever you set.
Members named mtx_world, mtx_view, mtx_proj, mtx_worldview, mtx_viewproj, mtx_worldviewproj and mtx_normal are declared this way for you. Use this call when your shader names them differently, or to turn one back into a plain user constant.
| Name | Type | Description |
|---|---|---|
material |
number |
Material handle |
name |
string |
Member name inside one of the shader's uniform blocks |
constant_type |
number |
One of the constants below |
| Constant | Meaning |
|---|---|
natiny.material.USER |
whatever you set |
natiny.material.WORLD |
the drawn object's world transform |
natiny.material.VIEW |
camera view |
natiny.material.PROJECTION |
projection |
natiny.material.WORLDVIEW |
view × world |
natiny.material.VIEWPROJ |
projection × view |
natiny.material.WORLDVIEWPROJ |
projection × view × world |
natiny.material.NORMAL |
inverse transpose of worldview |
natiny.material.INV_VIEW |
inverse of the camera view |
natiny.material.INV_PROJECTION |
inverse projection |
natiny.material.INV_VIEWPROJ |
inverse of projection × view |
natiny.material.CAMERA_POS |
vec4: the eye in world space |
natiny.material.TARGET_SIZE |
vec4: width, height, 1/width, 1/height |
natiny.material.set_vertex_constant(material, "u_mvp", natiny.material.WORLDVIEWPROJ)
WORLD through INV_VIEWPROJ) require a mat4 member. CAMERA_POS and TARGET_SIZE require a vec4 member. Members too small for the selected value are skipped.void natiny_material_unbind(void);
Removes the bound material override. The current batch is flushed first, so queued draws keep the material they were submitted with.
natiny_material_unbind();
natiny_material_bind(0); it does not
restore the previously bound material.natiny.material.unbind()
Removes the bound material override. The current batch is flushed first, so queued draws keep the material they were submitted with.
natiny.material.unbind()
NatinyMesh natiny_mesh_capsule(float radius, float height, uint32_t segments, uint32_t rings);
Builds a one-part capsule standing upright along Y and centred on the origin: a cylindrical side closed by a hemisphere at each end. The generated mesh has normals and UV coordinates but no texture.
| Name | Type | Description |
|---|---|---|
radius |
float |
Radius of the side and of both hemispheres; negative values are clamped to 0 |
height |
float |
Total height end to end, hemispheres included |
segments |
uint32_t |
Meridians - the detail around the Y axis; clamped up to 3 |
rings |
uint32_t |
Rings per hemisphere - the detail from equator to pole; clamped up to 1 |
| Type | Description |
|---|---|
NatinyMesh |
Mesh handle |
NatinyMesh capsule = natiny_mesh_capsule(0.4f, 1.8f, 32, 8);
NatinyModel player = natiny_model_create();
natiny_model_set_mesh(player, capsule);
height is the whole shape, so the cylindrical side is height - 2 * radius
tall and the mesh spans -height / 2 to +height / 2 along Y.height at or below 2 * radius produces a sphere: the side is clamped to
zero height rather than inverted, and the band that would draw it is skipped.rings counts the rings of one hemisphere, so a capsule at rings = 8 has
the same cap detail as natiny_mesh_sphere at rings = 16.u, and v runs from the +Y pole to -Y
proportionally to arc length, so a long side is not stretched against the
caps. Normals point outward.natiny_physics_capsule takes the same radius
and height and produces a collider that lines up with this mesh.natiny.mesh.capsule([radius [, height [, segments [, rings]]]]) -> mesh
Builds a one-part capsule standing upright along Y and centred on the origin: a cylindrical side closed by a hemisphere at each end. The generated mesh has normals and UV coordinates but no texture.
| Name | Type | Default | Description |
|---|---|---|---|
radius |
number |
0.5 |
Radius of the side and of both hemispheres |
height |
number |
radius * 4 |
Total height end to end, hemispheres included |
segments |
integer |
32 |
Meridians around Y; values below 3 are clamped |
rings |
integer |
8 |
Rings per hemisphere; values below 1 are clamped |
| Type | Description |
|---|---|
number |
Mesh handle |
local capsule = natiny.mesh.capsule(0.4, 1.8)
local player = natiny.model.create()
natiny.model.set_mesh(player, capsule)
height is the whole shape, so the cylindrical side is height - 2 * radius
tall and the mesh spans -height / 2 to +height / 2 along Y.height at or below 2 * radius produces a sphere: the side is clamped to
zero height rather than inverted, and the band that would draw it is skipped.rings counts the rings of one hemisphere, so a capsule at rings = 8 has
the same cap detail as natiny.mesh.sphere at rings = 16.u, and v runs from the +Y pole to -Y
proportionally to arc length, so a long side is not stretched against the
caps. Normals point outward.natiny.physics.capsule takes the same radius
and height and produces a collider that lines up with this mesh.NatinyMesh natiny_mesh_cube(float w, float h, float d, uint32_t subdiv);
Builds a one-part box centred on the origin. The generated mesh has normals and UV coordinates but no texture.
| Name | Type | Description |
|---|---|---|
w |
float |
Width, the full size along X |
h |
float |
Height, the full size along Y |
d |
float |
Depth, the full size along Z |
subdiv |
uint32_t |
Quads along each edge of a face; clamped up to 1 |
| Type | Description |
|---|---|
NatinyMesh |
Mesh handle |
NatinyMesh cube = natiny_mesh_cube(1.0f, 1.0f, 1.0f, 1);
subdiv is clamped to at least 1.0..1. Vertex count is 36 * subdiv².natiny.mesh.cube([w [, h [, d [, subdiv]]]]) -> mesh
Builds a one-part box centred on the origin. The generated mesh has normals and UV coordinates but no texture.
| Name | Type | Default | Description |
|---|---|---|---|
w |
number |
1 |
Width, the full size along X |
h |
number |
w |
Height, the full size along Y |
d |
number |
w |
Depth, the full size along Z |
subdiv |
integer |
1 |
Quads along each edge of a face; values below 1 are clamped |
| Type | Description |
|---|---|
number |
Mesh handle |
local cube = natiny.mesh.cube(1)
h and d default to w; subdiv defaults
to 1 and is clamped to at least 1.0..1. Vertex count is 36 * subdiv².void natiny_mesh_destroy(NatinyMesh mesh);
Frees the geometry: the vertices of every part, and the textures that were loaded from the source file's materials.
| Name | Type | Description |
|---|---|---|
mesh |
NatinyMesh |
Mesh handle |
natiny_mesh_destroy(mesh);
natiny_mesh_get_part_texture become invalid as well -
they belong to the mesh.natiny.mesh.destroy(mesh)
Frees the geometry: the vertices of every part, and the textures that were loaded from the source file's materials.
| Name | Type | Description |
|---|---|---|
mesh |
number |
Mesh handle |
natiny.mesh.destroy(mesh)
natiny.mesh.get_part_texture become invalid as well -
they belong to the mesh.uint32_t natiny_mesh_get_part_count(NatinyMesh mesh);
Returns the number of mesh parts.
| Name | Type | Description |
|---|---|---|
mesh |
NatinyMesh |
Mesh handle |
| Type | Description |
|---|---|
uint32_t |
Part count (0 if the handle is invalid) |
uint32_t count = natiny_mesh_get_part_count(mesh);
natiny.mesh.get_part_count(mesh) -> count
Returns the number of mesh parts.
| Name | Type | Description |
|---|---|---|
mesh |
number |
Mesh handle |
| Type | Description |
|---|---|
number |
Part count (0 if the handle is invalid) |
local count = natiny.mesh.get_part_count(mesh)
NatinyTexture natiny_mesh_get_part_texture(NatinyMesh mesh, uint32_t index);
Returns the texture owned by a mesh part.
| Name | Type | Description |
|---|---|---|
mesh |
NatinyMesh |
Mesh handle |
index |
uint32_t |
Zero-based part index |
| Type | Description |
|---|---|
NatinyTexture |
Texture handle, or 0 when the part has no texture or the index is out of range |
NatinyTexture texture = natiny_mesh_get_part_texture(mesh, 0);
natiny_texture_destroy on it.0.natiny.mesh.get_part_texture(mesh, index) -> texture
Returns the texture owned by a mesh part.
| Name | Type | Description |
|---|---|---|
mesh |
number |
Mesh handle |
index |
integer |
Zero-based part index; negative values are clamped to 0 |
| Type | Description |
|---|---|
number |
Texture handle, or 0 when the part has no texture or the index is out of range |
local texture = natiny.mesh.get_part_texture(mesh, 0)
natiny.texture.destroy on it.0.NatinyMesh natiny_mesh_load(NatinyResource mesh_resource);
Loads glTF 2.0 geometry from a resource.
Both binary (.glb) and text (.gltf + external .bin) glTF are accepted.
The model is split into parts: one part per material, so every part has its
own texture. Material textures are loaded automatically through
natiny_resource_load(), relative to the model's own path.
| Name | Type | Description |
|---|---|---|
mesh_resource |
NatinyResource |
Resource handle produced by natiny_resource_load(path) (a .gltf or .glb file) |
| Type | Description |
|---|---|
NatinyMesh |
Mesh handle (0 on failure) |
NatinyMesh mesh = natiny_mesh_load(mesh_resource);
.glb and .gltf are supported. Triangle primitives sharing a material are
merged into one part, and glTF node transforms are baked into its vertices..bin buffers, image URIs) are resolved relative to the path
used to load the source resource. A resource created from memory has no base
path, so its external references cannot be resolved.natiny_mesh_load returns.natiny.mesh.load(mesh_resource) -> mesh
Loads glTF 2.0 geometry from a resource.
Both binary (.glb) and text (.gltf + external .bin) glTF are accepted.
The model is split into parts: one part per material, so every part has its
own texture. Material textures are loaded automatically through
natiny.resource.load, relative to the model's own path.
| Name | Type | Description |
|---|---|---|
mesh_resource |
number |
Resource handle produced by natiny.resource.load(path) (a .gltf or .glb file) |
| Type | Description |
|---|---|
number |
Mesh handle (0 on failure) |
local mesh = natiny.mesh.load(mesh_resource)
.glb and .gltf are supported. Triangle primitives sharing a material are
merged into one part, and glTF node transforms are baked into its vertices..bin buffers, image URIs) are resolved relative to the path
used to load the source resource. A resource created from memory has no base
path, so its external references cannot be resolved.natiny.mesh.load returns.NatinyMesh natiny_mesh_plane(float w, float d, uint32_t cols, uint32_t rows);
Builds a one-part plane centred on the origin in the XZ plane, facing +Y.
The generated mesh has normals and UV coordinates but no texture.
| Name | Type | Description |
|---|---|---|
w |
float |
Width, the full size along X |
d |
float |
Depth, the full size along Z |
cols |
uint32_t |
Cells across X; clamped up to 1 |
rows |
uint32_t |
Cells along Z; clamped up to 1 |
| Type | Description |
|---|---|
NatinyMesh |
Mesh handle |
NatinyMesh plane = natiny_mesh_plane(20.0f, 10.0f, 4, 2);
+Y, and uses full dimensions w × d.cols and rows are clamped to at least 1. UVs span 0..1, all normals are
(0, 1, 0), and the vertex count is 6 * cols * rows.natiny.mesh.plane([w [, d [, cols [, rows]]]]) -> mesh
Builds a one-part plane centred on the origin in the XZ plane, facing +Y.
The generated mesh has normals and UV coordinates but no texture.
| Name | Type | Default | Description |
|---|---|---|---|
w |
number |
1 |
Width, the full size along X |
d |
number |
w |
Depth, the full size along Z |
cols |
integer |
1 |
Cells across X; values below 1 are clamped |
rows |
integer |
1 |
Cells along Z; values below 1 are clamped |
| Type | Description |
|---|---|
number |
Mesh handle |
local plane = natiny.mesh.plane(20, 10, 4, 2)
+Y, and uses full dimensions w × d.cols and rows are clamped to at least 1. UVs span 0..1, all normals are
(0, 1, 0), and the vertex count is 6 * cols * rows.int natiny_mesh_reload(NatinyMesh mesh, NatinyResource mesh_resource);
Replaces the geometry inside an existing mesh. The handle does not change.
| Name | Type | Description |
|---|---|---|
mesh |
NatinyMesh |
Live mesh handle from natiny_mesh_load |
mesh_resource |
NatinyResource |
Resource handle containing a glTF or GLB model |
| Type | Description |
|---|---|
int |
1 on success, 0 on failure |
NatinyMesh mesh = natiny_mesh_load(resource);
natiny_model_set_mesh(model, mesh);
NatinyResource replacement = natiny_resource_load("models/tree.glb");
/* The model keeps its place in the scene and shows the new shape. */
if (!natiny_mesh_reload(mesh, replacement)) {
/* The mesh still contains its previous geometry. */
}
natiny_resource_destroy(replacement);
0 if mesh is not live or the resource cannot be loaded as glTF
2.0. A failure leaves the mesh exactly as it was.natiny.mesh.reload(mesh, resource)
Replaces the geometry inside an existing mesh. The handle does not change.
| Name | Type | Description |
|---|---|---|
mesh |
number |
Live mesh handle from natiny.mesh.load |
resource |
number |
Resource handle containing a glTF or GLB model |
local mesh = natiny.mesh.load(resource)
natiny.model.set_mesh(model, mesh)
local replacement = natiny.resource.load("models/tree.glb")
-- the model keeps its place in the scene and shows the new shape
natiny.mesh.reload(mesh, replacement)
natiny.resource.destroy(replacement)
mesh is not live or the resource cannot be loaded as
glTF 2.0. A failure leaves the mesh exactly as it was.void natiny_mesh_set_dynamic(NatinyMesh mesh, bool dynamic);
Selects the mesh upload path.
| Name | Type | Description |
|---|---|---|
mesh |
NatinyMesh |
Mesh handle |
dynamic |
bool |
true to transform on the CPU every frame |
natiny_mesh_set_dynamic(mesh, true);
natiny.mesh.set_dynamic(mesh [, dynamic])
Selects the mesh upload path.
| Name | Type | Default | Description |
|---|---|---|---|
mesh |
number |
Mesh handle | |
dynamic |
boolean |
true |
true to transform on the CPU every frame |
natiny.mesh.set_dynamic(mesh, true)
NatinyMesh natiny_mesh_sphere(float radius, uint32_t segments, uint32_t rings);
Builds a one-part sphere centred on the origin. The generated mesh has normals and UV coordinates but no texture.
| Name | Type | Description |
|---|---|---|
radius |
float |
Radius |
segments |
uint32_t |
Meridians - the detail around the Y axis; clamped up to 3 |
rings |
uint32_t |
Rings - the detail from pole to pole; clamped up to 2 |
| Type | Description |
|---|---|
NatinyMesh |
Mesh handle |
NatinyMesh sphere = natiny_mesh_sphere(0.5f, 32, 16);
segments is clamped to at least 3 and rings to at least 2.u wraps around Y and v runs from the +Y pole
to -Y. Normals point outward from the center.natiny.mesh.sphere([radius [, segments [, rings]]]) -> mesh
Builds a one-part sphere centred on the origin. The generated mesh has normals and UV coordinates but no texture.
| Name | Type | Default | Description |
|---|---|---|---|
radius |
number |
1 |
Radius |
segments |
integer |
32 |
Meridians around Y; values below 3 are clamped |
rings |
integer |
16 |
Rings from pole to pole; values below 2 are clamped |
| Type | Description |
|---|---|
number |
Mesh handle |
local sphere = natiny.mesh.sphere(0.5, 32, 16)
segments is clamped to at least 3 and rings to at least 2.u wraps around Y and v runs from the +Y pole
to -Y. Normals point outward from the center.NatinyModel natiny_model_create(void);
Creates an empty model. A model handle is also a NatinyEntity, so
natiny_entity_* functions can transform and parent it.
| Type | Description |
|---|---|
NatinyModel |
Model handle, also usable as a NatinyEntity; 0 on failure |
NatinyModel box = natiny_model_create();
"model"
tag.0 when there is no room for another model.natiny.model.create([mesh [, material]]) -> model
Creates a model: a mesh instance placed in the scene. A model handle is also an
entity handle, so natiny.entity functions can transform and parent it.
| Name | Type | Default | Description |
|---|---|---|---|
mesh |
number |
nil |
Mesh handle whose geometry this model places |
material |
number |
nil |
Material handle; without one, the built-in material is used |
| Type | Description |
|---|---|
number |
Model handle, also usable as an entity handle |
local box = natiny.model.create(geom, stone)
"model"
tag.void natiny_model_destroy(NatinyModel model);
Frees a model and the node that carried it.
| Name | Type | Description |
|---|---|---|
model |
NatinyModel |
Model handle |
natiny_model_destroy(box);
natiny_entity_set_parent(child, 0) leaves its
local transform as its world transform, and the destroy passes it by.natiny.model.destroy(model)
Frees a model and the node that carried it.
| Name | Type | Description |
|---|---|---|
model |
number |
Model handle |
natiny.model.destroy(box)
natiny.entity.set_parent(child, 0) leaves its
local transform as its world transform, and the destroy passes it by.bool natiny_model_get_visible(NatinyModel model);
Whether the model is currently drawn by a pass.
| Name | Type | Description |
|---|---|---|
model |
NatinyModel |
Model handle |
| Type | Description |
|---|---|
bool |
true when the model is visible |
bool visible = natiny_model_get_visible(door);
false.natiny.model.get_visible(model) -> visible
Whether the model is currently drawn by a pass.
| Name | Type | Description |
|---|---|---|
model |
number |
Model handle |
| Type | Description |
|---|---|
boolean |
true when the model is visible |
local visible = natiny.model.get_visible(door)
false.void natiny_model_set_color(NatinyModel model, float r, float g, float b, float a);
Sets this model's tint - the per-model form of natiny_render_set_color, which a bulk draw has no way to set per object.
| Name | Type | Description |
|---|---|---|
model |
NatinyModel |
Model handle |
r |
float |
Red, 0..1 |
g |
float |
Green, 0..1 |
b |
float |
Blue, 0..1 |
a |
float |
Alpha, 0..1 |
natiny_model_set_color(buoy, 1.0f, 0.55f, 0.15f, 1.0f);
natiny.model.set_color(model [, r [, g [, b [, a]]]])
Sets this model's tint - the per-model form of natiny.render.set_color, which a bulk draw has no way to set per object.
| Name | Type | Default | Description |
|---|---|---|---|
model |
number |
Model handle | |
r |
number |
1 |
Red, 0..1 |
g |
number |
1 |
Green, 0..1 |
b |
number |
1 |
Blue, 0..1 |
a |
number |
1 |
Alpha, 0..1 |
natiny.model.set_color(buoy, 1.0, 0.55, 0.15, 1.0)
void natiny_model_set_material(NatinyModel model, NatinyMaterial material);
Sets the material for every part of the model, and with it the tags that decide which passes draw the model.
| Name | Type | Description |
|---|---|---|
model |
NatinyModel |
Model handle |
material |
NatinyMaterial |
Material handle, or 0 for the built-in material |
natiny_model_set_material(box, stone);
0 goes back to the built-in material, which carries the tag "model" and
nothing else.natiny.model.set_material(model [, material])
Sets the material for every part of the model, and with it the tags that decide which passes draw the model.
| Name | Type | Default | Description |
|---|---|---|---|
model |
number |
Model handle | |
material |
number |
0 |
Material handle, or 0 for the built-in material |
natiny.model.set_material(box, stone)
0 goes back to the built-in material, which carries the tag "model" and
nothing else.void natiny_model_set_mesh(NatinyModel model, NatinyMesh mesh);
Sets which geometry this model places.
| Name | Type | Description |
|---|---|---|
model |
NatinyModel |
Model handle |
mesh |
NatinyMesh |
Mesh handle, or 0 to empty the model |
natiny_model_set_mesh(box, sphere_mesh);
0 puts it
back in that state without destroying it.natiny.model.set_mesh(model, mesh)
Sets which geometry this model places.
| Name | Type | Description |
|---|---|---|
model |
number |
Model handle |
mesh |
number |
Mesh handle, or 0 to empty the model |
natiny.model.set_mesh(box, sphere_mesh)
0 puts it
back in that state without destroying it.void natiny_model_set_part_material(NatinyModel model, uint32_t part, NatinyMaterial material);
Overrides the material of one part of the model.
| Name | Type | Description |
|---|---|---|
model |
NatinyModel |
Model handle |
part |
uint32_t |
Zero-based part index |
material |
NatinyMaterial |
Material handle, or 0 to inherit the model's again |
natiny_model_set_part_material(house, 3, glass);
0 clears an override.natiny.model.set_part_material(model, part [, material])
Overrides the material of one part of the model.
| Name | Type | Default | Description |
|---|---|---|---|
model |
number |
Model handle | |
part |
integer |
Zero-based part index; negative values are clamped to 0 |
|
material |
number |
0 |
Material handle, or 0 to inherit the model material |
natiny.model.set_part_material(house, 3, glass)
0 clears an override.void natiny_model_set_part_texture(NatinyModel model, uint32_t part, NatinyTexture texture);
Overrides the texture of one part of the model, in place of the one the mesh loaded for it.
| Name | Type | Description |
|---|---|---|
model |
NatinyModel |
Model handle |
part |
uint32_t |
Zero-based part index |
texture |
NatinyTexture |
Texture handle, or 0 to go back to the mesh's |
natiny_model_set_part_texture(crate, 0, label);
0 clears one. They are
cleared by natiny_model_set_mesh.natiny.model.set_part_texture(model, part [, texture])
Overrides the texture of one part of the model, in place of the one the mesh loaded for it.
| Name | Type | Default | Description |
|---|---|---|---|
model |
number |
Model handle | |
part |
integer |
Zero-based part index; negative values are clamped to 0 |
|
texture |
number |
0 |
Texture handle, or 0 to inherit the mesh part texture |
natiny.model.set_part_texture(crate, 0, label)
0 clears one. They are
cleared by set_mesh.void natiny_model_set_visible(NatinyModel model, bool visible);
Sets whether draw passes include the model.
| Name | Type | Description |
|---|---|---|
model |
NatinyModel |
Model handle |
visible |
bool |
false to skip the model everywhere |
natiny_model_set_visible(door, open);
natiny.model.set_visible(model [, visible])
Sets whether draw passes include the model.
| Name | Type | Default | Description |
|---|---|---|---|
model |
number |
Model handle | |
visible |
boolean |
true |
false to skip the model everywhere |
natiny.model.set_visible(door, open)
void natiny_physics_apply_impulse(NatinyBody body, float ix, float iy, float iz);
Adds a kick through the centre of mass, in world space.
| Name | Type | Description |
|---|---|---|
body |
NatinyBody |
Body handle |
ix |
float |
Impulse along X, kg*m/s |
iy |
float |
Impulse along Y, kg*m/s |
iz |
float |
Impulse along Z, kg*m/s |
natiny_physics_apply_impulse(crate, 0.0f, 0.0f, -12.0f);
natiny_physics_set_velocity.natiny.physics.apply_impulse(body, ix, iy, iz)
Adds a kick through the centre of mass, in world space.
| Name | Type | Description |
|---|---|---|
body |
number |
Body handle |
ix |
number |
Impulse along X, kg*m/s |
iy |
number |
Impulse along Y, kg*m/s |
iz |
number |
Impulse along Z, kg*m/s |
natiny.physics.apply_impulse(crate, 0, 0, -12)
natiny.physics.set_velocity.NatinyBody natiny_physics_capsule(float radius, float height);
Creates a dynamic body with a capsule collider standing upright along Y and centred on its origin. Requires an initialised physics world.
| Name | Type | Description |
|---|---|---|
radius |
float |
Radius of the side and of both hemispheres, in metres; must be greater than 0 |
height |
float |
Total height end to end, hemispheres included, in metres |
| Type | Description |
|---|---|
NatinyBody |
Body handle, or 0 on failure |
NatinyBody player = natiny_physics_capsule(0.4f, 1.8f);
natiny_physics_set_fixed_rotation(player, true);
natiny_model_set_mesh(avatar, natiny_mesh_capsule(0.4f, 1.8f, 32, 8));
natiny_entity_set_parent(avatar, player);
0 when natiny_physics_init has not run, when radius
is not greater than 0, or when the shape could not be created; nothing is
left behind in that case.height is the whole shape, so the cylindrical side is height - 2 * radius
tall and the collider spans -height / 2 to +height / 2 along Y - the same
numbers natiny_mesh_capsule draws, so the pair line up
without an offset between them.height at or below 2 * radius becomes a sphere collider of that radius,
the same shape the mesh draws for those numbers.natiny_physics_set_type.natiny_physics_set_fixed_rotation so it cannot tip
over.natiny_entity_set_position and parent a model
to it so the model follows.natiny.physics.capsule([radius [, height]]) -> body
Creates a dynamic body with a capsule collider standing upright along Y and centred on its origin. Requires an initialised physics world.
| Name | Type | Default | Description |
|---|---|---|---|
radius |
number |
0.5 |
Radius of the side and of both hemispheres, in metres; must be greater than 0 |
height |
number |
radius * 4 |
Total height end to end, hemispheres included, in metres |
| Type | Description |
|---|---|
number |
Body handle |
local player = natiny.physics.capsule(0.4, 1.8)
natiny.physics.set_fixed_rotation(player, true)
natiny.model.set_mesh(avatar, natiny.mesh.capsule(0.4, 1.8))
natiny.entity.set_parent(avatar, player)
natiny.physics.init has not run or radius is not greater
than 0.height is the whole shape, so the cylindrical side is height - 2 * radius
tall and the collider spans -height / 2 to +height / 2 along Y - the same
numbers natiny.mesh.capsule draws, so the pair line up
without an offset between them.height at or below 2 * radius becomes a sphere collider of that radius,
the same shape the mesh draws for those numbers.natiny.physics.set_type.natiny.physics.set_fixed_rotation so it cannot tip
over.natiny.entity.set_position and parent a model
to it so the model follows.NatinyBody natiny_physics_cube(float w, float h, float d);
Creates a dynamic box-shaped body at the world origin.
| Name | Type | Description |
|---|---|---|
w |
float |
Width, the full size along X |
h |
float |
Height, the full size along Y |
d |
float |
Depth, the full size along Z |
| Type | Description |
|---|---|
NatinyBody |
Body handle, also usable as a NatinyEntity; 0 on failure |
NatinyBody body = natiny_physics_cube(1.0f, 1.0f, 1.0f);
natiny_physics_set_type changes that.0 when physics is not running or the body cannot be made.natiny.physics.cube([w [, h [, d]]]) -> body
Creates a dynamic box-shaped body at the world origin.
| Name | Type | Default | Description |
|---|---|---|---|
w |
number |
1 |
Width, the full size along X |
h |
number |
w |
Height, the full size along Y |
d |
number |
w |
Depth, the full size along Z |
| Type | Description |
|---|---|
number |
Body handle, also usable as an entity handle |
local body = natiny.physics.cube(1, 1, 1)
natiny.physics.set_type changes that.NatinyBody natiny_physics_cylinder(float radius, float height);
Creates a dynamic cylinder-shaped body at the world origin, standing upright along Y.
| Name | Type | Description |
|---|---|---|
radius |
float |
Radius in metres |
height |
float |
Full height along Y, in metres |
| Type | Description |
|---|---|
NatinyBody |
Body handle, also usable as a NatinyEntity; 0 on failure |
NatinyBody barrel = natiny_physics_cylinder(0.4f, 1.2f);
natiny_entity_set_position(barrel, NATINY_SPACE_WORLD, 0.0f, 5.0f, 0.0f);
height / 2 above and below it, and the body
starts at the world origin.natiny_entity_set_rotation(barrel, NATINY_SPACE_WORLD, 0.0f, 0.0f, 1.5708f).natiny_physics_set_type changes that.0 when physics is not running, when either size is zero or
negative, or when the body cannot be made.natiny.physics.cylinder([radius [, height]]) -> body
Creates a dynamic cylinder-shaped body at the world origin, standing upright along Y.
| Name | Type | Default | Description |
|---|---|---|---|
radius |
number |
1 |
Radius in metres |
height |
number |
radius * 2 |
Full height along Y, in metres |
| Type | Description |
|---|---|
number |
Body handle, also usable as an entity handle |
local barrel = natiny.physics.cylinder(0.4, 1.2)
natiny.entity.set_position(barrel, natiny.entity.SPACE_WORLD, 0, 5, 0)
height / 2 above and below it, and the body
starts at the world origin.natiny.entity.set_rotation(barrel, natiny.SPACE_WORLD, 0, 0, 90).natiny.physics.set_type changes that.void natiny_physics_destroy(NatinyBody body);
Removes a body from the world and frees its entity.
| Name | Type | Description |
|---|---|---|
body |
NatinyBody |
Body handle |
natiny_physics_destroy(body);
natiny_entity_set_parent(child, 0) leaves its
local transform as its world transform, and the destroy passes it by.natiny.physics.destroy(body)
Removes a body from the world and frees its entity.
| Name | Type | Description |
|---|---|---|
body |
number |
Body handle |
natiny.physics.destroy(body)
natiny.entity.set_parent(child, 0) leaves its
local transform as its world transform, and the destroy passes it by.void natiny_physics_get_velocity(NatinyBody body, float* vx, float* vy, float* vz);
Reads a body's linear velocity in world space.
| Name | Type | Description |
|---|---|---|
body |
NatinyBody |
Body handle |
vx |
float* |
Receives metres per second along X; may be null |
vy |
float* |
Receives metres per second along Y; may be null |
vz |
float* |
Receives metres per second along Z; may be null |
float vx, vy, vz;
natiny_physics_get_velocity(player, &vx, &vy, &vz);
natiny.physics.get_velocity(body) -> vx, vy, vz
Reads a body's linear velocity in world space.
| Name | Type | Description |
|---|---|---|
body |
number |
Body handle |
| Type | Description |
|---|---|
number, number, number |
Metres per second along X, Y and Z |
local vx, vy, vz = natiny.physics.get_velocity(player)
int natiny_physics_init(void);
Creates the rigid-body world and starts stepping it once per frame.
| Type | Description |
|---|---|
int |
1 when the world was created, otherwise 0 |
int ok = natiny_physics_init();
natiny_backend_init, because backend initialization resets
all engine state, including the physics world. Repeated calls do nothing and
return 1.natiny_physics_raycast returns false, velocity reads return zeroes, and
body constructors return 0.9.81f m/s
downwards along -Y; change it with natiny_physics_set_gravity.0.25f s of elapsed time
is accumulated per frame.natiny.physics.init() -> ok
Creates the rigid-body world and starts stepping it once per frame.
| Type | Description |
|---|---|
boolean |
true when the world was created, otherwise false |
local ok = natiny.physics.init()
natiny.backend.init, because backend initialization resets
all engine state, including the physics world. Repeated calls do nothing and
return true.raycast returns
nil, get_velocity returns zeroes, and body constructors raise an error.9.81 m/s
downwards along -Y; change it with natiny.physics.set_gravity.NatinyBody natiny_physics_mesh(NatinyMesh mesh);
Creates a static body whose collider is a mesh, triangle for triangle.
| Name | Type | Description |
|---|---|---|
mesh |
NatinyMesh |
Mesh handle, from natiny_mesh_load or a generated shape |
| Type | Description |
|---|---|
NatinyBody |
Body handle, also usable as a NatinyEntity; 0 on failure |
NatinyMesh level = natiny_mesh_load(natiny_resource_load("level.glb"));
NatinyBody body = natiny_physics_mesh(level);
0 when physics is not running, the handle is not a mesh, or a
collider cannot be built from its triangles.natiny.physics.mesh(mesh) -> body
Creates a static body whose collider is a mesh, triangle for triangle.
| Name | Type | Description |
|---|---|---|
mesh |
number |
Mesh handle, from natiny.mesh.load or a generated shape |
| Type | Description |
|---|---|
number |
Body handle, also usable as an entity handle |
local resource = natiny.resource.load("level.glb")
local level = natiny.mesh.load(resource)
local body = natiny.physics.mesh(level)
NatinyBody natiny_physics_plane(float w, float d);
Creates a static, flat rectangle in the XZ plane: the ground.
| Name | Type | Description |
|---|---|---|
w |
float |
Width, the full size along X |
d |
float |
Depth, the full size along Z |
| Type | Description |
|---|---|
NatinyBody |
Body handle, also usable as a NatinyEntity; 0 on failure |
NatinyBody ground = natiny_physics_plane(50.0f, 50.0f);
+Y, and is centred on the
body's origin.natiny_physics_cube for a moving floor.0 when physics is not running or the body cannot be made.natiny.physics.plane([w [, d]]) -> body
Creates a static, flat rectangle in the XZ plane: the ground.
| Name | Type | Default | Description |
|---|---|---|---|
w |
number |
1 |
Width, the full size along X |
d |
number |
w |
Depth, the full size along Z |
| Type | Description |
|---|---|
number |
Body handle, also usable as an entity handle |
local ground = natiny.physics.plane(50, 50)
+Y, and is centred on the
body's origin.natiny.physics.cube for a moving floor.bool natiny_physics_raycast(float ox, float oy, float oz,
float dx, float dy, float dz,
float max_distance,
NatinyRaycastHit* hit);
Finds the first body along a ray.
| Name | Type | Description |
|---|---|---|
ox, oy, oz |
float |
Where the ray starts, world space |
dx, dy, dz |
float |
Which way it points; need not be normalized |
max_distance |
float |
How far to look, in metres |
hit |
NatinyRaycastHit* |
Receives the hit; may be null when only the yes-or-no is wanted |
| Type | Description |
|---|---|
bool |
true when something was hit within max_distance |
typedef struct {
NatinyBody body;
float x, y, z;
float nx, ny, nz;
float distance;
} NatinyRaycastHit;
NatinyRaycastHit hit;
if (natiny_physics_raycast(px, py, pz, 0.0f, -1.0f, 0.0f, 2.0f, &hit)) {
/* hit.body is what we are standing on, hit.y where its surface is */
}
max_distance of zero or less returns false.hit is zeroed before anything else happens, so a miss leaves it blank
rather than stale.natiny.physics.raycast(ox, oy, oz, dx, dy, dz [, max_distance]) -> hit
Finds the first body along a ray.
| Name | Type | Default | Description |
|---|---|---|---|
ox, oy, oz |
number |
Where the ray starts, world space | |
dx, dy, dz |
number |
Which way it points; need not be normalized | |
max_distance |
number |
1000 |
How far to look, in metres |
| Type | Description |
|---|---|
table or nil |
The hit, or nil when the ray reached max_distance without touching anything |
The table has body, the hit point x, y, z, the surface normal nx,
ny, nz, and distance along the ray.
local hit = natiny.physics.raycast(px, py, pz, 0, -1, 0, 2)
max_distance of zero or less returns nil.void natiny_physics_set_fixed_rotation(NatinyBody body, bool enabled);
Locks or unlocks rotation of a body around all three axes at once. A locked body keeps the orientation it has and cannot be turned by gravity, contacts or impulses.
| Name | Type | Description |
|---|---|---|
body |
NatinyBody |
Body handle |
enabled |
bool |
true to lock all three rotation axes; false to release them |
NatinyBody player = natiny_physics_cylinder(0.4f, 1.8f);
natiny_physics_set_fixed_rotation(player, true);
natiny_entity_set_rotation, which is how an
upright character aims.natiny_physics_set_rotation_locks with the
same value on all three axes; either call overwrites what the other set.natiny.physics.set_fixed_rotation(body, enabled)
Locks or unlocks rotation of a body around all three axes at once. A locked body keeps the orientation it has and cannot be turned by gravity, contacts or impulses.
| Name | Type | Description |
|---|---|---|
body |
number |
Body handle |
enabled |
boolean |
true to lock all three rotation axes; false to release them |
local player = natiny.physics.cylinder(0.4, 1.8)
natiny.physics.set_fixed_rotation(player, true)
natiny.entity.set_rotation, which is how an
upright character aims.natiny.physics.set_rotation_locks with the
same value on all three axes; either call overwrites what the other set.void natiny_physics_set_gravity(float x, float y, float z);
Sets the acceleration applied to every dynamic body.
| Name | Type | Description |
|---|---|---|
x |
float |
Acceleration along X, m/s^2 |
y |
float |
Acceleration along Y, m/s^2 |
z |
float |
Acceleration along Z, m/s^2 |
natiny_physics_set_gravity(0.0f, -1.62f, 0.0f); /* the Moon */
(0, -9.81f, 0).natiny.physics.set_gravity(x, y, z)
Sets the acceleration applied to every dynamic body.
| Name | Type | Description |
|---|---|---|
x |
number |
Acceleration along X, m/s^2 |
y |
number |
Acceleration along Y, m/s^2 |
z |
number |
Acceleration along Z, m/s^2 |
natiny.physics.set_gravity(0, -1.62, 0) -- the Moon
(0, -9.81, 0).void natiny_physics_set_interpolation(NatinyBody body, bool enabled);
Enables or disables visual interpolation of a body's entity transform between fixed physics ticks.
| Name | Type | Description |
|---|---|---|
body |
NatinyBody |
Body handle |
enabled |
bool |
true to interpolate the transform; false to use the latest simulated transform |
natiny_physics_set_interpolation(crate, true);
natiny.physics.set_interpolation(body, enabled)
Enables or disables visual interpolation of a body's entity transform between fixed physics ticks.
| Name | Type | Description |
|---|---|---|
body |
number |
Body handle |
enabled |
boolean |
true to interpolate the transform; false to use the latest simulated transform |
natiny.physics.set_interpolation(crate, true)
void natiny_physics_set_material(NatinyBody body, float friction,
float restitution, float density);
Sets how a body's surface behaves and how heavy it is.
| Name | Type | Description |
|---|---|---|
body |
NatinyBody |
Body handle |
friction |
float |
Coulomb friction, usually 0..1; starts at 0.6f |
restitution |
float |
Bounce, 0 dead, 1 perfectly elastic; starts at 0.0f |
density |
float |
kg/m^3; starts at 1000.0f |
NatinyBody ball = natiny_physics_sphere(0.5f);
natiny_physics_set_material(ball, 0.3f, 0.8f, 200.0f); /* light and bouncy */
natiny.physics.set_material(body [, friction [, restitution [, density]]])
Sets how a body's surface behaves and how heavy it is.
| Name | Type | Default | Description |
|---|---|---|---|
body |
number |
Body handle | |
friction |
number |
0.6 |
Coulomb friction, usually 0..1 |
restitution |
number |
0.0 |
Bounce, 0 dead, 1 perfectly elastic |
density |
number |
1000 |
kg/m^3 |
natiny.physics.set_material(ball, 0.3, 0.8, 200) -- light and bouncy
void natiny_physics_set_rotation_locks(NatinyBody body, bool lock_x, bool lock_y, bool lock_z);
Locks or unlocks rotation of a body around each world axis independently. The solver cannot turn the body around a locked axis, while the axes left free keep responding to gravity, contacts and impulses.
| Name | Type | Description |
|---|---|---|
body |
NatinyBody |
Body handle |
lock_x |
bool |
true to prevent rotation around the world X axis (pitch) |
lock_y |
bool |
true to prevent rotation around the world Y axis (yaw) |
lock_z |
bool |
true to prevent rotation around the world Z axis (roll) |
/* Stays upright, but is still free to spin around Y */
natiny_physics_set_rotation_locks(player, true, false, true);
false is
released, not left alone.natiny_entity_set_rotation.natiny_physics_set_fixed_rotation is the same call
with one value for all three axes; either call overwrites what the other set.natiny.physics.set_rotation_locks(body, lock_x, lock_y, lock_z)
Locks or unlocks rotation of a body around each world axis independently. The solver cannot turn the body around a locked axis, while the axes left free keep responding to gravity, contacts and impulses.
| Name | Type | Description |
|---|---|---|
body |
number |
Body handle |
lock_x |
boolean |
true to prevent rotation around the world X axis (pitch) |
lock_y |
boolean |
true to prevent rotation around the world Y axis (yaw) |
lock_z |
boolean |
true to prevent rotation around the world Z axis (roll) |
-- Stays upright, but is still free to spin around Y
natiny.physics.set_rotation_locks(player, true, false, true)
false is
released, not left alone.natiny.entity.set_rotation.natiny.physics.set_fixed_rotation is the same call
with one value for all three axes; either call overwrites what the other set.void natiny_physics_set_type(NatinyBody body, uint32_t type);
Changes what the solver is allowed to do with a body.
| Name | Type | Description |
|---|---|---|
body |
NatinyBody |
Body handle |
type |
uint32_t |
NATINY_BODY_DYNAMIC, NATINY_BODY_STATIC or NATINY_BODY_KINEMATIC |
| Name | Description |
|---|---|
NATINY_BODY_DYNAMIC |
Has mass, moved by forces and collisions |
NATINY_BODY_STATIC |
Not moved by forces, velocity or collisions |
NATINY_BODY_KINEMATIC |
Moved by the game; pushes dynamic bodies without being pushed back |
natiny_physics_set_type(wall, NATINY_BODY_STATIC);
cube and sphere start dynamic, plane and mesh static.NATINY_BODY_KINEMATIC, or a box.natiny_entity_set_position or
natiny_physics_set_velocity.type value is treated as NATINY_BODY_DYNAMIC.natiny.physics.set_type(body, type)
Changes what the solver is allowed to do with a body.
| Name | Type | Description |
|---|---|---|
body |
number |
Body handle |
type |
number |
BODY_DYNAMIC, BODY_STATIC or BODY_KINEMATIC |
| Name | Description |
|---|---|
natiny.physics.BODY_DYNAMIC |
Has mass, moved by forces and collisions |
natiny.physics.BODY_STATIC |
Not moved by forces, velocity or collisions |
natiny.physics.BODY_KINEMATIC |
Moved by the game; pushes dynamic bodies without being pushed back |
natiny.physics.set_type(wall, natiny.physics.BODY_STATIC)
cube and sphere start dynamic, plane and mesh static.BODY_KINEMATIC, or a box.natiny.entity.set_position or
natiny.physics.set_velocity.type is treated as BODY_DYNAMIC.void natiny_physics_set_velocity(NatinyBody body, float vx, float vy, float vz);
Sets a body's linear velocity in world space and wakes it.
| Name | Type | Description |
|---|---|---|
body |
NatinyBody |
Body handle |
vx |
float |
Metres per second along X |
vy |
float |
Metres per second along Y |
vz |
float |
Metres per second along Z |
natiny_physics_set_velocity(player, 0.0f, 5.0f, 0.0f); /* jump */
natiny_physics_apply_impulse.natiny.physics.set_velocity(body, vx, vy, vz)
Sets a body's linear velocity in world space and wakes it.
| Name | Type | Description |
|---|---|---|
body |
number |
Body handle |
vx |
number |
Metres per second along X |
vy |
number |
Metres per second along Y |
vz |
number |
Metres per second along Z |
natiny.physics.set_velocity(player, 0, 5, 0) -- jump
natiny.physics.apply_impulse.void natiny_physics_shutdown(void);
Destroys the world and every body in it.
natiny_physics_shutdown();
NatinyBody becomes invalid for physics calls. Manual physics shutdown
does not destroy the corresponding entity nodes.natiny_backend_init and natiny_backend_shutdown both do this for you;
calling it by hand is for a program that wants the world gone earlier.natiny.physics.shutdown()
Destroys the world and every body in it.
natiny.physics.shutdown()
natiny.backend.init and natiny.backend.shutdown both do this for you;
calling it by hand is for a program that wants the world gone earlier.NatinyBody natiny_physics_sphere(float radius);
Creates a dynamic sphere-shaped body at the world origin.
| Name | Type | Description |
|---|---|---|
radius |
float |
Radius in metres |
| Type | Description |
|---|---|
NatinyBody |
Body handle, also usable as a NatinyEntity; 0 on failure |
NatinyBody ball = natiny_physics_sphere(0.5f);
natiny_physics_set_type changes that.0 when physics is not running or the body cannot be made.natiny.physics.sphere([radius]) -> body
Creates a dynamic sphere-shaped body at the world origin.
| Name | Type | Default | Description |
|---|---|---|---|
radius |
number |
1 |
Radius in metres |
| Type | Description |
|---|---|
number |
Body handle, also usable as an entity handle |
local ball = natiny.physics.sphere(0.5)
natiny.physics.set_type changes that.NatinyPivot natiny_pivot_create(void);
Creates a transform-only entity for grouping and positioning child entities. The pivot itself is not rendered and produces no audio.
| Type | Description |
|---|---|
NatinyPivot |
Pivot handle usable as a NatinyEntity, or 0 on failure |
NatinyPivot turret = natiny_pivot_create();
natiny_entity_set_parent((NatinyEntity)turret, (NatinyEntity)tank);
natiny_entity_set_position((NatinyEntity)turret, NATINY_SPACE_LOCAL,
0.0f, 1.2f, 0.0f);
natiny_entity_set_parent((NatinyEntity)barrel, (NatinyEntity)turret);
0; the initial scale is 1.0f on
every axis. The pivot has no parent.natiny_entity_* API to transform or parent a pivot.natiny_pivot_destroy. Destroying it
also destroys its descendants.natiny.pivot.create([parent]) -> pivot
Creates a transform-only entity for grouping and positioning child entities. The pivot itself is not rendered and produces no audio.
| Name | Type | Default | Description |
|---|---|---|---|
parent |
number |
nil |
Parent entity handle, or nil to create the pivot at the scene root |
| Type | Description |
|---|---|
number |
Pivot handle, also usable with the natiny.entity API |
local turret = natiny.pivot.create(tank)
natiny.entity.set_position(turret, natiny.entity.SPACE_LOCAL, 0, 1.2, 0)
natiny.entity.set_parent(barrel, turret)
0; the initial scale is 1
on every axis.natiny.entity API to transform or
reparent a pivot.parent is 0. Omit parent
or pass nil to create a root pivot.natiny.pivot.destroy. Destroying it
also destroys its descendants.void natiny_pivot_destroy(NatinyPivot pivot);
Destroys a pivot and all entities parented below it.
| Name | Type | Description |
|---|---|---|
pivot |
NatinyPivot |
Pivot handle from natiny_pivot_create |
natiny_pivot_destroy(turret);
natiny_entity_set_parent(child, 0). Its local transform becomes its world
transform.natiny.pivot.destroy(pivot)
Destroys a pivot and all entities parented below it.
| Name | Type | Description |
|---|---|---|
pivot |
number |
Pivot handle from natiny.pivot.create |
natiny.pivot.destroy(turret)
natiny.entity.set_parent(child, 0). Its local
transform becomes its world transform.void natiny_render_circle(float cx, float cy, float radius);
Draws a filled circle on the currently bound render target, using the current drawing color set by natiny_render_set_color().
| Name | Type | Description |
|---|---|---|
cx |
float |
Center X position |
cy |
float |
Center Y position |
radius |
float |
Circle radius |
natiny_render_circle(160.0f, 120.0f, 48.0f);
natiny.render.circle(cx, cy, radius)
Draws a filled circle on the currently bound render target, using the current drawing color set by natiny.render.set_color().
| Name | Type | Description |
|---|---|---|
cx |
number |
Center X position |
cy |
number |
Center Y position |
radius |
number |
Circle radius |
natiny.render.circle(160, 120, 48)
void natiny_render_circle_outline(float cx, float cy, float radius, float thickness);
Draws a circle outline on the currently bound render target, using the current drawing color set by natiny_render_set_color().
| Name | Type | Description |
|---|---|---|
cx |
float |
Center X position |
cy |
float |
Center Y position |
radius |
float |
Circle radius |
thickness |
float |
Outline thickness |
natiny_window_bind(win);
natiny_render_set_color(1.0f, 1.0f, 1.0f, 1.0f);
natiny_render_circle_outline(160.0f, 120.0f, 56.0f, 3.0f);
natiny_window_unbind();
natiny_render_set_color(). The default is opaque white.natiny_window_bind() or natiny_surface_bind().natiny.render.circle_outline(cx, cy, radius [, thickness])
Draws a circle outline on the currently bound render target, using the current drawing color set by natiny.render.set_color().
| Name | Type | Default | Description |
|---|---|---|---|
cx |
number |
Center X position | |
cy |
number |
Center Y position | |
radius |
number |
Circle radius | |
thickness |
number |
1.0 |
Outline thickness |
natiny.window.bind(win)
natiny.render.set_color(1.0, 1.0, 1.0, 1.0)
natiny.render.circle_outline(160, 120, 56, 3.0)
natiny.window.unbind()
natiny.render.set_color(). The default is opaque white.natiny.window.bind() or natiny.surface.bind().void natiny_render_clear(float r, float g, float b, float a);
Clears the currently bound render target with the specified color.
| Name | Type | Description |
|---|---|---|
r |
float |
Red channel (0.0 – 1.0) |
g |
float |
Green channel (0.0 – 1.0) |
b |
float |
Blue channel (0.0 – 1.0) |
a |
float |
Alpha channel (0.0 – 1.0) |
natiny_render_clear(0.1f, 0.1f, 0.15f, 1.0f);
natiny.render.clear([r [, g [, b [, a]]]])
Clears the currently bound render target with the specified color.
| Name | Type | Default | Description |
|---|---|---|---|
r |
number |
0.0 |
Red channel (0.0 – 1.0) |
g |
number |
0.0 |
Green channel (0.0 – 1.0) |
b |
number |
0.0 |
Blue channel (0.0 – 1.0) |
a |
number |
1.0 |
Alpha channel (0.0 – 1.0) |
natiny.render.clear(0.1, 0.1, 0.15, 1.0)
void natiny_render_cover(void);
A rectangle covering the current render target. Its UV coordinates run from
0 to 1 across the target.
natiny_render_cover();
natiny_render_clear, this is geometry and therefore uses the current
material, draw color, blending and depth state.(0, 0, 1).natiny.render.cover()
A rectangle covering the current render target. Its UV coordinates run from
0 to 1 across the target.
natiny.render.cover()
natiny.render.clear, this is geometry and therefore uses the current
material, draw color, blending and depth state.(0, 0, 1).void natiny_render_line(float x0, float y0, float x1, float y1, float thickness);
Draws a line segment on the currently bound render target, using the current drawing color set by natiny_render_set_color().
| Name | Type | Description |
|---|---|---|
x0 |
float |
Start X position |
y0 |
float |
Start Y position |
x1 |
float |
End X position |
y1 |
float |
End Y position |
thickness |
float |
Line thickness in pixels |
natiny_render_line(40.0f, 40.0f, 220.0f, 140.0f, 2.0f);
natiny.render.line(x0, y0, x1, y1 [, thickness])
Draws a line segment on the currently bound render target, using the current drawing color set by natiny.render.set_color().
| Name | Type | Default | Description |
|---|---|---|---|
x0 |
number |
Start X position | |
y0 |
number |
Start Y position | |
x1 |
number |
End X position | |
y1 |
number |
End Y position | |
thickness |
number |
1.0 |
Line thickness in pixels |
natiny.render.line(40, 40, 220, 140, 2.0)
void natiny_render_poly(float cx, float cy, uint32_t sides, float radius, float rotation);
Draws a filled regular polygon on the currently bound render target, using the current drawing color set by natiny_render_set_color().
| Name | Type | Description |
|---|---|---|
cx |
float |
Center X position |
cy |
float |
Center Y position |
sides |
uint32_t |
Number of polygon sides |
radius |
float |
Distance from the center to a vertex |
rotation |
float |
Rotation angle in degrees |
natiny_render_poly(160.0f, 120.0f, 6, 52.0f, 0.0f);
sides must be at least 3; smaller values draw nothing.radius is measured from the center to a vertex and rotation is in degrees.natiny.render.poly(cx, cy, sides, radius [, rotation])
Draws a filled regular polygon on the currently bound render target, using the current drawing color set by natiny.render.set_color().
| Name | Type | Default | Description |
|---|---|---|---|
cx |
number |
Center X position | |
cy |
number |
Center Y position | |
sides |
integer |
Number of polygon sides | |
radius |
number |
Distance from the center to a vertex | |
rotation |
number |
0.0 |
Rotation angle in degrees |
natiny.render.poly(160, 120, 6, 52, 0.0)
sides must be at least 3; smaller values draw nothing.radius is measured from the center to a vertex and rotation is in degrees.void natiny_render_poly_outline(
float cx, float cy, uint32_t sides, float radius, float rotation,
float thickness
);
Draws a regular polygon outline on the currently bound render target, using the current drawing color set by natiny_render_set_color().
| Name | Type | Description |
|---|---|---|
cx |
float |
Center X position |
cy |
float |
Center Y position |
sides |
uint32_t |
Number of polygon sides |
radius |
float |
Distance from the center to a vertex |
rotation |
float |
Rotation angle in degrees |
thickness |
float |
Outline thickness |
natiny_render_poly_outline(160.0f, 120.0f, 6, 60.0f, 0.0f, 2.0f);
sides must be at least 3; smaller values draw nothing.natiny.render.poly_outline(cx, cy, sides, radius [, rotation [, thickness]])
Draws a regular polygon outline on the currently bound render target, using the current drawing color set by natiny.render.set_color().
| Name | Type | Default | Description |
|---|---|---|---|
cx |
number |
Center X position | |
cy |
number |
Center Y position | |
sides |
integer |
Number of polygon sides | |
radius |
number |
Distance from the center to a vertex | |
rotation |
number |
0.0 |
Rotation angle in degrees |
thickness |
number |
1.0 |
Outline thickness |
natiny.render.poly_outline(160, 120, 6, 60, 0.0, 2.0)
sides must be at least 3; smaller values draw nothing.0 degrees and thickness 1 pixel.void natiny_render_rectangle(float x, float y, float w, float h);
Draws a filled rectangle on the currently bound render target, using the current drawing color set by natiny_render_set_color().
| Name | Type | Description |
|---|---|---|
x |
float |
Left edge X position |
y |
float |
Top edge Y position |
w |
float |
Width |
h |
float |
Height |
natiny_render_rectangle(40.0f, 40.0f, 120.0f, 80.0f);
(x, y) is the top-left corner; positive w and h extend right and down.natiny.render.rectangle(x, y, w, h)
Draws a filled rectangle on the currently bound render target, using the current drawing color set by natiny.render.set_color().
| Name | Type | Description |
|---|---|---|
x |
number |
Left edge X position |
y |
number |
Top edge Y position |
w |
number |
Width |
h |
number |
Height |
natiny.render.rectangle(40, 40, 120, 80)
(x, y) is the top-left corner; positive w and h extend right and down.void natiny_render_rectangle_outline(float x, float y, float w, float h, float thickness);
Draws a rectangle outline on the currently bound render target, using the current drawing color set by natiny_render_set_color().
| Name | Type | Description |
|---|---|---|
x |
float |
Left edge X position |
y |
float |
Top edge Y position |
w |
float |
Width |
h |
float |
Height |
thickness |
float |
Outline thickness |
natiny_render_rectangle_outline(40.0f, 40.0f, 120.0f, 80.0f, 2.0f);
natiny.render.rectangle_outline(x, y, w, h [, thickness])
Draws a rectangle outline on the currently bound render target, using the current drawing color set by natiny.render.set_color().
| Name | Type | Default | Description |
|---|---|---|---|
x |
number |
Left edge X position | |
y |
number |
Top edge Y position | |
w |
number |
Width | |
h |
number |
Height | |
thickness |
number |
1.0 |
Outline thickness |
natiny.render.rectangle_outline(40, 40, 120, 80, 2.0)
1 pixel.void natiny_render_rounded_rectangle(
float x, float y, float w, float h,
float radius_tl, float radius_tr, float radius_br, float radius_bl
);
Draws a filled rectangle with each corner rounded by a radius of its own, on
the currently bound render target, using the current drawing color set by
natiny_render_set_color().
Radii are ordered top-left, top-right, bottom-right, bottom-left.
| Name | Type | Description |
|---|---|---|
x |
float |
Left edge X position |
y |
float |
Top edge Y position |
w |
float |
Width |
h |
float |
Height |
radius_tl |
float |
Top-left corner radius |
radius_tr |
float |
Top-right corner radius |
radius_br |
float |
Bottom-right corner radius |
radius_bl |
float |
Bottom-left corner radius |
natiny_render_rounded_rectangle(40.0f, 40.0f, 240.0f, 140.0f,
14.0f, 14.0f, 14.0f, 14.0f);
0.0f; a zero or negative w or h draws nothing.0 to 1 across the rectangle bounds.natiny.render.rounded_rectangle(x, y, w, h, radius_tl [, radius_tr [, radius_br [, radius_bl]]])
Draws a filled rectangle with each corner rounded by a radius of its own, on
the currently bound render target, using the current drawing color set by
natiny.render.set_color().
Radii are ordered top-left, top-right, bottom-right, bottom-left. In Lua, one radius is shorthand for the same value on all four corners.
| Name | Type | Default | Description |
|---|---|---|---|
x |
number |
Left edge X position | |
y |
number |
Top edge Y position | |
w |
number |
Width | |
h |
number |
Height | |
radius_tl |
number |
Top-left corner radius | |
radius_tr |
number |
radius_tl |
Top-right corner radius |
radius_br |
number |
radius_tl |
Bottom-right corner radius |
radius_bl |
number |
radius_tl |
Bottom-left corner radius |
natiny.render.rounded_rectangle(40, 40, 240, 140, 14)
0; a zero or negative width or height draws nothing.0 to 1 across the rectangle bounds.void natiny_render_rounded_rectangle_outline(
float x, float y, float w, float h,
float radius_tl, float radius_tr, float radius_br, float radius_bl,
float thickness
);
Draws the outline of a rectangle with each corner rounded by a radius of its
own, on the currently bound render target, using the current drawing color set
by natiny_render_set_color().
The radii are given in the order the CSS box model uses - top-left, top-right, bottom-right, bottom-left.
| Name | Type | Description |
|---|---|---|
x |
float |
Left edge X position |
y |
float |
Top edge Y position |
w |
float |
Width |
h |
float |
Height |
radius_tl |
float |
Top-left corner radius |
radius_tr |
float |
Top-right corner radius |
radius_br |
float |
Bottom-right corner radius |
radius_bl |
float |
Bottom-left corner radius |
thickness |
float |
Outline thickness |
natiny_render_rounded_rectangle_outline(40.0f, 40.0f, 240.0f, 140.0f,
14.0f, 14.0f, 14.0f, 14.0f, 2.0f);
0.0f; oversized radii are scaled together to fit.
All-zero radii produce a rectangle outline.w, h or thickness draws nothing.natiny.render.rounded_rectangle_outline(x, y, w, h, radius_tl, radius_tr, radius_br, radius_bl [, thickness])
Draws the outline of a rectangle with each corner rounded by a radius of its
own, on the currently bound render target, using the current drawing color set
by natiny.render.set_color().
Radii are ordered top-left, top-right, bottom-right, bottom-left. All four are
required; Lua makes only thickness optional.
| Name | Type | Default | Description |
|---|---|---|---|
x |
number |
Left edge X position | |
y |
number |
Top edge Y position | |
w |
number |
Width | |
h |
number |
Height | |
radius_tl |
number |
Top-left corner radius | |
radius_tr |
number |
Top-right corner radius | |
radius_br |
number |
Bottom-right corner radius | |
radius_bl |
number |
Bottom-left corner radius | |
thickness |
number |
1.0 |
Outline thickness |
natiny.render.rounded_rectangle_outline(40, 40, 240, 140, 14, 14, 14, 14, 2)
0; oversized radii are scaled together to fit.
All-zero radii produce a rectangle outline.1 pixel.void natiny_render_scissor_begin(int x, int y, int width, int height);
Starts a scissor clip rectangle for subsequent draw calls on the currently bound render target.
| Name | Type | Description |
|---|---|---|
x |
int |
Left edge of the clip rectangle |
y |
int |
Top edge of the clip rectangle |
width |
int |
Clip rectangle width |
height |
int |
Clip rectangle height |
natiny_render_scissor_begin(40, 40, 220, 120);
draw_content();
natiny_render_scissor_end();
natiny_render_scissor_end(); nesting is
limited to 16 levels.natiny.render.scissor_begin(x, y, width, height [, fn])
Starts a scissor clip rectangle for subsequent draw calls on the currently bound render target.
| Name | Type | Default | Description |
|---|---|---|---|
x |
number |
Left edge of the clip rectangle | |
y |
number |
Top edge of the clip rectangle | |
width |
number |
Clip rectangle width | |
height |
number |
Clip rectangle height | |
fn |
function |
nil |
Called with no arguments while the clip is in force; the clip is ended when it returns |
natiny.render.scissor_begin(40, 40, 220, 120, function()
draw_content()
end)
Without the callback, end the clip by hand:
natiny.render.scissor_begin(40, 40, 220, 120)
draw_content()
natiny.render.scissor_end()
fn, the clip is ended when fn returns, including when it raises an
error - the error is re-raised afterwards. A skipped scissor_end leaves
the stack one level deep for the rest of the frame, and every draw after it
clipped to a rectangle nobody asked for.natiny.render.scissor_end(); nesting is
limited to 16 levels.void natiny_render_scissor_end(void);
Pops the current scissor rectangle. A nested scissor restores its parent; drawing becomes unrestricted only when the stack becomes empty.
natiny_render_scissor_begin(40, 40, 220, 120);
natiny_render_scissor_end();
natiny_render_scissor_begin(...).natiny.render.scissor_end()
Pops the current scissor rectangle. A nested scissor restores its parent; drawing becomes unrestricted only when the stack becomes empty.
natiny.render.scissor_begin(40, 40, 220, 120)
natiny.render.scissor_end()
natiny.render.scissor_begin(...).void natiny_render_set_color(float r, float g, float b, float a);
Sets the draw color used to tint all subsequent non-clear draws until it is changed or another window is bound.
| Name | Type | Description |
|---|---|---|
r |
float |
Red channel (0.0 – 1.0) |
g |
float |
Green channel (0.0 – 1.0) |
b |
float |
Blue channel (0.0 – 1.0) |
a |
float |
Alpha channel (0.0 – 1.0) |
natiny_render_set_color(1.0f, 0.0f, 0.0f, 1.0f);
(1, 1, 1, 1) on natiny_window_bind.natiny_render_clear.natiny.render.set_color(r, g, b, a)
Sets the draw color used to tint all subsequent non-clear draws until it is changed or another window is bound.
| Name | Type | Default | Description |
|---|---|---|---|
r |
number |
1.0 |
Red channel (0.0 – 1.0) |
g |
number |
1.0 |
Green channel (0.0 – 1.0) |
b |
number |
1.0 |
Blue channel (0.0 – 1.0) |
a |
number |
1.0 |
Alpha channel (0.0 – 1.0) |
natiny.render.set_color(1.0, 0.0, 0.0, 1.0)
(1, 1, 1, 1) on natiny.window.bind.natiny.render.clear.void natiny_render_surface(
NatinySurface surf,
float x, float y, float w, float h
);
Draws the surface contents as a textured quad using the current draw color.
| Name | Type | Description |
|---|---|---|
surf |
NatinySurface |
Surface handle from natiny_surface_create |
x |
float |
X position |
y |
float |
Y position |
w |
float |
Draw width |
h |
float |
Draw height |
natiny_render_surface(rt, 100.0f, 100.0f, 512.0f, 512.0f);
natiny.render.surface(surf, x, y [, w [, h]])
Draws the surface contents as a textured quad using the current draw color.
| Name | Type | Default | Description |
|---|---|---|---|
surf |
number |
Surface handle from natiny.surface.create |
|
x |
number |
X position | |
y |
number |
Y position | |
w |
number |
Surface width | Draw width |
h |
number |
Surface height | Draw height |
natiny.render.surface(rt, 100, 100)
void natiny_render_tag(const char* tag, uint32_t sort);
void natiny_render_tags(const char* const* tags, uint32_t count, uint32_t sort);
Draws every visible model whose own material has the requested tag.
natiny_render_tags requires the material to have every requested tag. Per-part
materials and a material bound with natiny_material_bind
do not affect selection.
| Name | Type | Description |
|---|---|---|
tag |
const char* |
One tag name |
tags |
const char* const* |
Array of distinct tag names |
count |
uint32_t |
Number of entries in tags; maximum 8 |
sort |
uint32_t |
Draw order; one of the NATINY_SORT_* constants below |
| Constant | Value | Description |
|---|---|---|
NATINY_SORT_MATERIAL |
0 |
Group by model material, then mesh |
NATINY_SORT_NONE |
1 |
Keep model storage order without sorting |
NATINY_SORT_BACK_TO_FRONT |
2 |
Sort model bounds centers from farthest to nearest |
NATINY_SORT_FRONT_TO_BACK |
3 |
Sort model bounds centers from nearest to farthest |
const char* tags[] = { "world", "transparent" };
natiny_render_tags(tags, 2, NATINY_SORT_BACK_TO_FRONT);
"model" tag. A custom
material matches tags added with
natiny_material_add_tag.natiny_render_tags does nothing when tags is null, count is 0, a tag is
unknown, or an entry is null or empty. A count above 8 is reported to
stderr and nothing is drawn.natiny.render.tag(tag_or_tags [, sort])
Draws every visible model whose own material has the requested tag or all requested tags. Per-part materials and a material bound with natiny.material.bind do not affect selection.
| Name | Type | Default | Description |
|---|---|---|---|
tag_or_tags |
string or table |
One tag name or a list of distinct tag names | |
sort |
integer |
natiny.render.SORT_MATERIAL |
Draw order; one of the constants below |
| Constant | Description |
|---|---|
natiny.render.SORT_MATERIAL |
Group by model material, then mesh |
natiny.render.SORT_NONE |
Keep model storage order without sorting |
natiny.render.SORT_BACK_TO_FRONT |
Sort model bounds centers from farthest to nearest |
natiny.render.SORT_FRONT_TO_BACK |
Sort model bounds centers from nearest to farthest |
natiny.render.tag({ "world", "transparent" }, natiny.render.SORT_BACK_TO_FRONT)
"model" tag. A custom
material matches tags added at creation or with
natiny.material.add_tag.void natiny_render_text(
NatinyFont font,
float x, float y,
const char* text
);
Draws text with the given font on the currently bound render target using the current draw color.
| Name | Type | Description |
|---|---|---|
font |
NatinyFont |
Font handle from natiny_font_create |
x |
float |
X position in pixels |
y |
float |
Y position in pixels |
text |
const char* |
UTF-8 text to draw |
natiny_render_text(font, 20.0f, 20.0f, "Score: 100");
text is decoded as UTF-8 and glyphs are cached by the font on demand.(x, y) is the top-left of the first line; the baseline is derived from the
font ascent. The current draw color tints the glyphs. starts a new line and moves to the next tab stop, four spaces
apart. Lines are stacked by natiny_font_get_line_height.natiny_render_text_box for text that has to fit a width,
and natiny_render_text_ex to turn it.natiny_font_measure_text measures exactly what this draws.natiny.render.text(font, x, y, text)
Draws text with the given font on the currently bound render target using the current draw color.
| Name | Type | Description |
|---|---|---|
font |
number |
Font handle from natiny.font.create |
x |
number |
X position in pixels |
y |
number |
Y position in pixels |
text |
string |
UTF-8 text to draw |
natiny.render.text(font, 20, 20, "Score: 100")
text is decoded as UTF-8 and glyphs are cached by the font on demand.(x, y) is the top-left of the first line; the baseline is derived from the
font ascent. The current draw color tints the glyphs. starts a new line and moves to the next tab stop, four spaces
apart. Lines are stacked by natiny.font.get_line_height.natiny.render.text_box for text that has to fit a width,
and natiny.render.text_ex to turn it.natiny.font.measure_text measures exactly what this draws.void natiny_render_text_box(NatinyFont font, float x, float y,
float w, float h, const char* text,
uint32_t align);
Draws wrapped UTF-8 text inside a layout rectangle on the current render target.
| Name | Type | Description |
|---|---|---|
font |
NatinyFont |
Font handle from natiny_font_create |
x, y |
float |
Top-left corner of the rectangle in drawing units |
w |
float |
Rectangle width and wrap width in drawing units; values at or below 0.0f disable wrapping |
h |
float |
Rectangle height in drawing units, used only for vertical alignment |
text |
const char* |
Null-terminated UTF-8 text to draw |
align |
uint32_t |
Horizontal and vertical alignment flags |
Combine one horizontal flag with one vertical flag using |:
| Axis | Values |
|---|---|
| Horizontal | NATINY_ALIGN_LEFT, NATINY_ALIGN_CENTER, NATINY_ALIGN_RIGHT |
| Vertical | NATINY_ALIGN_TOP, NATINY_ALIGN_MIDDLE, NATINY_ALIGN_BOTTOM |
NATINY_ALIGN_LEFT and NATINY_ALIGN_TOP are both 0.
Horizontal alignment is applied to each line separately; vertical alignment
positions the complete text block.
natiny_render_text_box(font, 20.0f, 20.0f, 300.0f, 120.0f, message,
NATINY_ALIGN_CENTER | NATINY_ALIGN_MIDDLE);
natiny_render_scissor_begin when clipping is required.natiny_font_measure_wrapped.\n, and between Han, kana, and Hangul
characters. A word wider than w is cut where it stops fitting.natiny_render_set_color.NULL text are ignored.natiny.render.text_box(font, x, y, w, h, text [, align])
Draws wrapped UTF-8 text inside a layout rectangle on the current render target.
| Name | Type | Default | Description |
|---|---|---|---|
font |
number |
Font handle from natiny.font.create |
|
x, y |
number |
Top-left corner of the rectangle in drawing units | |
w |
number |
Rectangle width and wrap width in drawing units; values at or below 0 disable wrapping |
|
h |
number |
Rectangle height in drawing units, used only for vertical alignment | |
text |
string |
UTF-8 text to draw | |
align |
number |
natiny.render.ALIGN_LEFT \| natiny.render.ALIGN_TOP |
Horizontal and vertical alignment flags |
Combine one horizontal flag with one vertical flag using |:
| Axis | Values |
|---|---|
| Horizontal | natiny.render.ALIGN_LEFT, natiny.render.ALIGN_CENTER, natiny.render.ALIGN_RIGHT |
| Vertical | natiny.render.ALIGN_TOP, natiny.render.ALIGN_MIDDLE, natiny.render.ALIGN_BOTTOM |
natiny.render.ALIGN_LEFT and natiny.render.ALIGN_TOP are both 0.
Horizontal alignment is applied to each line separately; vertical alignment
positions the complete text block.
natiny.render.text_box(font, 20, 20, 300, 120, message,
natiny.render.ALIGN_CENTER | natiny.render.ALIGN_MIDDLE)
natiny.render.scissor_begin when clipping is required.natiny.font.measure_wrapped.\n, and between Han, kana, and Hangul
characters. A word wider than w is cut where it stops fitting.natiny.render.set_color.void natiny_render_text_ex(NatinyFont font, float x, float y,
const char* text, float origin_x,
float origin_y, float rotation);
Draws an unwrapped UTF-8 text block with a local origin and rotation.
| Name | Type | Description |
|---|---|---|
font |
NatinyFont |
Font handle from natiny_font_create |
x, y |
float |
Position of the local origin on the current target, in drawing units |
text |
const char* |
Null-terminated UTF-8 text to draw |
origin_x |
float |
Horizontal origin offset from the block's left edge, in drawing units |
origin_y |
float |
Vertical origin offset from the block's top edge, in drawing units |
rotation |
float |
Clockwise rotation around (x, y), in degrees |
float width = 0.0f, height = 0.0f;
natiny_font_measure_text(font, "SPIN", &width, &height);
natiny_render_text_ex(font, 400.0f, 300.0f, "SPIN",
width * 0.5f, height * 0.5f, angle);
(origin_x, origin_y) selects the point in the unrotated text block placed
at (x, y). The block rotates around that target position.0.0f for all transform arguments is equivalent to
natiny_render_text.0.0f
degrees, normal text snapping is preserved.\n starts a new line; all lines rotate together as one block. This
function does not perform word wrapping.natiny_render_set_color.NULL text are ignored.natiny.render.text_ex(font, x, y, text [, origin_x, origin_y, rotation])
Draws an unwrapped UTF-8 text block with a local origin and rotation.
| Name | Type | Default | Description |
|---|---|---|---|
font |
number |
Font handle from natiny.font.create |
|
x, y |
number |
Position of the local origin on the current target, in drawing units | |
text |
string |
UTF-8 text to draw | |
origin_x |
number |
0 |
Horizontal origin offset from the block's left edge, in drawing units |
origin_y |
number |
0 |
Vertical origin offset from the block's top edge, in drawing units |
rotation |
number |
0 |
Clockwise rotation around (x, y), in degrees |
local text = "SPIN"
local w, h = natiny.font.measure_text(font, text)
natiny.render.text_ex(font, 400, 300, text, w / 2, h / 2, angle)
(origin_x, origin_y) selects the point in the unrotated text block placed
at (x, y). The block rotates around that target position.natiny.render.text.0 degrees,
normal text snapping is preserved.\n starts a new line; all lines rotate together as one block. This
function does not perform word wrapping.natiny.render.set_color.void natiny_render_texture(
NatinyTexture texture,
float x, float y, float w, float h
);
Draws a texture at the specified position and size using the current draw color.
| Name | Type | Description |
|---|---|---|
texture |
NatinyTexture |
Texture handle from natiny_texture_create |
x |
float |
X position in pixels |
y |
float |
Y position in pixels |
w |
float |
Positive draw width in pixels |
h |
float |
Positive draw height in pixels |
natiny_window_bind(win);
natiny_render_set_color(1.0f, 1.0f, 1.0f, 1.0f);
natiny_render_texture(tex, 100.0f, 50.0f, 64.0f, 64.0f);
natiny_window_unbind();
w and h are required. Non-positive sizes do not draw anything.natiny.render.texture(tex, x, y, w, h)
Draws a texture at the specified position and size using the current draw color.
| Name | Type | Description |
|---|---|---|
tex |
number |
Texture handle from natiny.texture.create |
x |
number |
X position in pixels |
y |
number |
Y position in pixels |
w |
number |
Positive draw width in pixels |
h |
number |
Positive draw height in pixels |
natiny.window.bind(win)
natiny.render.set_color(1.0, 1.0, 1.0, 1.0)
natiny.render.texture(tex, 100.0, 50.0, 64.0, 64.0)
natiny.window.unbind()
natiny.window.bind() or natiny.surface.bind().natiny.render.set_color(). Call it before drawing to change the color.w and h are required. Non-positive sizes do not draw anything.void natiny_render_triangle(float x0, float y0, float x1, float y1, float x2, float y2);
Draws a filled triangle on the currently bound render target, using the current drawing color set by natiny_render_set_color().
| Name | Type | Description |
|---|---|---|
x0 |
float |
First vertex X position |
y0 |
float |
First vertex Y position |
x1 |
float |
Second vertex X position |
y1 |
float |
Second vertex Y position |
x2 |
float |
Third vertex X position |
y2 |
float |
Third vertex Y position |
natiny_render_triangle(160.0f, 40.0f, 100.0f, 160.0f, 220.0f, 160.0f);
natiny.render.triangle(x0, y0, x1, y1, x2, y2)
Draws a filled triangle on the currently bound render target, using the current drawing color set by natiny.render.set_color().
| Name | Type | Description |
|---|---|---|
x0 |
number |
First vertex X position |
y0 |
number |
First vertex Y position |
x1 |
number |
Second vertex X position |
y1 |
number |
Second vertex Y position |
x2 |
number |
Third vertex X position |
y2 |
number |
Third vertex Y position |
natiny.render.triangle(160, 40, 100, 160, 220, 160)
void natiny_render_triangle_outline(
float x0, float y0, float x1, float y1, float x2, float y2,
float thickness
);
Draws a triangle outline on the currently bound render target, using the current drawing color set by natiny_render_set_color().
| Name | Type | Description |
|---|---|---|
x0 |
float |
First vertex X position |
y0 |
float |
First vertex Y position |
x1 |
float |
Second vertex X position |
y1 |
float |
Second vertex Y position |
x2 |
float |
Third vertex X position |
y2 |
float |
Third vertex Y position |
thickness |
float |
Outline thickness |
natiny_window_bind(win);
natiny_render_set_color(0.0f, 1.0f, 1.0f, 1.0f);
natiny_render_triangle_outline(160.0f, 40.0f, 100.0f, 160.0f, 220.0f, 160.0f, 2.0f);
natiny_window_unbind();
natiny_render_set_color(). The default is opaque white.natiny_window_bind() or natiny_surface_bind().natiny.render.triangle_outline(x0, y0, x1, y1, x2, y2 [, thickness])
Draws a triangle outline on the currently bound render target, using the current drawing color set by natiny.render.set_color().
| Name | Type | Default | Description |
|---|---|---|---|
x0 |
number |
First vertex X position | |
y0 |
number |
First vertex Y position | |
x1 |
number |
Second vertex X position | |
y1 |
number |
Second vertex Y position | |
x2 |
number |
Third vertex X position | |
y2 |
number |
Third vertex Y position | |
thickness |
number |
1.0 |
Outline thickness |
natiny.window.bind(win)
natiny.render.set_color(0.0, 1.0, 1.0, 1.0)
natiny.render.triangle_outline(160, 40, 100, 160, 220, 160, 2.0)
natiny.window.unbind()
natiny.render.set_color(). The default is opaque white.natiny.window.bind() or natiny.surface.bind().void natiny_render_viewport_begin(int x, int y, int width, int height);
Sets a rectangular viewport on the current window or surface render target.
The viewport becomes the local drawing area: its top-left corner is (0, 0),
and its dimensions are used by 2D drawing and cameras bound afterwards.
| Name | Type | Description |
|---|---|---|
x |
int |
Left edge in render-target pixels |
y |
int |
Top edge in render-target pixels |
width |
int |
Width in pixels |
height |
int |
Height in pixels |
natiny_render_viewport_begin(0, 0, 320, 180);
draw_scene();
natiny_render_viewport_end();
natiny_render_cover() covers the viewport. Bind a camera after setting the
viewport to use the viewport's aspect ratio.natiny.render.viewport_begin(x, y, width, height [, fn])
Sets a rectangular viewport on the current window or surface render target.
The viewport becomes the local drawing area: its top-left corner is (0, 0),
and its dimensions are used by 2D drawing and cameras bound afterwards.
| Name | Type | Default | Description |
|---|---|---|---|
x |
number |
Left edge in render-target pixels | |
y |
number |
Top edge in render-target pixels | |
width |
number |
Width in pixels | |
height |
number |
Height in pixels | |
fn |
function |
nil |
Called with no arguments while the viewport is in force; the viewport is ended when it returns |
for i, pane in ipairs(panes) do
natiny.render.viewport_begin(pane.x, pane.y, pane.w, pane.h, function()
natiny.camera.bind(pane.camera)
draw_scene()
end)
end
Without the callback, end the viewport by hand:
natiny.render.viewport_begin(0, 0, 320, 180)
draw_scene()
natiny.render.viewport_end()
fn, the viewport is ended when fn returns, including when it
raises an error - the error is re-raised afterwards. That is the difference
worth having: a skipped viewport_end leaves a viewport open, and every
viewport_begin for the rest of the frame is then refused.fn closes the outer viewport instead.natiny.render.cover() covers the viewport. Bind a camera after setting the
viewport to use the viewport's aspect ratio.void natiny_render_viewport_end(void);
Ends the current viewport and restores the full window or surface render target as the local drawing area.
natiny_render_viewport_begin(0, 0, 320, 180);
draw_scene();
natiny_render_viewport_end();
natiny_render_cover(), and cameras bound
afterwards.natiny.render.viewport_end()
Ends the current viewport and restores the full window or surface render target as the local drawing area.
natiny.render.viewport_begin(0, 0, 320, 180)
draw_scene()
natiny.render.viewport_end()
natiny.render.cover(), and cameras bound
afterwards.void natiny_resource_destroy(NatinyResource resource);
Destroys a resource and releases its memory.
| Name | Type | Description |
|---|---|---|
resource |
NatinyResource |
Resource handle from natiny_resource_load |
NatinyResource res = natiny_resource_load("data/file.bin");
natiny_resource_destroy(res);
natiny.resource.destroy(resource)
Destroys a resource and releases its memory.
| Name | Type | Description |
|---|---|---|
resource |
number |
Resource handle from natiny.resource.load |
local res = natiny.resource.load("data/file.bin")
natiny.resource.destroy(res)
NatinyResource natiny_resource_from_data(const uint8_t* data, uint32_t data_len);
Creates a resource from bytes already in memory.
| Name | Type | Description |
|---|---|---|
data |
const uint8_t* |
Raw byte buffer |
data_len |
uint32_t |
Buffer length in bytes |
| Type | Description |
|---|---|
NatinyResource |
Resource handle |
const uint8_t embedded[] = { 0x89, 0x50, 0x4E, 0x47 }; /* PNG signature bytes */
NatinyResource res = natiny_resource_from_data(embedded, sizeof(embedded));
natiny_resource_destroy(res);
local resource = natiny.resource.from_data(data)
Creates a resource from bytes already in memory.
| Name | Type | Description |
|---|---|---|
data |
string |
Raw bytes as a binary-safe Lua string |
| Type | Description |
|---|---|
number |
Resource handle |
local embedded = string.char(0x89, 0x50, 0x4E, 0x47) -- PNG signature bytes
local res = natiny.resource.from_data(embedded)
natiny.resource.destroy(res)
const uint8_t* natiny_resource_get_data(NatinyResource resource);
Returns the raw bytes of a loaded resource.
| Name | Type | Description |
|---|---|---|
resource |
NatinyResource |
Resource handle from natiny_resource_load |
| Type | Description |
|---|---|
const uint8_t* |
Pointer to resource data, or NULL for an invalid handle |
NatinyResource res = natiny_resource_load("data/file.bin");
const uint8_t* data = natiny_resource_get_data(res);
natiny_resource_shutdown() is called.natiny.resource.get_data(resource) -> bytes
Returns the raw bytes of a loaded resource.
| Name | Type | Description |
|---|---|---|
resource |
number |
Resource handle from natiny.resource.load |
| Type | Description |
|---|---|
string |
Resource bytes (binary-safe Lua string) |
local res = natiny.resource.load("data/file.bin")
local bytes = natiny.resource.get_data(res)
uint32_t natiny_resource_get_size(NatinyResource resource);
Returns the byte count of a loaded resource.
| Name | Type | Description |
|---|---|---|
resource |
NatinyResource |
Resource handle from natiny_resource_load |
| Type | Description |
|---|---|
uint32_t |
Size in bytes |
uint32_t len = natiny_resource_get_size(res);
0 for an invalid handle or an empty resource.natiny.resource.get_size(resource) -> size
Returns the byte count of a loaded resource.
| Name | Type | Description |
|---|---|---|
resource |
number |
Resource handle from natiny.resource.load |
| Type | Description |
|---|---|
number |
Size in bytes |
local len = natiny.resource.get_size(res)
0 for an invalid handle or an empty resource.NatinyResource natiny_resource_load(const char* path);
Synchronously loads a resource from disk (or network on web).
| Name | Type | Description |
|---|---|---|
path |
const char* |
Path to the resource file |
| Type | Description |
|---|---|
NatinyResource |
Resource handle, or 0 when loading fails |
NatinyResource res = natiny_resource_load("data/image.png");
0.natiny_resource_load_async().natiny.resource.load(path) -> resource
Synchronously loads a resource from disk (or network on web).
| Name | Type | Description |
|---|---|---|
path |
string |
Path to the resource file |
| Type | Description |
|---|---|
number |
Resource handle |
local res = natiny.resource.load("data/image.png")
natiny.resource.load_async.void natiny_resource_load_async(
const char* path,
natiny_resource_async_cb callback,
void* userdata
);
Asynchronously loads a resource. The callback fires when loading completes.
| Name | Type | Description |
|---|---|---|
path |
const char* |
Path to the resource file |
callback |
natiny_resource_async_cb |
Called when loading completes |
userdata |
void* |
User pointer passed to the callback |
The callback receives:
- success - 1 on success, 0 on failure.
- data - pointer to loaded bytes (valid only during callback).
- data_len - number of loaded bytes.
- userdata - forwarded user pointer.
void on_loaded(int success, const uint8_t* data, uint32_t data_len, void* ud) {
if (!success || !data || data_len == 0) {
return;
}
NatinyResource res = natiny_resource_from_data(data, data_len);
/* use res */
natiny_resource_destroy(res);
}
void start_load(void) {
natiny_resource_load_async("data/big.bin", on_loaded, NULL);
}
natiny_backend_loop
before the frame callback runs.natiny.resource.load_async(path, callback)
Asynchronously loads a resource. The callback fires when the resource is ready.
| Name | Type | Description |
|---|---|---|
path |
string |
Path to the resource file |
callback |
function |
function(resource) called on completion |
The callback receives one argument:
- resource - loaded resource handle, or nil on failure.
natiny.resource.load_async("data/textures/texture.png", function(resource)
if resource then
tex = natiny.texture.create(resource)
else
print("Failed to load texture!")
end
end)
natiny.backend.loop
before the frame callback runs.int natiny_resource_save(NatinyResource resource, const char* path);
Writes a resource's bytes to a file, replacing an existing file at that path.
| Name | Type | Description |
|---|---|---|
resource |
NatinyResource |
Resource handle |
path |
const char* |
Where to write it; an existing file is overwritten |
| Type | Description |
|---|---|
int |
1 when every byte was written, 0 otherwise |
int ok = natiny_resource_save(resource, "data/copy.bin");
natiny.resource.save(resource, path) -> ok
Writes a resource's bytes to a file, replacing an existing file at that path.
| Name | Type | Description |
|---|---|---|
resource |
number |
Resource handle |
path |
string |
Where to write it; an existing file is overwritten |
| Type | Description |
|---|---|
boolean |
true when every byte was written |
local ok = natiny.resource.save(resource, "data/copy.bin")
false; the file may be left partly written.void natiny_resource_shutdown(void);
Shuts down the resource manager and invalidates all resource handles.
natiny.resource.shutdown()
Shuts down the resource manager and invalidates all resource handles.
void natiny_shader_destroy(NatinyShader shader);
Destroys a custom shader and frees its GPU resources.
| Name | Type | Description |
|---|---|---|
shader |
NatinyShader |
Shader handle from natiny_shader_load |
natiny_shader_destroy(shader);
natiny.shader.destroy(shader)
Destroys a custom shader and frees its GPU resources.
| Name | Type | Description |
|---|---|---|
shader |
number |
Shader handle from natiny.shader.load |
natiny.shader.destroy(shader)
NatinyShader natiny_shader_load(NatinyResource nsl_source);
Compiles an NSL resource containing vertex and fragment stages for the active graphics backend.
| Name | Type | Description |
|---|---|---|
nsl_source |
NatinyResource |
Resource holding the .nsl source |
| Type | Description |
|---|---|
NatinyShader |
Shader handle, or 0 when the source does not compile |
NatinyShader shader = natiny_shader_load(source);
0.local shader = natiny.shader.load(nsl_source)
Compiles an NSL resource containing vertex and fragment stages for the active graphics backend.
| Name | Type | Description |
|---|---|---|
nsl_source |
number |
Resource holding the .nsl source |
| Type | Description |
|---|---|
number |
Shader handle, or an error when the source does not compile |
local shader = natiny.shader.load(source)
int natiny_shader_reload(NatinyShader shader, NatinyResource nsl_source);
Replaces the source of an existing shader. The handle does not change.
| Name | Type | Description |
|---|---|---|
shader |
NatinyShader |
Live shader handle from natiny_shader_load |
nsl_source |
NatinyResource |
Resource handle containing the new .nsl source |
| Type | Description |
|---|---|
int |
1 on success, 0 on failure |
NatinyShader shader = natiny_shader_load(source);
NatinyMaterial material = natiny_material_create(shader);
NatinyResource replacement = natiny_resource_load("shaders/water.nsl");
/* Edit the .nsl, load it again, and the material draws with the new code. */
if (!natiny_shader_reload(shader, replacement)) {
/* The shader still contains its previous compiled code. */
}
natiny_resource_destroy(replacement);
0 if shader is not live, the source does not compile or its
declarations changed. A failure leaves the shader exactly as it was.natiny.shader.reload(shader, nsl_source)
Replaces the source of an existing shader. The handle does not change.
| Name | Type | Description |
|---|---|---|
shader |
number |
Live shader handle from natiny.shader.load |
nsl_source |
number |
Resource handle containing the new .nsl source |
local shader = natiny.shader.load(source)
local material = natiny.material.create(shader)
local replacement = natiny.resource.load("shaders/water.nsl")
-- edit the .nsl, load it again, and the material draws with the new code
natiny.shader.reload(shader, replacement)
natiny.resource.destroy(replacement)
shader is not live, the source does not compile or its
declarations changed. A failure leaves the shader exactly as it was.void natiny_surface_bind(NatinySurface surf, uint32_t slot);
Makes the surface a render target. Bind it between natiny_window_bind and
natiny_window_unbind so drawing has an active frame command buffer. Multiple
surfaces can be bound to distinct slots for one pass.
| Name | Type | Description |
|---|---|---|
surf |
NatinySurface |
Surface handle from natiny_surface_create |
slot |
uint32_t |
Target slot, 0 to NATINY_MAX_TARGETS - 1 |
natiny_window_bind(win);
natiny_surface_bind(rt, 0);
draw_scene();
natiny_surface_unbind();
natiny_window_unbind();
natiny_window_bind and
natiny_window_unbind.0 opens a target set and determines its size, 2D projection, and depth
attachment. Other slots add colour targets to that set. All surfaces in a
set must have identical dimensions.natiny_surface_unbind ends the entire target set; individual
slots cannot be unbound.0 suspends the current target state. The matching unbind
restores it; see The bind stack.natiny_render_clear explicitly when they should be replaced.0 so its
projection uses the target's aspect ratio.natiny.surface.bind(surf [, slot] [, fn])
Makes the surface a render target. Bind it while a window is bound so drawing has an active frame command buffer. Multiple surfaces can be bound to distinct slots for one pass.
| Name | Type | Default | Description |
|---|---|---|---|
surf |
number |
Surface handle from natiny.surface.create |
|
slot |
number |
0 |
Target slot, 0 to natiny.surface.MAX_TARGETS - 1 |
fn |
function |
nil |
Callback run while the surface is bound |
natiny.window.bind(win, function()
natiny.surface.bind(rt, function()
draw_scene()
end)
end)
0 opens a target set and determines its size, 2D projection, and depth
attachment. Other slots add colour targets to that set. All surfaces in a
set must have identical dimensions.natiny.surface.unbind ends the entire target set; individual
slots cannot be unbound.0 suspends the current target state. The matching unbind
restores it; see The bind stack.natiny.render.clear explicitly when they should be replaced.0 so its
projection uses the target's aspect ratio.fn, Lua unbinds the target set even if the callback raises an error,
then propagates the error.NatinySurface natiny_surface_create(uint32_t width, uint32_t height,
uint32_t format, bool depth);
Creates an off-screen render target: one colour texture, and a depth buffer of its own unless it is asked for without one.
| Name | Type | Description |
|---|---|---|
width |
uint32_t |
Surface width in pixels |
height |
uint32_t |
Surface height in pixels |
format |
uint32_t |
Pixel format of the colour texture |
depth |
bool |
Give it a depth buffer |
| Define | Value | Meaning |
|---|---|---|
NATINY_FORMAT_COLOR |
0 | Window-compatible BGRA8 color format |
NATINY_FORMAT_RGBA8 |
1 | Explicit RGBA8 channel order |
NATINY_FORMAT_RGBA16F |
2 | half float - normals, positions, anything past 1 |
NATINY_FORMAT_RGBA32F |
3 | full float |
NATINY_FORMAT_R32F |
4 | one float channel, e.g. linear depth |
| Type | Description |
|---|---|
NatinySurface |
Surface handle, or 0 on failure |
NatinySurface scene = natiny_surface_create(
1280, 720, NATINY_FORMAT_COLOR, true);
0.NATINY_FORMAT_COLOR matches the window-compatible BGRA8 layout;
NATINY_FORMAT_RGBA8 has explicit RGBA channel order. Floating-point formats
preserve values outside 0..1.natiny.surface.create(width, height [, format [, depth]]) -> surface
Creates an off-screen render target: one colour texture, and a depth buffer of its own unless it is asked for without one.
| Name | Type | Default | Description |
|---|---|---|---|
width |
number |
Surface width in pixels | |
height |
number |
Surface height in pixels | |
format |
number |
natiny.surface.COLOR |
Pixel format of the colour texture |
depth |
boolean |
true |
Give it a depth buffer |
| Constant | Meaning |
|---|---|
natiny.surface.COLOR |
Window-compatible BGRA8 color format |
natiny.surface.RGBA8 |
Explicit RGBA8 channel order |
natiny.surface.RGBA16F |
half float - normals, positions, anything past 1 |
natiny.surface.RGBA32F |
full float |
natiny.surface.R32F |
one float channel, e.g. linear depth |
| Type | Description |
|---|---|
number |
Surface handle |
local scene = natiny.surface.create(1280, 720)
COLOR matches the window-compatible BGRA8 layout; RGBA8 has explicit
RGBA channel order. Floating-point formats preserve values outside 0..1.void natiny_surface_destroy(NatinySurface surf);
Destroys a surface and releases its GPU resources.
| Name | Type | Description |
|---|---|---|
surf |
NatinySurface |
Surface handle from natiny_surface_create |
natiny_surface_destroy(rt);
natiny.surface.destroy(surf)
Destroys a surface and releases its GPU resources.
| Name | Type | Description |
|---|---|---|
surf |
number |
Surface handle from natiny.surface.create |
local rt = natiny.surface.create(512, 512)
natiny.surface.destroy(rt)
NatinyTexture natiny_surface_get_depth(NatinySurface surf);
The surface's depth buffer, as a texture handle - for a shader that wants to know how far away each pixel was, rather than only what colour it ended up.
Returns 0 for a surface created without depth.
| Name | Type | Description |
|---|---|---|
surf |
NatinySurface |
Surface handle |
| Type | Description |
|---|---|
NatinyTexture |
Texture handle, or 0 when the surface has no depth buffer |
NatinyTexture depth = natiny_surface_get_depth(scene);
0.natiny.surface.get_depth(surf) -> texture
The surface's depth buffer, as a texture handle - for a shader that wants to know how far away each pixel was, rather than only what colour it ended up.
Returns 0 for a surface created without depth.
| Name | Type | Description |
|---|---|---|
surf |
number |
Surface handle |
| Type | Description |
|---|---|
number |
Texture handle, or 0 when the surface has no depth buffer |
local depth = natiny.surface.get_depth(scene)
0.uint32_t natiny_surface_get_height(NatinySurface surf);
Returns the surface height in pixels.
| Name | Type | Description |
|---|---|---|
surf |
NatinySurface |
Surface handle from natiny_surface_create |
| Type | Description |
|---|---|
uint32_t |
Height in pixels, or 0 for an invalid handle |
uint32_t h = natiny_surface_get_height(rt);
natiny.surface.get_height(surf) -> height
Returns the surface height in pixels.
| Name | Type | Description |
|---|---|---|
surf |
number |
Surface handle from natiny.surface.create |
| Type | Description |
|---|---|
number |
Height in pixels, or 0 for an invalid handle |
local h = natiny.surface.get_height(rt)
NatinyTexture natiny_surface_get_texture(NatinySurface surf);
Returns the surface's color attachment as a texture handle.
The handle stays valid until the surface is destroyed - or until a resize, which replaces the attachment, after which this has to be asked again.
| Name | Type | Description |
|---|---|---|
surf |
NatinySurface |
Surface handle |
| Type | Description |
|---|---|
NatinyTexture |
Texture handle, or 0 if the surface is not valid |
NatinyTexture texture = natiny_surface_get_texture(surface);
natiny_texture_destroy would leave the surface with nothing to draw into. natiny_surface_destroy frees both attachments.natiny.surface.get_texture(surf) -> texture
Returns the surface's color attachment as a texture handle.
The handle stays valid until the surface is destroyed - or until a resize, which replaces the attachment, after which this has to be asked again.
| Name | Type | Description |
|---|---|---|
surf |
number |
Surface handle |
| Type | Description |
|---|---|
number |
Texture handle, or 0 if the surface is not valid |
local texture = natiny.surface.get_texture(surface)
natiny.texture.destroy would leave the surface with nothing to draw into. natiny.surface.destroy frees both attachments.uint32_t natiny_surface_get_width(NatinySurface surf);
Returns the surface width in pixels.
| Name | Type | Description |
|---|---|---|
surf |
NatinySurface |
Surface handle from natiny_surface_create |
| Type | Description |
|---|---|
uint32_t |
Width in pixels, or 0 for an invalid handle |
uint32_t w = natiny_surface_get_width(rt);
natiny.surface.get_width(surf) -> width
Returns the surface width in pixels.
| Name | Type | Description |
|---|---|---|
surf |
number |
Surface handle from natiny.surface.create |
| Type | Description |
|---|---|
number |
Width in pixels, or 0 for an invalid handle |
local w = natiny.surface.get_width(rt)
void natiny_surface_resize(NatinySurface surface, uint32_t width, uint32_t height);
Replaces a surface's attachments with new attachments of the requested size.
| Name | Type | Description |
|---|---|---|
surface |
NatinySurface |
Surface handle from natiny_surface_create |
width |
uint32_t |
New surface width in pixels |
height |
uint32_t |
New surface height in pixels |
natiny_surface_resize(surface, 1920, 1080);
natiny.surface.resize(surf, width, height)
Replaces a surface's attachments with new attachments of the requested size.
| Name | Type | Description |
|---|---|---|
surf |
number |
Surface handle from natiny.surface.create |
width |
number |
New surface width in pixels |
height |
number |
New surface height in pixels |
natiny.surface.resize(surf, 1920, 1080)
void natiny_surface_unbind(void);
Ends the current surface target set and restores the target state suspended by
its slot 0 bind.
natiny_surface_unbind();
natiny.surface.unbind()
Ends the current surface target set and restores the target state suspended by
its slot 0 bind.
natiny.surface.unbind()
NatinyTexture natiny_texture_create(NatinyResource resource);
Creates a texture from an image resource.
| Name | Type | Description |
|---|---|---|
resource |
NatinyResource |
Resource handle containing encoded image data |
| Type | Description |
|---|---|
NatinyTexture |
Texture handle, or 0 on failure |
NatinyTexture texture = natiny_texture_create(image_resource);
0.natiny.texture.create(resource) -> texture
Creates a texture from an image resource.
| Name | Type | Description |
|---|---|---|
resource |
number |
Resource handle containing encoded image data |
| Type | Description |
|---|---|
number |
Texture handle |
local texture = natiny.texture.create(image_resource)
void natiny_texture_destroy(NatinyTexture texture);
Destroys a texture and frees its GPU resources.
| Name | Type | Description |
|---|---|---|
texture |
NatinyTexture |
Texture handle from natiny_texture_create |
natiny_texture_destroy(tex);
natiny.texture.destroy(tex)
Destroys a texture and frees its GPU resources.
| Name | Type | Description |
|---|---|---|
tex |
number |
Texture handle from natiny.texture.create |
natiny.texture.destroy(tex)
uint32_t natiny_texture_get_height(NatinyTexture texture);
Returns the texture height in pixels.
| Name | Type | Description |
|---|---|---|
texture |
NatinyTexture |
Texture handle from natiny_texture_create |
| Type | Description |
|---|---|
uint32_t |
Height in pixels, or 0 for an invalid handle |
uint32_t h = natiny_texture_get_height(tex);
natiny.texture.get_height(tex) -> height
Returns the texture height in pixels.
| Name | Type | Description |
|---|---|---|
tex |
number |
Texture handle from natiny.texture.create |
| Type | Description |
|---|---|
number |
Height in pixels, or 0 for an invalid handle |
local h = natiny.texture.get_height(tex)
uint32_t natiny_texture_get_width(NatinyTexture texture);
Returns the texture width in pixels.
| Name | Type | Description |
|---|---|---|
texture |
NatinyTexture |
Texture handle from natiny_texture_create |
| Type | Description |
|---|---|
uint32_t |
Width in pixels, or 0 for an invalid handle |
uint32_t w = natiny_texture_get_width(tex);
natiny.texture.get_width(tex) -> width
Returns the texture width in pixels.
| Name | Type | Description |
|---|---|---|
tex |
number |
Texture handle from natiny.texture.create |
| Type | Description |
|---|---|
number |
Width in pixels, or 0 for an invalid handle |
local w = natiny.texture.get_width(tex)
int natiny_texture_update(NatinyTexture texture, NatinyResource resource);
Replaces the image inside an existing texture. The handle does not change.
| Name | Type | Description |
|---|---|---|
texture |
NatinyTexture |
Live texture handle from natiny_texture_create |
resource |
NatinyResource |
Resource handle containing encoded image data |
| Type | Description |
|---|---|
int |
1 on success, 0 on failure |
NatinyTexture texture = natiny_texture_create(image_resource);
natiny_material_set_texture(material, "albedo", texture,
NATINY_FILTER_LINEAR, NATINY_FILTER_LINEAR);
NatinyResource replacement = natiny_resource_load("textures/winter.png");
/* Later, with a different picture: the material is not touched again. */
if (!natiny_texture_update(texture, replacement)) {
/* The texture still shows its previous image. */
}
natiny_resource_destroy(replacement);
0 if the handle is not a live texture, or if the image cannot be
decoded. A failure leaves the texture showing what it showed.natiny.texture.update(texture, resource)
Replaces the image inside an existing texture. The handle does not change.
| Name | Type | Description |
|---|---|---|
texture |
number |
Live texture handle from natiny.texture.create |
resource |
number |
Resource handle containing encoded image data |
local texture = natiny.texture.create(image_resource)
natiny.material.set_texture(material, "albedo", texture)
local replacement = natiny.resource.load("textures/winter.png")
-- later, with a different picture: the material is not touched again
natiny.texture.update(texture, replacement)
natiny.resource.destroy(replacement)
void natiny_window_bind(NatinyWindow id);
Binds a window as the render target until it is unbound or replaced.
| Name | Type | Description |
|---|---|---|
id |
NatinyWindow |
Window handle from natiny_window_create |
natiny_window_bind(win);
draw_frame();
natiny_window_unbind();
natiny_render_clear when
the existing pixels should be replaced.natiny_window_unbind starts a new frame for the previous
window.natiny_backend_loop callback preserves
its earlier draws. The accumulated image is presented after the callback.NATINY_SCALE_MODE_STRETCH, the off-screen canvas is copied to the window
once when the engine frame is presented.natiny.window.bind(win [, fn])
Binds a window as the render target until it is unbound or replaced. With a callback, the window is unbound after the callback returns.
| Name | Type | Default | Description |
|---|---|---|---|
win |
number |
Window handle from natiny.window.create |
|
fn |
function |
nil |
Callback whose draws target this window |
natiny.window.bind(win, function()
draw_frame()
end)
natiny.render.clear when
the existing pixels should be replaced.natiny.window.unbind starts a new frame for the previous
window.natiny.backend.loop callback preserves
its earlier draws. The accumulated image is presented after the callback.SCALE_MODE_STRETCH, the off-screen canvas is copied to the window once
when the engine frame is presented.fn, Lua unbinds the window even if the callback raises an error, then
propagates the error.NatinyWindow natiny_window_create(const char* title, uint32_t width, uint32_t height);
Creates a new application window.
| Name | Type | Description |
|---|---|---|
title |
const char* |
Window title |
width |
uint32_t |
Requested window width; its on-screen size follows OS display scaling |
height |
uint32_t |
Requested window height; its on-screen size follows OS display scaling |
| Type | Description |
|---|---|
NatinyWindow |
Window handle |
NatinyWindow win = natiny_window_create("My Game", 1280, 720);
natiny_window_get_scale to read the scaling factor.NATINY_SCALE_MODE_PIXELS, which draws pixel for pixel and
does not apply the display scaling factor.<canvas> element.natiny.window.create(title, width, height)
Creates a new application window.
| Name | Type | Description |
|---|---|---|
title |
string |
Window title |
width |
number |
Requested window width; its on-screen size follows OS display scaling |
height |
number |
Requested window height; its on-screen size follows OS display scaling |
| Type | Description |
|---|---|
number |
Window handle |
local win = natiny.window.create("My Game", 1280, 720)
natiny.window.get_scale to read the scaling factor.natiny.window.SCALE_MODE_PIXELS, which draws pixel for pixel
and does not apply the display scaling factor.<canvas> element.void natiny_window_destroy(NatinyWindow id);
Destroys a window and frees its resources.
| Name | Type | Description |
|---|---|---|
id |
NatinyWindow |
Window handle from natiny_window_create |
NatinyWindow win = natiny_window_create("Temp", 400, 300);
// ...
natiny_window_destroy(win);
natiny_window_get_alive reports false from this call onward, and the
handle never resolves again - a later window reusing its place gets a
handle of its own.natiny_backend_loop, the window is released at the
next frame boundary rather than in the middle of the frame that asked.natiny.window.destroy(win)
Destroys a window and frees its resources.
| Name | Type | Description |
|---|---|---|
win |
number |
Window handle from natiny.window.create |
local win = natiny.window.create("Temp", 400, 300)
-- ...
natiny.window.destroy(win)
natiny.window.get_alive reports false from this call onward, and the
handle never resolves again - a later window reusing its place gets a
handle of its own.natiny.backend.loop, the window is released at the
next frame boundary rather than in the middle of the frame that asked.bool natiny_window_get_alive(NatinyWindow id);
Checks whether a window is still open.
| Name | Type | Description |
|---|---|---|
id |
NatinyWindow |
Window handle from natiny_window_create |
| Type | Description |
|---|---|
bool |
true if the window is open, false otherwise |
bool open = natiny_window_get_alive(win);
natiny.window.get_alive(win) -> boolean
Checks whether a window is still open.
| Name | Type | Description |
|---|---|---|
win |
number |
Window handle from natiny.window.create |
| Type | Description |
|---|---|
boolean |
true if the window is open, false otherwise |
local open = natiny.window.get_alive(win)
int natiny_window_get_height(NatinyWindow id);
Returns the current height of a window in drawing units.
| Name | Type | Description |
|---|---|---|
id |
NatinyWindow |
Window handle from natiny_window_create |
| Type | Description |
|---|---|
int |
Window height, 0 if the window does not exist |
NatinyWindow win = natiny_window_create("My Game", 1280, 720);
int h = natiny_window_get_height(win);
printf("Height: %d\n", h);
NATINY_SCALE_MODE_PIXELS, the result is the number of window pixels. In
NATINY_SCALE_MODE_STRETCH and NATINY_SCALE_MODE_HIDPI, it is the height
used by 2D drawing and mouse input.natiny.window.get_height(win)
Returns the current height of a window in drawing units.
| Name | Type | Description |
|---|---|---|
win |
number |
Window handle from natiny.window.create |
| Type | Description |
|---|---|
number |
Window height, 0 if the window does not exist |
local win = natiny.window.create("My Game", 1280, 720)
local h = natiny.window.get_height(win)
print("Height: " .. h)
natiny.window.SCALE_MODE_PIXELS, the result is the number of window
pixels. In SCALE_MODE_STRETCH and SCALE_MODE_HIDPI, it is the height used
by 2D drawing and mouse input.uint32_t natiny_window_get_orientation(NatinyWindow id);
Returns the current window orientation derived from its dimensions.
| Name | Type | Description |
|---|---|---|
id |
NatinyWindow |
Window handle from natiny_window_create |
| Type | Description |
|---|---|
uint32_t |
NATINY_ORIENTATION_LANDSCAPE when width is greater than height, NATINY_ORIENTATION_PORTRAIT otherwise, or 0 for an invalid handle |
if (natiny_window_get_orientation(win) == NATINY_ORIENTATION_LANDSCAPE)
draw_toolbar_down_the_side();
else
draw_toolbar_across_the_bottom();
NATINY_ORIENTATION_PORTRAIT_FLIPPED and
NATINY_ORIENTATION_LANDSCAPE_FLIPPED are never returned because dimensions
do not identify which edge is at the top.NATINY_ORIENTATION_PORTRAIT.natiny_window_get_width and
natiny_window_get_height.natiny_window_set_orientation to set the allowed
Android orientations. This function does not return that mask.natiny.window.get_orientation(win) -> orientation
Returns the current window orientation derived from its dimensions.
| Name | Type | Description |
|---|---|---|
win |
number |
Window handle from natiny.window.create |
| Type | Description |
|---|---|
number |
natiny.window.ORIENTATION_LANDSCAPE when width is greater than height, natiny.window.ORIENTATION_PORTRAIT otherwise, or 0 for an invalid handle |
if natiny.window.get_orientation(win) == natiny.window.ORIENTATION_LANDSCAPE then
draw_toolbar_down_the_side()
else
draw_toolbar_across_the_bottom()
end
natiny.window.ORIENTATION_PORTRAIT.natiny.window.ORIENTATION_PORTRAIT_FLIPPED or
natiny.window.ORIENTATION_LANDSCAPE_FLIPPED because dimensions do not
identify which edge is at the top.natiny.window.get_width and
natiny.window.get_height.natiny.window.set_orientation to set the allowed
Android orientations. This function does not return that mask.uint32_t natiny_window_get_render_calls(NatinyWindow id);
Returns the number of draw calls issued in the previous frame for a specific window. Useful for performance debugging.
| Name | Type | Description |
|---|---|---|
id |
NatinyWindow |
Window handle from natiny_window_create |
| Type | Description |
|---|---|
uint32_t |
Draw call count, 0 if the window does not exist |
uint32_t n = natiny_window_get_render_calls(win);
printf("Draw calls: %u\n", n);
natiny.window.get_render_calls(win)
Returns the number of draw calls issued in the previous frame for a specific window. Useful for performance debugging.
| Name | Type | Description |
|---|---|---|
win |
number |
Window handle from natiny.window.create |
| Type | Description |
|---|---|
number |
Draw call count, 0 if the window does not exist |
local n = natiny.window.get_render_calls(win)
print("Draw calls: " .. n)
float natiny_window_get_scale(NatinyWindow id);
Returns the display scaling factor captured when the window was created.
| Name | Type | Description |
|---|---|---|
id |
NatinyWindow |
Window handle from natiny_window_create |
| Type | Description |
|---|---|
float |
Display scale: 1 for 100%, 1.5 for 150%, 2 for 200%, or 0 if the window does not exist |
Manual display scaling in the default pixel-for-pixel mode:
NatinyWindow win = natiny_window_create("My Game", 1280, 720);
float s = natiny_window_get_scale(win);
NatinyFont font = natiny_font_create(ttf, 16.0f * s);
natiny_render_text(font, 20.0f * s, 20.0f * s, "hello");
NATINY_SCALE_MODE_STRETCH and NATINY_SCALE_MODE_HIDPI apply this factor
automatically. NATINY_SCALE_MODE_PIXELS ignores it.natiny.window.get_scale(win)
Returns the display scaling factor captured when the window was created.
| Name | Type | Description |
|---|---|---|
win |
number |
Window handle from natiny.window.create |
| Type | Description |
|---|---|
number |
Display scale: 1 for 100%, 1.5 for 150%, 2 for 200%, or 0 if the window does not exist |
Manual display scaling in the default pixel-for-pixel mode:
local win = natiny.window.create("My Game", 1280, 720)
local s = natiny.window.get_scale(win)
local font = natiny.font.create(ttf, 16 * s)
natiny.render.text(font, 20 * s, 20 * s, "hello")
SCALE_MODE_STRETCH and SCALE_MODE_HIDPI apply this factor automatically.
SCALE_MODE_PIXELS ignores it.int natiny_window_get_state(NatinyWindow id);
Returns the current window state observed by the platform.
| Name | Type | Description |
|---|---|---|
id |
NatinyWindow |
Window handle from natiny_window_create |
| Type | Description |
|---|---|
int |
One of the NATINY_WINDOW_STATE_* constants; NATINY_WINDOW_STATE_NORMAL for an invalid handle |
if (natiny_window_get_state(win) != NATINY_WINDOW_STATE_MINIMIZED)
draw_the_world();
NATINY_WINDOW_STATE_NORMAL or
NATINY_WINDOW_STATE_FULLSCREEN.natiny_window_set_state: NATINY_WINDOW_STATE_NORMAL or
NATINY_WINDOW_STATE_FULLSCREEN.NATINY_WINDOW_STATE_MINIMIZED.natiny.window.get_state(win) -> state
Returns the current window state observed by the platform.
| Name | Type | Description |
|---|---|---|
win |
number |
Window handle from natiny.window.create |
| Type | Description |
|---|---|
number |
One of the natiny.window.STATE_* constants; natiny.window.STATE_NORMAL for an invalid handle |
if natiny.window.get_state(win) ~= natiny.window.STATE_MINIMIZED then
draw_the_world()
end
natiny.window.STATE_NORMAL or
natiny.window.STATE_FULLSCREEN.natiny.window.set_state: natiny.window.STATE_NORMAL or
natiny.window.STATE_FULLSCREEN.natiny.window.STATE_MINIMIZED.int natiny_window_get_width(NatinyWindow id);
Returns the current width of a window in drawing units.
| Name | Type | Description |
|---|---|---|
id |
NatinyWindow |
Window handle from natiny_window_create |
| Type | Description |
|---|---|
int |
Window width, 0 if the window does not exist |
NatinyWindow win = natiny_window_create("My Game", 1280, 720);
int w = natiny_window_get_width(win);
printf("Width: %d\n", w);
NATINY_SCALE_MODE_PIXELS, the result is the number of window pixels. In
NATINY_SCALE_MODE_STRETCH and NATINY_SCALE_MODE_HIDPI, it is the width
used by 2D drawing and mouse input.natiny.window.get_width(win)
Returns the current width of a window in drawing units.
| Name | Type | Description |
|---|---|---|
win |
number |
Window handle from natiny.window.create |
| Type | Description |
|---|---|
number |
Window width, 0 if the window does not exist |
local win = natiny.window.create("My Game", 1280, 720)
local w = natiny.window.get_width(win)
print("Width: " .. w)
natiny.window.SCALE_MODE_PIXELS, the result is the number of window
pixels. In SCALE_MODE_STRETCH and SCALE_MODE_HIDPI, it is the width used
by 2D drawing and mouse input.void natiny_window_set_orientation(NatinyWindow id, uint32_t allowed);
Sets the screen orientations that an Android device may use for the window.
| Name | Type | Description |
|---|---|---|
id |
NatinyWindow |
Window handle from natiny_window_create |
allowed |
uint32_t |
Nonzero mask of the orientation constants below, combined with bitwise OR (|) |
| Constant | Value | Meaning |
|---|---|---|
NATINY_ORIENTATION_PORTRAIT |
1 |
Portrait with the device in its natural orientation |
NATINY_ORIENTATION_PORTRAIT_FLIPPED |
2 |
Reverse portrait |
NATINY_ORIENTATION_LANDSCAPE |
4 |
Landscape with the device in its natural rotation |
NATINY_ORIENTATION_LANDSCAPE_FLIPPED |
8 |
Reverse landscape |
NATINY_ORIENTATION_ANY |
15 |
Any portrait or landscape orientation |
One constant locks the screen to one orientation. Combining constants allows
the device to rotate between them. After a rotation,
natiny_window_get_width and
natiny_window_get_height report the new dimensions.
NatinyWindow win = natiny_window_create("My Game", 1280, 720);
/* Allow both landscape orientations, but no portrait orientation. */
natiny_window_set_orientation(win, NATINY_ORIENTATION_LANDSCAPE |
NATINY_ORIENTATION_LANDSCAPE_FLIPPED);
NATINY_ORIENTATION_ANY is rejected with a message on stderr and changes
nothing.natiny_window_get_orientation returns the current
orientation, not the allowed mask.main starts.natiny.window.set_orientation(win, allowed)
Sets the screen orientations that an Android device may use for the window.
| Name | Type | Description |
|---|---|---|
win |
number |
Window handle from natiny.window.create |
allowed |
number |
Nonzero mask of the orientation constants below, combined with bitwise OR (|) |
| Constant | Meaning |
|---|---|
natiny.window.ORIENTATION_PORTRAIT |
Portrait with the device in its natural orientation |
natiny.window.ORIENTATION_PORTRAIT_FLIPPED |
Reverse portrait |
natiny.window.ORIENTATION_LANDSCAPE |
Landscape with the device in its natural rotation |
natiny.window.ORIENTATION_LANDSCAPE_FLIPPED |
Reverse landscape |
natiny.window.ORIENTATION_ANY |
Any portrait or landscape orientation |
One constant locks the screen to one orientation. Combining constants allows
the device to rotate between them. After a rotation,
natiny.window.get_width and
natiny.window.get_height report the new dimensions.
local win = natiny.window.create("My Game", 1280, 720)
-- Allow both landscape orientations, but no portrait orientation.
natiny.window.set_orientation(win,
natiny.window.ORIENTATION_LANDSCAPE |
natiny.window.ORIENTATION_LANDSCAPE_FLIPPED)
natiny.window.ORIENTATION_ANY is rejected with a diagnostic message and
leaves the current policy unchanged.natiny.window.get_orientation returns the current
orientation, not the allowed mask.void natiny_window_set_scale_mode(NatinyWindow id, int mode);
Selects how 2D drawing is enlarged on a display that uses scaling.
| Name | Type | Description |
|---|---|---|
id |
NatinyWindow |
Window handle from natiny_window_create |
mode |
int |
One of the constants below |
| Constant | Behavior | Result |
|---|---|---|
NATINY_SCALE_MODE_PIXELS |
Draws pixel for pixel | One drawing unit equals one window pixel; display scaling is ignored; default |
NATINY_SCALE_MODE_STRETCH |
Stretches the finished frame | Draws a smaller frame and enlarges it to fill the window; may look blurry |
NATINY_SCALE_MODE_HIDPI |
Draws at full detail | Enlarges coordinates while drawing to all window pixels; shapes and text stay sharp |
A drawing unit is the unit used for positions and sizes in 2D drawing commands.
If natiny_window_get_scale returns 2, coordinate 100 reaches
pixel 200 in NATINY_SCALE_MODE_STRETCH and NATINY_SCALE_MODE_HIDPI. In
NATINY_SCALE_MODE_PIXELS, it reaches pixel 100. Window dimensions and mouse
input always use the same units as 2D drawing.
For a practical explanation and help choosing a mode, see Window scaling.
NatinyWindow win = natiny_window_create("My App", 1280, 720);
natiny_window_set_scale_mode(win, NATINY_SCALE_MODE_HIDPI);
NatinyFont font = natiny_font_create(ttf, 16.0f);
/* inside the frame callback */
natiny_window_bind(win);
int w = natiny_window_get_width(win);
int h = natiny_window_get_height(win);
natiny_render_rectangle(20, 20, 300, 40);
natiny_render_text(font, 30, 30, "hello");
natiny_window_unbind();
natiny_window_bind; calls for a bound window
are rejected.natiny_render_scissor_begin and natiny_render_viewport_begin use the selected
coordinate system.NATINY_CONSTANT_TARGET_SIZE stays in pixels. A fragment shader runs per
physical pixel.natiny.window.set_scale_mode(win, mode)
Selects how 2D drawing is enlarged on a display that uses scaling.
| Name | Type | Description |
|---|---|---|
win |
number |
Window handle from natiny.window.create |
mode |
number |
One of the constants below |
| Constant | Behavior | Result |
|---|---|---|
natiny.window.SCALE_MODE_PIXELS |
Draws pixel for pixel | One drawing unit equals one window pixel; display scaling is ignored; default |
natiny.window.SCALE_MODE_STRETCH |
Stretches the finished frame | Draws a smaller frame and enlarges it to fill the window; may look blurry |
natiny.window.SCALE_MODE_HIDPI |
Draws at full detail | Enlarges coordinates while drawing to all window pixels; shapes and text stay sharp |
A drawing unit is the unit used for positions and sizes in 2D drawing commands.
If natiny.window.get_scale returns 2, coordinate 100 reaches
pixel 200 in SCALE_MODE_STRETCH and SCALE_MODE_HIDPI. In
SCALE_MODE_PIXELS, it reaches pixel 100. Window dimensions and mouse input
always use the same units as 2D drawing.
For a practical explanation and help choosing a mode, see Window scaling.
local win = natiny.window.create("My App", 1280, 720)
natiny.window.set_scale_mode(win, natiny.window.SCALE_MODE_HIDPI)
local font = natiny.font.create(ttf, 16)
natiny.backend.loop(function(dt)
natiny.window.bind(win)
local w = natiny.window.get_width(win)
local h = natiny.window.get_height(win)
natiny.render.rectangle(20, 20, 300, 40)
natiny.render.text(font, 30, 30, "hello")
natiny.window.unbind()
end)
natiny.window.bind; calls for a bound window
are rejected.natiny.render.scissor_begin and natiny.render.viewport_begin use the selected
coordinate system.void natiny_window_set_state(NatinyWindow id, int state);
Requests one of four exclusive window states: normal, minimized, maximized or fullscreen.
| Name | Type | Description |
|---|---|---|
id |
NatinyWindow |
Window handle from natiny_window_create |
state |
int |
One of the constants below |
| Constant | Value | Desktop | Web | Android |
|---|---|---|---|---|
NATINY_WINDOW_STATE_NORMAL |
0 |
Restores the window from minimized, maximized or fullscreen | Leaves fullscreen | Shows the status bar |
NATINY_WINDOW_STATE_MINIMIZED |
1 |
Minimizes the window | No effect | No effect |
NATINY_WINDOW_STATE_MAXIMIZED |
2 |
Maximizes the window | No effect | No effect |
NATINY_WINDOW_STATE_FULLSCREEN |
3 |
Borderless fullscreen on the monitor the window is mostly on | Fullscreen API | Hides the status bar |
The requested state replaces the current state. For example, requesting
NATINY_WINDOW_STATE_MAXIMIZED for a fullscreen window leaves fullscreen before
maximizing the window.
if (natiny_input_get_key_pressed(NATINY_INPUT_KEY_F11)) {
int state = natiny_window_get_state(win);
natiny_window_set_state(win,
state == NATINY_WINDOW_STATE_FULLSCREEN ? NATINY_WINDOW_STATE_NORMAL
: NATINY_WINDOW_STATE_FULLSCREEN);
}
natiny_window_get_state immediately afterward can still
return the previous state.NATINY_WINDOW_STATE_NORMAL and
NATINY_WINDOW_STATE_FULLSCREEN. Other states have no effect.natiny_window_get_state to check
whether it was granted.NATINY_WINDOW_STATE_NORMAL has somewhere to return to. A window made
fullscreen by something other than this call returns to half the monitor's
work area, centered.stderr
and leaves the current state unchanged.natiny_window_get_width and
natiny_window_get_height change with it, from the frame
after the switch.natiny.window.set_state(win, state)
Requests one of four exclusive window states: normal, minimized, maximized or fullscreen.
| Name | Type | Description |
|---|---|---|
win |
number |
Window handle from natiny.window.create |
state |
number |
One of the constants below |
| Constant | Desktop | Web | Android |
|---|---|---|---|
natiny.window.STATE_NORMAL |
Restores the window from minimized, maximized or fullscreen | Leaves fullscreen | Shows the status bar |
natiny.window.STATE_MINIMIZED |
Minimizes the window | No effect | No effect |
natiny.window.STATE_MAXIMIZED |
Maximizes the window | No effect | No effect |
natiny.window.STATE_FULLSCREEN |
Borderless fullscreen on the monitor the window is mostly on | Fullscreen API | Hides the status bar |
The requested state replaces the current state. For example, requesting
natiny.window.STATE_MAXIMIZED for a fullscreen window leaves fullscreen before
maximizing the window.
if natiny.input.get_key_pressed(natiny.input.KEY_F11) then
if natiny.window.get_state(win) == natiny.window.STATE_FULLSCREEN then
natiny.window.set_state(win, natiny.window.STATE_NORMAL)
else
natiny.window.set_state(win, natiny.window.STATE_FULLSCREEN)
end
end
natiny.window.get_state immediately afterward can still
return the previous state.natiny.window.STATE_NORMAL and
natiny.window.STATE_FULLSCREEN. Other states have no effect.natiny.window.get_state to check whether it was granted.STATE_NORMAL has somewhere to return to. A window made fullscreen by
something other than this call returns to half the monitor's work area,
centered.natiny.window.get_width and
natiny.window.get_height change with it, from the frame
after the switch.void natiny_window_unbind(void);
Ends the current window frame. If another window was suspended by a nested bind, starts a new frame for that window; otherwise leaves no window bound.
natiny_window_unbind();
natiny_backend_loop, submission and presentation occur after the
loop callback returns. Outside the loop, they occur during this call.NATINY_SCALE_MODE_STRETCH the frame is drawn into an offscreen canvas,
and the canvas is copied to the window once when the engine frame is
presented.natiny.window.unbind()
Ends the current window frame. If another window was suspended by a nested bind, starts a new frame for that window; otherwise leaves no window bound.
natiny.window.unbind()
natiny.backend.loop, submission and presentation occur after the
loop callback returns. Outside the loop, they occur during this call.SCALE_MODE_STRETCH the frame is drawn into an offscreen canvas, and the
canvas is copied to the window once when the engine frame is presented.Your first native Natiny app needs one header and one library. It opens a window, runs a game loop, and draws a moving square without an asset pipeline.
Create main.c:
#include "natiny.h"
#include <stddef.h>
static NatinyWindow window;
static float x = -80.0f;
static void frame(float dt, void* userdata)
{
(void)userdata;
x += 180.0f * dt;
if (x > (float)natiny_window_get_width(window)) {
x = -80.0f;
}
natiny_window_bind(window);
natiny_render_clear(0.07f, 0.09f, 0.13f, 1.0f);
natiny_render_set_color(0.66f, 0.76f, 0.22f, 1.0f);
natiny_render_rectangle(x, 260.0f, 80.0f, 80.0f);
natiny_window_unbind();
}
int main(void)
{
if (!natiny_backend_init(NATINY_BACKEND_AUTO)) {
return 1;
}
window = natiny_window_create("Hello, Natiny", 800, 600);
if (!window) {
natiny_backend_shutdown();
return 1;
}
natiny_window_set_scale_mode(window, NATINY_SCALE_MODE_HIDPI);
natiny_backend_loop(frame, NULL);
natiny_backend_shutdown();
return 0;
}
The program needs natiny.h and one static archive, both out of an SDK
bundle, and a C compiler:
my_game/
├── include/
│ └── natiny.h
├── lib/
│ └── libnatiny_<platform>.a
└── main.c
The commands differ by platform, so each has a page of its own: Windows, Linux, macOS, Android and Web. Every one of them ends with a dark window and a green square moving across it.
if (!natiny_backend_init(NATINY_BACKEND_AUTO)) {
return 1;
}
A graphics backend is the platform-specific renderer Natiny uses to talk
to the GPU. natiny_backend_init prepares the
engine and selects one. NATINY_BACKEND_AUTO chooses Direct3D 12 on Windows,
Vulkan on Linux, Metal on macOS, and WebGPU in the browser.
The function returns zero when initialization fails, so the program stops before trying to create graphics resources.
window = natiny_window_create("Hello, Natiny", 800, 600);
if (!window) {
natiny_backend_shutdown();
return 1;
}
natiny_window_set_scale_mode(window, NATINY_SCALE_MODE_HIDPI);
natiny_window_create opens a window whose
requested size is 800 by 600. Its NatinyWindow value is a handle that
identifies the window in later calls. A web build maps its one window to the
page's canvas.
A high-density display can provide more pixels than the requested window size.
natiny_window_set_scale_mode keeps the
800 by 600 drawing space while using those extra pixels for sharper shapes.
The window scaling guide explains all three
modes and when to choose each one.
static void frame(float dt, void* userdata)
{
(void)userdata;
x += 180.0f * dt;
if (x > (float)natiny_window_get_width(window)) {
x = -80.0f;
}
/* Draw the frame here. */
}
natiny_backend_loop calls frame once per frame.
dt is the elapsed time in seconds since the previous frame. The second
parameter is a user pointer; this small example does not need it.
The square moves at 180 drawing units per second. Multiplying movement by dt
keeps that speed independent of the display refresh rate. Once the square
leaves the window width, it starts again just outside the left edge.
natiny_window_bind(window);
natiny_render_clear(0.07f, 0.09f, 0.13f, 1.0f);
natiny_render_set_color(0.66f, 0.76f, 0.22f, 1.0f);
natiny_render_rectangle(x, 260.0f, 80.0f, 80.0f);
natiny_window_unbind();
A render target is the image that receives drawing commands.
natiny_window_bind makes the window the current
render target and starts its frame. natiny_window_unbind completes and
presents that frame.
Between those calls, the drawing functions use a small amount of current state:
natiny_render_clear fills the background;natiny_render_set_color selects the colour for following draw calls;natiny_render_rectangle draws at (x, 260) with a size of 80 by 80 drawing
units.Window coordinates begin at the top-left. X grows to the right and Y grows downward.
When the last window closes, the loop returns. natiny_backend_shutdown then
releases every engine resource that remains.
Try one small change at a time:
natiny_render_set_color;natiny_render_circle(x, 300.0f, 40.0f);180.0f to control the speed;You now have the essential game loop: update state, bind a target, draw a frame. Input, textures, models, physics, sound, and custom NSL shaders all build on that same loop.
Your first Natiny app needs no project generator and no assets. In a few lines it opens a window, runs a game loop, and draws a moving square.
You can paste this program into the browser sandbox and run it
immediately, or save it as data/main.lua in the Lua release bundle.
local natiny = require("natiny")
assert(natiny.backend.init())
local window = natiny.window.create("Hello, Natiny", 800, 600)
assert(window ~= 0, "failed to create the window")
natiny.window.set_scale_mode(window, natiny.window.SCALE_MODE_HIDPI)
local x = -80
natiny.backend.loop(function(dt)
x = x + 180 * dt
if x > natiny.window.get_width(window) then
x = -80
end
natiny.window.bind(window, function()
natiny.render.clear(0.07, 0.09, 0.13)
natiny.render.set_color(0.66, 0.76, 0.22, 1.0)
natiny.render.rectangle(x, 260, 80, 80)
end)
end)
Run it and a green square will travel across a dark window. That square is a useful first “hello”: it exercises the engine, the window, the frame loop, and 2D drawing without hiding any of them behind setup code.
local natiny = require("natiny")
assert(natiny.backend.init())
require("natiny") returns the engine module. Natiny does not add hidden
globals to Lua.
A graphics backend is the platform-specific renderer Natiny uses to talk
to the GPU. backend.init chooses the preferred
one: Direct3D 12 on Windows, Vulkan on Linux, Metal on macOS, and WebGPU in the
browser. assert stops with an error if initialization fails.
local window = natiny.window.create("Hello, Natiny", 800, 600)
assert(window ~= 0, "failed to create the window")
natiny.window.set_scale_mode(window, natiny.window.SCALE_MODE_HIDPI)
window.create opens a window whose requested
size is 800 by 600. In a web build, the same call uses the page's canvas. The
returned number is a handle that identifies this window in later calls.
A high-density display can provide more pixels than the requested window size.
window.set_scale_mode keeps the 800 by
600 drawing space while using those extra pixels for sharper shapes. The
window scaling guide explains all three modes
and when to choose each one.
local x = -80
natiny.backend.loop(function(dt)
x = x + 180 * dt
if x > natiny.window.get_width(window) then
x = -80
end
-- draw the frame here
end)
backend.loop calls the function once per frame.
dt is the elapsed time in seconds since the previous frame.
The square moves at 180 drawing units per second, not 180 units per frame.
Multiplying movement by dt keeps its speed stable on a 60 Hz display, a
144 Hz display, or a slower machine. Once the square leaves the width returned
by window.get_width, it starts again just outside the left edge.
natiny.window.bind(window, function()
natiny.render.clear(0.07, 0.09, 0.13)
natiny.render.set_color(0.66, 0.76, 0.22, 1.0)
natiny.render.rectangle(x, 260, 80, 80)
end)
A render target is the image that receives drawing commands.
window.bind makes this window the current render
target. Every draw call inside the callback goes into it, and the completed
frame is presented when the callback returns.
The drawing calls use a small amount of current state:
clear fills the background;set_color selects the colour for following draw calls;rectangle draws at (x, 260) with a size of 80 by 80 drawing units.Window coordinates begin at the top-left. X grows to the right and Y grows downward.
Download a Lua bundle and unzip it. The executable looks for
data/main.lua beside itself, so a complete project can be this small:
my_game/
├── data/
│ └── main.lua
└── natiny.exe
Each platform has a page of its own with the commands for it: Windows, Linux, macOS, Android and Web.
You can also paste this program into the browser sandbox and run it without downloading anything.
Try one small change at a time:
render.set_color;rectangle with circle(x, 300, 40);180 to control the speed;y value and move in two dimensions;You now have the essential game loop: update state, bind a target, draw a frame. Input, textures, models, physics, sound, and custom NSL shaders all build on that same loop.
A typical frame draws to several places: a shadow map, a reflection, a G-buffer, a bloom chain, and finally the window. Some targets are nested. Others are bound one after another.
That is where the questions start. If I bind a surface in the middle of a scene, what happens to the scene? When I unbind, do I get everything back, or only some of it? Does unbinding a camera go back to the previous camera? Why did my shadow pass come out flat, or in the wrong aspect ratio, or with only half the pixels updated?
This guide gives you one model for answering these questions. With that model, you can design a render pipeline without having to discover the rules through debugging. The guide assumes that you have already drawn into a surface. If not, start with render to a surface.
The complete mental model is simple:
Targets nest. Nothing else does.
A target is a surface or a window: the image that receives drawing commands. Binding a surface to slot 0 suspends the current target set and starts a new one. Unbinding it removes that set and restores the suspended state.
window <- bottom of the stack
scene surface <- bind
minimap surface <- bind
(unbind) -> back to the scene surface
(unbind) -> back to the window
Cameras, materials, and colours do not nest. They are values that affect the next draw, much like the current colour of a pen. Binding a new value replaces the previous one. This may seem to lose the scene camera when a shadow pass binds another camera. It does not, because the target stack saves the scene camera. The following sections explain how this works.
One fact about the hardware explains the rest of the rules.
Render passes do not nest on a GPU. A GPU cannot pause a pass, run another pass inside it, and then resume the first one. If you bind a minimap halfway through a scene, Natiny divides the work into three passes:
pass 1 scene, first half
pass 2 minimap
pass 3 scene, second half (loads pass 1's pixels back)
This has two important consequences.
First, a bind flushes the batch. Before Natiny binds the new target, it draws everything already queued into the current target. This work includes a draw call and a pass boundary, so bind a target around a group of draws, not around a single draw.
Second, the third pass usually needs the drawing state from the first pass. Natiny therefore restores the state that belongs to the suspended target. An explicit viewport is the exception. Changing targets ends it because its rectangle belongs to the old target.
The two directions of a bind are not symmetric. The table shows which state enters a surface and which state returns when you leave it.
| State | Carried into a surface bind | Restored at the unbind |
|---|---|---|
Draw colour (natiny_render_set_color) |
yes | yes |
| Clear colour | yes | yes |
| Bound material | yes | yes |
| Bound camera | no; starts empty | yes |
| Scissor rects | no; start empty | yes |
| Active viewport | no; the target change ends it | no |
| Coordinate scale | no; 1 unit = 1 pixel | yes |
| Current texture | no | yes |
Except for the viewport, Natiny restores the suspended state. State bound inside a surface does not affect its parent target after the surface is unbound.
The left column has one important division. Camera, scissor, viewport, and coordinate scale do not carry into the new target because each depends on the target's pixels. A camera projection uses the target's aspect ratio. A scissor rectangle belongs to a particular image. A coordinate scale for a 1920 by 1080 window has no useful meaning inside a 512 by 512 shadow map.
Carrying these values into the new target would make them incorrect. Natiny resets them when the bind begins. On unbind, it restores the camera, scissor, and coordinate scale. The explicit viewport remains ended.
The values marked "yes" do not depend on geometry: draw colour, clear colour, and material. Orange remains orange in every target. These values enter the surface, and Natiny restores them when the surface is unbound.
Note
Natiny restores a camera as matrices, not as a handle. If the camera node moves between the bind and the unbind, the restored view is still the one used by the suspended pass. It does not change to the camera node's new position.
Everything above is about binding state: what is currently bound, what colour is current, which rectangle is current. That is the state the stack saves and restores.
The stack does not change the second kind: object state. Object state describes the object itself, rather than which object is currently selected.
natiny_surface_bind(shadow_map, 0);
natiny_material_bind(shadow_mat); /* binding state: restored */
natiny_material_set_constant(shadow_mat, "bias",
0.002f, 0.0f, 0.0f, 0.0f); /* object state: permanent */
natiny_surface_unbind();
After the unbind, shadow_mat is no longer bound because Natiny restored the
previous binding. However, its bias constant remains 0.002 in every later
pass that uses this material. The call changed the material itself.
The same applies to natiny_camera_set_fov, natiny_camera_set_clip, node
transforms, and the pixels inside a surface. None of these changes is a
binding, so the stack does not restore them. If you need two configurations
in one frame, create two materials instead of reconfiguring one material
between passes.
A useful rule: bind is restored, set_* on a handle is not.
This rule is important: an unbind does not reveal an earlier camera or material binding.
natiny_camera_bind(scene_cam);
natiny_camera_bind(light_cam);
natiny_camera_unbind(); /* does NOT bring back scene_cam */
natiny_camera_unbind() means no camera. Drawing falls back to the target's
2D projection, and it is the same call as natiny_camera_bind(0).
It does not mean "one level back", because there are no levels. The same is
true of natiny_material_unbind(), which means no material override.
You do not need to bind the scene camera again after a shadow pass. The target stack restores it:
natiny_camera_bind(scene_cam);
natiny_surface_bind(shadow_map, 0);
natiny_render_clear(1.0f, 1.0f, 1.0f, 1.0f);
natiny_camera_bind(light_cam);
natiny_render_tag("shadow", NATINY_SORT_MATERIAL);
natiny_surface_unbind(); /* scene_cam is back, matrices and all */
natiny_render_tag("scene", NATINY_SORT_MATERIAL); /* drawn with scene_cam */
The shadow pass does not unbind its camera. When the surface is unbound, Natiny discards that camera binding and restores the binding for the parent target.
If you need a temporary camera without changing targets, bind the camera you want next explicitly:
natiny_camera_bind(portal_cam);
draw_world();
natiny_camera_bind(scene_cam); /* not unbind: name what you want */
The explicit bind makes the next camera clear in the code. There is no hidden camera stack to follow.
A window is also a target, but it cannot be bound in the same way as a surface.
A frame belongs to a window. Its coordinate system, acquired swapchain image, and command encoder all come from the window bound at the top. Binding a window therefore drops any surfaces still bound and reports their number:
[natiny] window_unbind: 2 surface bind(s) were still open and have been dropped
Balanced code reaches the end of a frame with no surfaces still bound. This message reports a missing unbind while its source is still easy to find.
Use this structure:
natiny_window_bind
natiny_surface_bind ... natiny_surface_unbind
natiny_surface_bind ... natiny_surface_unbind
natiny_window_unbind
Do not place a natiny_window_bind inside a surface bind. Two windows mean two
frames, regardless of how the calls are nested. An engine frame holds one
acquired image per window. Returning to a window after another window was
bound starts a new image instead of continuing the old one.
Note
Under
NATINY_SCALE_MODE_STRETCHthe window you bind is really an off-screen canvas at the window's logical size, copied onto the window once per frame, just before the commands are submitted. Binding and unbinding the window several times in one frame therefore costs one copy, not one copy per bind. This requires no action in application code. It only explains the extra target you may see when inspecting the stack.
A surface keeps its pixels between binds and between frames. A frame boundary does not clear it.
For colour, this is often harmless because the next pass draws over it. For depth, it can cause a confusing bug:
natiny_surface_bind(reflection, 0);
/* no clear */
natiny_camera_bind(reflect_cam);
natiny_render_tag("world", NATINY_SORT_MATERIAL);
natiny_surface_unbind();
The depth buffer still contains values from the previous frame. New fragments are tested against those values, so only fragments that are nearer can pass. The image then updates in patches and may look like a corrupted texture.
Therefore, clear any pass that redraws its complete image. This includes a scene from a moving camera, a reflection, a G-buffer, and a shadow map.
natiny_render_clear(0.0f, 0.0f, 0.0f, 1.0f);
A pass that replaces every pixel with an opaque full-screen draw, such as
natiny_render_cover with blending disabled, does not need a clear. Skipping
the clear in this case avoids unnecessary work.
The clear colour enters a surface bind, as shown in the table. If an earlier
pass set it to transparent black, the new pass inherits that value. Pass a
colour explicitly to natiny_render_clear to make the result clear from the
code.
The following example renders 32 shadow-casting lights with six depth maps each, followed by the scene and post-processing.
natiny_window_bind(win);
/* 192 shadow maps, one after another */
for (uint32_t light = 0; light < 32; light++) {
for (uint32_t face = 0; face < 6; face++) {
natiny_surface_bind(shadow[light][face], 0);
natiny_render_clear(1.0f, 1.0f, 1.0f, 1.0f);
natiny_camera_bind(light_cam[light][face]);
natiny_render_tag("shadow", NATINY_SORT_MATERIAL);
natiny_surface_unbind();
}
}
/* draw the scene into HDR; a new target starts with no camera */
natiny_surface_bind(hdr, 0);
natiny_render_clear(0.0f, 0.0f, 0.0f, 1.0f);
natiny_camera_bind(scene_cam);
natiny_render_tag("world", NATINY_SORT_MATERIAL);
natiny_surface_unbind();
/* tone-map onto the window */
natiny_material_bind(tonemap_mat);
natiny_render_cover();
natiny_material_unbind();
natiny_window_unbind();
The example shows two useful properties of the stack.
The stack never becomes deeper than one surface. The frame performs 192 binds, but each surface is unbound before the next one is bound. Nesting depth is different from the total number of binds. Natiny allows 64 nested levels. For comparison, a shadow atlas inside a reflection inside a frame uses three levels.
Saving target state is inexpensive. Natiny uses a fixed-size memory copy, with no allocation or GPU command. The significant cost comes from ending one render pass and starting another. When designing the pipeline, focus on the number of passes rather than the number of bind calls.
Everything ends up on the window, or the picture is a frame behind.
A surface was not unbound. Check stderr for the window_unbind: N surface
bind(s) were still open message. It tells you how many binds remained open.
Models inside a surface come out flat, or in the wrong place. A target bind starts with no camera. Bind a camera inside the surface.
The picture inside a surface has the wrong aspect ratio. The camera was bound before the target. Natiny builds the projection for the target that is current when the camera is bound. Bind the target first, then the camera.
Only some pixels update, and parts of the image are missing. The surface was not cleared, so the depth test uses values from an earlier frame.
A material behaves differently in a later pass than you configured it.
Another pass changed the material's object state with
natiny_material_set_constant or natiny_material_set_texture. The stack
does not restore these changes. Use separate materials for separate
configurations.
A camera you unbound did not go back to the previous one.
natiny_camera_unbind() always means no camera. Bind the camera you want, or
let unbinding a target restore it.
The complete model can be reduced to four rules:
natiny_render_clear in every pass that
redraws its complete image. Remember that old depth values can affect a new
frame.The model has one central idea: targets nest and restore their suspended drawing state. Cameras and materials are values stored with that state. The stack restores what is bound, but it does not undo changes to the objects themselves. Each bind creates a boundary between render passes, so pipeline cost depends mainly on the number of passes rather than the number of binds.
This model is enough to build a complete pipeline with a shadow atlas, a G-buffer, a bloom chain, or a reflection rendered during the scene. When one pass needs to write to several images at once, continue with multiple render targets, which fits into this model as one bind that happens to fill several slots.
A typical frame draws to several places: a shadow map, a reflection, a G-buffer, a bloom chain, and finally the window. Some targets are nested. Others are bound one after another.
That is where the questions start. If I bind a surface in the middle of a scene, what happens to the scene? When I unbind, do I get everything back, or only some of it? Does unbinding a camera go back to the previous camera? Why did my shadow pass come out flat, or in the wrong aspect ratio, or with only half the pixels updated?
This guide gives you one model for answering these questions. With that model, you can design a render pipeline without having to discover the rules through debugging. The guide assumes that you have already drawn into a surface. If not, start with render to a surface.
The complete mental model is simple:
Targets nest. Nothing else does.
A target is a surface or a window: the image that receives drawing commands. Binding a surface to slot 0 suspends the current target set and starts a new one. Unbinding it removes that set and restores the suspended state.
window <- bottom of the stack
scene surface <- bind
minimap surface <- bind
(unbind) -> back to the scene surface
(unbind) -> back to the window
Cameras, materials, and colours do not nest. They are values that affect the next draw, much like the current colour of a pen. Binding a new value replaces the previous one. This may seem to lose the scene camera when a shadow pass binds another camera. It does not, because the target stack saves the scene camera. The following sections explain how this works.
One fact about the hardware explains the rest of the rules.
Render passes do not nest on a GPU. A GPU cannot pause a pass, run another pass inside it, and then resume the first one. If you bind a minimap halfway through a scene, Natiny divides the work into three passes:
pass 1 scene, first half
pass 2 minimap
pass 3 scene, second half (loads pass 1's pixels back)
This has two important consequences.
First, a bind flushes the batch. Before Natiny binds the new target, it draws everything already queued into the current target. This work includes a draw call and a pass boundary, so bind a target around a group of draws, not around a single draw.
Second, the third pass usually needs the drawing state from the first pass. Natiny therefore restores the state that belongs to the suspended target. An explicit viewport is the exception. Changing targets ends it because its rectangle belongs to the old target.
The two directions of a bind are not symmetric. The table shows which state enters a surface and which state returns when you leave it.
| State | Carried into a surface bind | Restored at the unbind |
|---|---|---|
Draw colour (render.set_color) |
yes | yes |
| Clear colour | yes | yes |
| Bound material | yes | yes |
| Bound camera | no; starts empty | yes |
| Scissor rects | no; start empty | yes |
| Active viewport | no; the target change ends it | no |
| Coordinate scale | no; 1 unit = 1 pixel | yes |
| Current texture | no | yes |
Except for the viewport, Natiny restores the suspended state. State bound inside a surface does not affect its parent target after the surface is unbound.
The left column has one important division. Camera, scissor, viewport, and coordinate scale do not carry into the new target because each depends on the target's pixels. A camera projection uses the target's aspect ratio. A scissor rectangle belongs to a particular image. A coordinate scale for a 1920 by 1080 window has no useful meaning inside a 512 by 512 shadow map.
Carrying these values into the new target would make them incorrect. Natiny resets them when the bind begins. On unbind, it restores the camera, scissor, and coordinate scale. The explicit viewport remains ended.
The values marked "yes" do not depend on geometry: draw colour, clear colour, and material. Orange remains orange in every target. These values enter the surface, and Natiny restores them when the surface is unbound.
Note
Natiny restores a camera as matrices, not as a handle. If the camera node moves between the bind and the unbind, the restored view is still the one used by the suspended pass. It does not change to the camera node's new position.
Everything above is about binding state: what is currently bound, what colour is current, which rectangle is current. That is the state the stack saves and restores.
The stack does not change the second kind: object state. Object state describes the object itself, rather than which object is currently selected.
natiny.surface.bind(shadow_map, function()
natiny.material.bind(shadow_mat) -- binding state: restored
natiny.material.set_constant(shadow_mat, "bias", 0.002) -- object state: permanent
end)
After the unbind, shadow_mat is no longer bound because Natiny restored the
previous binding. However, its bias constant remains 0.002 in every later
pass that uses this material. The call changed the material itself.
The same applies to camera.set_fov, camera.set_clip, node transforms, and
the pixels inside a surface. None of these changes is a binding, so the stack
does not restore them. If you need two configurations in one frame, create two
materials instead of reconfiguring one material between passes.
A useful rule: bind is restored, set_* on a handle is not.
This rule is important: an unbind does not reveal an earlier camera or material binding.
natiny.camera.bind(scene_cam)
natiny.camera.bind(light_cam)
natiny.camera.unbind() -- does NOT bring back scene_cam
camera.unbind() means no camera. Drawing falls back to the target's 2D
projection. It does not mean "one level back", because there are no levels.
The same is true of material.unbind(), which means no material override.
You do not need to bind the scene camera again after a shadow pass. The target stack restores it:
natiny.camera.bind(scene_cam)
natiny.surface.bind(shadow_map, function()
natiny.render.clear(1, 1, 1, 1)
natiny.camera.bind(light_cam)
natiny.render.tag("shadow")
end) -- scene_cam is back, matrices and all
natiny.render.tag("scene") -- drawn with scene_cam
The shadow pass does not unbind its camera. When the surface is unbound, Natiny discards that camera binding and restores the binding for the parent target.
If you need a temporary camera without changing targets, use the callback form and then bind the camera you want next:
natiny.camera.bind(portal_cam, function()
draw_world()
end)
natiny.camera.bind(scene_cam) -- say so explicitly
The explicit bind makes the next camera clear in the code. There is no hidden camera stack to follow.
A window is also a target, but it cannot be bound in the same way as a surface.
A frame belongs to a window. Its coordinate system, acquired swapchain image, and command encoder all come from the window bound at the top. Binding a window therefore drops any surfaces still bound and reports their number:
[natiny] window_unbind: 2 surface bind(s) were still open and have been dropped
Balanced code reaches the end of a frame with no surfaces still bound. This
message reports a missing unbind while its source is still easy to find.
Use this structure:
window.bind
surface.bind ... surface.unbind
surface.bind ... surface.unbind
window.unbind
Do not place a window.bind inside a surface.bind. Two windows mean two
frames, regardless of how the calls are nested. An engine frame holds one
acquired image per window. Returning to a window after another window was
bound starts a new image instead of continuing the old one.
Note
Under
SCALE_MODE_STRETCHthe window you bind is really an off-screen canvas at the window's logical size, copied onto the window once per frame, just before the commands are submitted. Binding and unbinding the window several times in one frame therefore costs one copy, not one copy per bind. This requires no action in application code. It only explains the extra target you may see when inspecting the stack.
A surface keeps its pixels between binds and between frames. A frame boundary does not clear it.
For colour, this is often harmless because the next pass draws over it. For depth, it can cause a confusing bug:
natiny.surface.bind(reflection, function()
-- no clear
natiny.camera.bind(reflect_cam)
natiny.render.tag("world")
end)
The depth buffer still contains values from the previous frame. New fragments are tested against those values, so only fragments that are nearer can pass. The image then updates in patches and may look like a corrupted texture.
Therefore, clear any pass that redraws its complete image. This includes a scene from a moving camera, a reflection, a G-buffer, and a shadow map.
natiny.render.clear(0, 0, 0, 1)
A pass that replaces every pixel with an opaque full-screen draw, such as
render.cover with blending disabled, does not need a clear. Skipping the
clear in this case avoids unnecessary work.
The clear colour enters a surface bind, as shown in the table. If an earlier
pass set it to transparent black, the new pass inherits that value. Pass a
colour explicitly to render.clear to make the result clear from the code.
The following example renders 32 shadow-casting lights with six depth maps each, followed by the scene and post-processing.
natiny.window.bind(win, function()
-- 192 shadow maps, one after another
for light = 1, 32 do
for face = 1, 6 do
natiny.surface.bind(shadow[light][face], function()
natiny.render.clear(1, 1, 1, 1)
natiny.camera.bind(light_cam[light][face])
natiny.render.tag("shadow")
end)
end
end
-- draw the scene into HDR; a new target starts with no camera
natiny.surface.bind(hdr, function()
natiny.render.clear(0, 0, 0, 1)
natiny.camera.bind(scene_cam)
natiny.render.tag("world")
end)
-- tone-map onto the window
natiny.material.bind(tonemap_mat, natiny.render.cover)
end)
The example shows two useful properties of the stack.
The stack never becomes deeper than one surface. The frame performs 192 binds, but each surface is unbound before the next one is bound. Nesting depth is different from the total number of binds. Natiny allows 64 nested levels. For comparison, a shadow atlas inside a reflection inside a frame uses three levels.
Saving target state is inexpensive. Natiny uses a fixed-size memory copy, with no allocation or GPU command. The significant cost comes from ending one render pass and starting another. When designing the pipeline, focus on the number of passes rather than the number of bind calls.
Everything ends up on the window, or the picture is a frame behind.
A surface was not unbound. Check stderr for the window_unbind: N surface
bind(s) were still open message. It tells you how many binds remained open.
The callback form of surface.bind always unbinds the surface, even when the
callback raises an error.
Models inside a surface come out flat, or in the wrong place. A target bind starts with no camera. Bind a camera inside the surface.
The picture inside a surface has the wrong aspect ratio. The camera was bound before the target. Natiny builds the projection for the target that is current when the camera is bound. Bind the target first, then the camera.
Only some pixels update, and parts of the image are missing. The surface was not cleared, so the depth test uses values from an earlier frame.
A material behaves differently in a later pass than you configured it.
Another pass changed the material's object state with set_constant or
set_texture. The stack does not restore these changes. Use separate
materials for separate configurations.
A camera you unbound did not go back to the previous one.
camera.unbind() always means no camera. Bind the camera you want, or let
unbinding a target restore it.
The complete model can be reduced to four rules:
bind and one unbind,
or use the callback form. Natiny restores the target state, except for an
active viewport, which ends when the target changes.render.clear in every pass that redraws
its complete image. Remember that old depth values can affect a new frame.The model has one central idea: targets nest and restore their suspended drawing state. Cameras and materials are values stored with that state. The stack restores what is bound, but it does not undo changes to the objects themselves. Each bind creates a boundary between render passes, so pipeline cost depends mainly on the number of passes rather than the number of binds.
This model is enough to build a complete pipeline with a shadow atlas, a G-buffer, a bloom chain, or a reflection rendered during the scene. When one pass needs to write to several images at once, continue with multiple render targets, which fits into this model as one bind that happens to fill several slots.
By the end of this guide your own C program runs on an Android phone as an installable app.
Android needs one thing explained before anything else: it never starts a native executable. That single fact shapes the whole build, so the first section is about it.
An APK names a shared library in its manifest. Android loads that library and
calls ANativeActivity_onCreate inside it - that symbol is the entry point,
not main. Natiny provides it, and it starts the thread that calls your
main.
So your program and the engine have to end up in one .so file:
main.c + libnatiny_android_arm64.a → libhello.so → hello.apk
You write ordinary C with a normal main. Nothing about your source is
Android-specific.
You need the NDK for the compiler, the SDK build-tools and one platform for packaging, and a JDK 17 or newer for the signer:
sdkmanager "ndk;28.2.13676358" "build-tools;35.0.0" "platforms;android-35"
sdkmanager --licenses
export ANDROID_HOME="$HOME/Android/Sdk"
Another NDK works as well; only the version in the paths below changes.
Connect a phone with USB debugging on, or start an emulator, and check it is there:
adb devices
Download the SDK bundle for android-arm64:
my_game/
├── include/
│ └── natiny.h
├── lib/
│ └── libnatiny_android_arm64.a
├── android/
│ ├── pack.py
│ ├── AndroidManifest.xml.in
│ └── res/
└── main.c
There is no shared library of Natiny's in the bundle, and that is deliberate:
the .so Android loads is the one you are about to create from the engine and
your program together.
The window always covers the screen, so the size you ask for is ignored - use
natiny_window_get_width and natiny_window_get_height for the real one. A
green square on a dark background is enough to prove the whole chain worked.
#include "natiny.h"
#include <stddef.h>
static NatinyWindow window;
static void frame(float dt, void* userdata)
{
(void)dt;
(void)userdata;
natiny_window_bind(window);
natiny_render_clear(0.07f, 0.09f, 0.13f, 1.0f);
natiny_render_set_color(0.66f, 0.76f, 0.22f, 1.0f);
natiny_render_rectangle(360.0f, 260.0f, 80.0f, 80.0f);
natiny_window_unbind();
}
int main(void)
{
if (!natiny_backend_init(NATINY_BACKEND_AUTO)) {
return 1;
}
window = natiny_window_create("Hello, Natiny", 800, 600);
if (!window) {
natiny_backend_shutdown();
return 1;
}
natiny_backend_loop(frame, NULL);
natiny_backend_shutdown();
return 0;
}
NATINY_BACKEND_AUTO selects Vulkan, the only backend Android has.
The NDK's compiler is named after its target: the ABI and the minimum API level are both in the file name, so no flags are needed for either.
NDK_BIN="$ANDROID_HOME/ndk/28.2.13676358/toolchains/llvm/prebuilt/linux-x86_64/bin"
Use darwin-x86_64 on a Mac and windows-x86_64 on Windows.
"$NDK_BIN/aarch64-linux-android24-clang" -O2 -fPIC \
-Iinclude -c main.c -o main.o
"$NDK_BIN/aarch64-linux-android24-clang" -shared main.o \
lib/libnatiny_android_arm64.a -o libhello.so \
-u ANativeActivity_onCreate -landroid -llog -ldl -lm
Two of those options are not obvious:
-shared makes the .so that Android loads, rather than an executable that
nothing would start.-u ANativeActivity_onCreate keeps the entry point. Nothing on the link line
refers to it - Android finds it by name at run time - so without -u the
linker drops it, and the app installs and then fails at startup.Check that it survived, with the NDK's own nm - a host nm cannot always
read an aarch64 library:
"$NDK_BIN/llvm-nm" -D libhello.so | grep ANativeActivity_onCreate
A line with T ANativeActivity_onCreate means the symbol is exported. No
output means -u is missing.
pack.py builds the manifest, adds the resources, signs and aligns. Create a
folder for your data even if the program reads nothing yet:
mkdir -p assets
python android/pack.py --project ./assets --lib libhello.so \
--abi arm64-v8a --package com.example.hello --name "Hello" \
--output hello.apk
adb install -r hello.apk
--abi has to match the compiler you used: arm64-v8a goes with
aarch64-linux-android24-clang. For an x86_64 emulator, take the
android-amd64 SDK, compile with x86_64-linux-android24-clang, and pass
--abi x86_64.
Without --keystore the packer makes a debug key at ~/.natiny/debug.keystore
on first use and reuses it, which is what keeps adb install -r working
between builds. Use a real key for anything you publish, and keep it: Google
Play ties an application id to the key that first signed it.
Open Hello from the launcher. A dark screen with a green square means Android loaded your library, called Natiny's entry point and reached your frame callback.
When it closes instead, the log says why:
adb logcat -s natiny
Whatever you pass to --project becomes assets/data/ inside the APK, and
that is what the resource API reads:
assets/
└── player.png
NatinyResource player = natiny_resource_load("data/player.png");
That is the same string a desktop build uses. APK assets are read-only and
have no filesystem path, so they can only be read through the resource API. A
relative path given to natiny_resource_save goes to the app's private
directory, and natiny_resource_load looks there too.
-u
ANativeActivity_onCreate is missing from the link line.INSTALL_FAILED_NO_MATCHING_ABIS - --abi does not match the device, or
does not match the compiler you used.cannot find the Android SDK - set ANDROID_HOME, or pass --sdk.INSTALL_FAILED_UPDATE_INCOMPATIBLE - the same application id is
installed under a different key. Uninstall it or reuse the key.libhello.so holds your program, the engine and the entry point Android looks
for, and hello.apk is that library with your data, a manifest and a
signature around it. The C source and the resource paths are the ones a
desktop build uses.
Two things about the platform are worth knowing next. A touch arrives as the left mouse button, plus a callback that reports every finger. And when Android sends your app to the background it destroys the window you were drawing to: Natiny reports that as window iconification, and drawing after it is a crash. Stop inside that callback.
By the end of this guide your Lua project runs on an Android phone as an app you can install from the launcher.
Android is the easier of the two mobile platforms. Nothing has to be approved by anyone: the packer makes a signing key for you on first use, and a device in developer mode installs what you hand it. You need the Android SDK command-line tools and a phone or an emulator.
We cross-compile the engine and ship it inside a ready APK. Your project goes in beside it, and the result is signed and installed:
we build → natiny.apk the engine and Lua, already inside an APK
you pack → my_game.apk the same engine, running your scripts
The step in the middle is android/pack.py, which ships in the bundle. It
never compiles anything, so repacking after an edit takes a second.
You need the Android SDK build-tools, one platform, and a JDK 17 or newer for the signer. If you have Android Studio, you have all three. Otherwise install the command-line tools and run:
sdkmanager "build-tools;35.0.0" "platforms;android-35"
sdkmanager --licenses
export ANDROID_HOME="$HOME/Android/Sdk"
Set ANDROID_HOME to the real SDK directory - the path above is the usual one
on Linux, and macOS puts it in ~/Library/Android/sdk. The packer also takes
--sdk DIR if you would rather not export anything.
Then connect a phone with USB debugging turned on, or start an emulator, and check that it is visible:
adb devices
One device listed as device means you are ready. unauthorized means the
phone is waiting for you to accept the debugging prompt on its screen.
Download the Lua wrapper bundle - android-arm64 for a physical phone,
android-amd64 for an x86_64 emulator:
natiny-android_arm64-apk/
├── natiny.apk the engine and Lua, ready to install
├── android/
│ ├── pack.py puts your project into an APK
│ ├── AndroidManifest.xml.in
│ └── res/ the launcher icon
└── data/
└── main.lua the script inside natiny.apk, to read and to edit
Install it before touching anything else:
adb install -r natiny.apk
Open natiny from the launcher. The Natiny logo appears on a dark
background. That is data/main.lua running, and it tells you the device, the
architecture and the driver are all fine before any of your own code is
involved.
If the install itself fails, the phone usually needs Install unknown apps allowed for whatever is handing the APK over - this one is signed with a debug key, not by Google Play.
Make a project folder with main.lua at its root:
my_game/
└── main.lua
python android/pack.py --project ./my_game --apk natiny.apk \
--package com.example.my_game --name "My Game"
adb install -r my_game.apk
--apk natiny.apk is where the engine comes from: the packer reuses the
downloaded APK, so nothing is compiled. --package is the application id, and
two apps cannot share one - give your own before you hand the APK to anybody.
Open My Game. It runs your main.lua. When it closes during startup
instead, the reason is in the log:
adb logcat -s natiny
Why not just edit natiny.apk? Signature schemes v2 and v3 sign the archive itself, not its entries. Replacing a file inside a signed APK invalidates the signature and the device refuses to install it. Repacking is the only way, and that is what
pack.pydoes.
The whole --project folder becomes data/ inside the app:
my_game/
├── main.lua
├── player.lua
└── fonts/
└── roboto.ttf
local font_data = natiny.resource.load("data/fonts/roboto.ttf")
That is the same string a desktop build uses, so a project moves between platforms without touching its paths.
Assets inside an APK are read-only. Files written at run time go to the app's private directory instead, and the resource API looks there when loading, so a save and the load after it agree about where the file went:
natiny.resource.save(save_resource, "save.json")
local loaded = natiny.resource.load("save.json")
python android/pack.py --project ./my_game --apk natiny.apk \
--package com.example.my_game --name "My Game" \
--icon icon.png --orientation sensorLandscape \
--version-name 1.2.0 --version-code 7
--icon takes a square PNG, 192×192 or larger.--orientation is what the manifest asks for: portrait, landscape,
sensorLandscape, locked and a few more.--version-name is what users see. --version-code is the integer a device
compares when deciding whether an APK is an update, and it has to increase
with every release you publish.python android/pack.py --help lists the rest.
Without --keystore, the packer creates a debug key at
~/.natiny/debug.keystore on first use and reuses it afterwards. That is what
keeps adb install -r working between builds - a key made fresh each time
would make every build look like a different app.
A debug key is fine for testing and useless for distribution. For a release, sign with a key you keep:
python android/pack.py --project ./my_game --apk natiny.apk \
--package com.example.my_game --name "My Game" \
--keystore release.jks --key-alias mygame --key-pass "$KEYSTORE_PASSWORD"
Keep that keystore safe and backed up. Google Play ties an application id to the key that first signed it, and losing the key means you cannot publish an update to that app again.
cannot find the Android SDK - ANDROID_HOME does not point at the SDK
directory. Set it, or pass --sdk.main.lua at the root - --project points at the
wrong folder. The packer says it and carries on, and the app then starts
with nothing to run.INSTALL_FAILED_UPDATE_INCOMPATIBLE - an app with the same application
id is installed and was signed with a different key. Uninstall it, which
also removes its saved data, or sign with the original key.INSTALL_FAILED_NO_MATCHING_ABIS - the bundle's architecture does not
match the device. Use android-arm64 for phones and android-amd64 for an
x86_64 emulator.adb logcat -s natiny.Your scripts run on Android, and the loop is short: edit, repack, adb install
-r. The engine is never rebuilt, because it comes out of the APK you
downloaded.
A touch arrives as the left mouse button at the finger's position, so a
project written for a desktop works on a phone as it stands. For more than one
finger, natiny.input reports every touch.
By the end of this guide your own C program runs on your iPhone, launched from the home screen.
Read this first. iOS is the hardest platform Natiny supports, and most of the difficulty is Apple's, not the engine's. Every app on a device must be signed by a registered developer, so a large part of this guide is about certificates, provisioning profiles and Xcode. The guide assumes you already know what code signing is and how apps reach a device. If you have never signed anything, expect to spend an hour here.
Everything below happens on a Mac.
The engine is cross-compiled for you and shipped as a static archive. Your program is compiled and linked on your Mac, and the result is wrapped in a signed app bundle there too:
we cross-compile → libnatiny_ios_arm64.a (in the SDK bundle)
you compile and link → mygame (a Mach-O, not yet an app)
you pack and sign → MyGame.app (installable)
You need Xcode - the application, not only the command-line tools. The tools carry the macOS SDK and no other.
xcrun --sdk iphoneos --show-sdk-path
A path means you are ready. If it says SDK "iphoneos" cannot be located,
install Xcode from the App Store, open it once, then:
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
sudo xcodebuild -license accept
Download the SDK bundle for ios-arm64:
my_game/
├── include/
│ └── natiny.h
├── lib/
│ └── libnatiny_ios_arm64.a
├── ios/
│ ├── make_app.py
│ └── Info.plist.in
└── main.c
An iOS process starts on the main thread, and that thread has to be handed to
UIApplicationMain, which runs the event loop and never returns. Something
must therefore own main, and Natiny does: natiny.h renames your main to
natiny_main, provides one of its own, gives the main thread to UIKit and
calls yours on a separate thread.
You do not write anything iOS-specific because of this. The same main.c
compiles for Windows, Linux, macOS and iOS. If you ever need the real entry
point back, #undef main after including the header.
The window always covers the screen, so the size you ask for is ignored - use
natiny_window_get_width and natiny_window_get_height for the real one. A
green square on a dark background is enough to prove the whole chain worked.
#include "natiny.h"
#include <stddef.h>
static NatinyWindow window;
static void frame(float dt, void* userdata)
{
(void)dt;
(void)userdata;
natiny_window_bind(window);
natiny_render_clear(0.07f, 0.09f, 0.13f, 1.0f);
natiny_render_set_color(0.66f, 0.76f, 0.22f, 1.0f);
natiny_render_rectangle(360.0f, 260.0f, 80.0f, 80.0f);
natiny_window_unbind();
}
int main(void)
{
if (!natiny_backend_init(NATINY_BACKEND_AUTO)) {
return 1;
}
window = natiny_window_create("Hello, Natiny", 800, 600);
if (!window) {
natiny_backend_shutdown();
return 1;
}
natiny_backend_loop(frame, NULL);
natiny_backend_shutdown();
return 0;
}
NATINY_BACKEND_AUTO selects Metal, the only backend iOS has.
xcrun reaches Xcode's clang and its iOS SDK in one word. The -target
triple carries both the architecture and the deployment minimum, and it is
what makes the linker mark the binary as iOS rather than macOS:
xcrun clang -O2 -target arm64-apple-ios13.0 \
-c main.c -Iinclude -o main.o
xcrun clang -O2 -target arm64-apple-ios13.0 \
main.o lib/libnatiny_ios_arm64.a -o mygame \
-framework UIKit -framework Foundation -framework CoreGraphics \
-framework Metal -framework QuartzCore -framework AudioToolbox \
-framework AVFoundation
Keep the minimum at 13.0 - the objects inside the archive were compiled for it, and asking the linker for an older one will not make them run on an older phone.
Check what you produced:
otool -l mygame | grep -A4 LC_BUILD_VERSION
platform 2 is iOS and minos 13.0 is the deployment target. If you see
platform 1, the -target flag is missing from one of the two commands and
you have built for macOS.
mygame is a valid Mach-O that no phone will run yet: it is not in a bundle
and it is not signed. Both are the next steps.
An app on a device is identified by its bundle identifier, and Apple keeps one global namespace of them.
Warning:
com.example.*is taken, and Xcode answers "cannot be registered to your development team because it is not available". Derive your own from a domain or account you control -ru.yourname.mygame.
The same string has to appear in the Xcode project below, in the provisioning
profile and in --bundle-id.
Two things must exist before a phone runs your app: a certificate, proving a registered developer signed it, and a provisioning profile, saying this identifier may run on this device. A free Apple ID gets both, and Xcode issues them.
Step 5 is the one that issues the profile - building to a connected device is what registers the device with Apple. Afterwards the throwaway project can be deleted.
security find-identity -v -p codesigning
1) 4738DCEAD9968002836BCAC7DFD05A619052CCAD "Apple Development: you@mail.com (77UNP8U994)"
Either half works; the hash has no spaces and is harder to mistype.
Profiles live in one of two folders, depending on the Xcode version. Print which identifier each one covers:
for d in ~/Library/Developer/Xcode/UserData/Provisioning\ Profiles \
~/Library/MobileDevice/Provisioning\ Profiles; do
[ -d "$d" ] || continue
for p in "$d"/*.mobileprovision; do
echo "$p -> $(security cms -D -i "$p" | plutil -extract Entitlements.application-identifier raw -)"
done
done
Copy the one ending in your identifier next to your program as
mygame.mobileprovision. If both folders are empty, step 5 did not happen:
adding an Apple ID creates the certificate, but only a device build creates
the profile.
ios/make_app.py builds the bundle: Info.plist, your data folder, the icon,
the embedded profile and the signature. Create a folder for your data even if
the program reads nothing yet:
mkdir -p assets
python3 ios/make_app.py --project ./assets --executable mygame \
--name "My Game" --bundle-id ru.yourname.mygame \
--identity 4738DCEAD9968002836BCAC7DFD05A619052CCAD \
--profile mygame.mobileprovision
--executable is what tells the packer to wrap your Mach-O. Without it, the
packer would link the Lua wrapper's archive instead - that is the other half
of the release, and not what you want here.
xcrun devicectl list devices
xcrun devicectl device install app --device <UDID> "My Game.app"
On Xcode older than 15, use Window → Devices and Simulators and drag the
.app onto Installed Apps.
An app signed with a free account is refused on first launch until you trust the certificate: Settings → General → VPN & Device Management → Developer App. Then tap the icon. A dark screen with a green square means your C, the cross-compiled engine, Metal and your signature all worked.
To see why something failed, launch it from the Mac:
xcrun devicectl device process launch --console --device <UDID> ru.yourname.mygame
--console prints the program's output in your terminal.
The Simulator is not an option. Your binary is arm64-apple-ios, the
Simulator runs arm64-apple-ios-simulator, and those are different platforms
built against different SDKs. On Apple silicon the Simulator may install the
app and then crash it on launch, which looks like a bug in your code and is
not.
Whatever you pass to --project becomes data/ inside the bundle, beside the
executable, and that is where natiny_resource_load reads from - the working
directory of an iOS process is / and holds nothing:
assets/
└── player.png
NatinyResource player = natiny_resource_load("data/player.png");
That is the same string a desktop build uses. A bundle is read-only, so a
relative path given to natiny_resource_save goes to Documents/ inside the
app's container, and natiny_resource_load looks there too.
SDK "iphoneos" cannot be located - Xcode is missing or not selected.building for iOS but linking object file built for macOS - the
-target triple is missing from the compile step, the link step, or both.cannot be registered to your development team - the bundle identifier
is taken; choose your own.no signing identity matches ... - the packer prints the identities you
do have; use one of those.Your C program runs on an iPhone, and only one command in the whole project is iOS-shaped: the link line.
Two things about the platform are worth knowing next. A touch arrives as the left mouse button, plus a callback that reports every finger. And when iOS sends your app to the background it takes away the right to draw: Natiny reports that as window iconification, and iOS kills an app that keeps rendering anyway. Stop drawing inside that callback.
By the end of this guide your Lua project runs on your own iPhone, launched from the home screen.
Read this first. iOS is the hardest platform Natiny supports, and most of the difficulty has nothing to do with the engine. Apple requires every app on a device to be signed by a registered developer, so half of this guide is about certificates, provisioning profiles and Xcode. The guide assumes you already know what code signing is and how apps reach a device. If you have never signed anything before, expect to spend an hour here, and expect to use Xcode a little even though Natiny never asks you to.
Everything below happens on a Mac. There is no way around that: the tools that sign an app are Apple's and run nowhere else.
Every other platform gives you a file you can run. iOS gives you an archive, because a signature has to be added before a phone will accept the app - and a signature needs a certificate that identifies you. That certificate lives in your keychain and cannot travel inside a download.
So the work is split:
we cross-compile → libnatiny_player_ios_arm64.a (in the bundle)
you link, pack and sign → MyGame.app (on your Mac)
make_app.py, which ships inside the bundle, does the second half. You will
run it twice: once unsigned to check the toolchain, and once for real.
You need Xcode - the application, not only the command-line tools. The tools
carry the macOS SDK and no other, so a Mac can have clang, ld and codesign
and still be unable to compile for a phone.
xcrun --sdk iphoneos --show-sdk-path
A path means you are ready. If it says SDK "iphoneos" cannot be located,
install Xcode from the App Store, open it once so it finishes installing its
components, then point the tools at it:
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
sudo xcodebuild -license accept
Python 3.8 or newer is the only other requirement, and macOS already has it.
Download the Lua wrapper bundle for ios-arm64 and unzip it. Four things
matter:
natiny-ios_arm64-app/
├── libnatiny_player_ios_arm64.a the interpreter, the engine and Lua
├── make_app.py turns that archive into an .app
├── Info.plist.in the template it fills in
└── data/
└── main.lua the script that starts
data/ is the example project. Later you will point the packer at your own
folder; for now the example is what we want, because if something goes wrong
you will know it is not your Lua code.
Run the packer with nothing but a project and a name. It finds the archive and the template beside itself:
cd natiny-ios_arm64-app
python3 make_app.py --project ./data --name "My Game" \
--bundle-id com.example.mygame
You should see the link command, then a line like:
-> /Users/you/natiny-ios_arm64-app/My Game.app 1.3 MB com.example.mygame 1.0
My Game.app is a real app bundle: your executable, your data/ folder and
an Info.plist. What it is not is installable. Without --identity the packer
signs it ad-hoc, which no device accepts.
That is worth stating plainly, because the next thought is usually the
Simulator: this bundle will not run there either. It is built for
arm64-apple-ios, and the Simulator runs arm64-apple-ios-simulator - a
different platform, compiled against a different SDK. Natiny has no simulator
build. On an Apple silicon Mac the Simulator may even install it and then
crash on launch, which looks like a bug in the app and is not.
So: the toolchain works, and now we make the app installable.
An app on a device is identified by its bundle identifier, and Apple keeps one global namespace of them. Your identifier has to be unused.
Warning:
com.example.mygame, used above and in every example on this page, is taken. So is anything else starting withcom.example. Xcode will answer "cannot be registered to your development team because it is not available". Derive your own from a domain or an account you control -ru.yourname.mygameworks.
Pick yours now. It has to be the same string in three places: the Xcode
project below, the provisioning profile, and --bundle-id.
Two things have to exist before a phone will run your app:
A free Apple ID gets you both. Xcode is what issues them, and the shortest path is to let it do that once for a throwaway project.
That last step is the one that matters. Building to a connected device is what makes Xcode register the device and issue a profile for your identifier. When the throwaway app appears on the phone, you can close the project and never open it again.
The certificate:
security find-identity -v -p codesigning
1) 4738DCEAD9968002836BCAC7DFD05A619052CCAD "Apple Development: you@mail.com (77UNP8U994)"
Either half works: the long hash, or the name in quotes. The hash has no spaces in it, so it is harder to mistype.
The profile is a file on disk. Xcode 16 keeps profiles in one place and earlier versions in another, so look in both, and print which identifier each one covers:
for d in ~/Library/Developer/Xcode/UserData/Provisioning\ Profiles \
~/Library/MobileDevice/Provisioning\ Profiles; do
[ -d "$d" ] || continue
for p in "$d"/*.mobileprovision; do
echo "$p -> $(security cms -D -i "$p" | plutil -extract Entitlements.application-identifier raw -)"
done
done
Each line ends with TEAMID.your.bundle.id. Copy the one matching your
identifier next to the bundle:
cp <that-file>.mobileprovision ~/natiny-ios_arm64-app/mygame.mobileprovision
If both folders are empty, step 5 above did not happen. Adding an Apple ID creates the certificate; only a build to a connected device creates the profile.
Same command as before, plus the two you just found:
python3 make_app.py --project ./data --name "My Game" \
--bundle-id ru.yourname.mygame \
--identity 4738DCEAD9968002836BCAC7DFD05A619052CCAD \
--profile mygame.mobileprovision
The packer links the archive, writes Info.plist, copies data/ in, embeds
the profile, reads the entitlements out of it the way Xcode does, and signs
the bundle. It checks the identity against your keychain before it starts, so
a mistyped name fails immediately instead of after the build.
devicectl ships with Xcode 15 and newer:
xcrun devicectl list devices
xcrun devicectl device install app --device <UDID> "My Game.app"
On older Xcode, open Window → Devices and Simulators, select the phone,
and drag the .app onto Installed Apps.
The app is now on the home screen with a plain grey icon - we have not given it one yet.
The first launch of an app signed with a free account is refused: the phone does not trust your certificate yet. Go to Settings → General → VPN & Device Management → Developer App and trust it. Then tap the icon.
The Natiny logo appears on a dark background. That is the example
data/main.lua, and it means the whole chain works: cross-compiled engine,
your signature, your device.
When something goes wrong instead, launch it from the Mac so you can see why:
xcrun devicectl device process launch --console --device <UDID> ru.yourname.mygame
--console prints the program's output in your terminal. A crash with no
Natiny output at all is usually a signing problem; output that stops partway
is your Lua code.
Everything under --project becomes data/ inside the bundle, and
data/main.lua is what starts:
my_game/
├── main.lua
├── player.lua
└── fonts/
└── roboto.ttf
python3 make_app.py --project ./my_game --name "My Game" \
--bundle-id ru.yourname.mygame \
--identity 4738DCEAD9968002836BCAC7DFD05A619052CCAD \
--profile mygame.mobileprovision
Resource paths do not change between platforms:
local font_data = natiny.resource.load("data/fonts/roboto.ttf")
An app bundle is read-only, so files written at run time go to the app's
Documents folder instead. The resource API knows that and looks there when
loading, so a save and the load after it agree about where the file went:
natiny.resource.save(save_resource, "save.json")
local loaded = natiny.resource.load("save.json")
Nothing is compiled when you repack, so this step takes about a second.
python3 make_app.py --project ./my_game --name "My Game" \
--bundle-id ru.yourname.mygame \
--identity 4738DCEAD9968002836BCAC7DFD05A619052CCAD \
--profile mygame.mobileprovision \
--icon icon.png --version 1.2.0 --build 7
--icon takes a square PNG, 120×120 or larger. Without it you get the grey
square you saw above.--version is what the App Store shows; --build is the number that must
increase with every upload.--ipa MyGame.ipa additionally writes the zipped bundle that App Store
Connect and device installers take. You do not need it for cable installs.python3 make_app.py --help lists the rest.
SDK "iphoneos" cannot be located - Xcode is missing, or installed and
not selected. See the first section.cannot be registered to your development team - the bundle identifier
is taken. Choose one of your own.no signing identity matches ... - the string does not match your
keychain. The packer prints what you do have; copy one of those.Your Lua project runs on a real iPhone, and the loop is short: edit the
scripts, rerun make_app.py, reinstall. The engine never has to be rebuilt,
because the archive in the bundle already contains it.
A touch arrives as the left mouse button at the finger's position, so a
project written for a desktop works on a phone as it stands. For more than one
finger, natiny.input reports every touch.
Download the SDK bundle for linux-amd64 and put main.c beside the
folders it unzipped:
my_game/
├── include/
│ └── natiny.h
├── lib/
│ └── libnatiny_linux_amd64.a
└── main.c
#include "natiny.h"
#include <stddef.h>
static NatinyWindow window;
static void frame(float dt, void* userdata)
{
(void)dt;
(void)userdata;
natiny_window_bind(window);
natiny_render_clear(0.07f, 0.09f, 0.13f, 1.0f);
natiny_render_set_color(0.66f, 0.76f, 0.22f, 1.0f);
natiny_render_rectangle(360.0f, 260.0f, 80.0f, 80.0f);
natiny_window_unbind();
}
int main(void)
{
if (!natiny_backend_init(NATINY_BACKEND_AUTO)) {
return 1;
}
window = natiny_window_create("Hello, Natiny", 800, 600);
if (!window) {
natiny_backend_shutdown();
return 1;
}
natiny_backend_loop(frame, NULL);
natiny_backend_shutdown();
return 0;
}
Compile and link:
clang -O2 -Iinclude -c main.c -o main.o
clang -O2 main.o lib/libnatiny_linux_amd64.a -o app \
-lGL -lX11 -ldl -lpthread -lm
./app
A dark window opens with a green square in it. gcc works just as well;
nothing on that line is clang-specific.
libnatiny_linux_amd64.a is the whole engine - GLFW, the shader compiler, the
mixer and the physics solver are already inside it, so there is nothing else of
Natiny's to link. It is plain C with no C++ runtime anywhere, so a C compiler
is all you need.
The five libraries on the link line are the system's. You need their
development packages to build - on Debian and Ubuntu that is
libgl1-mesa-dev and libx11-dev - but not to run.
One archive carries both backends this platform can run, Vulkan and OpenGL.
NATINY_BACKEND_AUTO picks Vulkan; pass NATINY_BACKEND_OPENGL to choose the
other. Vulkan is opened at run time rather than imported, so a machine with no
Vulkan driver still starts and still runs OpenGL.
The SDK README in the bundle has the same commands for arm64 and i386, and
for linking libnatiny_linux_amd64.so instead of the archive.
A Lua project needs no compiler. Download the Lua bundle for
linux-amd64, unzip it, and put your script where the executable looks for
it:
my_game/
├── data/
│ └── main.lua
└── natiny
./natiny
natiny runs data/main.lua from beside itself, so relative paths inside the
script mean the same thing whichever directory you started it from. Name
another file to run that one instead:
./natiny data/experiment.lua
To ship, ship the folder. The executable needs X11, OpenGL and the C library, which any desktop Linux already has.
linux-arm64 and linux-i386 bundles exist too, and work the same way.
Download the SDK bundle - arm64 for Apple silicon, amd64 for an Intel
Mac - and put main.c beside the folders it unzipped:
my_game/
├── include/
│ └── natiny.h
├── lib/
│ └── libnatiny_macos_arm64.a
└── main.c
#include "natiny.h"
#include <stddef.h>
static NatinyWindow window;
static void frame(float dt, void* userdata)
{
(void)dt;
(void)userdata;
natiny_window_bind(window);
natiny_render_clear(0.07f, 0.09f, 0.13f, 1.0f);
natiny_render_set_color(0.66f, 0.76f, 0.22f, 1.0f);
natiny_render_rectangle(360.0f, 260.0f, 80.0f, 80.0f);
natiny_window_unbind();
}
int main(void)
{
if (!natiny_backend_init(NATINY_BACKEND_AUTO)) {
return 1;
}
window = natiny_window_create("Hello, Natiny", 800, 600);
if (!window) {
natiny_backend_shutdown();
return 1;
}
natiny_backend_loop(frame, NULL);
natiny_backend_shutdown();
return 0;
}
Compile and link:
clang -O2 -Iinclude -c main.c -o main.o
clang -O2 main.o lib/libnatiny_macos_arm64.a -o app \
-framework Cocoa -framework IOKit -framework CoreFoundation \
-framework CoreVideo -framework Metal -framework QuartzCore \
-lpthread -lm
./app
A dark window opens with a green square in it. Use
libnatiny_macos_amd64.a on an Intel Mac; the frameworks are the same.
libnatiny_macos_arm64.a is the whole engine - GLFW, the shader compiler, the
mixer and the physics solver are already inside it, so there is nothing else of
Natiny's to link. It is plain C with no C++ runtime anywhere, so a C compiler
is all you need. Every framework on that line ships with macOS.
Metal and only Metal. OpenGL on macOS stopped at 4.1 and has been
deprecated since 10.14, and Vulkan there is MoltenVK, which is Metal
underneath - so NATINY_BACKEND_AUTO picks Metal and there is nothing else to
pick. Shaders are compiled to MSL and handed to the driver as source when the
program starts, so a machine running your game needs no part of Xcode.
The SDK README in the bundle also covers linking
libnatiny_macos_arm64.dylib instead of the archive.
A Lua project needs no compiler. Download the Lua bundle - arm64 for
Apple silicon, amd64 for an Intel Mac - unzip it, and put your script where
the executable looks for it:
my_game/
├── data/
│ └── main.lua
└── natiny
./natiny
natiny runs data/main.lua from beside itself, so relative paths inside the
script mean the same thing whichever directory you started it from. Name
another file to run that one instead:
./natiny data/experiment.lua
The binaries are not notarised, so the first launch is refused. Open it once from Finder with Control-click → Open, or clear the quarantine flag:
xattr -d com.apple.quarantine natiny
To ship, ship the folder. Nothing has to be installed alongside it: everything the executable uses is part of macOS.
Download the SDK bundle for web, install the
Emscripten SDK,
and put main.c beside the folders the bundle unzipped:
my_game/
├── include/
│ └── natiny.h
├── lib/
│ └── libnatiny_webassembly.a
└── main.c
#include "natiny.h"
#include <stddef.h>
static NatinyWindow window;
static void frame(float dt, void* userdata)
{
(void)dt;
(void)userdata;
natiny_window_bind(window);
natiny_render_clear(0.07f, 0.09f, 0.13f, 1.0f);
natiny_render_set_color(0.66f, 0.76f, 0.22f, 1.0f);
natiny_render_rectangle(360.0f, 260.0f, 80.0f, 80.0f);
natiny_window_unbind();
}
int main(void)
{
if (!natiny_backend_init(NATINY_BACKEND_AUTO)) {
return 1;
}
window = natiny_window_create("Hello, Natiny", 800, 600);
if (!window) {
natiny_backend_shutdown();
return 1;
}
natiny_backend_loop(frame, NULL);
natiny_backend_shutdown();
return 0;
}
Compile and link:
emcc -O2 -Iinclude -c main.c -o main.o
em++ -O2 main.o lib/libnatiny_webassembly.a \
--use-port=emdawnwebgpu \
-sASYNCIFY -sALLOW_MEMORY_GROWTH=1 -sWASM_BIGINT \
-o app.html
That writes app.html, app.js and app.wasm. Serve them over HTTP and open
app.html:
python -m http.server 8000
A dark canvas with a green square.
Compile with emcc, link with em++. Natiny is C, but Emscripten's
emdawnwebgpu port is C++ and turns the plain C driver down. emcc
-sDEFAULT_TO_CXX is the same thing spelled as a flag.
--use-port=emdawnwebgpu is what provides WebGPU, the only backend here.
-sASYNCIFY is what lets natiny_backend_loop block the way it does
everywhere else while the browser still gets its frames back.
Dawn's code is linked into your program rather than into the archive, so its notice comes from your Emscripten install and ships with what you publish. BSD 3-clause, asking the same thing MIT does: keep the notice.
A page cannot read its own files from file://, so http.server above is not
a suggestion - opening the .html directly will fail to load the .wasm.
Download the Lua bundle for web and unzip it. Three files are the
runtime, and data/main.lua is your game:
my_game/
├── data/
│ └── main.lua
├── natiny.html
├── natiny.js
└── natiny.wasm
Serve the folder over HTTP and open natiny.html. Any static server does:
python -m http.server 8000
Then go to http://localhost:8000/natiny.html.
It has to be HTTP. Opening the .html from file:// fails: a page cannot
fetch its own files that way, and data/main.lua is fetched rather than read.
The window becomes the page's canvas, so natiny.window.create gives you the
canvas rather than a new window. Everything else in the script is the same
code that runs on a desktop.
WebGPU is the only backend here. It needs a current Chrome, Edge or Safari; Firefox is still behind a flag at the time of writing.
Download the SDK bundle for windows-amd64 and put main.c beside the
folders it unzipped:
my_game/
├── include/
│ └── natiny.h
├── lib/
│ └── libnatiny_windows_amd64.a
└── main.c
#include "natiny.h"
#include <stddef.h>
static NatinyWindow window;
static void frame(float dt, void* userdata)
{
(void)dt;
(void)userdata;
natiny_window_bind(window);
natiny_render_clear(0.07f, 0.09f, 0.13f, 1.0f);
natiny_render_set_color(0.66f, 0.76f, 0.22f, 1.0f);
natiny_render_rectangle(360.0f, 260.0f, 80.0f, 80.0f);
natiny_window_unbind();
}
int main(void)
{
if (!natiny_backend_init(NATINY_BACKEND_AUTO)) {
return 1;
}
window = natiny_window_create("Hello, Natiny", 800, 600);
if (!window) {
natiny_backend_shutdown();
return 1;
}
natiny_backend_loop(frame, NULL);
natiny_backend_shutdown();
return 0;
}
Compile and link:
clang -O2 -Iinclude -c main.c -o main.o
clang -O2 -static main.o lib/libnatiny_windows_amd64.a -o app.exe \
-ld3d12 -ldxgi -ld3dcompiler -ldxguid -lopengl32 \
-lgdi32 -luser32 -lshell32 -ldwmapi -lpthread
app.exe
A dark window opens with a green square in it.
libnatiny_windows_amd64.a is the whole engine - GLFW, the shader compiler,
the mixer and the physics solver are already inside it, so there is nothing
else of Natiny's to link. It is plain C with no C++ runtime anywhere, so a C
compiler is all you need.
Keep -static. Without it the executable imports libwinpthread-1.dll
out of your toolchain and will not start on a machine that has no toolchain
installed. Everything else on that line is part of Windows.
One archive carries every backend this platform can run - Direct3D 12, Vulkan
and OpenGL. NATINY_BACKEND_AUTO picks Direct3D 12; pass
NATINY_BACKEND_VULKAN or NATINY_BACKEND_OPENGL to choose. Vulkan is opened
at run time rather than imported, so a machine with no Vulkan driver still
starts and still runs the other two.
The SDK README in the bundle has the same commands for arm64 and i386, and
for linking natiny_windows_amd64.dll instead of the archive.
A Lua project needs no compiler. Download the Lua bundle for
windows-amd64, unzip it, and put your script where the executable looks for
it:
my_game/
├── data/
│ └── main.lua
└── natiny.exe
natiny.exe
natiny.exe runs data/main.lua from beside itself, so relative paths inside
the script mean the same thing whichever directory you started it from. Name
another file to run that one instead:
natiny.exe data/experiment.lua
Dropping a .lua file onto the executable does the same thing.
To ship, ship the folder. natiny.exe needs nothing else installed: every
library it uses is part of Windows.
A game window can have more pixels than its size suggests. A window that is 800 units wide may contain 1600 pixels on a display set to 200% scaling. If a game treats both numbers as the same thing, its interface becomes tiny or its finished image becomes blurry.
Natiny lets you choose which result you want. In this guide, we will keep 2D coordinates comfortable to use and make text and shapes stay sharp on a high-density display.
The operating system describes an on-screen size in units that remain useful at different display densities. The graphics device, however, must produce an actual colour for every pixel in the window.
The display scale is the relationship between those sizes. A scale of 1
means 100%. A scale of 2 means 200%: one unit of on-screen size covers two
pixels across and two pixels down.
natiny_window_get_scale reports this factor:
float scale = natiny_window_get_scale(window);
printf("Display scale: %.2f\n", scale);
Natiny calls the numbers passed to 2D drawing commands drawing units. The window's scale mode decides how those units reach its pixels.
Set the mode after creating the window and before drawing a frame:
NatinyWindow window = natiny_window_create("Scaled 2D", 800, 600);
natiny_window_set_scale_mode(window, NATINY_SCALE_MODE_HIDPI);
natiny_window_set_scale_mode provides
three modes. Their names describe the result:
NATINY_SCALE_MODE_PIXELS draws pixel for pixelOne drawing unit is one window pixel. Coordinate 100 reaches pixel 100,
even when the display scale is 2.
This is the default because it preserves the behaviour of applications written before Natiny had scale modes. It is also useful when code deliberately works in exact pixel coordinates. On a dense display, the same 16-pixel text occupies less physical space and can appear smaller.
NATINY_SCALE_MODE_STRETCH enlarges the finished frameNatiny draws the 2D frame into a smaller off-screen image, then stretches that
image over the window. At scale 2, coordinate 100 reaches pixel 200, but
the pixels in the smaller image are enlarged too. Text and diagonal edges can
look blurry.
This is useful when enlarging the whole finished image is the intended visual style. The path looks like this:
2D drawing -> smaller frame -> stretch to window pixels
NATINY_SCALE_MODE_HIDPI keeps the full detailHiDPI means high pixel density: the display has extra pixels available for the same on-screen size. In this mode, Natiny enlarges 2D coordinates but draws directly into every window pixel. It also rasterizes fonts at the display scale, so their outlines remain sharp.
At scale 2, coordinate 100 still reaches pixel 200, but there is no small
finished image to stretch:
2D drawing coordinates x display scale -> full-size window frame
Use this mode for a normally sized, sharp 2D interface.
Set the mode before creating fonts. A later mode change is supported, but it may rebuild live font atlases.
#include "natiny.h"
#include <stddef.h>
static NatinyWindow window;
static NatinyFont font;
static void frame(float dt, void* userdata)
{
(void)dt;
(void)userdata;
natiny_window_bind(window);
natiny_render_clear(0.05f, 0.07f, 0.10f, 1.0f);
natiny_render_set_color(0.18f, 0.48f, 0.82f, 1.0f);
natiny_render_rectangle(40.0f, 40.0f, 320.0f, 72.0f);
natiny_render_set_color(1.0f, 1.0f, 1.0f, 1.0f);
natiny_render_text(font, 60.0f, 64.0f, "Sharp at the same size");
natiny_window_unbind();
}
int main(void)
{
NatinyResource font_data;
if (!natiny_backend_init(NATINY_BACKEND_AUTO)) {
return 1;
}
window = natiny_window_create("Sharp interface", 800, 600);
natiny_window_set_scale_mode(window, NATINY_SCALE_MODE_HIDPI);
font_data = natiny_resource_load("data/fonts/roboto.ttf");
font = natiny_font_create(font_data, 24.0f);
if (!window || !font_data || !font) {
natiny_backend_shutdown();
return 1;
}
natiny_backend_loop(frame, NULL);
natiny_backend_shutdown();
return 0;
}
Run the program on displays using 100% and 200% scaling. The rectangle still uses the same drawing coordinates and occupies the expected on-screen size. On the denser display, Natiny uses the extra pixels to produce smoother edges and text.
The window size and mouse coordinates follow the selected drawing units too. This keeps layout and input in agreement:
int width = natiny_window_get_width(window);
int mouse_x = natiny_input_get_mouse_x();
/* Both values use the same horizontal coordinate system as drawing. */
In NATINY_SCALE_MODE_PIXELS, width is the number of window pixels. In
NATINY_SCALE_MODE_STRETCH and NATINY_SCALE_MODE_HIDPI, it is the width used
by the scaled 2D drawing commands.
The drawing target and coordinate system are fixed when the window is bound.
For that reason, Natiny rejects a mode change made between
natiny_window_bind and natiny_window_unbind:
/* Correct: the window is not bound yet. */
natiny_window_set_scale_mode(window, NATINY_SCALE_MODE_HIDPI);
natiny_window_bind(window);
/* Draw the frame here. Do not change its scale mode here. */
natiny_window_unbind();
Choose the mode during setup rather than setting it every frame. Switching can
rebuild font atlases and allocate or release the off-screen image used by
NATINY_SCALE_MODE_STRETCH.
The selected mode affects 2D window drawing, its viewport and scissor coordinates, window dimensions, and mouse coordinates. It does not scale 3D drawing or user-created surfaces. Surface dimensions always remain pixels.
A web build answers the same question the same way. A browser lays the page
out in CSS pixels and reports how many device pixels stand behind each of them;
Natiny reads that as the display scale, so a Retina screen reports 2, a
Windows display at 125% reports 1.25, and a page zoomed with Ctrl and +
reports whatever the zoom makes it. The canvas is given one drawing buffer
pixel per device pixel, and the scale, the window size and the font atlases
follow the zoom while the program runs.
Pick the mode for the result, exactly as on the desktop: HIDPI for a
normally sized, sharp interface, STRETCH for the same coordinates at the
cost of sharpness when the GPU is the constraint, and PIXELS when one
drawing unit must be one device pixel.
Use NATINY_SCALE_MODE_PIXELS when exact window pixels are part of the design.
Use NATINY_SCALE_MODE_STRETCH when you intentionally want to enlarge a
completed lower-resolution frame. Use NATINY_SCALE_MODE_HIDPI for sharp 2D
graphics that keep the same useful coordinate sizes across display densities.
All three modes answer the same underlying question: whether display scaling should be ignored, applied after the frame is complete, or applied while the full-resolution frame is drawn. Once that choice is explicit, drawing, window-size queries, and mouse input stay in one consistent coordinate system.
A game window can have more pixels than its size suggests. A window that is 800 units wide may contain 1600 pixels on a display set to 200% scaling. If a game treats both numbers as the same thing, its interface becomes tiny or its finished image becomes blurry.
Natiny lets you choose which result you want. In this guide, we will keep 2D coordinates comfortable to use and make text and shapes stay sharp on a high-density display.
The operating system describes an on-screen size in units that remain useful at different display densities. The graphics device, however, must produce an actual colour for every pixel in the window.
The display scale is the relationship between those sizes. A scale of 1
means 100%. A scale of 2 means 200%: one unit of on-screen size covers two
pixels across and two pixels down.
natiny.window.get_scale reports this factor:
local scale = natiny.window.get_scale(window)
print("Display scale: " .. scale)
Natiny calls the numbers passed to 2D drawing commands drawing units. The window's scale mode decides how those units reach its pixels.
Set the mode after creating the window and before drawing a frame:
local window = natiny.window.create("Scaled 2D", 800, 600)
natiny.window.set_scale_mode(window, natiny.window.SCALE_MODE_HIDPI)
natiny.window.set_scale_mode provides
three modes. Their names describe the result:
SCALE_MODE_PIXELS draws pixel for pixelOne drawing unit is one window pixel. Coordinate 100 reaches pixel 100,
even when the display scale is 2.
This is the default because it preserves the behaviour of applications written before Natiny had scale modes. It is also useful when code deliberately works in exact pixel coordinates. On a dense display, the same 16-pixel text occupies less physical space and can appear smaller.
SCALE_MODE_STRETCH enlarges the finished frameNatiny draws the 2D frame into a smaller off-screen image, then stretches that
image over the window. At scale 2, coordinate 100 reaches pixel 200, but
the pixels in the smaller image are enlarged too. Text and diagonal edges can
look blurry.
This is useful when enlarging the whole finished image is the intended visual style. The path looks like this:
2D drawing -> smaller frame -> stretch to window pixels
SCALE_MODE_HIDPI keeps the full detailHiDPI means high pixel density: the display has extra pixels available for the same on-screen size. In this mode, Natiny enlarges 2D coordinates but draws directly into every window pixel. It also rasterizes fonts at the display scale, so their outlines remain sharp.
At scale 2, coordinate 100 still reaches pixel 200, but there is no small
finished image to stretch:
2D drawing coordinates x display scale -> full-size window frame
Use this mode for a normally sized, sharp 2D interface.
Set the mode before creating fonts. A later mode change is supported, but it may rebuild live font atlases.
local natiny = require("natiny")
assert(natiny.backend.init())
local window = natiny.window.create("Sharp interface", 800, 600)
natiny.window.set_scale_mode(window, natiny.window.SCALE_MODE_HIDPI)
local font_data = natiny.resource.load("data/fonts/roboto.ttf")
local font = natiny.font.create(font_data, 24)
natiny.backend.loop(function(dt)
natiny.window.bind(window, function()
natiny.render.clear(0.05, 0.07, 0.10, 1)
natiny.render.set_color(0.18, 0.48, 0.82, 1)
natiny.render.rectangle(40, 40, 320, 72)
natiny.render.set_color(1, 1, 1, 1)
natiny.render.text(font, 60, 64, "Sharp at the same size")
end)
end)
Run the program on displays using 100% and 200% scaling. The rectangle still uses the same drawing coordinates and occupies the expected on-screen size. On the denser display, Natiny uses the extra pixels to produce smoother edges and text.
The window size and mouse coordinates follow the selected drawing units too. This keeps layout and input in agreement:
local width = natiny.window.get_width(window)
local mouse_x = natiny.input.get_mouse_x()
-- Both values use the same horizontal coordinate system as drawing.
In SCALE_MODE_PIXELS, width is the number of window pixels. In
SCALE_MODE_STRETCH and SCALE_MODE_HIDPI, it is the width used by the scaled
2D drawing commands.
The drawing target and coordinate system are fixed when the window is bound.
For that reason, Natiny rejects a mode change made from inside
natiny.window.bind:
-- Correct: the window is not bound yet.
natiny.window.set_scale_mode(window, natiny.window.SCALE_MODE_HIDPI)
natiny.window.bind(window, function()
-- Draw the frame here. Do not change its scale mode here.
end)
Choose the mode during setup rather than setting it every frame. Switching can
rebuild font atlases and allocate or release the off-screen image used by
SCALE_MODE_STRETCH.
The selected mode affects 2D window drawing, its viewport and scissor coordinates, window dimensions, and mouse coordinates. It does not scale 3D drawing or user-created surfaces. Surface dimensions always remain pixels.
A web build answers the same question the same way. A browser lays the page
out in CSS pixels and reports how many device pixels stand behind each of them;
Natiny reads that as the display scale, so a Retina screen reports 2, a
Windows display at 125% reports 1.25, and a page zoomed with Ctrl and +
reports whatever the zoom makes it. The canvas is given one drawing buffer
pixel per device pixel, and the scale, the window size and the font atlases
follow the zoom while the program runs.
Pick the mode for the result, exactly as on the desktop: HIDPI for a
normally sized, sharp interface, STRETCH for the same coordinates at the
cost of sharpness when the GPU is the constraint, and PIXELS when one
drawing unit must be one device pixel.
Use SCALE_MODE_PIXELS when exact window pixels are part of the design. Use
SCALE_MODE_STRETCH when you intentionally want to enlarge a completed
lower-resolution frame. Use SCALE_MODE_HIDPI for sharp 2D graphics that keep
the same useful coordinate sizes across display densities.
All three modes answer the same underlying question: whether display scaling should be ignored, applied after the frame is complete, or applied while the full-resolution frame is drawn. Once that choice is explicit, drawing, window-size queries, and mouse input stay in one consistent coordinate system.
Every key and mouse button the engine knows has a #define in natiny.h, so
nothing has to be written as a bare number:
if (natiny_input_get_key_down(NATINY_INPUT_KEY_W)) {
y -= speed * dt;
}
if (natiny_input_get_mouse_button_pressed(NATINY_INPUT_MOUSE_BUTTON_LEFT)) {
fire();
}
The constants are what natiny_input_get_key_down,
natiny_input_get_key_pressed,
natiny_input_get_key_released and the
mouse button functions take. Values run to 348, well
inside the 512 keys the engine tracks; a code outside that range reads as not
pressed rather than an error. The Lua binding exposes the same numbers under
the shorter natiny.input.KEY_W.
| Constant | Value | Key |
|---|---|---|
NATINY_INPUT_KEY_SPACE |
32 | Space |
NATINY_INPUT_KEY_APOSTROPHE |
39 | ' |
NATINY_INPUT_KEY_COMMA |
44 | , |
NATINY_INPUT_KEY_MINUS |
45 | - |
NATINY_INPUT_KEY_PERIOD |
46 | . |
NATINY_INPUT_KEY_SLASH |
47 | / |
NATINY_INPUT_KEY_0 |
48 | 0 |
NATINY_INPUT_KEY_1 |
49 | 1 |
NATINY_INPUT_KEY_2 |
50 | 2 |
NATINY_INPUT_KEY_3 |
51 | 3 |
NATINY_INPUT_KEY_4 |
52 | 4 |
NATINY_INPUT_KEY_5 |
53 | 5 |
NATINY_INPUT_KEY_6 |
54 | 6 |
NATINY_INPUT_KEY_7 |
55 | 7 |
NATINY_INPUT_KEY_8 |
56 | 8 |
NATINY_INPUT_KEY_9 |
57 | 9 |
NATINY_INPUT_KEY_SEMICOLON |
59 | ; |
NATINY_INPUT_KEY_EQUAL |
61 | = |
NATINY_INPUT_KEY_A |
65 | A |
NATINY_INPUT_KEY_B |
66 | B |
NATINY_INPUT_KEY_C |
67 | C |
NATINY_INPUT_KEY_D |
68 | D |
NATINY_INPUT_KEY_E |
69 | E |
NATINY_INPUT_KEY_F |
70 | F |
NATINY_INPUT_KEY_G |
71 | G |
NATINY_INPUT_KEY_H |
72 | H |
NATINY_INPUT_KEY_I |
73 | I |
NATINY_INPUT_KEY_J |
74 | J |
NATINY_INPUT_KEY_K |
75 | K |
NATINY_INPUT_KEY_L |
76 | L |
NATINY_INPUT_KEY_M |
77 | M |
NATINY_INPUT_KEY_N |
78 | N |
NATINY_INPUT_KEY_O |
79 | O |
NATINY_INPUT_KEY_P |
80 | P |
NATINY_INPUT_KEY_Q |
81 | Q |
NATINY_INPUT_KEY_R |
82 | R |
NATINY_INPUT_KEY_S |
83 | S |
NATINY_INPUT_KEY_T |
84 | T |
NATINY_INPUT_KEY_U |
85 | U |
NATINY_INPUT_KEY_V |
86 | V |
NATINY_INPUT_KEY_W |
87 | W |
NATINY_INPUT_KEY_X |
88 | X |
NATINY_INPUT_KEY_Y |
89 | Y |
NATINY_INPUT_KEY_Z |
90 | Z |
NATINY_INPUT_KEY_LEFT_BRACKET |
91 | [ |
NATINY_INPUT_KEY_BACKSLASH |
92 | \ |
NATINY_INPUT_KEY_RIGHT_BRACKET |
93 | ] |
NATINY_INPUT_KEY_GRAVE_ACCENT |
96 | ` |
NATINY_INPUT_KEY_WORLD_1 |
161 | Non-US key #1 |
NATINY_INPUT_KEY_WORLD_2 |
162 | Non-US key #2 |
| Constant | Value | Key |
|---|---|---|
NATINY_INPUT_KEY_ESCAPE |
256 | Escape |
NATINY_INPUT_KEY_ENTER |
257 | Enter / Return |
NATINY_INPUT_KEY_TAB |
258 | Tab |
NATINY_INPUT_KEY_BACKSPACE |
259 | Backspace |
NATINY_INPUT_KEY_INSERT |
260 | Insert |
NATINY_INPUT_KEY_DELETE |
261 | Delete |
NATINY_INPUT_KEY_RIGHT |
262 | Arrow right |
NATINY_INPUT_KEY_LEFT |
263 | Arrow left |
NATINY_INPUT_KEY_DOWN |
264 | Arrow down |
NATINY_INPUT_KEY_UP |
265 | Arrow up |
NATINY_INPUT_KEY_PAGE_UP |
266 | Page Up |
NATINY_INPUT_KEY_PAGE_DOWN |
267 | Page Down |
NATINY_INPUT_KEY_HOME |
268 | Home |
NATINY_INPUT_KEY_END |
269 | End |
| Constant | Value | Key |
|---|---|---|
NATINY_INPUT_KEY_CAPS_LOCK |
280 | Caps Lock |
NATINY_INPUT_KEY_SCROLL_LOCK |
281 | Scroll Lock |
NATINY_INPUT_KEY_NUM_LOCK |
282 | Num Lock |
NATINY_INPUT_KEY_PRINT_SCREEN |
283 | Print Screen |
NATINY_INPUT_KEY_PAUSE |
284 | Pause / Break |
| Constant | Value | Key |
|---|---|---|
NATINY_INPUT_KEY_F1 |
290 | F1 |
NATINY_INPUT_KEY_F2 |
291 | F2 |
NATINY_INPUT_KEY_F3 |
292 | F3 |
NATINY_INPUT_KEY_F4 |
293 | F4 |
NATINY_INPUT_KEY_F5 |
294 | F5 |
NATINY_INPUT_KEY_F6 |
295 | F6 |
NATINY_INPUT_KEY_F7 |
296 | F7 |
NATINY_INPUT_KEY_F8 |
297 | F8 |
NATINY_INPUT_KEY_F9 |
298 | F9 |
NATINY_INPUT_KEY_F10 |
299 | F10 |
NATINY_INPUT_KEY_F11 |
300 | F11 |
NATINY_INPUT_KEY_F12 |
301 | F12 |
NATINY_INPUT_KEY_F13 |
302 | F13 |
NATINY_INPUT_KEY_F14 |
303 | F14 |
NATINY_INPUT_KEY_F15 |
304 | F15 |
NATINY_INPUT_KEY_F16 |
305 | F16 |
NATINY_INPUT_KEY_F17 |
306 | F17 |
NATINY_INPUT_KEY_F18 |
307 | F18 |
NATINY_INPUT_KEY_F19 |
308 | F19 |
NATINY_INPUT_KEY_F20 |
309 | F20 |
NATINY_INPUT_KEY_F21 |
310 | F21 |
NATINY_INPUT_KEY_F22 |
311 | F22 |
NATINY_INPUT_KEY_F23 |
312 | F23 |
NATINY_INPUT_KEY_F24 |
313 | F24 |
NATINY_INPUT_KEY_F25 |
314 | F25 |
| Constant | Value | Key |
|---|---|---|
NATINY_INPUT_KEY_KP_0 |
320 | Keypad 0 |
NATINY_INPUT_KEY_KP_1 |
321 | Keypad 1 |
NATINY_INPUT_KEY_KP_2 |
322 | Keypad 2 |
NATINY_INPUT_KEY_KP_3 |
323 | Keypad 3 |
NATINY_INPUT_KEY_KP_4 |
324 | Keypad 4 |
NATINY_INPUT_KEY_KP_5 |
325 | Keypad 5 |
NATINY_INPUT_KEY_KP_6 |
326 | Keypad 6 |
NATINY_INPUT_KEY_KP_7 |
327 | Keypad 7 |
NATINY_INPUT_KEY_KP_8 |
328 | Keypad 8 |
NATINY_INPUT_KEY_KP_9 |
329 | Keypad 9 |
NATINY_INPUT_KEY_KP_DECIMAL |
330 | Keypad . |
NATINY_INPUT_KEY_KP_DIVIDE |
331 | Keypad / |
NATINY_INPUT_KEY_KP_MULTIPLY |
332 | Keypad * |
NATINY_INPUT_KEY_KP_SUBTRACT |
333 | Keypad - |
NATINY_INPUT_KEY_KP_ADD |
334 | Keypad + |
NATINY_INPUT_KEY_KP_ENTER |
335 | Keypad Enter |
NATINY_INPUT_KEY_KP_EQUAL |
336 | Keypad = |
| Constant | Value | Key |
|---|---|---|
NATINY_INPUT_KEY_LEFT_SHIFT |
340 | Left Shift |
NATINY_INPUT_KEY_LEFT_CONTROL |
341 | Left Ctrl |
NATINY_INPUT_KEY_LEFT_ALT |
342 | Left Alt |
NATINY_INPUT_KEY_LEFT_SUPER |
343 | Left Super / Win / Cmd |
NATINY_INPUT_KEY_RIGHT_SHIFT |
344 | Right Shift |
NATINY_INPUT_KEY_RIGHT_CONTROL |
345 | Right Ctrl |
NATINY_INPUT_KEY_RIGHT_ALT |
346 | Right Alt / AltGr |
NATINY_INPUT_KEY_RIGHT_SUPER |
347 | Right Super / Win / Cmd |
NATINY_INPUT_KEY_MENU |
348 | Menu / context |
| Constant | Value | Key |
|---|---|---|
NATINY_INPUT_MOUSE_BUTTON_LEFT |
0 | Left button |
NATINY_INPUT_MOUSE_BUTTON_RIGHT |
1 | Right button |
NATINY_INPUT_MOUSE_BUTTON_MIDDLE |
2 | Middle button - the wheel |
NATINY_INPUT_MOUSE_BUTTON_4 |
3 | Fourth button |
NATINY_INPUT_MOUSE_BUTTON_5 |
4 | Fifth button |
NATINY_INPUT_MOUSE_BUTTON_6 |
5 | Sixth button |
NATINY_INPUT_MOUSE_BUTTON_7 |
6 | Seventh button |
NATINY_INPUT_MOUSE_BUTTON_8 |
7 | Eighth button |
NATINY_INPUT_KEY_A is 65 and not 0. That is a physical key on a US
layout, not the character it types - for text, read
natiny_input_get_char_at instead, which gives what the keyboard's
own layout produced.keyCode is translated to the same numbers, so a
constant means the same key everywhere.keyCode of its
own for them: NATINY_INPUT_KEY_KP_EQUAL, NATINY_INPUT_KEY_F25,
NATINY_INPUT_KEY_WORLD_1 and NATINY_INPUT_KEY_WORLD_2.NATINY_INPUT_KEY_LEFT_SHIFT is true
for either Shift, and NATINY_INPUT_KEY_RIGHT_SHIFT never fires.
NATINY_INPUT_KEY_KP_ENTER arrives as NATINY_INPUT_KEY_ENTER for the
same reason.NATINY_INPUT_KEY_PRINT_SCREEN reaches a desktop window on release only,
which is what the operating system passes on.Every key and mouse button the engine knows has a name on natiny.input, so
nothing has to be written as a bare number:
if natiny.input.get_key_down(natiny.input.KEY_W) then
y = y - speed * dt
end
if natiny.input.get_mouse_button_pressed(natiny.input.MOUSE_BUTTON_LEFT) then
fire()
end
The names are what get_key_down, get_key_pressed, get_key_released and the get_mouse_button_down functions take. Lifting them into locals once, outside the loop, keeps the frame's table lookups down:
local KEY_W, KEY_S = natiny.input.KEY_W, natiny.input.KEY_S
local KEY_A, KEY_D = natiny.input.KEY_A, natiny.input.KEY_D
A key's value is a plain number, the same number on every platform, and the C
API spells the same constant NATINY_INPUT_KEY_W. Values run to 348, well
inside the 512 the engine tracks; anything outside that range simply reads as
not pressed rather than an error.
| Constant | Value | Key |
|---|---|---|
natiny.input.KEY_SPACE |
32 | Space |
natiny.input.KEY_APOSTROPHE |
39 | ' |
natiny.input.KEY_COMMA |
44 | , |
natiny.input.KEY_MINUS |
45 | - |
natiny.input.KEY_PERIOD |
46 | . |
natiny.input.KEY_SLASH |
47 | / |
natiny.input.KEY_0 |
48 | 0 |
natiny.input.KEY_1 |
49 | 1 |
natiny.input.KEY_2 |
50 | 2 |
natiny.input.KEY_3 |
51 | 3 |
natiny.input.KEY_4 |
52 | 4 |
natiny.input.KEY_5 |
53 | 5 |
natiny.input.KEY_6 |
54 | 6 |
natiny.input.KEY_7 |
55 | 7 |
natiny.input.KEY_8 |
56 | 8 |
natiny.input.KEY_9 |
57 | 9 |
natiny.input.KEY_SEMICOLON |
59 | ; |
natiny.input.KEY_EQUAL |
61 | = |
natiny.input.KEY_A |
65 | A |
natiny.input.KEY_B |
66 | B |
natiny.input.KEY_C |
67 | C |
natiny.input.KEY_D |
68 | D |
natiny.input.KEY_E |
69 | E |
natiny.input.KEY_F |
70 | F |
natiny.input.KEY_G |
71 | G |
natiny.input.KEY_H |
72 | H |
natiny.input.KEY_I |
73 | I |
natiny.input.KEY_J |
74 | J |
natiny.input.KEY_K |
75 | K |
natiny.input.KEY_L |
76 | L |
natiny.input.KEY_M |
77 | M |
natiny.input.KEY_N |
78 | N |
natiny.input.KEY_O |
79 | O |
natiny.input.KEY_P |
80 | P |
natiny.input.KEY_Q |
81 | Q |
natiny.input.KEY_R |
82 | R |
natiny.input.KEY_S |
83 | S |
natiny.input.KEY_T |
84 | T |
natiny.input.KEY_U |
85 | U |
natiny.input.KEY_V |
86 | V |
natiny.input.KEY_W |
87 | W |
natiny.input.KEY_X |
88 | X |
natiny.input.KEY_Y |
89 | Y |
natiny.input.KEY_Z |
90 | Z |
natiny.input.KEY_LEFT_BRACKET |
91 | [ |
natiny.input.KEY_BACKSLASH |
92 | \ |
natiny.input.KEY_RIGHT_BRACKET |
93 | ] |
natiny.input.KEY_GRAVE_ACCENT |
96 | ` |
natiny.input.KEY_WORLD_1 |
161 | Non-US key #1 |
natiny.input.KEY_WORLD_2 |
162 | Non-US key #2 |
| Constant | Value | Key |
|---|---|---|
natiny.input.KEY_ESCAPE |
256 | Escape |
natiny.input.KEY_ENTER |
257 | Enter / Return |
natiny.input.KEY_TAB |
258 | Tab |
natiny.input.KEY_BACKSPACE |
259 | Backspace |
natiny.input.KEY_INSERT |
260 | Insert |
natiny.input.KEY_DELETE |
261 | Delete |
natiny.input.KEY_RIGHT |
262 | Arrow right |
natiny.input.KEY_LEFT |
263 | Arrow left |
natiny.input.KEY_DOWN |
264 | Arrow down |
natiny.input.KEY_UP |
265 | Arrow up |
natiny.input.KEY_PAGE_UP |
266 | Page Up |
natiny.input.KEY_PAGE_DOWN |
267 | Page Down |
natiny.input.KEY_HOME |
268 | Home |
natiny.input.KEY_END |
269 | End |
| Constant | Value | Key |
|---|---|---|
natiny.input.KEY_CAPS_LOCK |
280 | Caps Lock |
natiny.input.KEY_SCROLL_LOCK |
281 | Scroll Lock |
natiny.input.KEY_NUM_LOCK |
282 | Num Lock |
natiny.input.KEY_PRINT_SCREEN |
283 | Print Screen |
natiny.input.KEY_PAUSE |
284 | Pause / Break |
| Constant | Value | Key |
|---|---|---|
natiny.input.KEY_F1 |
290 | F1 |
natiny.input.KEY_F2 |
291 | F2 |
natiny.input.KEY_F3 |
292 | F3 |
natiny.input.KEY_F4 |
293 | F4 |
natiny.input.KEY_F5 |
294 | F5 |
natiny.input.KEY_F6 |
295 | F6 |
natiny.input.KEY_F7 |
296 | F7 |
natiny.input.KEY_F8 |
297 | F8 |
natiny.input.KEY_F9 |
298 | F9 |
natiny.input.KEY_F10 |
299 | F10 |
natiny.input.KEY_F11 |
300 | F11 |
natiny.input.KEY_F12 |
301 | F12 |
natiny.input.KEY_F13 |
302 | F13 |
natiny.input.KEY_F14 |
303 | F14 |
natiny.input.KEY_F15 |
304 | F15 |
natiny.input.KEY_F16 |
305 | F16 |
natiny.input.KEY_F17 |
306 | F17 |
natiny.input.KEY_F18 |
307 | F18 |
natiny.input.KEY_F19 |
308 | F19 |
natiny.input.KEY_F20 |
309 | F20 |
natiny.input.KEY_F21 |
310 | F21 |
natiny.input.KEY_F22 |
311 | F22 |
natiny.input.KEY_F23 |
312 | F23 |
natiny.input.KEY_F24 |
313 | F24 |
natiny.input.KEY_F25 |
314 | F25 |
| Constant | Value | Key |
|---|---|---|
natiny.input.KEY_KP_0 |
320 | Keypad 0 |
natiny.input.KEY_KP_1 |
321 | Keypad 1 |
natiny.input.KEY_KP_2 |
322 | Keypad 2 |
natiny.input.KEY_KP_3 |
323 | Keypad 3 |
natiny.input.KEY_KP_4 |
324 | Keypad 4 |
natiny.input.KEY_KP_5 |
325 | Keypad 5 |
natiny.input.KEY_KP_6 |
326 | Keypad 6 |
natiny.input.KEY_KP_7 |
327 | Keypad 7 |
natiny.input.KEY_KP_8 |
328 | Keypad 8 |
natiny.input.KEY_KP_9 |
329 | Keypad 9 |
natiny.input.KEY_KP_DECIMAL |
330 | Keypad . |
natiny.input.KEY_KP_DIVIDE |
331 | Keypad / |
natiny.input.KEY_KP_MULTIPLY |
332 | Keypad * |
natiny.input.KEY_KP_SUBTRACT |
333 | Keypad - |
natiny.input.KEY_KP_ADD |
334 | Keypad + |
natiny.input.KEY_KP_ENTER |
335 | Keypad Enter |
natiny.input.KEY_KP_EQUAL |
336 | Keypad = |
| Constant | Value | Key |
|---|---|---|
natiny.input.KEY_LEFT_SHIFT |
340 | Left Shift |
natiny.input.KEY_LEFT_CONTROL |
341 | Left Ctrl |
natiny.input.KEY_LEFT_ALT |
342 | Left Alt |
natiny.input.KEY_LEFT_SUPER |
343 | Left Super / Win / Cmd |
natiny.input.KEY_RIGHT_SHIFT |
344 | Right Shift |
natiny.input.KEY_RIGHT_CONTROL |
345 | Right Ctrl |
natiny.input.KEY_RIGHT_ALT |
346 | Right Alt / AltGr |
natiny.input.KEY_RIGHT_SUPER |
347 | Right Super / Win / Cmd |
natiny.input.KEY_MENU |
348 | Menu / context |
| Constant | Value | Key |
|---|---|---|
natiny.input.MOUSE_BUTTON_LEFT |
0 | Left button |
natiny.input.MOUSE_BUTTON_RIGHT |
1 | Right button |
natiny.input.MOUSE_BUTTON_MIDDLE |
2 | Middle button - the wheel |
natiny.input.MOUSE_BUTTON_4 |
3 | Fourth button |
natiny.input.MOUSE_BUTTON_5 |
4 | Fifth button |
natiny.input.MOUSE_BUTTON_6 |
5 | Sixth button |
natiny.input.MOUSE_BUTTON_7 |
6 | Seventh button |
natiny.input.MOUSE_BUTTON_8 |
7 | Eighth button |
KEY_A is 65
and not 0. That is a physical key on a US layout, not the character it
types - for text, read get_char_at instead, which gives what the
keyboard's own layout produced.keyCode is translated to the same numbers, so a name
means the same key everywhere.keyCode of its
own for them: KEY_KP_EQUAL, KEY_F25, KEY_WORLD_1 and KEY_WORLD_2.KEY_LEFT_SHIFT is true for either Shift,
and KEY_RIGHT_SHIFT never fires. KEY_KP_ENTER arrives as KEY_ENTER
for the same reason.KEY_PRINT_SCREEN reaches a desktop window on release only, which is what
the operating system passes on.A fragment shader usually produces one colour image. Some rendering passes need several descriptions of the same pixel: its colour, normal, material ID, or motion. Drawing the geometry again for every image repeats work.
Multiple render targets (MRT) let one draw write those values to several surfaces at once. In this guide, one full-screen draw will produce two different gradients, which we will display side by side.
This guide builds on rendering to a surface. Read that first if binding and displaying a surface are not familiar yet.
Each bound surface occupies a numbered target slot. The fragment shader has a matching output for each slot:
one fragment shader invocation
|
+-- COLOR -> slot 0 -> first surface
+-- TARGET1 -> slot 1 -> second surface
Both values describe the same fragment and are produced by the same draw. MRT does not feed one surface into the next; that would require separate passes.
NSL exposes four outputs:
| Shader output | Target slot |
|---|---|
COLOR, or TARGET0 |
0 |
TARGET1 |
1 |
TARGET2 |
2 |
TARGET3 |
3 |
COLOR and TARGET0 are two names for the one output, not two outputs. Write
whichever reads better: COLOR in a shader that produces a single image, and
TARGET0 in a pass like the one below, where numbering every output the same
way makes the slot each one fills obvious.
Create data/mrt_preview.nsl:
attribute vec3 position;
attribute vec2 texcoord0;
varying vec2 uv;
uniform mat4 mtx_worldviewproj;
void vertex()
{
uv = texcoord0;
POSITION = mtx_worldviewproj * vec4(position, 1.0);
}
void fragment()
{
COLOR = vec4(uv.x, 0.2, uv.y, 1.0);
TARGET1 = vec4(uv.y, uv.x, 0.1, 1.0);
}
COLOR and TARGET1 deliberately swap the gradient channels. When both
images appear different, we will know that each output reached its intended
surface rather than seeing the same surface twice.
Load the shader and create the material that will run it:
NatinyResource source = natiny_resource_load("data/mrt_preview.nsl");
NatinyShader shader = natiny_shader_load(source);
natiny_resource_destroy(source);
NatinyMaterial material = natiny_material_create(shader);
Check every returned handle before continuing. The source resource is no
longer needed after natiny_shader_load, but the
shader and material must stay alive for every frame that uses them.
An MRT pass shares one viewport across all its outputs, so every attached surface must have exactly the same dimensions:
NatinySurface first = natiny_surface_create(
320, 180, NATINY_FORMAT_COLOR, false);
NatinySurface second = natiny_surface_create(
320, 180, NATINY_FORMAT_COLOR, false);
These gradients are 2D colour images, so neither surface needs a depth buffer. For a 3D pass, put the depth buffer on slot 0. Slot 0 controls the viewport, 2D projection, and depth attachment for the whole pass.
If the dimensions differ, binding the later surface is refused and the engine prints a message. Create and resize MRT surfaces as a group.
Inside a bound window, attach both surfaces before the first draw:
natiny_surface_bind(first, 0);
natiny_surface_bind(second, 1);
natiny_render_clear(0.0f, 0.0f, 0.0f, 1.0f);
natiny_material_bind(material);
natiny_render_cover();
natiny_material_unbind();
natiny_surface_unbind();
natiny_render_cover covers the complete target.
Every generated fragment runs the shader once: COLOR fills first, while
TARGET1 fills second.
Calling natiny_surface_unbind() once releases all target slots and returns
drawing to the window; there is no per-slot unbind.
The state after this step is:
first = COLOR gradient
second = TARGET1 gradient
current target = window
Now that the MRT pass is finished, draw the surfaces like ordinary images:
natiny_render_clear(0.04f, 0.05f, 0.08f, 1.0f);
natiny_render_set_color(1.0f, 1.0f, 1.0f, 1.0f);
natiny_render_surface(first, 20.0f, 20.0f, 320.0f, 180.0f);
natiny_render_surface(second, 360.0f, 20.0f, 320.0f, 180.0f);
You should see two differently coloured gradients next to each other. If the
second rectangle is black, check that second was bound to slot 1 before the
draw and that the shader writes TARGET1. If both rectangles are wrong or
empty, check that the window was bound and that the MRT pass was unbound before
the surfaces were displayed.
The entire frame now has two distinct passes: first produce both textures, then read them while drawing the window.
natiny_window_bind(window);
/* Pass 1: one draw writes two surfaces. */
natiny_surface_bind(first, 0);
natiny_surface_bind(second, 1);
natiny_render_clear(0.0f, 0.0f, 0.0f, 1.0f);
natiny_material_bind(material);
natiny_render_cover();
natiny_material_unbind();
natiny_surface_unbind();
/* Pass 2: display the completed surfaces in the window. */
natiny_render_clear(0.04f, 0.05f, 0.08f, 1.0f);
natiny_render_set_color(1.0f, 1.0f, 1.0f, 1.0f);
natiny_render_surface(first, 20.0f, 20.0f, 320.0f, 180.0f);
natiny_render_surface(second, 360.0f, 20.0f, 320.0f, 180.0f);
natiny_window_unbind();
Never sample or display an MRT surface before natiny_surface_unbind(). At
that point the GPU is still using it as an output, not as an input for a later
pass.
A deferred renderer first records scene properties, then reads them in a lighting pass. The first working example used gradients; this fragment shader applies the same mechanism to colour and a world-space normal:
attribute vec3 position;
attribute vec4 color0;
attribute vec3 normal;
varying vec4 vertex_color;
varying vec3 world_normal;
uniform mat4 mtx_worldviewproj;
uniform mat4 mtx_world;
void vertex()
{
vertex_color = color0;
world_normal = mat3(mtx_world) * normal;
POSITION = mtx_worldviewproj * vec4(position, 1.0);
}
void fragment()
{
vec3 n = normalize(world_normal);
TARGET0 = vertex_color;
TARGET1 = vec4(n * 0.5 + 0.5, 1.0);
}
TARGET0 here is the same output the first shader called COLOR. A pass that
fills a G-buffer usually writes it this way: the outputs then read as a
numbered set, and each one's slot is the number in its name.
Normal components range from -1 to 1. Multiplying by 0.5 and adding
0.5 maps that range to 0 through 1, so the normal can be inspected as a
colour image. For later lighting, use a floating-point surface to preserve
more precision:
NatinySurface albedo = natiny_surface_create(
width, height, NATINY_FORMAT_RGBA8, true);
NatinySurface normals = natiny_surface_create(
width, height, NATINY_FORMAT_RGBA16F, false);
Only albedo needs depth because it occupies slot 0 and supplies depth for the
entire pass. Both surfaces must still use the same width and height.
When the pass is no longer needed, release its resources while the engine is still running:
natiny_surface_destroy(first);
natiny_surface_destroy(second);
natiny_material_destroy(material);
natiny_shader_destroy(shader);
Destroy the material before its shader because the material refers to that shader. Do not use either surface after destroying it.
MRT is a good fit when every output describes the same rendered fragments. It saves repeated geometry work, but each additional surface still costs texture memory and write bandwidth. When an effect needs the completed output of an earlier effect, finish the first pass and start another instead.
The model to keep is: bind equal-sized surfaces to numbered slots, write the matching shader outputs, draw once, then unbind before reading any result. That pattern scales from the two visible gradients here to deferred-rendering buffers containing colour, normals, positions, and material data.
A fragment shader usually produces one colour image. Some rendering passes need several descriptions of the same pixel: its colour, normal, material ID, or motion. Drawing the geometry again for every image repeats work.
Multiple render targets (MRT) let one draw write those values to several surfaces at once. In this guide, one full-screen draw will produce two different gradients, which we will display side by side.
This guide builds on rendering to a surface. Read that first if binding and displaying a surface are not familiar yet.
Each bound surface occupies a numbered target slot. The fragment shader has a matching output for each slot:
one fragment shader invocation
|
+-- COLOR -> slot 0 -> first surface
+-- TARGET1 -> slot 1 -> second surface
Both values describe the same fragment and are produced by the same draw. MRT does not feed one surface into the next; that would require separate passes.
NSL exposes four outputs:
| Shader output | Target slot |
|---|---|
COLOR, or TARGET0 |
0 |
TARGET1 |
1 |
TARGET2 |
2 |
TARGET3 |
3 |
COLOR and TARGET0 are two names for the one output, not two outputs. Write
whichever reads better: COLOR in a shader that produces a single image, and
TARGET0 in a pass like the one below, where numbering every output the same
way makes the slot each one fills obvious.
Create data/mrt_preview.nsl:
attribute vec3 position;
attribute vec2 texcoord0;
varying vec2 uv;
uniform mat4 mtx_worldviewproj;
void vertex()
{
uv = texcoord0;
POSITION = mtx_worldviewproj * vec4(position, 1.0);
}
void fragment()
{
COLOR = vec4(uv.x, 0.2, uv.y, 1.0);
TARGET1 = vec4(uv.y, uv.x, 0.1, 1.0);
}
COLOR and TARGET1 deliberately swap the gradient channels. When both
images appear different, we will know that each output reached its intended
surface rather than seeing the same surface twice.
Load the shader and create the material that will run it:
local source = natiny.resource.load("data/mrt_preview.nsl")
local shader = natiny.shader.load(source)
natiny.resource.destroy(source)
local material = natiny.material.create(shader)
The source resource is no longer needed after
shader.load, but the shader and material must stay
alive for every frame that uses them.
An MRT pass shares one viewport across all its outputs, so every attached surface must have exactly the same dimensions:
local first = natiny.surface.create(
320, 180, natiny.surface.COLOR, false)
local second = natiny.surface.create(
320, 180, natiny.surface.COLOR, false)
These gradients are 2D colour images, so neither surface needs a depth buffer. For a 3D pass, put the depth buffer on slot 0. Slot 0 controls the viewport, 2D projection, and depth attachment for the whole pass.
If the dimensions differ, binding the later surface is refused and the engine prints a message. Create and resize MRT surfaces as a group.
Inside a bound window, attach both surfaces before the first draw:
natiny.surface.bind(first, 0)
natiny.surface.bind(second, 1)
natiny.render.clear(0, 0, 0, 1)
natiny.material.bind(material, natiny.render.cover)
natiny.surface.unbind()
render.cover covers the complete target. Every
generated fragment runs the shader once: COLOR fills first, while
TARGET1 fills second.
Use explicit binds for an MRT pass. Calling surface.unbind() once releases
all target slots and returns drawing to the window; there is no per-slot
unbind.
The state after this step is:
first = COLOR gradient
second = TARGET1 gradient
current target = window
Now that the MRT pass is finished, draw the surfaces like ordinary images:
natiny.render.clear(0.04, 0.05, 0.08, 1)
natiny.render.set_color(1, 1, 1, 1)
natiny.render.surface(first, 20, 20, 320, 180)
natiny.render.surface(second, 360, 20, 320, 180)
You should see two differently coloured gradients next to each other. If the
second rectangle is black, check that second was bound to slot 1 before the
draw and that the shader writes TARGET1. If both rectangles are wrong or
empty, check that the window was bound and that the MRT pass was unbound before
the surfaces were displayed.
The entire frame now has two distinct passes: first produce both textures, then read them while drawing the window.
natiny.window.bind(window, function()
-- Pass 1: one draw writes two surfaces.
natiny.surface.bind(first, 0)
natiny.surface.bind(second, 1)
natiny.render.clear(0, 0, 0, 1)
natiny.material.bind(material, natiny.render.cover)
natiny.surface.unbind()
-- Pass 2: display the completed surfaces in the window.
natiny.render.clear(0.04, 0.05, 0.08, 1)
natiny.render.set_color(1, 1, 1, 1)
natiny.render.surface(first, 20, 20, 320, 180)
natiny.render.surface(second, 360, 20, 320, 180)
end)
Never sample or display an MRT surface before surface.unbind(). At that
point the GPU is still using it as an output, not as an input for a later
pass.
A deferred renderer first records scene properties, then reads them in a lighting pass. The first working example used gradients; this fragment shader applies the same mechanism to colour and a world-space normal:
attribute vec3 position;
attribute vec4 color0;
attribute vec3 normal;
varying vec4 vertex_color;
varying vec3 world_normal;
uniform mat4 mtx_worldviewproj;
uniform mat4 mtx_world;
void vertex()
{
vertex_color = color0;
world_normal = mat3(mtx_world) * normal;
POSITION = mtx_worldviewproj * vec4(position, 1.0);
}
void fragment()
{
vec3 n = normalize(world_normal);
TARGET0 = vertex_color;
TARGET1 = vec4(n * 0.5 + 0.5, 1.0);
}
TARGET0 here is the same output the first shader called COLOR. A pass that
fills a G-buffer usually writes it this way: the outputs then read as a
numbered set, and each one's slot is the number in its name.
Normal components range from -1 to 1. Multiplying by 0.5 and adding
0.5 maps that range to 0 through 1, so the normal can be inspected as a
colour image. For later lighting, use a floating-point surface to preserve
more precision:
local albedo = natiny.surface.create(
w, h, natiny.surface.RGBA8, true)
local normals = natiny.surface.create(
w, h, natiny.surface.RGBA16F, false)
Only albedo needs depth because it occupies slot 0 and supplies depth for the
entire pass. Both surfaces must still use the same w and h.
When the pass is no longer needed, release its resources while the engine is still running:
natiny.surface.destroy(first)
natiny.surface.destroy(second)
natiny.material.destroy(material)
natiny.shader.destroy(shader)
Destroy the material before its shader because the material refers to that shader. Do not use either surface after destroying it.
MRT is a good fit when every output describes the same rendered fragments. It saves repeated geometry work, but each additional surface still costs texture memory and write bandwidth. When an effect needs the completed output of an earlier effect, finish the first pass and start another instead.
The model to keep is: bind equal-sized surfaces to numbered slots, write the matching shader outputs, draw once, then unbind before reading any result. That pattern scales from the two visible gradients here to deferred-rendering buffers containing colour, normals, positions, and material data.
Drawing normally goes straight to a window. Sometimes you need the finished picture before it reaches the window: a minimap, a security camera, a mirror, or the input for a post-processing effect.
In this guide, we will draw a moving circle into a 320 by 180 off-screen surface, then enlarge that image to fill a 960 by 540 window.
A render target is the image that receives drawing commands. The window is the usual render target. A surface is an off-screen render target whose colour image can later be drawn like a texture.
Our frame will follow this path:
shapes -> 320x180 surface -> window
Binding the surface temporarily replaces the window as the current target. Unbinding it returns drawing to the window. This order matters: a surface cannot be read as an image while the same surface is still receiving drawing commands.
Create the surface once, after creating the window:
NatinySurface scene = natiny_surface_create(
320, 180, NATINY_FORMAT_COLOR, true);
The dimensions are the resolution of the stored image, not its final size on
screen. We use a low resolution so the difference is easy to see when the
result is enlarged. Check that the returned handle is not 0 before using it.
The window must be bound first because the surface records its work inside the
window's active frame. Inside that frame, bind scene to target slot 0 and
draw the off-screen picture:
natiny_surface_bind(scene, 0);
natiny_render_clear(0.08f, 0.10f, 0.16f, 1.0f);
natiny_render_set_color(0.96f, 0.67f, 0.30f, 1.0f);
natiny_render_circle(160.0f, 90.0f, 24.0f);
natiny_surface_unbind();
natiny_surface_unbind finishes the off-screen
pass and restores the window target. At this point, scene contains a dark
background and an orange circle.
Now clear the window and draw the completed surface across it:
natiny_render_clear(0.03f, 0.04f, 0.06f, 1.0f);
natiny_render_set_color(1.0f, 1.0f, 1.0f, 1.0f);
natiny_render_surface(scene, 0.0f, 0.0f, 960.0f, 540.0f);
Keep the draw colour white. Like other 2D images, a surface is multiplied by the current draw colour; another colour would tint the result.
The target state has now changed twice:
surface bound: shapes -> scene
surface released: scene -> window
If the window is black, check that natiny_window_bind surrounds both parts.
If the surface appears empty, check that natiny_surface_unbind runs before
natiny_render_surface.
This program draws the surface every frame and then stretches it to the current window size. You should see an orange circle moving across a deliberately low-resolution image.
#include "natiny.h"
#include <math.h>
#include <stddef.h>
static NatinyWindow window;
static NatinySurface scene;
static float elapsed;
static void frame(float dt, void* userdata)
{
float x;
(void)userdata;
elapsed += dt;
x = 160.0f + cosf(elapsed * 2.0f) * 90.0f;
natiny_window_bind(window);
natiny_surface_bind(scene, 0);
natiny_render_clear(0.08f, 0.10f, 0.16f, 1.0f);
natiny_render_set_color(0.96f, 0.67f, 0.30f, 1.0f);
natiny_render_circle(x, 90.0f, 24.0f);
natiny_surface_unbind();
natiny_render_clear(0.03f, 0.04f, 0.06f, 1.0f);
natiny_render_set_color(1.0f, 1.0f, 1.0f, 1.0f);
natiny_render_surface(
scene, 0.0f, 0.0f,
(float)natiny_window_get_width(window),
(float)natiny_window_get_height(window));
natiny_window_unbind();
}
int main(void)
{
if (!natiny_backend_init(NATINY_BACKEND_AUTO)) {
return 1;
}
window = natiny_window_create("A tiny off-screen world", 960, 540);
scene = natiny_surface_create(
320, 180, NATINY_FORMAT_COLOR, true);
if (!window || !scene) {
natiny_backend_shutdown();
return 1;
}
natiny_backend_loop(frame, NULL);
natiny_surface_destroy(scene);
natiny_backend_shutdown();
return 0;
}
The surface is redrawn every frame before it is displayed, so the window always receives the latest position of the circle.
The example creates a window-compatible colour surface with a depth buffer:
NatinySurface scene = natiny_surface_create(
320, 180, NATINY_FORMAT_COLOR, true);
A depth buffer decides which of overlapping 3D fragments is in front. The 2D example does not need one, so it can save that allocation:
NatinySurface scene = natiny_surface_create(
320, 180, NATINY_FORMAT_COLOR, false);
Choose another format only when the surface stores more than a displayable picture:
| Format | Use it for |
|---|---|
NATINY_FORMAT_COLOR |
A picture that will be drawn into a window |
NATINY_FORMAT_RGBA8 |
Four explicit 8-bit colour channels |
NATINY_FORMAT_RGBA16F |
HDR colour, normals, or positions |
NATINY_FORMAT_RGBA32F |
Data that needs full floating-point precision |
NATINY_FORMAT_R32F |
One floating-point value per pixel |
Floating-point formats use more memory and bandwidth. For an ordinary scene,
NATINY_FORMAT_COLOR is the useful default.
Our 320 by 180 surface intentionally stays small. A full-resolution post-processing effect usually needs to follow the window instead:
uint32_t w = (uint32_t)natiny_window_get_width(window);
uint32_t h = (uint32_t)natiny_window_get_height(window);
if (w > 0 && h > 0 &&
(w != natiny_surface_get_width(scene) ||
h != natiny_surface_get_height(scene))) {
natiny_surface_resize(scene, w, h);
}
The positive-size check avoids trying to resize to zero while a window is
minimized. natiny_surface_resize discards the
old pixels, so redraw the surface after resizing. The texture handles are not
affected: a resize replaces what is behind them, not the handles themselves.
natiny_render_surface is enough to display the
image. Post-processing shaders instead need the surface's colour texture:
NatinyTexture texture = natiny_surface_get_texture(scene);
natiny_material_set_texture(
material, "scene", texture,
NATINY_FILTER_LINEAR, NATINY_FILTER_LINEAR);
If the surface was created with a depth buffer, its depth texture is available separately:
NatinyTexture depth = natiny_surface_get_depth(scene);
Both texture handles belong to the surface. Do not destroy them. They stay valid for the life of the surface, a resize included - a material given one of them once never has to be told again.
Destroy the surface while the engine is still running, once no draw or material uses it:
natiny_surface_destroy(scene);
scene = 0;
The surface releases the colour texture and its optional depth texture with it.
You now have the complete render-to-texture model: bind a window, redirect drawing into a surface, finish that pass, then read the surface from a later pass. The same sequence powers minimaps, mirrors, portals, and post-processing. When one pass must produce several images at once, continue with multiple render targets.
Drawing normally goes straight to a window. Sometimes you need the finished picture before it reaches the window: a minimap, a security camera, a mirror, or the input for a post-processing effect.
In this guide, we will draw a moving circle into a 320 by 180 off-screen surface, then enlarge that image to fill a 960 by 540 window.
A render target is the image that receives drawing commands. The window is the usual render target. A surface is an off-screen render target whose colour image can later be drawn like a texture.
Our frame will follow this path:
shapes -> 320x180 surface -> window
Binding the surface temporarily replaces the window as the current target. When the surface is unbound, drawing returns to the window. This order matters: a surface cannot be read as an image while the same surface is still receiving drawing commands.
Create the surface once, after creating the window:
local scene = natiny.surface.create(320, 180)
The dimensions are the resolution of the stored image, not its final size on screen. We use a low resolution so the difference is easy to see when the result is enlarged.
The window must be bound first because the surface records its work inside the
window's active frame. Inside that frame, bind scene and draw the off-screen
picture:
natiny.surface.bind(scene, function()
natiny.render.clear(0.08, 0.10, 0.16, 1.0)
natiny.render.set_color(0.96, 0.67, 0.30, 1.0)
natiny.render.circle(160, 90, 24)
end)
The callback form of
surface.bind restores the window target when the
callback finishes. At this point, scene contains a dark background and an
orange circle.
Now clear the window and draw the completed surface across it:
natiny.render.clear(0.03, 0.04, 0.06, 1.0)
natiny.render.set_color(1, 1, 1, 1)
natiny.render.surface(scene, 0, 0, 960, 540)
Keep the draw colour white. Like other 2D images, a surface is multiplied by the current draw colour; another colour would tint the result.
The target state has now changed twice:
surface bound: shapes -> scene
surface released: scene -> window
If the window is black, check that natiny.window.bind surrounds both parts.
If drawing the surface produces an error or an empty result, check that the
surface.bind callback has already returned before render.surface runs.
Paste this program into the browser sandbox. You should see an orange circle moving across a deliberately low-resolution image stretched to fill the window.
local natiny = require("natiny")
assert(natiny.backend.init())
local window = natiny.window.create("A tiny off-screen world", 960, 540)
local scene = natiny.surface.create(320, 180)
local time = 0
natiny.backend.loop(function(dt)
time = time + dt
natiny.window.bind(window, function()
natiny.surface.bind(scene, function()
natiny.render.clear(0.08, 0.10, 0.16, 1.0)
natiny.render.set_color(0.96, 0.67, 0.30, 1.0)
natiny.render.circle(160 + math.cos(time * 2) * 90, 90, 24)
end)
natiny.render.clear(0.03, 0.04, 0.06, 1.0)
natiny.render.set_color(1, 1, 1, 1)
natiny.render.surface(
scene, 0, 0,
natiny.window.get_width(window),
natiny.window.get_height(window)
)
end)
end)
natiny.surface.destroy(scene)
natiny.backend.shutdown()
The surface is redrawn every frame before it is displayed, so the window always receives the latest position of the circle.
The short form used above creates a window-compatible colour surface with a depth buffer:
local scene = natiny.surface.create(320, 180)
A depth buffer decides which of overlapping 3D fragments is in front. The 2D example does not need one, so it can save that allocation:
local scene = natiny.surface.create(
320, 180, natiny.surface.COLOR, false)
Choose another format only when the surface stores more than a displayable picture:
| Format | Use it for |
|---|---|
COLOR |
A picture that will be drawn into a window |
RGBA8 |
Four explicit 8-bit colour channels |
RGBA16F |
HDR colour, normals, or positions |
RGBA32F |
Data that needs full floating-point precision |
R32F |
One floating-point value per pixel |
Floating-point formats use more memory and bandwidth. For an ordinary scene,
COLOR is the useful default.
Our 320 by 180 surface intentionally stays small. A full-resolution post-processing effect usually needs to follow the window instead:
local w = natiny.window.get_width(window)
local h = natiny.window.get_height(window)
if w > 0 and h > 0 and
(w ~= natiny.surface.get_width(scene) or
h ~= natiny.surface.get_height(scene)) then
natiny.surface.resize(scene, w, h)
end
The positive-size check avoids trying to resize to zero while a window is
minimized. surface.resize discards the old
pixels, so redraw the surface after resizing. The texture handles are not
affected: a resize replaces what is behind them, not the handles themselves.
render.surface is enough to display the image.
Post-processing shaders instead need the surface's colour texture:
local texture = natiny.surface.get_texture(scene)
natiny.material.set_texture(material, "scene", texture)
If the surface was created with a depth buffer, its depth texture is available separately:
local depth = natiny.surface.get_depth(scene)
Both texture handles belong to the surface. Do not destroy them. They stay valid for the life of the surface, a resize included - a material given one of them once never has to be told again.
Destroy the surface while the engine is still running, once no draw or material uses it:
natiny.surface.destroy(scene)
scene = nil
The surface releases the colour texture and its optional depth texture with it.
You now have the complete render-to-texture model: bind a window, redirect drawing into a surface, finish that pass, then read the surface from a later pass. The same sequence powers minimaps, mirrors, portals, and post-processing. When one pass must produce several images at once, continue with multiple render targets.
NSL is Natiny's GLSL-like shader language. A single .nsl file contains both
the vertex and fragment stages.
If you have written GLSL before, you already know almost all of the language:
vec3, mat4, swizzles, mix, normalize, texture, and the usual operators
work the way you expect. The stages use vertex() and fragment() entry
points.
This guide starts with the smallest useful shader and builds it up one idea at a time.
Create a file named solid.nsl:
attribute vec3 position;
uniform mat4 mtx_worldviewproj;
void vertex()
{
POSITION = mtx_worldviewproj * vec4(position, 1.0);
}
void fragment()
{
COLOR = vec4(0.65, 0.78, 0.22, 1.0);
}
This is a complete shader. It draws the geometry in a solid green color.
There are only four new ideas here:
attribute is data that arrives with each vertex.uniform is one value shared by the whole draw.POSITION is the final vertex position.COLOR is the final pixel color.mtx_worldviewproj is a name Natiny recognizes. The engine fills it before
every draw, so the same line positions a 2D shape or a 3D model correctly.
This is the basic shape of every NSL shader: declare the data you need, write
POSITION in vertex(), and write COLOR in fragment().
The vertex stage runs for vertices. The fragment stage runs for the pixels
between them. A varying carries a value from one to the other, interpolating
it smoothly across the surface.
Here is a shader that displays the color stored in each vertex:
attribute vec3 position;
attribute vec4 color0;
varying vec4 vertex_color;
uniform mat4 mtx_worldviewproj;
void vertex()
{
vertex_color = color0;
POSITION = mtx_worldviewproj * vec4(position, 1.0);
}
void fragment()
{
COLOR = vertex_color;
}
The name and type must match in both stages: vertex() writes vertex_color,
and fragment() reads it.
Textures add one more varying and one uniform:
attribute vec3 position;
attribute vec2 texcoord0;
varying vec2 uv;
uniform mat4 mtx_worldviewproj;
uniform sampler2D tex0;
void vertex()
{
uv = texcoord0;
POSITION = mtx_worldviewproj * vec4(position, 1.0);
}
void fragment()
{
COLOR = texture(tex0, uv);
}
texcoord0 contains the texture coordinates supplied by the mesh or 2D draw.
They travel through uv into the fragment stage, where texture() samples the
image.
Declare a texture as sampler2D and sample it with texture(). tex0 receives
the texture supplied by the draw unless the material assigns one explicitly.
Use the material texture API to assign a
texture and choose its filtering.
A custom uniform turns a hard-coded number into a material parameter. Add a
tint to the texture shader:
uniform vec4 tint;
void fragment()
{
COLOR = texture(tex0, uv) * tint;
}
Set tint by name through the material constant API:
material: my_material
name: "tint"
value: 1.0, 0.7, 0.5, 1.0
The exact call is shown in the linked API for the selected language. The important part is the contract: the name in your code and the name in the NSL file are the same.
Uniforms are also how you pass time, light colors, effect strength, or any other value that changes while the game runs. NSL does not hide these inputs; if a shader depends on something, it declares it.
The pieces above combine into the pattern used by Natiny's default material:
attribute vec3 position;
attribute vec2 texcoord0;
attribute vec4 color0;
varying vec2 uv;
varying vec4 vertex_color;
uniform mat4 mtx_worldviewproj;
uniform sampler2D tex0;
uniform vec4 tint;
void vertex()
{
uv = texcoord0;
vertex_color = color0;
POSITION = mtx_worldviewproj * vec4(position, 1.0);
}
void fragment()
{
vec4 texel = texture(tex0, uv);
COLOR = texel * vertex_color * tint;
}
It works for sprites, text, shapes, and models. From here, effects are ordinary
GLSL-style math: distort uv, move position, blend colors, sample another
texture, or write a lighting function.
Declare only the vertex attributes you use. Natiny recognizes these names:
| Attribute | Type | Contains |
|---|---|---|
position |
vec3 |
Vertex position |
texcoord0 |
vec2 |
Texture coordinate |
color0 |
vec4 |
Vertex color, including the current draw color |
normal |
vec3 |
Model normal; (0, 0, 1) for 2D geometry |
Attribute names are part of the contract with the engine. A different name,
such as vertex_position, cannot be filled and material creation will report
an error.
Natiny also fills several uniforms when you declare them with their recognized names:
| Uniform | Type | Contains |
|---|---|---|
mtx_world |
mat4 |
Object-to-world transform |
mtx_view |
mat4 |
Camera view transform |
mtx_proj |
mat4 |
Projection transform |
mtx_worldviewproj |
mat4 |
Complete object-to-clip transform |
mtx_normal |
mat4 |
Inverse-transpose worldview transform |
camera_pos |
vec4 |
Camera position in world space; w is 1 |
target_size |
vec4 |
Width, height, 1 / width, 1 / height |
Most shaders only need mtx_worldviewproj. The others are there when you begin
writing lighting, screen-space effects, or custom geometry passes.
NSL provides these stage values:
| Name | Where | What to do with it |
|---|---|---|
POSITION |
Vertex | Write the final clip-space position |
COLOR |
Fragment | Write the first render target |
TARGET0 |
Fragment | The same output as COLOR, under a numbered name |
TARGET1 ... TARGET3 |
Fragment | Write additional render targets |
FRAGCOORD |
Fragment | Read the pixel position from the target's top-left |
FRONT_FACING |
Fragment | Read whether the triangle faces forward |
VERTEX_INDEX |
Vertex | Read the current vertex index |
INSTANCE_INDEX |
Vertex | Read the current instance index |
DEPTH |
Fragment | Optionally write a custom depth |
For an ordinary material, you only need POSITION and COLOR. A pass that
writes several targets at once can spell the first one TARGET0 instead, so
every output it writes is numbered the same way - see
multiple render targets.
NSL supports the familiar scalar, vector, and matrix types:
bool int uint float
vec2 vec3 vec4
ivec2 ivec3 ivec4
uvec2 uvec3 uvec4
bvec2 bvec3 bvec4
mat2 mat3 mat4
Swizzles such as .xyz, .rgba, and .st work normally. So do control flow,
local variables, arrays, operators, and functions such as:
abs clamp cross dot floor fract length max min mix normalize pow
reflect refract sin smoothstep sqrt step texture textureLod
You can write your own functions too. Define a function before the first place that calls it; NSL does not use forward declarations.
vertex() and fragment() instead of
maintaining two shader files.#version,
layout, register numbers, binding numbers or uniform blocks.sampler2D. Sample them with
texture(texture_name, uv) and configure filtering on the material.count is an int, write
value * float(count). An unsuffixed literal such as 2 can adapt to its
context; 2u and 2.0 have fixed types.NSL does not currently have #include, #define, struct, or compute shaders.
NSL uses these coordinate conventions:
-1 to 1, with +1 at the top;0 at the near plane to 1 at the far plane;V = 0 is the top row;FRAGCOORD starts at the top-left of the render target.The matrices supplied by Natiny already follow these rules. You only need to
think about them when constructing POSITION yourself or implementing a
screen-space effect.
NSL diagnostics include the file, line, column, source line, and a pointer to the failing expression. Start with the first error; later errors are often a consequence of it.
The most common fixes are simple:
vertex() writes POSITION;fragment() writes COLOR;Shaders are compiled when you load them through the shader loader, so mistakes are reported before the material is drawn.
NSL is Natiny's GLSL-like shader language. A single .nsl file contains both
the vertex and fragment stages.
If you have written GLSL before, you already know almost all of the language:
vec3, mat4, swizzles, mix, normalize, texture, and the usual operators
work the way you expect. The stages use vertex() and fragment() entry
points.
This guide starts with the smallest useful shader and builds it up one idea at a time.
Create a file named solid.nsl:
attribute vec3 position;
uniform mat4 mtx_worldviewproj;
void vertex()
{
POSITION = mtx_worldviewproj * vec4(position, 1.0);
}
void fragment()
{
COLOR = vec4(0.65, 0.78, 0.22, 1.0);
}
This is a complete shader. It draws the geometry in a solid green color.
There are only four new ideas here:
attribute is data that arrives with each vertex.uniform is one value shared by the whole draw.POSITION is the final vertex position.COLOR is the final pixel color.mtx_worldviewproj is a name Natiny recognizes. The engine fills it before
every draw, so the same line positions a 2D shape or a 3D model correctly.
This is the basic shape of every NSL shader: declare the data you need, write
POSITION in vertex(), and write COLOR in fragment().
The vertex stage runs for vertices. The fragment stage runs for the pixels
between them. A varying carries a value from one to the other, interpolating
it smoothly across the surface.
Here is a shader that displays the color stored in each vertex:
attribute vec3 position;
attribute vec4 color0;
varying vec4 vertex_color;
uniform mat4 mtx_worldviewproj;
void vertex()
{
vertex_color = color0;
POSITION = mtx_worldviewproj * vec4(position, 1.0);
}
void fragment()
{
COLOR = vertex_color;
}
The name and type must match in both stages: vertex() writes vertex_color,
and fragment() reads it.
Textures add one more varying and one uniform:
attribute vec3 position;
attribute vec2 texcoord0;
varying vec2 uv;
uniform mat4 mtx_worldviewproj;
uniform sampler2D tex0;
void vertex()
{
uv = texcoord0;
POSITION = mtx_worldviewproj * vec4(position, 1.0);
}
void fragment()
{
COLOR = texture(tex0, uv);
}
texcoord0 contains the texture coordinates supplied by the mesh or 2D draw.
They travel through uv into the fragment stage, where texture() samples the
image.
Declare a texture as sampler2D and sample it with texture(). tex0 receives
the texture supplied by the draw unless the material assigns one explicitly.
Use the material texture API to assign a
texture and choose its filtering.
A custom uniform turns a hard-coded number into a material parameter. Add a
tint to the texture shader:
uniform vec4 tint;
void fragment()
{
COLOR = texture(tex0, uv) * tint;
}
Set tint by name through the material constant API:
material: my_material
name: "tint"
value: 1.0, 0.7, 0.5, 1.0
The exact call is shown in the linked API for the selected language. The important part is the contract: the name in your code and the name in the NSL file are the same.
Uniforms are also how you pass time, light colors, effect strength, or any other value that changes while the game runs. NSL does not hide these inputs; if a shader depends on something, it declares it.
The pieces above combine into the pattern used by Natiny's default material:
attribute vec3 position;
attribute vec2 texcoord0;
attribute vec4 color0;
varying vec2 uv;
varying vec4 vertex_color;
uniform mat4 mtx_worldviewproj;
uniform sampler2D tex0;
uniform vec4 tint;
void vertex()
{
uv = texcoord0;
vertex_color = color0;
POSITION = mtx_worldviewproj * vec4(position, 1.0);
}
void fragment()
{
vec4 texel = texture(tex0, uv);
COLOR = texel * vertex_color * tint;
}
It works for sprites, text, shapes, and models. From here, effects are ordinary
GLSL-style math: distort uv, move position, blend colors, sample another
texture, or write a lighting function.
Declare only the vertex attributes you use. Natiny recognizes these names:
| Attribute | Type | Contains |
|---|---|---|
position |
vec3 |
Vertex position |
texcoord0 |
vec2 |
Texture coordinate |
color0 |
vec4 |
Vertex color, including the current draw color |
normal |
vec3 |
Model normal; (0, 0, 1) for 2D geometry |
Attribute names are part of the contract with the engine. A different name,
such as vertex_position, cannot be filled and material creation will report
an error.
Natiny also fills several uniforms when you declare them with their recognized names:
| Uniform | Type | Contains |
|---|---|---|
mtx_world |
mat4 |
Object-to-world transform |
mtx_view |
mat4 |
Camera view transform |
mtx_proj |
mat4 |
Projection transform |
mtx_worldviewproj |
mat4 |
Complete object-to-clip transform |
mtx_normal |
mat4 |
Inverse-transpose worldview transform |
camera_pos |
vec4 |
Camera position in world space; w is 1 |
target_size |
vec4 |
Width, height, 1 / width, 1 / height |
Most shaders only need mtx_worldviewproj. The others are there when you begin
writing lighting, screen-space effects, or custom geometry passes.
NSL provides these stage values:
| Name | Where | What to do with it |
|---|---|---|
POSITION |
Vertex | Write the final clip-space position |
COLOR |
Fragment | Write the first render target |
TARGET0 |
Fragment | The same output as COLOR, under a numbered name |
TARGET1 ... TARGET3 |
Fragment | Write additional render targets |
FRAGCOORD |
Fragment | Read the pixel position from the target's top-left |
FRONT_FACING |
Fragment | Read whether the triangle faces forward |
VERTEX_INDEX |
Vertex | Read the current vertex index |
INSTANCE_INDEX |
Vertex | Read the current instance index |
DEPTH |
Fragment | Optionally write a custom depth |
For an ordinary material, you only need POSITION and COLOR. A pass that
writes several targets at once can spell the first one TARGET0 instead, so
every output it writes is numbered the same way - see
multiple render targets.
NSL supports the familiar scalar, vector, and matrix types:
bool int uint float
vec2 vec3 vec4
ivec2 ivec3 ivec4
uvec2 uvec3 uvec4
bvec2 bvec3 bvec4
mat2 mat3 mat4
Swizzles such as .xyz, .rgba, and .st work normally. So do control flow,
local variables, arrays, operators, and functions such as:
abs clamp cross dot floor fract length max min mix normalize pow
reflect refract sin smoothstep sqrt step texture textureLod
You can write your own functions too. Define a function before the first place that calls it; NSL does not use forward declarations.
vertex() and fragment() instead of
maintaining two shader files.#version,
layout, register numbers, binding numbers or uniform blocks.sampler2D. Sample them with
texture(texture_name, uv) and configure filtering on the material.count is an int, write
value * float(count). An unsuffixed literal such as 2 can adapt to its
context; 2u and 2.0 have fixed types.NSL does not currently have #include, #define, struct, or compute shaders.
NSL uses these coordinate conventions:
-1 to 1, with +1 at the top;0 at the near plane to 1 at the far plane;V = 0 is the top row;FRAGCOORD starts at the top-left of the render target.The matrices supplied by Natiny already follow these rules. You only need to
think about them when constructing POSITION yourself or implementing a
screen-space effect.
NSL diagnostics include the file, line, column, source line, and a pointer to the failing expression. Start with the first error; later errors are often a consequence of it.
The most common fixes are simple:
vertex() writes POSITION;fragment() writes COLOR;Shaders are compiled when you load them through the shader loader, so mistakes are reported before the material is drawn.