Scenes & Rendering
Scenes are essentially "containers" for model instances and lights.
Renderers take a Scene and a Camera and render them to a target (e.g. a Window).
Scenes#
You can add the same objects to multiple scenes simultaneously and render them at different times according to some logic.
Attempting to add an object already in a scene to that same scene again has no effect; it is an idempotent operation. The same applies to removing objects, even if they were never added to the scene in the first place.
Backdrops#
Scenes can include a backdrop; either a flat colour or an HDR image.
Use SetBackdrop() to set the backdrop to either a colour or a BackdropTexture containing a loaded HDR image. A BackdropTexture is a resource and can be created with the factory's AssetLoader.
Indirect Lighting#
By default, all objects in the scene are globally lit by the backdrop.
When using a plain colour backdrop the illumination is the same colour uniformly applied to every surface. When using a backdrop texture the lighting is applied to each surface differently according to the brightness and colour of the sky facing that surface.
The ambient occlusion map used for any material is used to dim indirect lighting.
You can also set a backdrop with indirect lighting disabled, if desired (see below).
Backdrop Methods#
Scene has the following functions for controlling backdrops and indirect lighting:
-
SetBackdrop(ColorVect color, float indirectLightingIntensity = 1f) -
Sets the backdrop of the scene to the given
color.You can also optionally set a value for
indirectLightingIntensity, where1fis the default (meaning 100%). Setting this value will not affect the backdrop color. -
SetBackdrop(BackdropTexture backdrop, float backdropIntensity = 1f, Rotation? rotation = null) -
Sets the backdrop of the scene to the given
backdrop.You can also optionally set a value for
backdropIntensity, where1fis the default (meaning 100%). Setting this value changes the intensity of indirect lighting and also the brightness/intensity of the backdrop.Finally, you can also set an optional
rotationvalue which can be used to rotate the skybox texture/backdrop. -
SetBackdropWithoutIndirectLighting(ColorVect color) -
Sets the backdrop of the scene to the given
color.This method will simply set a backdrop color but disables all indirect lighting. This means objects will appear to be pitch-black unless lit by another light source.
-
SetBackdropWithoutIndirectLighting(BackdropTexture backdrop, float backdropIntensity = 1f, Rotation? rotation = null) -
Sets the backdrop of the scene to the given
backdrop.This method will set the environment backdrop as the backdrop/sky, but will not use it to apply any indirect lighting. This means objects will appear to be pitch-black unless lit by another light source.
You can still set the intensity/brightness of the backdrop using the optional
backdropIntensityvalue. This will only adjust the brightness of the sky.Finally, you can also set an optional
rotationvalue which can be used to rotate the skybox texture/backdrop. -
RemoveBackdrop() -
If you prefer no backdrop and no indirect lighting, you can use this method to have a scene with no backdrop at all.
For most intents and purposes this is the same as setting the backdrop to a solid black colour.
-
Scene.LuxToBrightness(float lux) -
This static method can convert a real-life value in lux to a brightness/intensity value used in the methods above.
For example, to set a backdrop with an illuminance of 30000 lux:
myScene.SetBackdrop(StandardColor.White, Scene.LuxToBrightness(30_000f)); -
Scene.BrightnessToLux(float brightness) -
This static method reverses the conversion made in
LuxToBrightness().
Cameras#
Scenes are ultimately rendered to a render target (such as a window) by a Renderer, using a Camera. The Camera captures the scene from a specific direction and with specific parameters. The renderer takes that capture and turns it in to a texture/frame.
Cameras offer the following controls:
-
Position -
Where in the scene/world the camera should capture its next frame from.
-
ViewDirection -
Where the camera should be looking.
Note that TinyFFR keeps this value auto-orthogonalized with the
UpDirection. Changing one of eitherViewDirectionorUpDirectionmay automatically change the other. -
UpDirection -
Which way "up" the camera should be rotated.
Note that TinyFFR keeps this value auto-orthogonalized with the
ViewDirection. Changing one of eitherViewDirectionorUpDirectionmay automatically change the other. -
HorizontalFieldOfView -
Represents how wide the viewing angle is as captured by the camera lens.
This property lets you get/set the viewing angle as specified in the horizontal field (i.e. this property sets the amount of scene seen from left-to-right on the rendered frame).
Changing this will automatically change
VerticalFieldOfViewaccording to the currently-setAspectRatio.Must be between
Camera.FieldOfViewMin(0°) andCamera.FieldOfViewMax(360°). -
VerticalFieldOfView -
Represents how wide the viewing angle is as captured by the camera lens.
This property lets you get/set the viewing angle as specified in the vertical field (i.e. this property sets the amount of scene seen from top-to-bottom on the rendered frame).
Changing this will automatically change
HorizontalFieldOfViewaccording to the currently-setAspectRatio.Must be between
Camera.FieldOfViewMin(0°) andCamera.FieldOfViewMax(360°). -
AspectRatio -
Defines the ratio between the output frame's width and height. For example, for a 1920 x 1080 output, this value should be
1920f/1080f.By default, the
Rendererwill automatically update this value on the camera as appropriate for the render target/window.If you wish to control this value manually, specify a
RendererCreationConfigand setAutoUpdateCameraAspectRatiotofalsewhen callingCreateRenderer()on theRendererBuilder. -
NearPlaneDistance -
This sets how close something has to be to the camera before it is no longer rendered.
Setting this lower will let you render things closer to the camera, but may cause Z-fighting for objects further away unless you also reduce the
FarPlaneDistance.In general you should try to keep the
FarPlaneDistanceno more than 5 orders of magnitude more thanNearPlaneDistance, 6 at an absolute max. TinyFFR will automatically adjust theFarPlaneDistanceto make sure it is never more than 1E6 times higher thanNearPlaneDistance.The default value is
CameraCreationConfig.DefaultNearPlaneDistance(0.15m). This can not be 0 due to perspective divide; the lowest permitted value isCamera.NearPlaneDistanceMin(1E-5m). -
FarPlaneDistance -
This sets how far something can be from the camera before it is no longer rendered.
Setting this higher will let you render things further from the camera, but may cause Z-fighting for objects further away unless you also increase the
NearPlaneDistance.In general you should try to keep the
FarPlaneDistanceno more than 5 orders of magnitude more thanNearPlaneDistance, 6 at an absolute max. TinyFFR will automatically adjust theFarPlaneDistanceto make sure it is never more than 1E6 times higher thanNearPlaneDistance.The default value is
CameraCreationConfig.DefaultFarPlaneDistance(5000m). This can not be lower than or equal toNearPlaneDistance. -
SetViewAndUpDirection(Direction newViewDirection, Direction newUpDirection, bool enforceOrthogonality = true) -
Sets the
ViewDirectionandUpDirectiontogether. This can be useful if you want to avoid the auto-orthogonalization calculations when setting them separately.If
enforceOrthogonalityisfalse, TinyFFR will not orthogonalize the two directions at all. If they are not orthogonal this can lead to some unexpected or even confusing perspective distortions. -
LookAt(Location target) -
LookAt(Location target, Direction upDirection) -
Rotates the camera to look at the specified
target.If you specify an
upDirection, the camera will maintain that direction as itsUpDirection, auto-orthogonalizing with the resultantViewDirection. If you do not specify anupDirection, the camera will simply rotate itsViewDirectionto face thetargetand auto-orthogonalize the existingUpDirectionaccording to that rotation.In most cases, you will probably want to specify an
upDirection. Without anupDirectionthe camera will likely spin around over time.
Renderers#
Renderers must be constructed with a scene to render, a camera to capture the scene with, and a render target to output to (e.g. a Window).
When creating a Renderer you can supply an optional RendererCreationConfig that has the following options:
-
AutoUpdateCameraAspectRatio -
If
true, when the target surface (e.g. theWindow)'s aspect ratio changes (i.e. its dimensions change), the renderer will automatically update theAspectRatioproperty of theCamerait was built with.This is useful if the renderer is associated with one camera and one target/Window only; but may not be what you want in a multi-renderer setup. If you set this to
falseyou will be responsible for setting theAspectRatioof the camera manually.Defaults to
true. -
GpuSynchronizationFrameBufferCount -
This is an advanced option that controls how this renderer synchronizes the CPU with the GPU; it controls how many frames can be "in progress" on the GPU side before the CPU waits in order to not get too far ahead.
-
Values between
1and5set a maximum number of frames that can be "queued" or "in progress" before the call toRender()will block the calling thread. A higher value generally increases your average throughput/FPS, but can also increase input latency. -
A value of
0completely stops all asynchronous rendering. This means every call toRender()will always block the calling thread until the frame is fully rendered and displayed on the target/Window. Setting this value can drastically lower average throughput/FPS; a value of at least1is recommended in most scenarios. -
A value of
-1disables synchronization entirely. This meansRender()will never block the calling thread; but over time commands submitted to the GPU may exceed the GPU's capability to keep up, resulting in stuttering or even errors. Setting this value is only recommended when using a multi-renderer setup (set all renderers except your last/"primary" renderer to-1).
Defaults to
3.Setting -1 also disables resource disposal protection
Another reason to never set this value to
-1for all your renderers is that resource disposal is no longer synchronized.Behind the scenes, TinyFFR ensures that your resources are not deleted from GPU memory until scenes using them are fully rendered. This may be after you call
.Dispose()on that resource; TinyFFR uses GPU synchronization fences to protect against use-after-dispose race conditions. When you have no renderers with non-negative values forGpuSynchronizationFrameBufferCount, there is no longer any fence to synchronize on.The only reason to set this value to
-1is for additionalRenderers: It's okay (and even encouraged for performance) to disable synchronization on secondary/tertiary/etcRenderers as long as at least one is still synchronizing commands on the GPU.Make sure that one
Rendererin your application (usually the "primary" one, i.e. the last one in the loop that renders every frame) always has a non-negative value forGpuSynchronizationFrameBufferCount.If you have no
Rendererthat is guaranteed to render every frame/iteration, you should not setGpuSynchronizationFrameBufferCountto-1on anyRenderer. -
Renderer Members#
Every Renderer has the following members:
-
Render() -
Renders the configured scene, captured using the configured camera, to the configured target output (e.g. a
Window). -
WaitForGpu() -
As described above in the documentation for
GpuSynchronizationFrameBufferCount, invokingRender()actually enqueues a command list on the GPU which will then be completed at a later time.In some circumstances you may wish to pause the current application on the CPU side until the GPU has completed its execution and outputted all remaining frames.
WaitForGpu()will stall the calling thread until this time.In other words, by the time
WaitForGpu()returns, all previous frames queued by a call toRender()will have completed. Any handlers attached toRenderOutputBuffers (described below) will have executed.When running a render loop,
WaitForGpu()causes a stall in the render pipeline, meaning your application will appear to stutter momentarily. InvokingWaitForGpu()frequently may drastically lower your maximum framerate. -
RenderAndWaitForGpu() -
A convenience method that invokes
Render()and then immediately invokesWaitForGpu().You can use this function in place of
Render()when you want your scene render (and correlated callbacks) to be completed by the time the render function returns.Just like
WaitForGpu()this function has performance implications (seeWaitForGpu()documentation above). -
SetQuality() -
Changes the render quality settings of this
Renderer. All subsequent calls toRender()will use the new quality settings. -
CaptureScreenshot(...) -
This function can either take an argument specifying the file path of a BMP file to be written with the captured screenshot, or a handler can be specified for more manual processing of captured frame data.
-
When specifying a bitmap file path, be aware that this function may throw an
IOExceptionif the file path could not be written to. -
When specifying a handler, see the documentation below for
RenderOutputBuffer.ReadNextFrame()for more information on the arguments.
You may also optionally specify the capture resolution for the screenshot (
captureResolution). Leaving this asnullwill mean the captured screenshot will use the same resolution as the configured render target (e.g. theWindow).The
presentFrameTopToBottomparameter (only present when specifying ahandler) is also optional (falseby default). Iftrue, the data passed to thehandlerwill be arranged such that the first row in the data represents the top of the texture. Iffalse(the default), the data will be arranged from bottom-to-top.Note that this function re-renders the configured scene with the configured camera and then stalls the GPU pipeline until the output is received before immediately executing the bitmap file write or handler callback. This has two implications:
CaptureScreenshot()takes a significant toll on performance, similar toWaitForGpu()(but worse). If you want to continuously capture frames, it is preferable to use aRenderOutputBuffer(see below).- The current setup of the configured scene and camera is used at the time
CaptureScreenshot()is invoked, meaning if the camera or scene have changed since the lastRender()call, the captured screenshot will not match the last rendered output.
-
RenderOutputBuffers#
Instead of rendering directly to a window, you can also render to an internal texture buffer; optionally re-displaying this texture elsewhere on an in-scene surface or copying its data to memory/file. In TinyFFR, these buffers are referred to as RenderOutputBuffers.
Creating a RenderOutputBuffer is done via the renderer builder:
using var buffer = factory.RendererBuilder.CreateRenderOutputBuffer(textureDimensions: (1024, 1024)); // (1)!
- The texture dimensions of the buffer is the only required parameter. In this example we're creating a 1024x1024 texture.
You can then pass a RenderOutputBuffer to CreateRenderer() (instead of a Window):
RenderOutputBuffer Members#
When invoking Render() on the renderer, the scene captured with the camera will be rendered on to the buffer. You can use the following various members to access or use the buffer data:
-
TextureDimensions -
Returns the X/Y dimensions of the buffer.
-
StartReadingFrames(handler, presentFrameTopToBottom) -
ReadNextFrame(handler, presentFrameTopToBottom) -
StartReadingFramesinstructs TinyFFR that you wish to begin continuously reading all frames rendered to this buffer from this point onwards, until stopped.ReadNextFrameinstructs TinyFFR that you wish to read back the next rendered scene from this buffer. All frames rendered after the next one will not be handled.The
handlershould be either anAction<XYPair<int>, ReadOnlySpan<TexelRgb24>>or its function pointer equivalent.-
The first parameter to the handler is the texture dimensions of the output buffer (
Xcolumns andYrows). The totalLengthof the given span will be equal to theAreaproperty of thisXYPair. -
The second is a span of texels containing the row-major(1) frame data arranged without gaps(2). The texel data is only valid for as long as the handler is executing.
-
The first
Xtexels in the span will constitute the first row of texel data; the secondXtexels will constitute the second, etc; for a total ofYrows.By default, the first row is considered to be the bottom of the texture data (as this matches the 2D texture convention TinyFFR uses). However, this can be reversed with the "
presentFrameTopToBottom" parameter described below. -
There is no "stride" or blank data at the end of rows; each row is packed without padding and the beginning of one row starts immediately after the end of the previous one in the data.
-
The
presentFrameTopToBottomparameter is optional (falseby default). Iftrue, the data passed to thehandlerwill be arranged such that the first row in the data represents the top of the texture. Iffalse(the default), the data will be arranged from bottom-to-top. -
-
StopReadingFrames(cancelQueuedFrames) -
This instructs TinyFFR that you wish to stop a previously-started continuous frame reading operation.
If
cancelQueuedFramesis true, any previously-rendered frames that are currently still queued in the GPU command pipeline will not be passed to yourhandler. Otherwise, those remaining frames will still be passed to thehandler. Any framesRender()ed afterStopReadingFrames()is invoked will not be passed to the previously-sethandlereither way.
Single Handler Restriction
Note that each RenderOutputBuffer can only have one readback handler set at any given time.
That means that only the most-recent handler passed to either StartReadingFrames or ReadNextFrame will be invoked. Calling StartReadingFrames or ReadNextFrame again will "erase" the previously-set handler.
If you wish to execute multiple actions for a RenderOutputBuffer frame, you should use a single handler and pass the received data to multiple further sub-functions.
Asynchrony in GPU Command Pipeline
When calling Render() for a Renderer created with a RenderOutputBuffer target with a handler set, the handler may not be invoked immediately or at all until more frames are rendered later on. This is because (by default) frames are queued to be rendered on the GPU, and the GPU asynchronously executes the commandlist. TinyFFR checks for previously-completed frames when Render() is called for subsequent frames.
If you require immediate readback for set handlers when calling Render(), consider using RenderAndWaitForGpu() instead. Note however that this will quite adversely affect framerate in a render loop.
You can also call WaitForGpu() at specific points after dispatching multiple Render() calls in order to force all dispatched frames to be handled at that point. This also adversely affects framerate in a render loop.
See also the documentation for GpuSynchronizationFrameBufferCount and WaitForGpu() above.
-
CreateDynamicTexture() -
This method can be used to create a
Textureresource that always contains the last-rendered frame to thisRenderOutputBufferas its data.You can pass this
Textureto other parts of the library just as you would any otherTexture; including setting it as the data for a color, normal, or ORM map on aMaterial.Note that the lifetime of this
Textureis forever intrinsically tied to its "parent"RenderOutputBuffer. You must dispose allTextures created this way before disposing the parentRenderOutputBuffer.