Graphics.DspRat - lordmundi/wikidoctest GitHub Wiki

Dsp Rat

Author: Todd Heino

On this page… (hide)

  1. 1. Description
  2. 2. Installation
    1. 2.1 Plugin Code
    2. 2.2 Example Setup

1.  Description

This plugin demonstrates the ability to create a Runtime Adjustable Texture (RAT) within EDGE. Changing a texture at runtime is nothing new in computer graphics. However, graphics developers have a desire for optimization and often have no need for changing textures. As a result, static, pre-loaded textures have become the norm. In fact, EDGE has no method (to my knowledge) of modifying a texture for your model at runtime.

There are several motivations for a RAT within EDGE. Using RATs, one could create a monitor-like screen within your world. If your world included a manned-vehicle and you wanted to show an astronaut perspective of a mission you may want to include the same displays the astronauts would see. These displays are constantly changing in the world and would be a good application of RATs. These could be created using pre-recorded video files of what the display would look like or created within EDGE by drawing directly to the RAT. In my own exploration, I was even able to create a second EDGE world within a RAT. Complete with lighting and any models you wish to put in it.

For the purposes of ALHAT, our motivation was to load a new hazard map into the world whenever output by our sim. The basic functionality of this is demonstrated in this plugin. It will accept a path to either a TGA or PGM file and use that as a texture for a square drawn by the plugin. The plugin looks at a model named "RAT" and uses the XYZ, PYR and visible parameters to display the RAT square appropriately. Whenever it detects a change in the files modified attribute, it automatically reloads its texture to match the changes in the file. Therefore, when our sim outputs a new hazard map, it will be reflected in the world.

2.  Installation

2.1  Plugin Code

As this plugin is more for demonstration purposes than a ready-to-use tool, I am posting the source code files with the hope of inspiring community development. It has lots of comments attempting to explain every detail of what I was doing. If you've read through the EDGE Newbie Tutorial on here, you should be ready to understand everything that is going on in this plugin.

The plugin source code: Attach:dsp_rat.c
Last Updated: August 18, 2010, at 02:19 PM
[Show dsp_rat.c complete source code][9]

/*
File name: dsp_rat.c
Author: Todd Heino
Written: August 14, 2010

For lots of information all about how textures work in OpenGL:
http://glprogramming.com/red/chapter09.html
*/

#include "doug.h"
#include "dsp_double.h"
#include <time.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>

//This pdouble contains all our scene data. We use this to get information about what is going on in EDGE.
static DSS_DOUBLE_DATA *pdouble = NULL;
/*
I just made a static variable that contains the name of the texture file. 
You could have put this as a parameter in your load texture functions. 
*/
static const char* texFilename = "/home/theino/Desktop/diceTexture2.pgm";
//This is a openGL integer and is a unique identifier for a texture
static GLuint texName;
/*
Contains data about our EDGE node RAT. This is just a node created in a configuration file
that this plugin reads to adjust orientation of our drawn texture
*/
static DSS_NODE *RAT;
//The id of the node RAT
static int rat_id;
/*
A variable to keep track of the current version of the texture we have loaded.
Since we're reading from a file, this tells us what the modified time on that file was when we 
last loaded the file into the texture.
*/
static time_t currModifiedTime;

/*
A function which draws the square that our texture is mapped to. The y and z parameters can be used
to increase the size of the object drawn.
*/
void DrawObject(double y, double z)
{
	//Scale the size of our square to the parameters specified
	glScaled(1, y, z);
	/*
	This allows our square to be seen on both sides. Normally when working with polygons in
	openGL it is ok to only draw one side of the polygon. Often the other side of the polygon is
	inside a 3d object and you wouldn't see the inside anyway. So when drawing a cube, openGL would
	only render the outside of the cube.
	*/
	glDisable(GL_CULL_FACE);
	//We're going to start doing textures
	glEnable(GL_TEXTURE_2D);
	//Active blend mode so partially transparent objects work properly.
	glEnable(GL_BLEND);
	//Defines what kind of blending we're doing
	glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);	
	//We want the texture to replace whatever surface the polygon may have without it
	glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
	//Use our texture defined by the id texName on whatever we're drawing next
	glBindTexture(GL_TEXTURE_2D, texName);
	//Draw a square
	glBegin(GL_QUADS);
		glTexCoord2f(1,0); glVertex3f(0,1,1);
		glTexCoord2f(0,0); glVertex3f(0,-1,1);
		glTexCoord2f(0,1); glVertex3f(0,-1,-1);
		glTexCoord2f(1,1); glVertex3f(0,1,-1);
	glEnd();
	glDisable(GL_TEXTURE_2D);
}

