MappingImageExample.cpp

<< Click to Display Table of Contents >>

Navigation:  Simplygon 7.1 examples >

MappingImageExample.cpp

///////////////////////////////////////////////////////////////////////////
//
//  System:    Simplygon
//  File:      MappingImageExample.cpp
//  Language:  C++
//
//  Copyright (c) 2015 Donya Labs AB. All rights reserved.
//
//  This is private property, and it is illegal to copy or distribute in
//  any form, without written authorization by the copyright owner(s).
//
///////////////////////////////////////////////////////////////////////////
//
//  #Description#
//
//  A scene is loaded from a WaveFront file and the geometries are reduced.
//
//  The input objects are textured with a procedurally generated texture.
//
//  A mapping image object is created for the reduced scene.
//  The mapping image is used to manually cast the original material onto the LOD.
//
//  The textures and optimized objects are saved to file.
//
///////////////////////////////////////////////////////////////////////////
#include "../Common/Example.h"
#include "BMPheader.h"
#include <map>
#include <sstream>
#include <string>
void RunTextureCastingWithMappingImage(const std::string& readFrom, const std::string& writeTo);
spImageData GenerateBrickImage(unsigned int image_width, unsigned int image_height);
float clamp(float value, float min, float max);
int clamp(int value, int min, int max);
int main( int argc , char* argv[] )
    {   
    InitExample();
    // Set global variable. Using Orthonormal method for calculating
    // tangentspace.
    sg->SetGlobalSetting( "DefaultTBNType" , SG_TANGENTSPACEMETHOD_ORTHONORMAL );
    std::string assetPath = GetAssetPath();
    // Run the example code
    RunTextureCastingWithMappingImage( assetPath + "material_casting_test_object.obj", "object_textured" );
    DeinitExample();
    return 0;
    }
