Render to texture bilinear filter

If you are a new Irrlicht Engine user, and have a newbie-question, this is the forum for you. You may also post general programming questions here.
Post Reply
Noiecity
Posts: 336
Joined: Wed Aug 23, 2023 7:22 pm
Contact:

Render to texture bilinear filter

Post by Noiecity »

I was on Linux, and I was trying to render in low resolution (128x128) and use the bilinear filter to smooth out the pixelation. However, I don't know if I'm doing it correctly. For example, when I render to texture and pass the texture to a cube, I only need to enable or disable the bilinear filter on the cube to see if the pixels are smoothed out or not. However, when I use a 2D texture directly, I don't see any difference. Maybe I'm not doing it right.
I mean, the filter may or may not be enabled by default, but I would like to see .

Code: Select all

/** Example 013 Render To Texture

This tutorial shows how to render to a texture using Irrlicht. Render to
texture is a feature where everything which would usually be rendered to
the screen is instead written to a (special) texture. This can be used to
create nice special effects.
In addition, this tutorial shows how to enable specular highlights.

In the beginning, everything as usual. Include the needed headers, ask the user
for the rendering driver, create the Irrlicht device:
*/

#include <irrlicht.h>

using namespace irr;

#ifdef _MSC_VER
#pragma comment(lib, "Irrlicht.lib")
#endif

