//----------------------------------------------------------------------------- // Torque Game Engine // // Written by Melvyn May, Started on 9th September 2002. // // "My code is written for the Torque community, so do your worst with it, // just don't rip-it-off and call it your own without even thanking me". // // - Melv. // //----------------------------------------------------------------------------- //$foo = new fxRenderObject() { position = LocalClientConnection.player.getPosition(); Texture = "starter.fps/data/water/water.png"; }; #include "dgl/dgl.h" #include "console/consoleTypes.h" #include "core/bitStream.h" #include "math/mathIO.h" #include "game/gameConnection.h" #include "console/simBase.h" #include "sceneGraph/sceneGraph.h" #include "sceneGraph/sgUtil.h" #include "fxRenderObject.h" //------------------------------------------------------------------------------ // // Okay, here's the wordy-wordy explanation of what's going on here. // // The object is inheriting from the core SceneObject. It is essential that // you inherit from at least this object to be able to add your object into // the SceneGraph. // // DECLARE_CONOBJECT(fxRenderObject); // IMPLEMENT_CO_NETOBJECT_V1(fxRenderObject); // // The lines above are vitally important when creating these network objects. // They register the object with the engine and you can't proceed without them. // The DECLARE goes in your class definition and the IMPLEMENT goes into your // implementation. Anyone who has used MFC should recognise this procedure. // // // *** CONSTRUCTOR *** // // In the constructor you will notice that I set the mTypeMask. This is used // as a standard way of identifing object 'types'. It's called a mask because // you can merge multiple types. One of the most common uses for this is // the collision detection system. The collision detection system allows you // to include certain types when searching. It's these flags here that are // used. You can create your own but be careful as there are only so many // available. Normally you wouldn't do this as you should find one that fits // into what you are doing but the option is always there. // // I also set the net flags. These are a bunch of flags that signify some // important aspects of the object. You should really look at the networking // documentation on the GG site for a basic explanation of ghosts and scoping // as it's a bit much to explain here. The default settings should suffice // unless you are doing something weird (like my fxShapeReplicator)! // // I'm also resetting the mLastRenderTime variable as I used this as a // timestamp. Subtracting the current time from the last obviously gives // me the elapsed time and it's this value that can be used to create // frame-independant animation. // // I also reset most other stuff here to their defaults. It is vitally // important to note that even on a single machine there are two objects // created, one on the server and one on the client. The client could // potentially be on another machine but Torque handles that for you. // // The constructor will be called for both the server and client objects and // so this is a great place to initialise stuff. Try not to allocate memory // in the constructor though, this goes for OOP everywhere though! // // // *** DESTRUCTOR *** // // My destructor is empty because I don't need to do anything for shutdown. // If I had textures allocated then the variable holding that texture // reference will go out of scope and the resource manager will remove the // reference and possibly destroy the texture (if no other objects have // a reference to the same texture object). // // // *** InitPersistFields *** // // It's here that we add our custom fields. Note that this gets called // when the engine is starting up and *not* when the object is created. // What you are essentially doing here is registering with the engine, the // fields that objects of this 'class' will have. Don't put any // object-instance specific code in here otherwise you will be disappointed! // // Use the addField function to define your fields but be careful not to // use duplicate fieldnames or ones with spaces in them. You can happily // use underscores though. // // If you've merged with the [HEAD] then you can used my new field-grouping // which enables you to group blocks of fields that are related. They look // much nicer in the editor when they are grouped and most things become // easier to find. You can use addGroup("MyGroup1")/endGroup("MyGroup1") // entries around you field-blocks. I have left an example in the code for // you to look at (commented-out). // // // *** OnAdd *** // // After the object is built (either from you creating one in the editor or // upon mission start-up) this function is called by the engine to allow the // object to add itself into the scene and setup scene specific attributes. // // First-up always try to call parent functions if they are there. // // Next I setup the object box. This is an object-space box that defines the // region which the object occupies. It is important that you get this correct // as the SceneTraversal routines uses this box to see if the object is in the // view. Setting it too small results in the object not being rendered when // in-fact it should be. Too big and the objects render function will get // called when it's not on the screen which is just wasted processing. If the // object dynamically changes shape during gameplay then you can either create // an object-box at start-up that completely encapsulates its' volume or you // can dynamically change the object-box. Remember though, that if you must // reset the world box and set the render transform after doing so and that // you must do this client-side as well. // // After you've set the object-space box you *must always* reset the world-box. // What this does is calculate, using the current objects position, the // world-space coordinates of this box. It also calculates the world-sphere // which is a sphere that encapsulates the specified box. // Also, you must set the render transform. Failure to do this can result in // weird and hard to track results in animations and editor manipulation. // // Finally we add the object to the scene with the simple call "addToScene()". // // Returning true here confirms that everything was okay. If you return false // then the server will assume that there has been a problem and will // disconnect the client informing them that this resource failed to load and // that you may have incorrect resources or wrong engine version, so watch out! // // // *** onRemove *** // // This one's dead easy. "removeFromScene()" does exactly that. Again, call // the parent functions where needed. // // // *** inspectPostApply *** // // This is an interesting function as it's called (Server-Side only) when the // user hits the "Apply" button when editing the object. We respond by doing // the nice thing and calling the parent and then signalling which bits of data // need to get transferred to the client. // // We do this using "setMaskBits(fxRenderObjectMask)". Doing this will result // in the "packUpdate(...)" (see below) function being called. Essentially, // the mask you specify here is your customised mask you created in the header // of the object. // // Quite often you will only have one and it will trigger all the object data // being sent. This can be wasteful and if your object does this alot during // gameplay then you would want to send only the data that has changed or at // least only relevant groups of data that has changed. I explain these masks // more in the "packUpdate" section below. // // // *** onEditorEnable / onEditorDisable *** // // I left these functions in as they can be extremely useful when you want your // object to act differently when you know the editor is on. For instance, I // used this in my fxShapeReplicator to signal that the editor was on. When it // was I would additionally draw a sweeping arc defining the replication area. // // // *** prepRenderImage *** // // Let me just say before I start an explanation here that this function should // work without modification for most objects you will want to create and so if // I loose you then just skip it. ;) // // Let me open-up the inner workings of the engine a little... // // After an object has been created client-side, it's "OnAdd" function is called // (see above) and it adds itself to the scene. But what does this mean? // // Well the engine has lots of structures (believe me lots) one of which is a // container. This, as it's name implies, can contain objects. You've heard // me throw the term SceneGraph around but what does it do? Well, in Torque, all // client-side objects are in a Container named "gClientContainer". Similarly, // all server-side objects are in a Container named "gServerContainer" but let's // focus on the client-side here as it's ultimately the client-side rendering we // are interested in for this discussion. // // A container isn't just a big ol' list of objects, it's more than that. You may // have seen various error messages like "Object is not out of the bins" etc? // // The Container splits up the world into cubes called bins. When an object is // added to the scene, it's bounding-box is used to find which of these bins it // belongs in. Basically it identifies a box in the world. If the object is too // big then it gets put into a linear list called the overflow bin (where naughty // objects go). // // When you do a CastRay using the engine it basically checks which of these // bins it intersects. It then calls all the objects in turn that reside in these // bins. The objects can then do their own, perhaps more detailed, collision check // that may be polygon perfect or just the bounding-box, it's entirely upto the // object itself. // // Where is this going you say? Well, this system is also used to determine what // objects are in the view. The viewing frustum defines a region that can be // checked to see which bins it intersects (plus the naughty objects in the overflow // bin). Using this, these objects "in-scope" have their "prepRenderImage" called // to allow them to do exactly that, prepare a render image. // // Phew! Well what's a render image you say? Boy am I glad you asked that! // // A "SceneRenderImage" is simply a structure that defines the rendering attributes // of the object that control the way the SceneTraversal routines handle it. // Important factors such as is the object translucent (in which case it needs // sorted back->front) can be specified here. // // In my example, you will see me dynamically allocating a "SceneRenderImage", // populating relevant fields and then inserting it into the SceneState. You don't // need to destroy it, the SceneGraph will do that after the frame is complete. // // There are lots of fields that I just haven't got the energy to go into here but // two important ones I will. The "isTranslucent" field tells the SceneGraph that // at least one portion of your object has transparency. If you didn't already // know it, rendering transparent objects requires that objects be rendered from // back to front (far to near) order to look correct. I won't go into why this is // so because then I'm getting into imaging and then I'll really bore you. ;) // // The second important field is "sortType" which allows you to specify the way // the object is handled by the scene. You can use the following ... // // Non-translucent objects ... // // Sky - Specialised for Sky object only. // Terrain - Specialised for Terrain object only. // Normal - Standard one to use for non-translucent objects. // // Translucent objects only ... // // Point - Object is a point defined by state->poly[0] // Plane - Object is a plane defined by state->poly[0-3] // EndSort - Object should appear after everything else has rendered // BeginSort - Object should appear before everything else has rendered // // The "EndSort" is useful for rendering lens flares which need to be rendered // after everything else has been rendered. // // The "BeginSort" is handy for rendering stuff behind the terrain but after // the Sky like my fxSunLight object which renders a remote object and a local // object. // // This brings me to an interesting point. You can insert more than one // SceneRenderImage into the SceneState during the call to "prepRenderImage" // which results in the "renderObject" being called more than once at the // point defined by the "SceneRenderImage"s attributes. I do this with the // fxSunLight but be sure you know what you are doing with this as you can // significantly reduce your framerate by causing the same objects to be // rendered multiple times. // // Why this complexity? Well, it's a very powerful method to use because you // do all sorts of wizardry by traversing scene portals, clipping the frustum // and iterating the new frustum upto a specified depth to give you the // ability to create mirrors, water reflections and other portal effects. // // // *** renderObject *** // // Alrighty then! The SceneTraversal is stuffed with "SceneRenderImage"s, it // then does all the sorting it needs and then proceeds to actually act on them. // // Because each "SceneRenderImage" has a reference to the object (notice the line // that adds "state->obj = this", it then calls the "renderObject" for each object. // // Now we are nearly ready to render. When you enter the "renderObjec" function // we are in what is known as the Canonical state. It's just another way of // saying a standard state or a consistent state that is guaranteed everytime // we get to this position. This is handy for many reason but mostly because // you reduce the possibilty of other objects' state settings interacting with // your functions. Imagine having to reset every possible state setting that could // possibly affect you, nightmare! // // It's always good to check for this state with an AssertFatal at the beginning // and end of this function. // // First-up, we get the current timestamp. We use "getVirtualMilliseconds" here // that gives us the virtual game timeslice timestamp and we calculate the // elapsed time since our last render. This is useful because we can use it by // multiplying it with our changes to give us a frame-rate independant animation. // This is also handy as you can normalise your object attributes in units/second. // // We then check the texture handle in the example, we can't render without it. // This is specific to this example and can be ignored for you own object if you wish. // Note though, that as soon as you have changed the rendering state in any way // then you should restore the canonical state before returning otherwise you // will get a fatal assertion further on down the line! // // I'm not going to explain the rendering pipeline here because there are many // books which do the same. Drop me an email and I'll recommend one, I think // I've personally got them all!! // // We *must* save the PROJECTION and MODELVIEW matrices plus the VIEWPORT so // that we can restore them to our nice and friendly canonical state before exit. // // With everything saved, we multiply the MODELVIEW by our objects transform // to effectively move our origin to the objects' position. All our graphical // commands are then in object-space (or at-least relative to the objects origin). // // I won't explain the rest of the function because it's just OpenGL (possibly // translated into DirectX) and I'm not here to teach you that! // // Before we exit we restore canonical state which is really important. As I // stated above, I do an assertion to double-check this. You can never be to // cautious with this and it goes away in the RELEASE build anyway. // // // *** packUpdate *** // // Right, we are back to the networking. Remember above (inspectPostApply) when // we made a call to "setMaskBits(...)"? // // After we did that, this function gets called. Remember that we are now // server-side. This function effectively queues-up a bit-stream to be sent // to the client (irrelevant of whether the client is on the same PC or half-way // around the world). // // First-up we call the parent so that it can get any data it owns sent-out. // // We then do something a little clever. When the function was called, it was // passed the mask that we sent using the "setMaskBits(...)" function. // // When designing your objects you can split the data up into groups that // you can get sent using the appropriate mask(s). // // For each group you *always* write a flag signalling whether the group has // data to send. The client will check this and know whether the following // data is the rest of the group data or another group flag. // // In the example I have only one group controlled by the mask, // "fxRenderObjectMask". Because I do a boolean "AND" against the passed-in // mask I effectively send whether the group is being sent or not. The // "writeFlag" function is also kind enough to return the result of this // masking so that you can use it as a condition in an if(cond) statement as // I do. // // Within this block you can call a multitude of functions from the BitStream // class that allow you to write-out your data to the network. // // The return value here is important as it becomes the new state mask used by // the networking code. If you manage to send all your data then you should // return a zero. Returning any other mask value results in the "packUpdate" // function being called again with the returned mask. Typically you would // return the value from the "Parent::packUpdate" function" which which returns // zero. If there are 'blocks' of data that you couldn't pack for any reason // and you want to try again next time then you should return something like // "mask & ~(state flags sent)". To cut a long story short though, returning // zero indicates that you send all your data and the story ends there unless // you are doing something a little more technical! // // // *** unpackUpdate *** // // If you've understood the above then you shouldn't have any problems here. // // You should also know that we are now client-side. // // Eventually, after the "packUpdate" function was called (for each client), // the data is sent out to respective clients. It is important to note that // the "packUpdate" function is called once for each client connected and that // each client maintains it's own state mask. // // First-up here is to do the friendly thing and call the parents "unpackUpdate" // routine to allow it to load it's data from the stream. // // We then call "readFlag" which should correspond to the flag we wrote in the // "packUpdate" function. If it's true then we know that the group data follows // and we proceed to read it. // // It's using this "readFlag" then reading group data that we can control exactly // what data elements get sent/received. You could do a mask per data item but that // would be overkill. If you look deeper you find that the stream read/write // commands also let you save bandwidth by allowing you to specify bit-lengths // of data items but be sure you know what you are doing with these. // // Another interesting note is that after the data has been loaded I load-up // any specified textures here. Most of the standard engine objects *don't* do // this and only load them up in the "onAdd()" function (see above) which // obviously only gets called once at startup. This accounts for why you need // to restart the mission to see your changes. I recently modified the "Sky" // object by adding the "loadDML()" call into the client-side "unpackUpdate" // function. // // Also, note that you should never load texture resources on the server as the // server doesn't actually do any rendering and so it's pointless. // // Also, because some functions are called server-side *and* client-side you need a // method of knowing which side you are on and doing an appropriate action. // You can do this with the calls "isClientObject()" and "isServerObject()". // // ****************** // // Well, I hope you found this informative and I'm sorry if I didn't go into // some areas in enough detail but I had to limit this somehow. // // If you have any specific questions then drop me an email to... // // melv.may@btinternet.com // // All the best everyone, // // - Melv. // //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ // // Put the function in /example/common/editor/ObjectBuilderGui.gui [around line 458] ... // // function ObjectBuilderGui::buildfxRenderObject(%this) // { // %this.className = "fxRenderObject"; // %this.process(); // } // //------------------------------------------------------------------------------ // // Put this in /example/common/editor/EditorGui.cs in [function Creator::init( %this )] // // %Environment_Item[ next free entry ] = "fxRenderObject"; <-- ADD THIS. // //------------------------------------------------------------------------------ extern bool gEditingMission; //------------------------------------------------------------------------------ IMPLEMENT_CO_NETOBJECT_V1(fxRenderObject); //------------------------------------------------------------------------------ // Class: fxRenderObject //------------------------------------------------------------------------------ fxRenderObject::fxRenderObject() { // Setup NetObject. mTypeMask |= StaticObjectType | StaticTSObjectType | StaticRenderedObjectType; mNetFlags.set(Ghostable); // Reset Last Render Time. mLastRenderTime = 0; // Texture Handle. mTextureHandle = NULL; // Flare Texture Name. mTextureName = StringTable->insert(""); // Reset Quad Size. mQuadSize = 5.0f; // Reset Quad Rotate Speed. mQuadRotateSpeed = 90.0f; // Reset Current Angle. mCurrentAngle = 0.0f; mObjColor = ColorI(255, 0, 0); } //------------------------------------------------------------------------------ fxRenderObject::~fxRenderObject() { } //------------------------------------------------------------------------------ void fxRenderObject::initPersistFields() { // Initialise parents' persistent fields. Parent::initPersistFields(); // Add out own persistent fields. //addGroup( "MyFirstGroup1" ); addField( "QuadSize", TypeF32, Offset( mQuadSize, fxRenderObject ) ); addField( "QuadRotateSpeed",TypeF32, Offset( mQuadRotateSpeed, fxRenderObject ) ); addField( "Texture", TypeFilename, Offset( mTextureName, fxRenderObject ) ); addField( "ObjColor", TypeColorI, Offset( mObjColor, fxRenderObject ) ); //enddGroup( "MyFirstGroup1" ); } //------------------------------------------------------------------------------ bool fxRenderObject::onAdd() { if(!Parent::onAdd()) return(false); // Calculate Quad Radius. F32 QuadHalfSize = mQuadSize / 2.0f; // Set initial bounding box. // // NOTE:- Set this box to completely encapsulate your object. // You must reset the world box and set the render transform // after changing this. mObjBox.min.set( -QuadHalfSize, -0.5f, -QuadHalfSize ); mObjBox.max.set( QuadHalfSize, +0.5f, QuadHalfSize ); // Reset the World Box. resetWorldBox(); // Set the Render Transform. setRenderTransform(mObjToWorld); // Add to Scene. addToScene(); // Return OK. return(true); } //------------------------------------------------------------------------------ void fxRenderObject::onRemove() { // Remove from Scene. removeFromScene(); // Do Parent. Parent::onRemove(); } //------------------------------------------------------------------------------ void fxRenderObject::inspectPostApply() { // Set Parent. Parent::inspectPostApply(); // Set fxPortal Mask. setMaskBits(fxRenderObjectMask); } //------------------------------------------------------------------------------ void fxRenderObject::onEditorEnable() { } //------------------------------------------------------------------------------ void fxRenderObject::onEditorDisable() { } //------------------------------------------------------------------------------ bool fxRenderObject::prepRenderImage( SceneState* state, const U32 stateKey, const U32 startZone, const bool modifyBaseZoneState) { // Return if last state. if (isLastState(state, stateKey)) return false; // Set Last State. setLastState(state, stateKey); // Is Object Rendered? if (state->isObjectRendered(this)) { // Yes, so get a SceneRenderImage. SceneRenderImage* image = new SceneRenderImage; // Populate it. image->obj = this; image->isTranslucent = false; image->sortType = SceneRenderImage::Normal; // Insert it into the scene images. state->insertRenderImage(image); } return false; } //------------------------------------------------------------------------------ void fxRenderObject::renderObject(SceneState* state, SceneRenderImage*) { // Check we are in Canonical State. AssertFatal(dglIsInCanonicalState(), "Error, GL not in canonical state on entry"); // Calculate Elapsed Time and take new Timestamp. S32 Time = Platform::getVirtualMilliseconds(); F32 ElapsedTime = (Time - mLastRenderTime) * 0.001f; mLastRenderTime = Time; // Return if we don't have a texture. if (!mTextureHandle) return; // Save state. RectI viewport; // Save Projection Matrix so we can restore Canonical state at exit. glMatrixMode(GL_PROJECTION); glPushMatrix(); // Save Viewport so we can restore Canonical state at exit. dglGetViewport(&viewport); // Setup the projection to the current frustum. // // NOTE:- You should let the SceneGraph drive the frustum as it // determines portal clipping etc. // It also leaves us with the MODELVIEW current. // state->setupBaseProjection(); // Save ModelView Matrix so we can restore Canonical state at exit. glPushMatrix(); // Transform by the objects' transform e.g move it. dglMultMatrix(&getTransform()); // Rotate by Rotate Speed. // // NOTE:- We use the elapsed time as a coeficient, // that way we get consistent rotational speed // independant of frame-rate. // mCurrentAngle += mFmod(mQuadRotateSpeed * ElapsedTime, 360); // Rotate Quad by current roation. glRotatef(mCurrentAngle, 0,0,1); // Setup our rendering state (alpha blending). glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // Enable Texturing. glEnable(GL_TEXTURE_2D); // Select the objects' texture. glBindTexture(GL_TEXTURE_2D, mTextureHandle.getGLName()); // Set Colour/Alpha. glColor4f(1,1,1,1); // Calculate Quad Radius. F32 QuadHalfSize = mQuadSize / 2.0f; // Draw a Quad. // // NOTE:- We draw in object space here and *not* world-space. // Notice that Z is UP in this system and XY lie on the terrain plane. // glBegin(GL_QUADS); glTexCoord2f(0,0); glVertex3f(-QuadHalfSize,0,+QuadHalfSize); glTexCoord2f(1,0); glVertex3f(+QuadHalfSize,0,+QuadHalfSize); glTexCoord2f(1,1); glVertex3f(+QuadHalfSize,0,-QuadHalfSize); glTexCoord2f(0,1); glVertex3f(-QuadHalfSize,0,-QuadHalfSize); glEnd(); // Restore our canonical rendering state. glDisable(GL_BLEND); glDisable(GL_TEXTURE_2D); // Restore our canonical matrix state. glPopMatrix(); glMatrixMode(GL_PROJECTION); glPopMatrix(); glMatrixMode(GL_MODELVIEW); // Restore our canonical viewport state. dglSetViewport(viewport); // Check we have restored Canonical State. AssertFatal(dglIsInCanonicalState(), "Error, GL not in canonical state on exit"); } //------------------------------------------------------------------------------ U32 fxRenderObject::packUpdate(NetConnection * con, U32 mask, BitStream * stream) { // Pack Parent. U32 retMask = Parent::packUpdate(con, mask, stream); // Write fxPortal Mask Flag. if (stream->writeFlag(mask & fxRenderObjectMask)) { // Write Object Transform. stream->writeAffineTransform(mObjToWorld); // Write Texture Name. stream->writeString(mTextureName); // Write Quad Size. stream->write(mQuadSize); // Write Quad Rotate Speed. stream->write(mQuadRotateSpeed); // Write Object Color stream->write( mObjColor ); } // Were done ... return(retMask); } //------------------------------------------------------------------------------ void fxRenderObject::unpackUpdate(NetConnection * con, BitStream * stream) { // Unpack Parent. Parent::unpackUpdate(con, stream); // Read fxPortal Mask Flag. if(stream->readFlag()) { MatrixF ObjectMatrix; // Read Object Transform. stream->readAffineTransform(&ObjectMatrix); // Read Texture Name. mTextureName = StringTable->insert(stream->readSTString()); // Read Quad Size. stream->read(&mQuadSize); // Read Quad Rotate Speed. stream->read(&mQuadRotateSpeed); // Read Object Color stream->read( &mObjColor ); // Set Transform. setTransform(ObjectMatrix); // Reset our previous texture handle. mTextureHandle = NULL; // Load the texture (if we've got one) if (*mTextureName) mTextureHandle = TextureHandle(mTextureName, BitmapTexture, true); // Calculate Quad Radius. F32 QuadHalfSize = mQuadSize / 2.0f; // Set bounding box. // // NOTE:- Set this box to completely encapsulate your object. // You must reset the world box and set the render transform // after changing this. mObjBox.min.set( -QuadHalfSize, -0.5f, -QuadHalfSize ); mObjBox.max.set( QuadHalfSize, +0.5f, QuadHalfSize ); // Reset the World Box. resetWorldBox(); // Set the Render Transform. setRenderTransform(mObjToWorld); } }