void RunTextureCastingWithMappingImage(const std::string& readFrom, const std::string& writeTo)
    {
    //Read object and set output paths
    //Import geometries from a file and optimize them.
    std::string output_geometry_filename = GetExecutablePath() + writeTo + ".obj";
    std::string bmp_output_name = GetExecutablePath() + writeTo + "_diffuse.bmp";
    //Load object from file
    spWavefrontImporter objReader = sg->CreateWavefrontImporter();
    objReader->SetImportFilePath( readFrom.c_str() );
    if( !objReader->RunImport() )
        return;
    //Get the scene from the importer
    spScene scene = objReader->GetScene();
    //Get the material table
    spMaterialTable original_materials = scene->GetMaterialTable();
    //Create a new texture and texture the original geometry with it
    uint input_texture_width = 512;
    uint input_texture_height = 512;
    spImageData input_texture = GenerateBrickImage(input_texture_width, input_texture_height);
    //Preserve the diffuse texture colors
    original_materials->GetMaterial(0)->SetColor(SG_MATERIAL_CHANNEL_DIFFUSE, 1.0f,1.0f,1.0f,1.0f);
    //Darken the specular texture
    original_materials->GetMaterial(0)->SetColor(SG_MATERIAL_CHANNEL_SPECULAR, 0.2f,0.2f,0.2f,1.0f);
    spUnsignedCharArray input_colors = SafeCast<IUnsignedCharArray>(input_texture->GetColors());
    // Select all the SceneMesh nodes from the scene
    spSelectionSet allGeometriesSelectionSet = scene->GetSelectionSetTable()->GetSelectionSet(scene->SelectNodes("ISceneMesh"));
    //Store a copy of the original GeometyData objects
    std::vector<spGeometryData> original_unchanged_geometries;
    //Map from the node with the geometry to the index in the copied original geometry vector
    std::map<spSceneMesh, uint> scene_mesh_node_to_index;
    for(uint i = 0 ; i < allGeometriesSelectionSet->GetItemCount() ; ++i)
        {
        spSceneMesh sceneMesh = Cast<ISceneMesh>(scene->GetNodeByGUID(allGeometriesSelectionSet->GetItem(i)));
        //Give the scene mesh a name if it doesn't have one
        rstring current_name = sceneMesh->GetName();
        if( current_name->IsEmpty() )
            {
            char new_geometry_name[256];
            sprintf(new_geometry_name, "Object%d", i);
            sceneMesh->SetName(new_geometry_name);
            }
        scene_mesh_node_to_index.insert(std::pair<spSceneMesh, uint>(sceneMesh, (uint)original_unchanged_geometries.size()));
        original_unchanged_geometries.push_back(sceneMesh->GetGeometry()->NewCopy(true));
        }
    //Reducer
    spReductionProcessor red = sg->CreateReductionProcessor();
    red->SetScene(scene);
    //Set the Repair Settings.
    spRepairSettings repair_settings = red->GetRepairSettings();
    repair_settings->SetWeldDist( 0.0f );
    repair_settings->SetTjuncDist( 0.0f );
    //Set the Reduction Settings.
    spReductionSettings reduction_settings = red->GetReductionSettings();
    //Texture coordinates will be calculated after the reduction is done,
    //so there is no need for texture coordinates to be preserved.
    reduction_settings->SetTextureImportance(0.0f);
    //Reduce to one tenth original triangles
    reduction_settings->SetTriangleRatio(0.1f);
    // Set the Normal Calculation Settings.
    spNormalCalculationSettings normal_settings = red->GetNormalCalculationSettings();
    normal_settings->SetReplaceNormals( true );
    normal_settings->SetHardEdgeAngleInRadians( 3.14159f * 90.f / 180.0f );
    //Setup the mapping image properties
    uint texture_width = 512; // Should be a multiple of 256
    uint texture_height = 512; // Should be a multiple of 256
    uint multisampling = 4; // 256 should be divisible by this
    // Set the Image Mapping Settings for the reducer
    spMappingImageSettings mapping_settings = red->GetMappingImageSettings();
    // generate diffuse, specular, normal maps and custom channel later.
    mapping_settings->SetGenerateMappingImage( true );
    // Set to generate new texture coordinates.
    mapping_settings->SetGenerateTexCoords( true );
    // The higher the number, the fewer texture-borders.
    mapping_settings->SetParameterizerMaxStretch( 0.9f );
    // Buffer space for when texture is mip-mapped, so color values don't blend over.
    mapping_settings->SetGutterSpace( 4 );
    mapping_settings->SetWidth( texture_width );
    mapping_settings->SetHeight( texture_height );
    mapping_settings->SetMultisamplingLevel( multisampling );
    // Reduce the geometry
    red->RunProcessing();
    // Create a BMP to store the new texture in
    output_image new_texture;
    new_texture.setup(texture_width,texture_height);
    new_texture.set_mapping(0,0);
    // Use the Mapping image to create a new texture for the reduced geometry
    // Get the mapping image object that contains information back to the original geometries
    spMappingImage mapping_image = red->GetMappingImage();
    // spMappingImageMeshData contains information needed to convert the mapping data to the individual
    // geometries from the collection of geometries
    spMappingImageMeshData mapping_image_mesh_data = mapping_image->GetMappingMeshData();
    // The spChunkedImageData contains per-texel mapping information back to the original geometries
    // as two fields:
    //
    //  - Original triangle id, from a geometry consisted of all the original geometry combined.
    //    This means that the local triangle IDs from each original geometries will not valid here.
    //    Instead, the triangle ID is a global ID from the combined geometry.
    //    To map back to local triangle IDs, the spMappingImageMeshData is used.
    //   
    //  - The original Barycentric coordinates
    //
    spChunkedImageData chunked_image_data = mapping_image->GetImageData();
    // Get the number of image data chunks
    uint x_chunks = chunked_image_data->GetXSize();
    uint y_chunks = chunked_image_data->GetYSize();
    // Loop the image data
    // First by chunk
    // Then by pixel
    // Then by sub-pixel (if multisampling > 1)
    for(uint x_chunk = 0 ; x_chunk < x_chunks ; ++x_chunk)
        {
        for(uint y_chunk = 0 ; y_chunk < y_chunks ; ++y_chunk)
            {
            // Lock and fetch a chunk of mapping image pixels
            spImageData current_chunk = chunked_image_data->LockChunk2D(x_chunk, y_chunk);
            // Get the mapping image fields "TriangleIds" & "BarycentricCoords"
            spRidArray triangle_ids = SafeCast<IRidArray>(current_chunk->GetField( "TriangleIds" ));
            spUnsignedShortArray barycentric_coords = SafeCast<IUnsignedShortArray>(current_chunk->GetField( "BarycentricCoords" ));
            // Continue with next iteration if any of the fields doesn't exist
            if(!triangle_ids || !barycentric_coords)
                continue;
            // Get the number of pixels in the chunk, excluding sub-pixels
            uint px_size = current_chunk->GetXSize() / multisampling;
            uint py_size = current_chunk->GetYSize() / multisampling;
            //Loop the pixels
            for(uint py = 0 ; py < py_size ; ++py)
                {
                for(uint px = 0 ; px < px_size ; ++px)
                    {
                    // Initialize pixel color
                    int pixel_r = 0;
                    int pixel_g = 0;
                    int pixel_b = 0;
                    // samples_used is used when multisampling to divide by the number of samples used per particular pixel
                    int samples_used = 0;
                    // Loop the sub-pixels (will only loop once if multisampling is 1)
                    for(uint py_multisample = 0 ; py_multisample < multisampling ; ++py_multisample)
                        {
                        for(uint px_multisample = 0 ; px_multisample < multisampling ; ++px_multisample)
                            {
                            // The one-dimensional index of the current sub-pixel
                            // index = column + row * column_size
                            rid sub_pixel_index = ( px * multisampling + px_multisample ) + ( py * multisampling + py_multisample ) * ( px_size * multisampling );
                            // The global triangle ID in the combined geometry
                            rid global_triangle_id = triangle_ids->GetItem(sub_pixel_index);
                            // Continue with the next sub-pixel, should the current sub-pixel not map to a triangle
                            if(global_triangle_id < 0)
                                continue;
                            // Get the corresponding barycentric coordinates in the original geometry
                            unsigned short barycentric_x = barycentric_coords->GetItem(sub_pixel_index * 2 + 0);
                            unsigned short barycentric_y = barycentric_coords->GetItem(sub_pixel_index * 2 + 1);
                            unsigned short barycentric_z = 65535 - barycentric_x - barycentric_y;
                            // Get the normalized barycentric coordinates
                            float barycentric_x_normalized = barycentric_x / float(65535);
                            float barycentric_y_normalized = barycentric_y / float(65535);
                            float barycentric_z_normalized = barycentric_z / float(65535);
                            // Declare the local triangle id variable
                            uint local_triangle_id = -1;
                            uint geometry_count = mapping_image_mesh_data->GetMappedGeometriesCount();
                            int mapped_geometry_id = -1;
                            // To find which original geometry is being mapped to using the global triangle id
                            // the mapping image mesh data is used.
                            for(uint g_id = 0 ; g_id < geometry_count ; ++g_id)
                                {
                                int next_geometry_starting_triangle_id = -1;
                                // If g_id is the last geometry, then
                                // keep next_geometry_starting_triangle_id as -1
                                if(g_id < geometry_count - 1)
                                    {
                                    next_geometry_starting_triangle_id = mapping_image_mesh_data->GetStartTriangleIdOfGeometry(g_id + 1);
                                    }
                                // If the global triangle id is below the next geometry's starting triangle id,
                                // we know that the current geomety (g_id) will contain the current triangle
                                //
                                // Also if next_geometry_starting_triangle_id is -1, since then we're at the last geometry
                                if( global_triangle_id < next_geometry_starting_triangle_id || next_geometry_starting_triangle_id == -1 )
                                    {
                                    mapped_geometry_id = g_id;
                                    uint starting_triangle_index = mapping_image_mesh_data->GetStartTriangleIdOfGeometry(g_id);
                                    local_triangle_id = global_triangle_id - starting_triangle_index;
                                    break;
                                    }
                                }
                            // Make sure the mapped-to geometry is valid
                            assert(mapped_geometry_id >= 0 && mapped_geometry_id < (int)geometry_count );
                            //////
                            // The scene_path can be retrieved from the mapping image
                            // the path corresponds to its place in the scene and the scene mesh name
                            rstring scene_path = mapping_image_mesh_data->GetScenePathOfGeometry(mapped_geometry_id);
                            //////
                            spSceneMesh sceneMesh = Cast<ISceneMesh>(scene->GetNodeFromPath(scene_path));
                            // Now we can get whatever information in the original geometry that we want
                            // In this example, we fetch the original texture coordinates
                            uint original_geometry_index = scene_mesh_node_to_index.find(sceneMesh)->second;
                            spRealArray texcoords = original_unchanged_geometries[original_geometry_index]->GetTexCoords(0);
                            int corner0 = local_triangle_id * 3 + 0;
                            int corner1 = local_triangle_id * 3 + 1;
                            int corner2 = local_triangle_id * 3 + 2;
                            float u_corner0 = texcoords->GetItem(corner0 * 2 + 0);
                            float v_corner0 = texcoords->GetItem(corner0 * 2 + 1);
                            float u_corner1 = texcoords->GetItem(corner1 * 2 + 0);
                            float v_corner1 = texcoords->GetItem(corner1 * 2 + 1);
                            float u_corner2 = texcoords->GetItem(corner2 * 2 + 0);
                            float v_corner2 = texcoords->GetItem(corner2 * 2 + 1);
                            // With the barycentric coordinates we can interpolate to a precise texture coordinate value in the triangle
                            float u = barycentric_x_normalized * u_corner0 + barycentric_y_normalized * u_corner1 + barycentric_z_normalized * u_corner2;
                            float v = barycentric_x_normalized * v_corner0 + barycentric_y_normalized * v_corner1 + barycentric_z_normalized * v_corner2;
                            // With the texture coords, we sample the original texture (the one we created in the beginning)
                            int sample_x = int(u * input_texture_width);
                            int sample_y = int(v * input_texture_height);
                            // Clamp
                            sample_x = sample_x > (int)input_texture_width - 1 ? input_texture_width - 1 : sample_x;
                            sample_y = sample_y > (int)input_texture_height - 1 ? input_texture_height - 1 : sample_y;
                            // Fetch input texture color
                            unsigned char rc = input_colors->GetItem( ( sample_x + sample_y * input_texture_width ) * 3 + 0);
                            unsigned char gc = input_colors->GetItem( ( sample_x + sample_y * input_texture_width ) * 3 + 1);
                            unsigned char bc = input_colors->GetItem( ( sample_x + sample_y * input_texture_width ) * 3 + 2);
                            // Accumulate colors in the pixel color variables
                            pixel_r += rc;
                            pixel_g += gc;
                            pixel_b += bc;
                            samples_used++;
                            }
                        }
                    // Normalize colors based on number of sub-pixels used
                    if(samples_used > 0)
                        {
                        pixel_r /= samples_used;
                        pixel_g /= samples_used;
                        pixel_b /= samples_used;
                        }
                    // Set the output texture color
                    uint texture_px = x_chunk * px_size + px;
                    uint texture_py = y_chunk * py_size + py;
                    new_texture.set_pixel(texture_px, texture_py, 2, pixel_r);
                    new_texture.set_pixel(texture_px, texture_py, 1, pixel_g);
                    new_texture.set_pixel(texture_px, texture_py, 0, pixel_b);
                    }
                }
            // Unlock the chunk of image data
            chunked_image_data->UnlockChunk2D(x_chunk, y_chunk);
            }
        }
    // Store the new texture as a BMP
    new_texture.write_to_file( bmp_output_name.c_str() );
    // Add the new texture to the reduced geometry
    // Create new material table.
    spMaterialTable output_materials = sg->CreateMaterialTable();
    //  Create new material for the table.
    spMaterial output_material = sg->CreateMaterial();
    output_material->SetName( "diffuse_brick_texture" );
    //Add the new material to the table
    output_materials->AddMaterial( output_material );
    //Add the diffuse texture to the diffuse channel in the output material
    output_materials->GetMaterial(0)->SetTexture(SG_MATERIAL_CHANNEL_DIFFUSE, bmp_output_name.c_str());
    scene->GetMaterialTable()->Clear();
    scene->GetMaterialTable()->Copy(output_materials);
    // Export the LOD
    spWavefrontExporter objexp = sg->CreateWavefrontExporter();
    objexp->SetExportFilePath( output_geometry_filename.c_str() );
    objexp->SetScene(scene);
    objexp->RunExport();
    }