int main()
{

	// create device and exit if creation failed

	IrrlichtDevice *device =
		createDevice(video::EDT_OPENGL, core::dimension2d<u32>(640, 480),
		16, false, false);

	if (device == 0)
		return 1; // could not create selected driver.

	video::IVideoDriver* driver = device->getVideoDriver();
	scene::ISceneManager* smgr = device->getSceneManager();
	gui::IGUIEnvironment* env = device->getGUIEnvironment();


	/*
	Now, we load an animated mesh to be displayed. As in most examples,
	we'll take the fairy md2 model. The difference here: We set the
	shininess of the model to a value other than 0 which is the default
	value. This enables specular highlights on the model if dynamic
	lighting is on. The value influences the size of the highlights.
	*/

	// load and display animated fairy mesh

	scene::IAnimatedMeshSceneNode* fairy = smgr->addAnimatedMeshSceneNode(
		smgr->getMesh("../../media/faerie.md2"));

	if (fairy)
	{
		fairy->setMaterialTexture(0,
				driver->getTexture("../../media/faerie2.bmp")); // set diffuse texture
		fairy->setMaterialFlag(video::EMF_LIGHTING, true); // enable dynamic lighting
		fairy->getMaterial(0).Shininess = 20.0f; // set size of specular highlights
		fairy->setPosition(core::vector3df(-10,0,-100));
		fairy->setMD2Animation ( scene::EMAT_STAND );
	}

	/*
	To make specular highlights appear on the model, we need a dynamic
	light in the scene. We add one directly in vicinity of the model. In
	addition, to make the model not that dark, we set the ambient light to
	gray.
	*/

	// add white light
	smgr->addLightSceneNode(0, core::vector3df(-15,5,-105),
			video::SColorf(1.0f, 1.0f, 1.0f));

	// set ambient light
	smgr->setAmbientLight(video::SColor(0,60,60,60));

	/*
	The next is just some standard stuff: Add a test cube and let it rotate
	to make the scene more interesting. The user defined camera and cursor
	setup is made later on, right before the render loop.
	*/

	driver->setTextureCreationFlag(video::ETCF_CREATE_MIP_MAPS, false);

	scene::ISceneNode* skybox=smgr->addSkyBoxSceneNode(
		driver->getTexture("../../media/irrlicht2_up.jpg"),
		driver->getTexture("../../media/irrlicht2_dn.jpg"),
		driver->getTexture("../../media/irrlicht2_lf.jpg"),
		driver->getTexture("../../media/irrlicht2_rt.jpg"),
		driver->getTexture("../../media/irrlicht2_ft.jpg"),
		driver->getTexture("../../media/irrlicht2_bk.jpg"));
	scene::ISceneNode* skydome=smgr->addSkyDomeSceneNode(driver->getTexture("../../media/skydome.jpg"),16,8,0.95f,2.0f);

	driver->setTextureCreationFlag(video::ETCF_CREATE_MIP_MAPS, true);
	// create test cube
	scene::ISceneNode* cube = smgr->addCubeSceneNode(60);

	// let the cube rotate and set some light settings
	scene::ISceneNodeAnimator* anim = smgr->createRotationAnimator(
		core::vector3df(0.3f, 0.3f,0));

	cube->setPosition(core::vector3df(-100,0,-100));
	cube->setMaterialFlag(video::EMF_LIGHTING, false); // disable dynamic lighting
	cube->setMaterialFlag(video::EMF_BILINEAR_FILTER, true);
	cube->addAnimator(anim);
	anim->drop();

	// set window caption
	device->setWindowCaption(L"Irrlicht Engine - Render to Texture and Specular Highlights example");

	/*
	To test out the render to texture feature, we need to define our
	new rendertarget. The rendertarget will need one texture to receive
	the result you would otherwise see on screen and one texture
	which is used as depth-buffer.

	(Note: If you worked with older Irrlicht versions (before 1.9) you might be
	used to only create a rendertarget texture and no explicit rendertarget. While
	that's still possible, it's no longer recommended.)

	The rendertarget textures are not like standard textures, but need to be created
	first. To create them, we call IVideoDriver::addRenderTargetTexture()
	and specify the size of the texture and the type.
	For depth-maps you can use types ECF_D16, ECF_D32 or ECF_D24S8. When ECF_D24S8
	you can also use a stencil-buffer.

	Because we want to render the scene not from the user camera into the
	texture, we add another fixed camera to the scene. But before we do all
	this, we check if the current running driver is able to render to
	textures. If it is not, we simply display a warning text.
	*/

	// create render target
	video::IRenderTarget* renderTarget = 0;
	scene::ICameraSceneNode* fixedCam = 0;

	if (driver->queryFeature(video::EVDF_RENDER_TO_TARGET))
	{
		const core::dimension2d<u32> rtDim(128, 128);	// always use same size for render target texture and it's depth-buffer
		video::ITexture* renderTargetTex = driver->addRenderTargetTexture(rtDim, "RTT1", video::ECF_A8R8G8B8);
		video::ITexture* renderTargetDepth = driver->addRenderTargetTexture(rtDim, "DepthStencil", video::ECF_D16);

		renderTarget = driver->addRenderTarget();
		renderTarget->setTexture(renderTargetTex, renderTargetDepth);

		cube->setMaterialTexture(0, renderTargetTex); // set material of cube to render target

		// add fixed camera
		fixedCam = smgr->addCameraSceneNode(0, core::vector3df(10,10,-80),
			core::vector3df(-10,10,-100));
        fixedCam->setFOV(53.4f * core::DEGTORAD);
	}
	else
	{
		// create problem text
		gui::IGUISkin* skin = env->getSkin();
		gui::IGUIFont* font = env->getFont("../../media/fonthaettenschweiler.bmp");
		if (font)
			skin->setFont(font);

		gui::IGUIStaticText* text = env->addStaticText(
			L"Your hardware or this renderer is not able to use the "\
			L"render to texture feature. RTT Disabled.",
			core::rect<s32>(150,20,470,60));

		text->setOverrideColor(video::SColor(100,255,255,255));
	}

	// add fps camera
	scene::ICameraSceneNode* fpsCamera = smgr->addCameraSceneNodeFPS();
	fpsCamera->setPosition(core::vector3df(-50,50,-150));
	fpsCamera->setFOV(53.4f * core::DEGTORAD);

	// disable mouse cursor
	device->getCursorControl()->setVisible(false);

	/*
	Nearly finished. Now we need to draw everything. Every frame, we draw
	the scene twice. Once from the fixed camera into the render target
	texture and once as usual. When rendering into the render target, we
	need to disable the visibility of the test cube, because it has the
	render target texture applied to it. That's it, wasn't too complicated
	I hope. :)
	*/

	int lastFPS = -1;

	while(device->run())
	if (device->isWindowActive())
	{
		driver->beginScene(video::ECBF_COLOR | video::ECBF_DEPTH, video::SColor(0));

		if (renderTarget)
		{
			// draw scene into render target

			// set render target
			driver->setRenderTargetEx(renderTarget, video::ECBF_COLOR | video::ECBF_DEPTH, video::SColor(0,0,0,255));

			// make cube invisible and set fixed camera as active camera
			cube->setVisible(false);
			smgr->setActiveCamera(fixedCam);

			// draw whole scene into render buffer
			smgr->drawAll();

			// set back old render target (the screen)
			driver->setRenderTargetEx(0, 0);

			// make the cube visible and set the user controlled camera as active one
			cube->setVisible(true);
			smgr->setActiveCamera(fpsCamera);
		}

		// draw scene normally
		smgr->drawAll();
		env->drawAll();

		driver->endScene();

		// display frames per second in window title
		int fps = driver->getFPS();
		if (lastFPS != fps)
		{
			core::stringw str = L"Irrlicht Engine - Render to Texture and Specular Highlights example";
			str += " FPS:";
			str += fps;

			device->setWindowCaption(str.c_str());
			lastFPS = fps;
		}
	}

	device->drop(); // drop device
	return 0;
}