/*
A function which reads a PGM file specified by texFilename and loads it into our texture.
PGM is a Portable Gray Map format and only specifies a single gray value for each pixel.
For our application of hazard maps, I made this gray value map to red. The texture is also set
at a constant 50% transparency.
*/
void LoadNewTexturePGM()
{
	//This will contain the initial data we get from the file
	char *data;
	//Header information (we probably won't need a full 128)
	char header[128];
	//Just an intermediate variable to tokenize some of our data
	char *token;
	/*
	This is the data that will get passed into the texture.
	It undergoes some very minor processing from the data variable
	*/
	char *texData;
	//Pointer to our file
	FILE *file;
	//Width height and depth for data size purposes
	int fileWidth, fileHeight, fileDepth; 
	//Maximum gray scale number and an indexing variable
	int grayScale, pixel;
	file = fopen(texFilename, "rb");
	//The do-while structure allows us to keep reading until we get something that isn't a comment
	do
	{
		//Get a line
		fgets(header, 128, file);
		//Ignore it if it is a comment
		if(header[0] != '#')
		{
			//P5 is the unique header of a pgm file. If this doesn't appear, we aren't looking at a pgm
			if(!(header[0] == 'P' && header[1] == '5'))
			{
				//Not a pgm so exit
				fclose(file);
				printf("PGM format invalid");
				exit(0);
			}
		}
	} while(header[0] == '#');
	//Another do-while looking for comments
	do
	{	
		//Get a line
		fgets(header, 128, file);
		//Ignore it if it is a comment
		if(header[0] != '#')
		{
			//At this point we should get a line for the width of our file and the height
			token = strtok(header," ");
			fileWidth = atoi(token);
			token = strtok(NULL," ");
			fileHeight = atoi(token);
		}
	} while(header[0] == '#');
	//Yet another do-while for comments
	do
	{
		//Get a line
		fgets(header, 128, file);
		//Ignore it if it is a comment
		if(header[0] != '#')
		{
			//A line containing only the maximum gray scale value
			grayScale = atoi(header);
			//If our gray scale value is greater than 255, each of our pixels is specified with 2 bytes
			fileDepth = grayScale > 255 ? 2 : 1;
		}
	} while(header[0] == '#');
	//Based on our height width and size of each pixel we can know how much data we need to allocate
	data = malloc(fileHeight*fileWidth*fileDepth);
	//Set index to 0
	pixel = 0;
	//Read in the rest of our data
	fread(data,fileWidth*fileHeight*fileDepth,1,file);
	//If our data was 2 bytes in length, lets just shrink it back into somehting that will fit into 1 byte
	if(fileDepth == 2)
	{
		while(pixel < fileWidth*fileHeight)
		{
			data[pixel] = (char)(((int)data[pixel*2] + (int)data[pixel*2 + 1])/256);
			pixel++;
		}
	}
	//Our texture data is going to have 4 bytes per pixel, corresponding to rgba
	texData = malloc(fileHeight*fileWidth*4);
	//Set index back to 0
	pixel = 0;
	/*
	Take our current data and put it into our texData variable.
	Remember we want the gray value to map to red and our transparency to be 50% (or 127)
	*/
	while(pixel < fileHeight*fileWidth)
	{
		texData[pixel*4] = data[pixel];
		texData[pixel*4 + 1] = 0;
		texData[pixel*4 + 2] = 0;
		texData[pixel*4 + 3] = 127;
		pixel++;
	}
	//Bind our texture "texName". This allows us to change around certain settings of our texture.
	glBindTexture(GL_TEXTURE_2D, texName);
	/*
	These are just parameters that define how we deal with our texture.
	You can find out more about these online
	*/
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	//This is the final line that actually says "make our texture"
	glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA,fileWidth,fileHeight,0,GL_RGBA,GL_UNSIGNED_BYTE,texData);
}


