It is an example of isometric graphics and bloom effect, as well as saving file settings, something simple but functional.
Always render at 256x256, apply the filters, and scale the image considering the height of the window.

Code: Select all
#include <irrlicht.h>
#include <vector>
#include <algorithm>
#include <cmath>
#include <cstdlib>
using namespace irr;
using namespace core;
using namespace scene;
using namespace video;
using namespace io;
using namespace gui;
struct BloomParams {
float threshold;
float softness;
int radius;
float strength;
};
const int RENDER_WIDTH = 256;
const int RENDER_HEIGHT = 256;
// Function to apply Gaussian blur
void ApplyGaussianBlur(std::vector<SColor>& source, std::vector<SColor>& dest,
int width, int height, int radius) {
if (radius < 1) {
dest = source;
return;
}
std::vector<SColor> temp(width * height);
// Create Gaussian kernel
int kernelSize = radius * 2 + 1;
std::vector<float> kernel(kernelSize);
float sigma = radius / 2.0f;
float sum = 0.0f;
for (int i = 0; i < kernelSize; ++i) {
int x = i - radius;
kernel[i] = std::exp(-(x * x) / (2 * sigma * sigma));
sum += kernel[i];
}
// Normalize kernel
for (int i = 0; i < kernelSize; ++i) {
kernel[i] /= sum;
}
// Horizontal blur
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
float r = 0, g = 0, b = 0;
for (int k = -radius; k <= radius; ++k) {
int px = std::max(0, std::min(width - 1, x + k));
SColor pixel = source[y * width + px];
float weight = kernel[k + radius];
r += pixel.getRed() * weight;
g += pixel.getGreen() * weight;
b += pixel.getBlue() * weight;
}
temp[y * width + x] = SColor(255, (u32)r, (u32)g, (u32)b);
}
}
// Vertical blur
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
float r = 0, g = 0, b = 0;
for (int k = -radius; k <= radius; ++k) {
int py = std::max(0, std::min(height - 1, y + k));
SColor pixel = temp[py * width + x];
float weight = kernel[k + radius];
r += pixel.getRed() * weight;
g += pixel.getGreen() * weight;
b += pixel.getBlue() * weight;
}
dest[y * width + x] = SColor(255, (u32)r, (u32)g, (u32)b);
}
}
}
// Function to apply bloom effect
void ApplyBloomToImage(IImage* image, const BloomParams& params) {
const dimension2du size = image->getDimension();
const int width = size.Width;
const int height = size.Height;
std::vector<SColor> originalPixels(width * height);
std::vector<SColor> brightPixels(width * height);
std::vector<SColor> bloomPixels(width * height);
// 1. Read original pixels
for (u32 y = 0; y < height; ++y) {
for (u32 x = 0; x < width; ++x) {
originalPixels[y * width + x] = image->getPixel(x, y);
}
}
// 2. Extract bright areas with smoothing
for (int i = 0; i < width * height; ++i) {
SColor pixel = originalPixels[i];
float brightness = (pixel.getRed() * 0.299f +
pixel.getGreen() * 0.587f +
pixel.getBlue() * 0.114f) / 255.0f;
if (brightness > params.threshold) {
// Apply smoothing based on softness parameter
float softFactor = 1.0f - params.softness;
float intensity = (brightness - params.threshold) / (1.0f - params.threshold);
intensity = std::pow(intensity, softFactor * 2.0f + 0.5f);
brightPixels[i] = SColor(
255,
(u32)(pixel.getRed() * intensity),
(u32)(pixel.getGreen() * intensity),
(u32)(pixel.getBlue() * intensity)
);
} else {
brightPixels[i] = SColor(0, 0, 0, 0);
}
}
// 3. Apply blur
ApplyGaussianBlur(brightPixels, bloomPixels, width, height, params.radius);
// 4. Combine with original image
for (int i = 0; i < width * height; ++i) {
SColor original = originalPixels[i];
SColor bloom = bloomPixels[i];
u32 r = original.getRed() + (u32)(bloom.getRed() * params.strength);
u32 g = original.getGreen() + (u32)(bloom.getGreen() * params.strength);
u32 b = original.getBlue() + (u32)(bloom.getBlue() * params.strength);
r = r > 255 ? 255 : r;
g = g > 255 ? 255 : g;
b = b > 255 ? 255 : b;
image->setPixel(i % width, i / width, SColor(255, r, g, b));
}
}
// Function to save bloom parameters to XML
bool SaveBloomConfig(const BloomParams& params, const char* filename) {
IrrlichtDevice* device = createDevice(video::EDT_NULL);
if (!device) return false;
IXMLWriter* writer = device->getFileSystem()->createXMLWriter(filename);
if (!writer) {
device->drop();
return false;
}
writer->writeXMLHeader();
writer->writeElement(L"BloomConfig");
writer->writeLineBreak();
// Write parameters
core::stringw thresholdStr = core::stringw(params.threshold);
core::stringw softnessStr = core::stringw(params.softness);
core::stringw radiusStr = core::stringw(params.radius);
core::stringw strengthStr = core::stringw(params.strength);
writer->writeElement(L"Threshold", false, L"value", thresholdStr.c_str());
writer->writeLineBreak();
writer->writeElement(L"Softness", false, L"value", softnessStr.c_str());
writer->writeLineBreak();
writer->writeElement(L"Radius", false, L"value", radiusStr.c_str());
writer->writeLineBreak();
writer->writeElement(L"Strength", false, L"value", strengthStr.c_str());
writer->writeLineBreak();
writer->writeClosingTag(L"BloomConfig");
writer->writeLineBreak();
writer->drop();
device->drop();
return true;
}
// Function to load bloom parameters from XML
bool LoadBloomConfig(BloomParams& params, const char* filename) {
IrrlichtDevice* device = createDevice(video::EDT_NULL);
if (!device) return false;
// Check if file exists
if (!device->getFileSystem()->existFile(filename)) {
device->drop();
return false;
}
IXMLReader* reader = device->getFileSystem()->createXMLReader(filename);
if (!reader) {
device->drop();
return false;
}
// Default values in case of error
params.threshold = 0.3f;
params.softness = 0.8f;
params.radius = 8;
params.strength = 0.6f;
while (reader->read()) {
switch (reader->getNodeType()) {
case io::EXN_ELEMENT: {
core::stringw nodeName = reader->getNodeName();
if (nodeName == L"Threshold") {
// Read as string and convert to float
const wchar_t* value = reader->getAttributeValue(L"value");
if (value) {
char buffer[32];
wcstombs(buffer, value, sizeof(buffer));
params.threshold = strtof(buffer, NULL);
}
}
else if (nodeName == L"Softness") {
const wchar_t* value = reader->getAttributeValue(L"value");
if (value) {
char buffer[32];
wcstombs(buffer, value, sizeof(buffer));
params.softness = strtof(buffer, NULL);
}
}
else if (nodeName == L"Radius") {
params.radius = reader->getAttributeValueAsInt(L"value");
}
else if (nodeName == L"Strength") {
const wchar_t* value = reader->getAttributeValue(L"value");
if (value) {
char buffer[32];
wcstombs(buffer, value, sizeof(buffer));
params.strength = strtof(buffer, NULL);
}
}
break;
}
}
}
reader->drop();
device->drop();
return true;
}
class SaveButtonEventReceiver : public IEventReceiver {
private:
bool saveButtonPressed;
BloomParams* bloomParams;
IGUIButton* saveButton;
public:
SaveButtonEventReceiver() : saveButtonPressed(false), bloomParams(nullptr), saveButton(nullptr) {}
virtual bool OnEvent(const SEvent& event) {
if (event.EventType == EET_GUI_EVENT) {
if (event.GUIEvent.EventType == EGET_BUTTON_CLICKED) {
if (event.GUIEvent.Caller == saveButton) {
saveButtonPressed = true;
return true;
}
}
}
return false;
}
void setBloomParams(BloomParams* params) {
bloomParams = params;
}
void setSaveButton(IGUIButton* button) {
saveButton = button;
}
bool isSaveButtonPressed() {
if (saveButtonPressed) {
saveButtonPressed = false;
return true;
}
return false;
}
};
int main() {
SaveButtonEventReceiver receiver;
IrrlichtDevice *device = createDevice(
video::EDT_OPENGL,
dimension2d<u32>(640, 480),
16, false, true, false, &receiver
);
if (!device)
return 1;
device->setWindowCaption(L"256x256 Render with Bloom - Floor, Light & Shadows");
IVideoDriver* driver = device->getVideoDriver();
ISceneManager* smgr = device->getSceneManager();
IGUIEnvironment* guienv = device->getGUIEnvironment();
// Simple config file name - will be saved in current directory
const char* configFileName = "bloom_config.xml";
// Bloom parameters with default values
BloomParams bloomParams;
bloomParams.threshold = 0.3f;
bloomParams.softness = 0.8f;
bloomParams.radius = 8;
bloomParams.strength = 0.6f;
// Try to load configuration from file
if (LoadBloomConfig(bloomParams, configFileName)) {
printf("Configuration loaded from %s\n", configFileName);
} else {
printf("No configuration file found, using default values\n");
}
// Bloom controls with unique IDs
guienv->addStaticText(L"Threshold:", rect<s32>(10,10,100,30), false, false, 0, 1000);
IGUIScrollBar* thresholdScroll = guienv->addScrollBar(true, rect<s32>(110,10,250,30), 0, 1001);
thresholdScroll->setMax(100);
thresholdScroll->setPos((s32)(bloomParams.threshold * 100));
guienv->addStaticText(L"Softness:", rect<s32>(10,40,100,60), false, false, 0, 1002);
IGUIScrollBar* softnessScroll = guienv->addScrollBar(true, rect<s32>(110,40,250,60), 0, 1003);
softnessScroll->setMax(100);
softnessScroll->setPos((s32)(bloomParams.softness * 100));
guienv->addStaticText(L"Radius:", rect<s32>(10,70,100,90), false, false, 0, 1004);
IGUIScrollBar* radiusScroll = guienv->addScrollBar(true, rect<s32>(110,70,250,90), 0, 1005);
radiusScroll->setMax(20);
radiusScroll->setPos(bloomParams.radius);
guienv->addStaticText(L"Strength:", rect<s32>(10,100,100,120), false, false, 0, 1006);
IGUIScrollBar* strengthScroll = guienv->addScrollBar(true, rect<s32>(110,100,250,120), 0, 1007);
strengthScroll->setMax(100);
strengthScroll->setPos((s32)(bloomParams.strength * 100 / 1.5f));
// Save button
IGUIButton* saveButton = guienv->addButton(rect<s32>(10, 130, 250, 160), 0, 1008, L"Save Configuration");
receiver.setSaveButton(saveButton);
receiver.setBloomParams(&bloomParams);
// Load model
IAnimatedMesh* mesh = smgr->getMesh("../../media/sydney.md2");
IAnimatedMeshSceneNode* node = nullptr;
if (!mesh) {
printf("Could not load ../../media/sydney.md2\n");
device->drop();
return 1;
} else {
node = smgr->addAnimatedMeshSceneNode(mesh);
if (node) {
node->setMaterialFlag(EMF_LIGHTING, true);
node->setMD2Animation(scene::EMAT_STAND);
node->setMaterialTexture(0, driver->getTexture("../../media/sydney.bmp"));
// Enable shadow volume
node->addShadowVolumeSceneNode();
node->setMaterialFlag(EMF_NORMALIZE_NORMALS, true);
}
}
// Create floor plane using addHillPlaneMesh with correct parameters
IAnimatedMesh* planeMesh = smgr->addHillPlaneMesh("floor",
dimension2d<f32>(20,20), // Tile size
dimension2d<u32>(25,25), // Tile count
0, // Material (0 for default)
0.0f, // Hill height (0 for flat plane)
dimension2d<f32>(0,0), // Hill count
dimension2d<f32>(20,20)); // Texture repeat count
IMeshSceneNode* floor = smgr->addMeshSceneNode(planeMesh->getMesh(0));
if (floor) {
floor->setPosition(vector3df(0,-25,0));
floor->setMaterialTexture(0, driver->getTexture("../../media/wall.bmp"));
floor->setMaterialFlag(EMF_LIGHTING, true);
floor->setMaterialFlag(EMF_BILINEAR_FILTER, false);
}
// Create rotating light
ILightSceneNode* light = smgr->addLightSceneNode(0, vector3df(0,0,0),
SColorf(1.0f, 1.0f, 1.0f, 1.0f), 100.0f);
if (light) {
light->setPosition(vector3df(0, 20, 30));
light->getLightData().DiffuseColor.set(1.0f, 1.0f, 1.0f);
light->getLightData().SpecularColor.set(0.5f, 0.5f, 0.5f);
light->getLightData().AmbientColor.set(0.2f, 0.2f, 0.2f);
}
// Create isometric camera
ICameraSceneNode* camera = smgr->addCameraSceneNode(0);
// Configure orthographic projection
matrix4 proj;
f32 viewWidth = 200.0f;
f32 viewHeight = 200.0f;
proj.buildProjectionMatrixOrthoLH(viewWidth, viewHeight, 0.1f, 1000.0f);
camera->setProjectionMatrix(proj, true);
vector3df cameraPosition(30, 30, 30);
vector3df cameraTarget(0, 5, 0);
camera->setPosition(cameraPosition);
camera->setTarget(cameraTarget);
// Create 256x256 render target texture
ITexture* renderTexture = driver->addRenderTargetTexture(
dimension2d<u32>(RENDER_WIDTH, RENDER_HEIGHT), "render256");
// Create image for processing
IImage* processedImage = driver->createImage(ECF_A8R8G8B8,
dimension2d<u32>(RENDER_WIDTH, RENDER_HEIGHT));
ITexture* displayTexture = driver->addTexture("display", processedImage);
// Variables for light rotation animation
f32 lightAngle = 0.0f;
f32 lightRadius = 30.0f;
f32 lightHeight = 20.0f;
// Main loop
while(device->run()) {
// UPDATE PARAMETERS - ensure they are read correctly
bloomParams.threshold = thresholdScroll->getPos() / 100.0f;
bloomParams.softness = softnessScroll->getPos() / 100.0f;
bloomParams.radius = radiusScroll->getPos();
bloomParams.strength = strengthScroll->getPos() / 100.0f * 1.5f;
// Check if save button was pressed
if (receiver.isSaveButtonPressed()) {
if (SaveBloomConfig(bloomParams, configFileName)) {
printf("Configuration saved to %s\n", configFileName);
} else {
printf("Error saving configuration\n");
}
}
// Animate light rotation
lightAngle += 0.01f;
if (lightAngle > 360.0f) lightAngle = 0.0f;
if (light) {
f32 x = cosf(lightAngle) * lightRadius;
f32 z = sinf(lightAngle) * lightRadius;
light->setPosition(vector3df(x, lightHeight, z));
}
// Render 3D scene to 256x256 texture
driver->setRenderTarget(renderTexture, true, true, SColor(255,100,101,140));
smgr->drawAll();
// Read and process texture
IImage* renderImage = driver->createImage(renderTexture,
position2d<s32>(0,0), dimension2d<u32>(RENDER_WIDTH, RENDER_HEIGHT));
if (renderImage) {
// Copy and apply bloom
for (u32 y = 0; y < RENDER_HEIGHT; ++y) {
for (u32 x = 0; x < RENDER_WIDTH; ++x) {
processedImage->setPixel(x, y, renderImage->getPixel(x, y));
}
}
ApplyBloomToImage(processedImage, bloomParams);
renderImage->drop();
}
// Update display texture
driver->removeTexture(displayTexture);
displayTexture = driver->addTexture("display", processedImage);
// Display on main screen
driver->setRenderTarget(0, true, true, SColor(255,100,101,140));
// Calculate dimensions for 1:1 aspect ratio
const dimension2du screenSize = driver->getScreenSize();
const u32 displaySize = screenSize.Height;
const u32 xOffset = (screenSize.Width - displaySize) / 2;
// Draw scaled texture
driver->draw2DImage(displayTexture,
rect<s32>(xOffset, 0, xOffset + displaySize, displaySize),
rect<s32>(0, 0, RENDER_WIDTH, RENDER_HEIGHT),
0, 0, true);
// Draw GUI on top
guienv->drawAll();
driver->endScene();
}
// Cleanup
driver->removeTexture(renderTexture);
driver->removeTexture(displayTexture);
processedImage->drop();
device->drop();
return 0;
}