/*
**/

--------

Code: Select all

cube->setMaterialFlag(video::EMF_BILINEAR_FILTER, false);
Image
---------

Code: Select all

cube->setMaterialFlag(video::EMF_BILINEAR_FILTER, true);
Image
---------
I see that it works very well. The lower the resolution, the stronger the bilinear filter seems to be, giving it a retro style and consuming very little GPU power, as well as dramatically increasing the number of frames per second.

Now the problem arises when I try to render directly onto a 2D texture. I don't know if the bilinear filter is active. I would like it to be, but I also want to find a way to notice the differences. If the bilinear filter is less strong than the render on the cube, I plan to use a 3D model to show the texture.

Code: Select all

#include <irrlicht.h>

using namespace irr;

#ifdef _IRR_WINDOWS_
#pragma comment(lib, "Irrlicht.lib")
#endif

// Window settings
const u32 WINDOW_WIDTH = 800;
const u32 WINDOW_HEIGHT = 600;

// Render texture settings (virtual resolution)
const u32 RENDER_WIDTH = 128;   // Width of the render target texture
const u32 RENDER_HEIGHT = 128;  // Height of the render target texture

/**
 * Manages a render-to-texture (RTT) system.
 * Handles creating the render target, rendering to it, and then drawing the
 * resulting texture onto the screen with proper aspect ratio and centering.
 */
class RenderTextureManager {
private:
    IrrlichtDevice* device;
    video::IVideoDriver* driver;
    video::ITexture* renderTexture;  // The off-screen render target
    u32 renderWidth;
    u32 renderHeight;
    f32 aspectRatio;                // Calculated as width/height

public:
    /**
     * Constructor.
     * @param dev    Pointer to the Irrlicht device.
     * @param width  Desired width of the render texture.
     * @param height Desired height of the render texture.
     */
    RenderTextureManager(IrrlichtDevice* dev, u32 width, u32 height)
        : device(dev), renderWidth(width), renderHeight(height) {
        driver = device->getVideoDriver();

        // Compute aspect ratio for later scaling
        aspectRatio = (f32)renderWidth / (f32)renderHeight;

        // Create a render target texture with the specified dimensions
        renderTexture = driver->addRenderTargetTexture(
            core::dimension2d<u32>(renderWidth, renderHeight), "RTT");
    }

    /**
     * Destructor. Cleans up the render target texture.
     */
    ~RenderTextureManager() {
        if (renderTexture) {
            driver->removeTexture(renderTexture);
        }
    }

    /**
     * Sets the render target to the internal texture and clears it.
     * Also forces the viewport to exactly match the render texture size.
     * @param clearColor Background color used when clearing the texture.
     */
    void beginRenderToTexture(const video::SColor& clearColor = video::SColor(255, 100, 101, 140)) {
        driver->setRenderTarget(renderTexture, true, true, clearColor);

        // CRITICAL: the viewport must be set to the exact dimensions of the render target.
        // Otherwise the scene will be stretched or clipped incorrectly.
        driver->setViewPort(core::rect<s32>(0, 0, renderWidth, renderHeight));
    }