/*
A function which reads a TGA file specified by texFilename and loads it into our texture.
TGA is a format that allows specification of rgba values for each pixel. Gimp allows you to make
TGA files. It is a larger specification than PGM and this loader doesn't support everything TGA does.
Files read into this must not use compression.
*/
void LoadNewTextureTGA()
{
	/*
	The data variable in this case is used to take in data from the file and then modified
	in the same variable to work for the texture. The header is for the header data.
	*/
	char *data, *header;
	//Point to our file
	FILE *file;
	//Width height and depth for data size purposes. Pixel is an indexing variable
	int fileWidth, fileHeight, fileDepth, pixel;
	//This is used to set the color mode which can either be GL_RGB or GL_RGBA
	unsigned int colorMode;
	//This is a temporary variable used for a byte swap
	char temp;
	//Open our file
	file = fopen(texFilename, "rb");
	//Set header size ot use (may not use all of this)
	header = malloc(20);
	//Read the first 18 bytes of the file
	fread(header, 18, 1, file);
	//Just checking some bytes to make sure this is a TGA file that we can read
	if (header[2] != 2 && header[2] != 10)
	{
		fclose(file);
		printf("TGA Type Error\n");
		exit(0);
	}
	//Skips ahead of any comments
	if (header[0])
	{
		fseek(file, header[0], SEEK_CUR);
	}
	//Getting our width, height and depth. In this case depth is either 3 or 4 depending on if we use an alpha channel
	fileWidth = header[13] * 256 + header[12];
	fileHeight = header[15] * 256 + header[14];
	fileDepth = header[16] / 8;
	//If we don't have a depth of 3 or 4, we have a problem
	if(fileDepth != 3 && fileDepth != 4)
	{
		fclose(file);
		printf("File BPP Error\n");
		exit(0);
	}
	//Set the size of the data needed to be read in
	data = malloc(fileWidth*fileHeight*fileDepth);
	//Read in the image data from our file
	fread(data,fileWidth*fileHeight*fileDepth,1,file);
	//Set index to 0
	pixel = 0;
	/*
	TGA format specifies its colors in BGR format. The texture we are using needs it to be in RGB format.
	This while-loop goes through the data and flips the r and the b for each pixel
	*/
	while(pixel < fileWidth*fileHeight*fileDepth)
	{
		temp = data[pixel];
		data[pixel] = data[pixel+2];
		data[pixel+2] = temp;
		pixel += fileDepth;
	}
	//We're done reading the file, lets close it
	fclose(file);

	/*
	I'm a fan of the ternary operator. This is just saying if fileDepth is equal to 3, then set colorMode to GL_RGB... 
	otherwise set it to GL_RGBA. An equivalent form would be:
	if(fileDepth == 3)
	{
		colorMode = GL_RGB; //Alpha isn't specified
	}
	else
	{
		colorMode = GL_RGBA; //Alpha is specified
	}
	*/
	colorMode = fileDepth==3 ? GL_RGB : GL_RGBA;

	//Bind our texture "texName". This allows us to change around certain settings of our texture.
	glBindTexture(GL_TEXTURE_2D, texName);
	/*
	These are just parameters that define how we deal with our texture.
	You can find out more about these online
	*/
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	//This is the final line that actually says "make our texture"
	glTexImage2D(GL_TEXTURE_2D,0,colorMode,fileWidth,fileHeight,0,colorMode,GL_UNSIGNED_BYTE,data);
}