spImageData GenerateBrickImage(unsigned int image_width, unsigned int image_height)
    {
    spImageData img = sg->CreateImageData();
    //Set imagedata dimensions
    img->Set2DSize(image_width, image_height);
    //Add rgb colors to imagedata
    img->AddColors(TYPES_ID_UCHAR, "RGB");
    //Get the reference to the colors to array with tuple size 3, tuple count (image_width x image_height)
    spUnsignedCharArray colors = SafeCast<IUnsignedCharArray>(img->GetColors());
    //Create bmp to store texture
    //Not needed, only used for debugging and displaying
    output_image bmpOut;
    bmpOut.setup(image_width,image_height);
    bmpOut.set_mapping(0,0);
    //Brick texture parameters
    unsigned int brick_height = 15;
    unsigned int brick_length = 35;
    unsigned int brick_apart = 1;
    int brick_R = 165;
    int brick_G = 66;
    int brick_B = 25;
    int between_bricks_color = 20;
    int between_bricks_noise_color = 170;
    //Iterate over the image pixels and set the array values
    for(unsigned int y = 0 ; y < image_height ; y++)
        for(unsigned int x = 0 ; x < image_width ; x++)
            {
            //One dimensional index
            int i = image_width * y + x;
            //Generate random values to add noise to the texture
            int random1 = rand()%50 - 25;
            int random2 = rand()%50 - 25;
            int random3 = rand()%50 - 25;
            //Every second row of bricks is offset half a brick length
            unsigned int brick_offset = (uint)(0.5 * brick_length) * ((uint)(y / brick_height) % 2);
            //Check if xy-coordinate is on or betweeen bricks
            bool between_bricks = ( (x + brick_offset) % brick_length < brick_apart ) || ( y % brick_height < brick_apart );
            //Add some noise to the coordinates
            bool between_bricks_noise = ( (x + random1/20 + brick_offset) % brick_length < brick_apart ) || ( (y + random2/20) % brick_height < brick_apart );
            //Brick colors to array
            colors->SetItem(i*3+0, clamp((int)( between_bricks_noise ? between_bricks_noise_color+random1 : (between_bricks ? between_bricks_color+random1 : brick_R+random1 ) ), 0, 255));
            colors->SetItem(i*3+1, clamp((int)( between_bricks_noise ? between_bricks_noise_color+random1 : (between_bricks ? between_bricks_color+random1 : brick_G+random2 ) ), 0, 255));
            colors->SetItem(i*3+2, clamp((int)( between_bricks_noise ? between_bricks_noise_color+random1 : (between_bricks ? between_bricks_color+random1 : brick_B+random3 ) ), 0, 255));
            //Copy colors to bmp
            bmpOut.set_pixel(x, y, 2, colors->GetItem(i*3+0));
            bmpOut.set_pixel(x, y, 1, colors->GetItem(i*3+1));
            bmpOut.set_pixel(x, y, 0, colors->GetItem(i*3+2));
            }
        //Store bmp
        bmpOut.write_to_file("brick_texture.bmp");
        return img;
    }
float clamp(float value, float min, float max)
    {
    return value < min ? min : ( value > max ? max : value );
    }
int clamp(int value, int min, int max)
    {
    return value < min ? min : ( value > max ? max : value );
    }