    /**
     * Restores the render target to the primary screen (backbuffer).
     * Also resets the viewport to cover the whole window.
     */
    void endRenderToTexture() {
        driver->setRenderTarget(0); // Switch back to main screen

        // Restore full screen viewport
        core::dimension2d<u32> screenSize = driver->getScreenSize();
        driver->setViewPort(core::rect<s32>(0, 0, screenSize.Width, screenSize.Height));
    }

    /**
     * Draws the render texture onto the screen.
     * The image is scaled to fit the screen while preserving its aspect ratio,
     * and is centered. A red outline is drawn around the image area.
     */
    void drawToScreen() {
        core::dimension2d<u32> screenSize = driver->getScreenSize();

        // Compute destination size that maintains the original aspect ratio
        u32 destHeight = screenSize.Height;
        u32 destWidth = (u32)(destHeight * aspectRatio);

        // If the computed width exceeds the screen width, recalculate using width as base
        if (destWidth > screenSize.Width) {
            destWidth = screenSize.Width;
            destHeight = (u32)(destWidth / aspectRatio);
        }

        // Center the image on screen
        s32 destX = (screenSize.Width - destWidth) / 2;
        s32 destY = (screenSize.Height - destHeight) / 2;

        // Enable bilinear filtering and basic anti‑aliasing for smoother scaling
        video::SMaterial& mat2D = driver->getMaterial2D();
        mat2D.TextureLayer[0].BilinearFilter = true;
        mat2D.AntiAliasing = video::EAAM_FULL_BASIC;

        // Draw the rendered texture onto the screen, scaled and centered
        driver->draw2DImage(renderTexture,
                           core::rect<s32>(destX, destY, destX + destWidth, destY + destHeight),
                           core::rect<s32>(0, 0, renderWidth, renderHeight),
                           0, 0, true);

        // Draw a red border around the drawn area for visual reference
        driver->draw2DRectangleOutline(core::rect<s32>(destX, destY, destX + destWidth, destY + destHeight),
                                      video::SColor(255, 255, 0, 0));
    }

    // Getters
    u32 getRenderWidth() const { return renderWidth; }
    u32 getRenderHeight() const { return renderHeight; }
    f32 getAspectRatio() const { return aspectRatio; }
    video::ITexture* getRenderTexture() const { return renderTexture; }

    /**
     * Dynamically changes the resolution of the render texture.
     * The old texture is removed and a new one is created with the new dimensions.
     * @param width  New render width.
     * @param height New render height.
     */
    void setRenderSize(u32 width, u32 height) {
        if (renderTexture) {
            driver->removeTexture(renderTexture);
        }

        renderWidth = width;
        renderHeight = height;
        aspectRatio = (f32)width / (f32)height;

        renderTexture = driver->addRenderTargetTexture(
            core::dimension2d<u32>(width, height), "RTT");
    }
};