/*
This function gets called repeatedly to update the plugin. Anything you want to happen on each refresh should be put in here
*/
void DSP_UpdateCube()
{
	//This contains information about the file we want to open. In this case, all we care about is the last time it was modified
	struct stat fileInfo;
	//Check to make sure the stat was successful
	if (!stat(texFilename,&fileInfo))
	{
		//Is the modified time greater than the last time we updated the texture
		if(currModifiedTime < fileInfo.st_mtime)
		{
			/* 
			This is just making sure that the modified time isn't equal to the current time. We want to wait a second in case
			we are in the middle of writing our new texture to file.
			*/
			if(time(NULL) - 1 > fileInfo.st_mtime)
			{
				/* 
				We're going to call the texture functions to modify our texture, so we need to update currModifiedTime to tell
				it we're making this update.
				*/
				currModifiedTime = fileInfo.st_mtime;
				//If the last character in our texFilename is an a, we know it refers to a tga file... otherwise its pgm
				if(texFilename[strlen(texFilename)-1] == 'a')
				{
					LoadNewTextureTGA();
				}
				else 
				{
					LoadNewTexturePGM();
				}
			}
		}
	}
	/*
	This tells EDGE to continue doing everything else it is doing. If a plugin function does not call 
	ExecuteCore, execution of that stack terminates in that function. Check the wiki for more details:
	Graphics.DougPluginFunctionReference#toc9
	*/
	DSF_ExecuteCore();
	/*
	I asked Frankie about BgnSceneAccess and this is what he had to say:
	the BgnSceneAccess call is really a semaphore protection for when
	DOUG is running with shared memory and multiple processes.  It doesn't
	hurt but probably wouldn't matter if you didn't have it since people
	rarely do this anymore.
	*/
	DSF_BgnSceneAccess( DSV_scene, DSD_READ_ACCESS );
		//I've also found this put be mostly unnecessary. Not entirely sure what it does.
		DSF_PushDrawingState();
			/*
			Lets ignore lighting for now. In the DrawObject method we're telling OpenGL to draw the texture 
			directly to the object. Therefore the lighting would have no affect on what we're doing, so why
			waste processor cycles trying
			*/
			glDisable( GL_LIGHTING );
			/*
			The next two lines are saying switch to the modelview matrix and then load in EDGE's current matrix.
			We do this so our object exists in "EDGE space" rather than following the camera around.
			*/
			glMatrixMode( GL_MODELVIEW );
			glLoadMatrixd(pdouble->DSV_modelview_matrix[0]);
			/*
			We translate and rotate our object to match what is specified in our RAT object. This
			is something that is specified in the EDGE configuration files. Rather than assigning it
			to some pre-made model, we leave it empty and just use the values that can be specified.
			*/
			glTranslatef( RAT->x, RAT->y, RAT->z);
	      glRotatef(RAT->P, 0, 1, 0 );
			glRotatef(RAT->R, 1, 0, 0 );
			glRotatef(RAT->Y, 0, 0, 1 );
			/*
			RAT->attrib contains information about the object. One of them you see in the GUI is
			whether or not you want it to be visible. This is contained in the first bit of attrib,
			and that is the only attribute this is checking right now. If it isn't visible, don't DrawObject.
			You could extend it to implement the other things as well.
			*/
         if(!(RAT->attrib&1))
				DrawObject( 3, 3);
		//PopDrawingState matches with PushDrawingState	
		DSF_PopDrawingState();
	//EndSceneAccess matches with BgnSceneAccess
	DSF_EndSceneAccess();
}

/*
This function gets called once when first creating our plugin within the EDGE environment.
*/
DSP_InitializePlugin( DSS_PLUGIN *plugin )
{
	/*
	Get data about our scene and make pdouble a pointer to that data. The pointer allows us to see
	any changes that may occur, without calling DSF_GetDataFromScene again.
	*/
	pdouble = DSF_GetDataFromScene( DSV_scene, "DSS_DOUBLE_DATA" );
	/*
	Checks to see if pdouble was set properly. If it wasn't write out the error message.
	The main reason for this is you aren't in double precision mode, although this is default now for
	EDGE, so you're unlikely to encounter this. I just kept this in here from the EDGE Newbie tutorial.
	*/
	if (!pdouble) {
		char msg[300];
		sprintf( msg, "\nWorking in single precision mode. Restart with double argument in command line: ./run_graphics -double\n");
		DSF_LogWarning( msg );
		return 0;
	}
	//Get our rat_id, so we can find specific EDGE object
	rat_id = DSF_GetNodeID("RAT");
	/*
	Set RAT to that EDGE object. node_list is a pointer to all the EDGE objects. To get a
	pointer to our specific one, just add rat_id
	*/
	RAT = DSV_scene->node_list + rat_id;
	/*
	Make our previously defined DSP_UpdateCube method occur on DSD_PLUGIN_DRAW_SCENE.
	There are other options for this, and I have found it is easiest to just experiment to
	determine which one you want. This one seems to occur everytime the scene is re-drawn.
	Which for my simple test case is at about 500Hz.
	*/
	DSF_AppendPluginFunction( plugin->handle, DSP_UpdateCube, DSD_PLUGIN_DRAW_SCENE);
	return  1;
}

Take a look through the source code, it should explain all the programming details through comments. If something isn't clear, feel free to post on here or send me an email.

2.2  Example Setup

Under Construction

[9]: javascript:toggleObj('togglecode_id0','hide','Show dsp_rat.c complete source code','Show dsp_rat.c complete source code','')

⚠️ **GitHub.com Fallback** ⚠️