int main() {
    // Create an Irrlicht device with OpenGL renderer and the specified window size
    IrrlichtDevice* device = createDevice(video::EDT_OPENGL,
                                        core::dimension2d<u32>(WINDOW_WIDTH, WINDOW_HEIGHT),
                                        16, false, false, false, 0);

    if (!device) return 1;

    video::IVideoDriver* driver = device->getVideoDriver();
    scene::ISceneManager* smgr = device->getSceneManager();
    gui::IGUIEnvironment* guienv = device->getGUIEnvironment();

    // Create the render texture manager with the desired virtual resolution
    RenderTextureManager rtManager(device, RENDER_WIDTH, RENDER_HEIGHT);
	driver->setTextureCreationFlag(video::ETCF_CREATE_MIP_MAPS, false);

	scene::ISceneNode* skybox=smgr->addSkyBoxSceneNode(
		driver->getTexture("../../media/irrlicht2_up.jpg"),
		driver->getTexture("../../media/irrlicht2_dn.jpg"),
		driver->getTexture("../../media/irrlicht2_lf.jpg"),
		driver->getTexture("../../media/irrlicht2_rt.jpg"),
		driver->getTexture("../../media/irrlicht2_ft.jpg"),
		driver->getTexture("../../media/irrlicht2_bk.jpg"));
	scene::ISceneNode* skydome=smgr->addSkyDomeSceneNode(driver->getTexture("../../media/skydome.jpg"),16,8,0.95f,2.0f);

	driver->setTextureCreationFlag(video::ETCF_CREATE_MIP_MAPS, true);
    // Add a first‑person style camera
    scene::ICameraSceneNode* camera = smgr->addCameraSceneNodeFPS();
    camera->setPosition(core::vector3df(0, 0, -80));

    // CRITICAL: The camera's aspect ratio must match the render texture's aspect ratio,
    // otherwise the scene will be distorted when rendered to the off‑screen target.
    camera->setAspectRatio(rtManager.getAspectRatio());

    // Optional: adjust the field of view (FOV) to a comfortable value
    camera->setFOV(53.4f * core::DEGTORAD);

    // Load and add a test mesh (Sydney model from Irrlicht media folder)
    scene::IAnimatedMesh* mesh = smgr->getMesh("../../media/sydney.md2");
    if (mesh) {
        scene::IAnimatedMeshSceneNode* node = smgr->addAnimatedMeshSceneNode(mesh);
        if (node) {
            node->setMaterialFlag(video::EMF_LIGHTING, false);
            node->setMD2Animation(scene::EMAT_STAND);
            node->setMaterialTexture(0, driver->getTexture("../../media/sydney.bmp"));
            node->setPosition(core::vector3df(-20, 0, 0));
        }
    }

    // Add a couple of textured cubes for visual variety
    scene::ISceneNode* cube1 = smgr->addCubeSceneNode(15);
    if (cube1) {
        cube1->setPosition(core::vector3df(20, 0, 0));
        cube1->setMaterialFlag(video::EMF_LIGHTING, false);
        cube1->setMaterialTexture(0, driver->getTexture("../../media/wall.bmp"));
    }

    scene::ISceneNode* cube2 = smgr->addCubeSceneNode(10);
    if (cube2) {
        cube2->setPosition(core::vector3df(0, 15, 0));
        cube2->setMaterialFlag(video::EMF_LIGHTING, false);
        cube2->setMaterialTexture(0, driver->getTexture("../../media/wall.bmp"));
    }

    // Use the built‑in GUI font for on‑screen information
    gui::IGUIFont* font = guienv->getBuiltInFont();
    core::stringw infoText;

    device->setWindowCaption(L"Configurable Render to Texture - Dynamic Aspect Ratio");

    // Main rendering loop
    while (device->run()) {
        if (device->isWindowActive()) {
            // Update camera aspect ratio every frame – important if the render
            // texture size is changed dynamically at runtime.
            camera->setAspectRatio(rtManager.getAspectRatio());

            // 1. Render the whole scene into the off‑screen render texture.
            rtManager.beginRenderToTexture();
            smgr->drawAll();
            rtManager.endRenderToTexture();

            // 2. Clear the main window (backbuffer).
            driver->beginScene(true, true, video::SColor(255, 0, 0, 0));

            // 3. Draw the render texture onto the screen, scaled and centered.
            rtManager.drawToScreen();

            // Display debug information about the render texture and scaling.
            core::dimension2d<u32> screenSize = driver->getScreenSize();

            u32 destHeight = screenSize.Height;
            u32 destWidth = (u32)(destHeight * rtManager.getAspectRatio());
            if (destWidth > screenSize.Width) {
                destWidth = screenSize.Width;
                destHeight = (u32)(destWidth / rtManager.getAspectRatio());
            }

            infoText = L"Render Texture: ";
            infoText += rtManager.getRenderWidth();
            infoText += L"x";
            infoText += rtManager.getRenderHeight();
            infoText += L"\nAspect Ratio: ";
            infoText += rtManager.getAspectRatio();
            infoText += L"\nScaled to: ";
            infoText += destWidth;
            infoText += L"x";
            infoText += destHeight;
            infoText += L"\nScreen: ";
            infoText += screenSize.Width;
            infoText += L" x ";
            infoText += screenSize.Height;

            font->draw(infoText.c_str(),
                      core::rect<s32>(10, 10, 500, 130),
                      video::SColor(255, 255, 255, 255));

            driver->endScene();
        }
    }

    // Clean up
    device->drop();
    return 0;
}
Image


This part of the code seems to have no effect; I probably don't know how to write it:

Code: Select all

        video::SMaterial& mat2D = driver->getMaterial2D();
        mat2D.TextureLayer[0].BilinearFilter = true;
        mat2D.AntiAliasing = video::EAAM_FULL_BASIC;
:mrgreen: :mrgreen:

By the way, I was wrong in a previous post regarding the FOV, the ocular FOV in a vertical camera depends on the aspect ratio, in a 1:1 aspect ratio the fov is 53.4. I don't know why Gemini said it was 90, I asked him again recently and he told me it was 53.4, when I checked the calculations, yes, that was correct.

The calculation of FOV is based on this premise: I put my head at a distance from a notebook fixed in its position straight, all times looking at the same distance with the same values, only the distance of the line changed, the line always measured 7 cm. I did not manage to save the first distance between my head and the first line, however I noted that the line at that distance was 5.4 cm, the second line was 10 cm further away in a straight line from the first line, it now measured 4.05 cm, the third line was 10 cm further away in a straight line from the second line, it measured 3.3 cm. All the lines measure the same, simply that from a distance this could be measured by looking at the ruler.
--
Although the eye has a wider range of vision if we consider that we have two eyes, we obtain a wider aspect ratio than it is tall, I wanted the size of the objects to be similar to how my eyes see them. The size of the object does not modify the spatial deformation by the fov, for example, if you have an object at a relative distance of 25% of its size with respect to the camera, if you scale the 3d object it will move away and look exactly the same.
--
Irrlicht is love, Irrlicht is life, long live to Irrlicht
CuteAlien
Admin
Posts: 9942
Joined: Mon Mar 06, 2006 2:25 pm
Location: Tübingen, Germany
Contact:

Re: Render to texture bilinear filter

Post by CuteAlien »

Yeah, a bit confusing - you have to call driver->enableMaterial2D(true) otherwise it doesn't use that material but some base material which you can't modify. So call that first.
IRC: #irrlicht on irc.libera.chat
Code snippet repository: https://github.com/mzeilfelder/irr-playground-micha
Free racer made with Irrlicht: http://www.irrgheist.com/hcraftsource.htm
Noiecity
Posts: 336
Joined: Wed Aug 23, 2023 7:22 pm
Contact:

Re: Render to texture bilinear filter

Post by Noiecity »

CuteAlien wrote: Wed Feb 11, 2026 6:30 pm Yeah, a bit confusing - you have to call driver->enableMaterial2D(true) otherwise it doesn't use that material but some base material which you can't modify. So call that first.
Thank you mr cuteAlien, sorry for the long text I wrote
Irrlicht is love, Irrlicht is life, long live to Irrlicht
CuteAlien
Admin
Posts: 9942
Joined: Mon Mar 06, 2006 2:25 pm
Location: Tübingen, Germany
Contact:

Re: Render to texture bilinear filter

Post by CuteAlien »

No worries - nothing makes my life easier as when questions come with complete examples which work :-)
IRC: #irrlicht on irc.libera.chat
Code snippet repository: https://github.com/mzeilfelder/irr-playground-micha
Free racer made with Irrlicht: http://www.irrgheist.com/hcraftsource.htm
Noiecity
Posts: 336
Joined: Wed Aug 23, 2023 7:22 pm
Contact:

Re: Render to texture bilinear filter

Post by Noiecity »

CuteAlien wrote: Wed Feb 11, 2026 7:37 pm No worries - nothing makes my life easier as when questions come with complete examples which work :-)
Image
omg it's work.
I can imagine adding shadows and reflections. With such low-resolution renders, it's almost free. Reflections are practically just a matter of adding an extra camera on the floor looking up and passing the render as a texture in a reflection2layer material.

Edit: I think reflection_2layer doesn't work well for flat geometries that face upwards while the camera faces forward. It's only good when you want to reflect an object in front of the camera, such as a mirror or armor.
You can use vertex alpha material and do RTT. Vertex alpha materials seem to ignore shadow volume, so you have to add an extra model with another texture if you want shadow volume. It's not a perfect or exact reflection, but it's works.
Irrlicht is love, Irrlicht is life, long live to Irrlicht
Post Reply