You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1210 lines
32 KiB
1210 lines
32 KiB
/*==LICENSE==* |
|
|
|
CyanWorlds.com Engine - MMOG client, server and tools |
|
Copyright (C) 2011 Cyan Worlds, Inc. |
|
|
|
This program is free software: you can redistribute it and/or modify |
|
it under the terms of the GNU General Public License as published by |
|
the Free Software Foundation, either version 3 of the License, or |
|
(at your option) any later version. |
|
|
|
This program is distributed in the hope that it will be useful, |
|
but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
GNU General Public License for more details. |
|
|
|
You should have received a copy of the GNU General Public License |
|
along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
|
|
Additional permissions under GNU GPL version 3 section 7 |
|
|
|
If you modify this Program, or any covered work, by linking or |
|
combining it with any of RAD Game Tools Bink SDK, Autodesk 3ds Max SDK, |
|
NVIDIA PhysX SDK, Microsoft DirectX SDK, OpenSSL library, Independent |
|
JPEG Group JPEG library, Microsoft Windows Media SDK, or Apple QuickTime SDK |
|
(or a modified version of those libraries), |
|
containing parts covered by the terms of the Bink SDK EULA, 3ds Max EULA, |
|
PhysX SDK EULA, DirectX SDK EULA, OpenSSL and SSLeay licenses, IJG |
|
JPEG Library README, Windows Media SDK EULA, or QuickTime SDK EULA, the |
|
licensors of this Program grant you additional |
|
permission to convey the resulting work. Corresponding Source for a |
|
non-source form of such a combination shall include the source code for |
|
the parts of OpenSSL and IJG JPEG Library used as well as that of the covered |
|
work. |
|
|
|
You can contact Cyan Worlds, Inc. by email legal@cyan.com |
|
or by snail mail at: |
|
Cyan Worlds, Inc. |
|
14617 N Newport Hwy |
|
Mead, WA 99021 |
|
|
|
*==LICENSE==*/ |
|
////////////////////////////////////////////////////////////////////////////// |
|
// // |
|
// plGeometrySpan Class Functions // |
|
// // |
|
//// Version History ///////////////////////////////////////////////////////// |
|
// // |
|
// Created 3.8.2001 mcn // |
|
// // |
|
////////////////////////////////////////////////////////////////////////////// |
|
|
|
#include "hsWindows.h" |
|
#include "hsTypes.h" |
|
#include "plGeometrySpan.h" |
|
#include "../plSurface/hsGMaterial.h" |
|
#include "../plSurface/plLayerInterface.h" |
|
#include "hsBitVector.h" |
|
|
|
|
|
//// Static Data ///////////////////////////////////////////////////////////// |
|
|
|
hsBitVector plGeometrySpan::fInstanceGroupIDFlags; |
|
UInt32 plGeometrySpan::fHighestReadInstanceGroup = 0; |
|
|
|
hsTArray<hsTArray<plGeometrySpan *> *> plGeometrySpan::fInstanceGroups; |
|
|
|
|
|
//// Constructor and Destructor ////////////////////////////////////////////// |
|
|
|
plGeometrySpan::plGeometrySpan() |
|
{ |
|
IClearMembers(); |
|
} |
|
|
|
plGeometrySpan::plGeometrySpan( const plGeometrySpan *instance ) |
|
{ |
|
IClearMembers(); |
|
MakeInstanceOf( instance ); |
|
} |
|
|
|
plGeometrySpan::~plGeometrySpan() |
|
{ |
|
ClearBuffers(); |
|
} |
|
|
|
void plGeometrySpan::IClearMembers( void ) |
|
{ |
|
fVertexData = nil; |
|
fIndexData = nil; |
|
fMaterial = nil; |
|
fNumVerts = fNumIndices = 0; |
|
fBaseMatrix = fNumMatrices = 0; |
|
fLocalUVWChans = 0; |
|
fMaxBoneIdx = 0; |
|
fPenBoneIdx = 0; |
|
fCreating = false; |
|
fFogEnviron = nil; |
|
fProps = 0; |
|
|
|
fMinDist = fMaxDist = -1.f; |
|
|
|
fWaterHeight = 0; |
|
|
|
fMultColor = nil; |
|
fAddColor = nil; |
|
|
|
fDiffuseRGBA = nil; |
|
fSpecularRGBA = nil; |
|
fInstanceRefs = nil; |
|
fInstanceGroupID = kNoGroupID; |
|
fSpanRefIndex = (UInt32)-1; |
|
|
|
fLocalToOBB.Reset(); |
|
fOBBToLocal.Reset(); |
|
|
|
fDecalLevel = 0; |
|
|
|
fMaxOwner = nil; |
|
} |
|
|
|
//// ClearBuffers //////////////////////////////////////////////////////////// |
|
|
|
void plGeometrySpan::ClearBuffers( void ) |
|
{ |
|
// If UserOwned, the actual buffer data belongs to someone else (like a BufferGroup). |
|
// Just erase our knowledge of it and move on. |
|
if( fProps & kUserOwned ) |
|
{ |
|
IClearMembers(); |
|
return; |
|
} |
|
|
|
bool removeData = true; |
|
|
|
|
|
// If we have an instanceRefs array, remove ourselves from |
|
// the array. If we are the last in the array, remove the array itself |
|
if( fInstanceRefs != nil ) |
|
{ |
|
if( fInstanceRefs->GetCount() == 1 ) |
|
{ |
|
delete fInstanceRefs; |
|
|
|
// Remove the group ID flag as well |
|
IClearGroupID( fInstanceGroupID ); |
|
} |
|
else |
|
{ |
|
int idx = fInstanceRefs->Find( this ); |
|
hsAssert( idx != fInstanceRefs->kMissingIndex, "Invalid instance ref data in plGeometrySpan::ClearBuffers()" ); |
|
|
|
fInstanceRefs->Remove( idx ); |
|
removeData = false; // Don't remove data until we're the last one |
|
} |
|
fInstanceRefs = nil; |
|
fInstanceGroupID = kNoGroupID; |
|
} |
|
|
|
if( removeData ) |
|
{ |
|
delete [] fVertexData; |
|
fVertexData = nil; |
|
|
|
delete [] fMultColor; |
|
delete [] fAddColor; |
|
fMultColor = nil; |
|
fAddColor = nil; |
|
} |
|
|
|
delete [] fIndexData; |
|
delete [] fDiffuseRGBA; |
|
delete [] fSpecularRGBA; |
|
fIndexData = nil; |
|
fDiffuseRGBA = fSpecularRGBA = nil; |
|
} |
|
|
|
//// MakeInstanceOf ////////////////////////////////////////////////////////// |
|
// Note: instancing copies the index buffer but just copies the POINTERS |
|
// for the vertex buffers and source colors. This is because the indices |
|
// will eventually change but the vertices won't. |
|
|
|
void plGeometrySpan::MakeInstanceOf( const plGeometrySpan *instance ) |
|
{ |
|
hsTArray<plGeometrySpan *> *array; |
|
|
|
|
|
ClearBuffers(); |
|
|
|
/// Adjust the instanceRefs array |
|
array = instance->fInstanceRefs; |
|
if( array == nil ) |
|
{ |
|
// Go find a new groupID |
|
instance->fInstanceGroupID = IAllocateNewGroupID(); |
|
|
|
instance->fInstanceRefs = array = TRACKED_NEW hsTArray<plGeometrySpan *>; |
|
// Go figure, it won't append the const version to the array...this is a cheat, |
|
// but then, so is making fInstanceRefs mutable :) |
|
array->Append( (plGeometrySpan *)instance ); |
|
} |
|
|
|
fInstanceGroupID = instance->fInstanceGroupID; |
|
array->Append( this ); |
|
fInstanceRefs = array; |
|
|
|
/// Copy over the data |
|
IDuplicateUniqueData( instance ); |
|
fVertexData = instance->fVertexData; |
|
fMultColor = instance->fMultColor; |
|
fAddColor = instance->fAddColor; |
|
|
|
/// All done! |
|
} |
|
|
|
void plGeometrySpan::IUnShareData() |
|
{ |
|
if( fVertexData ) |
|
{ |
|
UInt8* oldVtxData = fVertexData; |
|
|
|
UInt32 size = GetVertexSize( fFormat ); |
|
|
|
fVertexData = TRACKED_NEW UInt8[ size * fNumVerts ]; |
|
memcpy( fVertexData, oldVtxData, size * fNumVerts ); |
|
} |
|
|
|
if( fMultColor ) |
|
{ |
|
hsColorRGBA* oldMult = fMultColor; |
|
|
|
fMultColor = TRACKED_NEW hsColorRGBA[ fNumVerts ]; |
|
memcpy( fMultColor, oldMult, sizeof(hsColorRGBA) * fNumVerts ); |
|
} |
|
|
|
if( fAddColor ) |
|
{ |
|
hsColorRGBA* oldAdd = fAddColor; |
|
|
|
fAddColor = TRACKED_NEW hsColorRGBA[ fNumVerts ]; |
|
memcpy( fAddColor, oldAdd, sizeof(hsColorRGBA) * fNumVerts ); |
|
} |
|
} |
|
|
|
void plGeometrySpan::BreakInstance() |
|
{ |
|
hsAssert(fInstanceRefs, "Breaking instancing when I'm not instanced"); |
|
int idx = fInstanceRefs->Find(this); |
|
hsAssert(idx != fInstanceRefs->kMissingIndex, "I'm not in my own instance refs list"); |
|
fInstanceRefs->Remove(idx); |
|
hsAssert(fInstanceRefs->GetCount(), "Don't BreakInstance if I'm the last one, use UnInstance instead"); |
|
|
|
fInstanceGroupID = IAllocateNewGroupID(); |
|
fInstanceRefs = TRACKED_NEW hsTArray<plGeometrySpan*>; |
|
fInstanceRefs->Append(this); |
|
|
|
IUnShareData(); |
|
|
|
fProps |= plGeometrySpan::kInstanced; |
|
} |
|
|
|
void plGeometrySpan::ChangeInstance(plGeometrySpan* newInstance) |
|
{ |
|
hsAssert(fInstanceRefs, "Changing instancing when I'm not instanced"); |
|
int idx = fInstanceRefs->Find(this); |
|
hsAssert(idx != fInstanceRefs->kMissingIndex, "I'm not in my own instance refs list"); |
|
fInstanceRefs->Remove(idx); |
|
|
|
fInstanceGroupID = newInstance->fInstanceGroupID; |
|
fInstanceRefs = newInstance->fInstanceRefs; |
|
fInstanceRefs->Append(this); |
|
|
|
fVertexData = newInstance->fVertexData; |
|
fMultColor = newInstance->fMultColor; |
|
fAddColor = newInstance->fAddColor; |
|
|
|
fProps |= plGeometrySpan::kInstanced; |
|
} |
|
|
|
void plGeometrySpan::UnInstance() |
|
{ |
|
hsAssert(fInstanceRefs, "UnInstancing a non-instance"); |
|
|
|
int idx = fInstanceRefs->Find(this); |
|
hsAssert(idx != fInstanceRefs->kMissingIndex, "I'm not in my own instance refs list"); |
|
fInstanceRefs->Remove(idx); |
|
|
|
// If we're the last one, we just take ownership of the shared data, |
|
// else we make our own copy of it. |
|
if( !fInstanceRefs->GetCount() ) |
|
{ |
|
delete fInstanceRefs; |
|
} |
|
else |
|
{ |
|
IUnShareData(); |
|
} |
|
fInstanceRefs = nil; |
|
fInstanceGroupID = kNoGroupID; |
|
fProps &= ~plGeometrySpan::kInstanced; |
|
} |
|
|
|
//// IAllocateNewGroupID ///////////////////////////////////////////////////// |
|
// Static function that allocates a new groupID by finding an empty slot in |
|
// the bitVector, then marking it as used and returning that bit # |
|
|
|
UInt32 plGeometrySpan::IAllocateNewGroupID( void ) |
|
{ |
|
UInt32 id; |
|
|
|
|
|
for( id = 0; id < fInstanceGroupIDFlags.GetSize(); id++ ) |
|
{ |
|
if( !fInstanceGroupIDFlags.IsBitSet( id ) ) |
|
break; |
|
} |
|
|
|
fInstanceGroupIDFlags.SetBit( id, true ); |
|
return id + 1; |
|
} |
|
|
|
//// IClearGroupID /////////////////////////////////////////////////////////// |
|
// Done with this groupID, so clear the entry in the bit table. |
|
|
|
void plGeometrySpan::IClearGroupID( UInt32 groupID ) |
|
{ |
|
fInstanceGroupIDFlags.ClearBit( groupID - 1 ); |
|
} |
|
|
|
//// IGetInstanceGroup /////////////////////////////////////////////////////// |
|
// This does the whole hash table thing lookup during read. If: |
|
// - The group ID does not yet exist: |
|
// - Allocates a new hsTArray, puts it into the hash table if expectedCount > 1, |
|
// sets the groupID flag, and returns a pointer to the array. |
|
// - The group ID does exist: |
|
// - If the array count is less than expectedCount - 1, returns the array |
|
// - else sets the hash entry to nil and returns the array. |
|
// Since we want to clear the hash table as soon as possible, but don't want |
|
// to search the entire hash table every time to make sure its empty, we |
|
// keep an ID of the highest element in the hash table that's set; every time |
|
// we remove an entry, we decrement this ID until we hit a used pointer again; |
|
// if we don't find one, we reset the array. |
|
|
|
hsTArray<plGeometrySpan *> *plGeometrySpan::IGetInstanceGroup( UInt32 groupID, UInt32 expectedCount ) |
|
{ |
|
hsTArray<plGeometrySpan *> *array; |
|
|
|
|
|
groupID--; // Make it our array index |
|
|
|
if( fInstanceGroups.GetCount() <= groupID || fInstanceGroups[ groupID ] == nil ) |
|
{ |
|
// Not yet in the list--make a new hsTArray |
|
array = TRACKED_NEW hsTArray<plGeometrySpan *>; |
|
fInstanceGroupIDFlags.SetBit( groupID, true ); |
|
|
|
if( expectedCount > 1 ) |
|
{ |
|
if( fInstanceGroups.GetCount() <= groupID ) |
|
fInstanceGroups.ExpandAndZero( groupID + 1 ); |
|
|
|
fInstanceGroups[ groupID ] = array; |
|
if( fHighestReadInstanceGroup < groupID + 1 ) |
|
fHighestReadInstanceGroup = groupID + 1; |
|
} |
|
|
|
return array; |
|
} |
|
else |
|
{ |
|
// In the list...get it, but are we done with it? |
|
array = fInstanceGroups[ groupID ]; |
|
if( expectedCount == array->GetCount() + 1 ) // I.E. next Append() will make it == |
|
{ |
|
// Done with it, remove from hash table |
|
fInstanceGroups[ groupID ] = nil; |
|
|
|
// Find new fHighestReadInstanceGroup |
|
for( ; fHighestReadInstanceGroup > 0 && fInstanceGroups[ fHighestReadInstanceGroup - 1 ] == nil; |
|
fHighestReadInstanceGroup-- ); |
|
|
|
if( fHighestReadInstanceGroup == 0 ) |
|
fInstanceGroups.Reset(); |
|
} |
|
|
|
// Either way, return the array |
|
return array; |
|
} |
|
} |
|
|
|
//// IDuplicateUniqueData //////////////////////////////////////////////////// |
|
// Does the copy on all unique data--i.e. data copied for both clones and |
|
// instances. |
|
|
|
void plGeometrySpan::IDuplicateUniqueData( const plGeometrySpan *source ) |
|
{ |
|
fCreating = source->fCreating; |
|
fVertAccum = source->fVertAccum; |
|
fIndexAccum = source->fIndexAccum; |
|
|
|
fMaterial = source->fMaterial; |
|
fLocalToWorld = source->fLocalToWorld; |
|
fWorldToLocal = source->fWorldToLocal; |
|
fLocalBounds = source->fLocalBounds; |
|
fWorldBounds = source->fWorldBounds; |
|
fFogEnviron = source->fFogEnviron; |
|
|
|
fBaseMatrix = source->fBaseMatrix; |
|
fNumMatrices = source->fNumMatrices; |
|
fLocalUVWChans = source->fLocalUVWChans; |
|
|
|
fFormat = source->fFormat; |
|
fProps = source->fProps; |
|
fNumVerts = source->fNumVerts; |
|
fNumIndices = source->fNumIndices; |
|
fDecalLevel = source->fDecalLevel; |
|
|
|
if( source->fIndexData != nil ) |
|
{ |
|
fIndexData = TRACKED_NEW UInt16[ fNumIndices ]; |
|
memcpy( fIndexData, source->fIndexData, sizeof( UInt16 ) * fNumIndices ); |
|
} |
|
else |
|
fIndexData = nil; |
|
|
|
if( source->fDiffuseRGBA ) |
|
{ |
|
fDiffuseRGBA = TRACKED_NEW UInt32[ fNumVerts ]; |
|
memcpy( fDiffuseRGBA, source->fDiffuseRGBA, sizeof( UInt32 ) * fNumVerts ); |
|
} |
|
else |
|
fDiffuseRGBA = nil; |
|
|
|
if( source->fSpecularRGBA ) |
|
{ |
|
fSpecularRGBA = TRACKED_NEW UInt32[ fNumVerts ]; |
|
memcpy( fSpecularRGBA, source->fSpecularRGBA, sizeof( UInt32 ) * fNumVerts ); |
|
} |
|
else |
|
fSpecularRGBA = nil; |
|
|
|
fLocalToOBB = source->fLocalToOBB; |
|
fOBBToLocal = source->fOBBToLocal; |
|
} |
|
|
|
//// CopyFrom //////////////////////////////////////////////////////////////// |
|
// Duplicate this span from a given span. |
|
|
|
void plGeometrySpan::CopyFrom( const plGeometrySpan *source ) |
|
{ |
|
UInt32 size; |
|
|
|
|
|
// Just to make sure |
|
ClearBuffers(); |
|
|
|
IDuplicateUniqueData( source ); |
|
|
|
fInstanceRefs = nil; |
|
fInstanceGroupID = kNoGroupID; |
|
|
|
if( source->fVertexData != nil ) |
|
{ |
|
size = GetVertexSize( fFormat ); |
|
|
|
fVertexData = TRACKED_NEW UInt8[ size * fNumVerts ]; |
|
memcpy( fVertexData, source->fVertexData, size * fNumVerts ); |
|
} |
|
else |
|
fVertexData = nil; |
|
|
|
if( source->fMultColor ) |
|
{ |
|
fMultColor = TRACKED_NEW hsColorRGBA[ fNumVerts ]; |
|
memcpy( fMultColor, source->fMultColor, sizeof(hsColorRGBA) * fNumVerts ); |
|
} |
|
else |
|
{ |
|
fMultColor = nil; |
|
} |
|
|
|
if( source->fAddColor ) |
|
{ |
|
fAddColor = TRACKED_NEW hsColorRGBA[ fNumVerts ]; |
|
memcpy( fAddColor, source->fAddColor, sizeof(hsColorRGBA) * fNumVerts ); |
|
} |
|
else |
|
{ |
|
fAddColor = nil; |
|
} |
|
} |
|
|
|
//// Read //////////////////////////////////////////////////////////////////// |
|
|
|
void plGeometrySpan::Read( hsStream *stream ) |
|
{ |
|
UInt32 size, i; |
|
|
|
|
|
hsAssert( !fCreating, "Cannot read geometry span while creating" ); |
|
|
|
// Just to make sure |
|
ClearBuffers(); |
|
|
|
fCreating = false; |
|
|
|
// WARNING: can't read in the material here, so hopefully the owner will set it for us... |
|
// Same with the fog environ |
|
|
|
fLocalToWorld.Read( stream ); |
|
fWorldToLocal.Read( stream ); |
|
fLocalBounds.Read( stream ); |
|
fWorldBounds = fLocalBounds; |
|
fWorldBounds.Transform(&fLocalToWorld); |
|
|
|
fOBBToLocal.Read(stream); |
|
fLocalToOBB.Read(stream); |
|
|
|
fBaseMatrix = stream->ReadSwap32(); |
|
fNumMatrices = stream->ReadByte(); |
|
fLocalUVWChans = stream->ReadSwap16(); |
|
fMaxBoneIdx = stream->ReadSwap16(); |
|
fPenBoneIdx = stream->ReadSwap16(); |
|
|
|
fMinDist = stream->ReadSwapScalar(); |
|
fMaxDist = stream->ReadSwapScalar(); |
|
|
|
fFormat = stream->ReadByte(); |
|
fProps = stream->ReadSwap32(); |
|
fNumVerts = stream->ReadSwap32(); |
|
fNumIndices = stream->ReadSwap32(); |
|
|
|
// FIXME MAJOR VERSION |
|
// remove these two lines. No more patches. |
|
stream->ReadSwap32(); |
|
stream->ReadByte(); |
|
|
|
fDecalLevel = stream->ReadSwap32(); |
|
|
|
if( fProps & kWaterHeight ) |
|
fWaterHeight = stream->ReadSwapScalar(); |
|
|
|
if( fNumVerts > 0 ) |
|
{ |
|
size = GetVertexSize( fFormat ); |
|
|
|
fVertexData = TRACKED_NEW UInt8[ size * fNumVerts ]; |
|
stream->Read( size * fNumVerts, fVertexData ); |
|
|
|
fMultColor = TRACKED_NEW hsColorRGBA[ fNumVerts ]; |
|
fAddColor = TRACKED_NEW hsColorRGBA[ fNumVerts ]; |
|
for( i = 0; i < fNumVerts; i++ ) |
|
{ |
|
fMultColor[ i ].Read( stream ); |
|
fAddColor[ i ].Read( stream ); |
|
} |
|
|
|
fDiffuseRGBA = TRACKED_NEW UInt32[ fNumVerts ]; |
|
fSpecularRGBA = TRACKED_NEW UInt32[ fNumVerts ]; |
|
stream->ReadSwap32( fNumVerts, fDiffuseRGBA ); |
|
stream->ReadSwap32( fNumVerts, fSpecularRGBA ); |
|
} |
|
else |
|
{ |
|
fVertexData = nil; |
|
fMultColor = nil; |
|
fAddColor = nil; |
|
fDiffuseRGBA = nil; |
|
fSpecularRGBA = nil; |
|
} |
|
|
|
if( fNumIndices > 0 ) |
|
{ |
|
fIndexData = TRACKED_NEW UInt16[ fNumIndices ]; |
|
stream->ReadSwap16( fNumIndices, fIndexData ); |
|
} |
|
else |
|
fIndexData = nil; |
|
|
|
// Read the group ID, then look up our instanceRef array from it |
|
fInstanceGroupID = stream->ReadSwap32(); |
|
if( fInstanceGroupID != kNoGroupID ) |
|
{ |
|
UInt32 count = stream->ReadSwap32(); |
|
|
|
fInstanceRefs = IGetInstanceGroup( fInstanceGroupID, count ); |
|
fInstanceRefs->Append( this ); |
|
hsAssert( fInstanceRefs != nil, "Cannot locate fInstanceRefs on plGeometrySpan::Read()" ); |
|
} |
|
} |
|
|
|
//// Write /////////////////////////////////////////////////////////////////// |
|
|
|
void plGeometrySpan::Write( hsStream *stream ) |
|
{ |
|
UInt32 size, i; |
|
|
|
|
|
hsAssert( !fCreating, "Cannot write geometry span while creating" ); |
|
|
|
// WARNING: can't write out the material here, so hopefully the owner will do it for us... |
|
// Same with the fog environ |
|
|
|
fLocalToWorld.Write( stream ); |
|
fWorldToLocal.Write( stream ); |
|
fLocalBounds.Write( stream ); |
|
|
|
fOBBToLocal.Write(stream); |
|
fLocalToOBB.Write(stream); |
|
|
|
stream->WriteSwap32( fBaseMatrix ); |
|
stream->WriteByte( fNumMatrices ); |
|
stream->WriteSwap16(fLocalUVWChans); |
|
stream->WriteSwap16(fMaxBoneIdx); |
|
stream->WriteSwap16((UInt16)fPenBoneIdx); |
|
|
|
stream->WriteSwapScalar(fMinDist); |
|
stream->WriteSwapScalar(fMaxDist); |
|
|
|
stream->WriteByte( fFormat ); |
|
stream->WriteSwap32( fProps ); |
|
stream->WriteSwap32( fNumVerts ); |
|
stream->WriteSwap32( fNumIndices ); |
|
|
|
// FIXME MAJOR VERSION |
|
// Remove these two lines. |
|
stream->WriteSwap32(0); |
|
stream->WriteByte(0); |
|
|
|
stream->WriteSwap32( fDecalLevel ); |
|
|
|
if( fProps & kWaterHeight ) |
|
stream->WriteSwapScalar(fWaterHeight); |
|
|
|
if( fNumVerts > 0 ) |
|
{ |
|
size = GetVertexSize( fFormat ); |
|
|
|
stream->Write( size * fNumVerts, fVertexData ); |
|
|
|
for( i = 0; i < fNumVerts; i++ ) |
|
{ |
|
fMultColor[ i ].Write( stream ); |
|
fAddColor[ i ].Write( stream ); |
|
} |
|
stream->WriteSwap32( fNumVerts, fDiffuseRGBA ); |
|
stream->WriteSwap32( fNumVerts, fSpecularRGBA ); |
|
} |
|
|
|
if( fNumIndices > 0 ) |
|
{ |
|
stream->WriteSwap16( fNumIndices, fIndexData ); |
|
} |
|
|
|
// Write the groupID as well as the count for instanceRefs. This way |
|
stream->WriteSwap32( fInstanceGroupID ); |
|
if( fInstanceGroupID != kNoGroupID ) |
|
stream->WriteSwap32( fInstanceRefs->GetCount() ); |
|
else |
|
{ |
|
hsAssert( fInstanceRefs == nil, "Nil instanceRefs array but no group ID, non sequitur" ); |
|
} |
|
} |
|
|
|
//// GetVertexSize /////////////////////////////////////////////////////////// |
|
|
|
UInt32 plGeometrySpan::GetVertexSize( UInt8 format ) |
|
{ |
|
UInt32 size; |
|
|
|
|
|
size = sizeof( float ) * ( 3 + 3 ); // pos + normal |
|
// Taken out 8.6.2001 mcn - Diffuse and specular are now separate arrays |
|
// size += sizeof( DWORD ) * 2; // diffuse + specular |
|
|
|
size += sizeof( float ) * 3 * CalcNumUVs( format ); |
|
|
|
switch( format & kSkinWeightMask ) |
|
{ |
|
case kSkinNoWeights: break; |
|
case kSkin1Weight: size += sizeof( float ) * 1; break; |
|
case kSkin2Weights: size += sizeof( float ) * 2; break; |
|
case kSkin3Weights: size += sizeof( float ) * 3; break; |
|
default: hsAssert( false, "Bad weight count in GetVertexSize()" ); |
|
} |
|
if( format & kSkinIndices ) |
|
size += sizeof(UInt32); |
|
|
|
return size; |
|
} |
|
|
|
//// BeginCreate ////////////////////////////////////////////////////////////// |
|
|
|
void plGeometrySpan::BeginCreate( hsGMaterial *material, const hsMatrix44 &l2wMatrix, UInt8 format ) |
|
{ |
|
fCreating = true; |
|
|
|
fMaterial = material; |
|
fLocalToWorld = l2wMatrix; |
|
fLocalToWorld.GetInverse( &fWorldToLocal ); |
|
fFormat = format; |
|
} |
|
|
|
//// AddVertex //////////////////////////////////////////////////////////////// |
|
// Note: uvPtrArray is an array of pointers to hsPoint3s. If a pointer is nil, |
|
// that UV channel (and all above them) are not used. The array of pointers |
|
// MUST be of size kMaxNumUVChannels. |
|
|
|
UInt16 plGeometrySpan::AddVertex( hsPoint3 *position, hsPoint3 *normal, hsColorRGBA& multColor, hsColorRGBA& addColor, |
|
hsPoint3 **uvPtrArray, float weight1, float weight2, float weight3, UInt32 indices ) |
|
{ |
|
AddVertex( position, normal, 0, 0, uvPtrArray, weight1, weight2, weight3, indices ); |
|
|
|
int idx = fVertAccum.GetCount() - 1; |
|
|
|
TempVertex& vert = fVertAccum[idx]; |
|
vert.fMultColor = multColor; |
|
vert.fAddColor = addColor; |
|
|
|
|
|
return idx; |
|
} |
|
|
|
UInt16 plGeometrySpan::AddVertex( hsPoint3 *position, hsPoint3 *normal, UInt32 hexColor, UInt32 specularColor, |
|
hsPoint3 **uvPtrArray, float weight1, float weight2, float weight3, UInt32 indices ) |
|
{ |
|
TempVertex vert; |
|
int i, numWeights; |
|
|
|
|
|
hsAssert( fCreating, "Calling AddVertex() on a non-creating plGeometrySpan!" ); |
|
|
|
// UV channels |
|
if( uvPtrArray != nil ) |
|
{ |
|
for( i = 0; i < kMaxNumUVChannels; i++ ) |
|
{ |
|
if( uvPtrArray[ i ] == nil ) |
|
break; |
|
vert.fUVs[ i ] = *(uvPtrArray[ i ]); |
|
} |
|
} |
|
else |
|
i = 0; |
|
hsAssert( GetNumUVs() == i, "Incorrect number of UVs passed to AddVertex()" ); |
|
|
|
switch( fFormat & kSkinWeightMask ) |
|
{ |
|
case kSkin3Weights: |
|
numWeights = 3; |
|
vert.fWeights[ 0 ] = weight1; |
|
vert.fWeights[ 1 ] = weight2; |
|
vert.fWeights[ 2 ] = weight3; |
|
vert.fIndices = indices; |
|
break; |
|
case kSkin2Weights: |
|
numWeights = 2; |
|
vert.fWeights[ 0 ] = weight1; |
|
vert.fWeights[ 1 ] = weight2; |
|
vert.fIndices = indices; |
|
break; |
|
case kSkin1Weight: |
|
numWeights = 1; |
|
vert.fWeights[ 0 ] = weight1; |
|
vert.fIndices = indices; |
|
break; |
|
default: |
|
case kSkinNoWeights: |
|
numWeights = 0; |
|
break; |
|
} |
|
|
|
vert.fPosition = *position; |
|
vert.fNormal = *normal; |
|
vert.fColor = hexColor; |
|
vert.fSpecularColor = specularColor; |
|
vert.fMultColor.Set( 1.f, 1.f, 1.f, 1.f ); |
|
vert.fAddColor.Set( 0, 0, 0, 1.f ); |
|
|
|
fVertAccum.Append( vert ); |
|
return fVertAccum.GetCount() - 1; |
|
} |
|
|
|
//// AddIndex Variations ////////////////////////////////////////////////////// |
|
|
|
void plGeometrySpan::AddIndex( UInt16 index ) |
|
{ |
|
hsAssert( fCreating, "Calling AddIndex() on a non-creating plGeometrySpan!" ); |
|
|
|
fIndexAccum.Append( index ); |
|
} |
|
|
|
void plGeometrySpan::AddTriIndices( UInt16 index1, UInt16 index2, UInt16 index3 ) |
|
{ |
|
hsAssert( fCreating, "Calling AddTriIndices() on a non-creating plGeometrySpan!" ); |
|
|
|
fIndexAccum.Append( index1 ); |
|
fIndexAccum.Append( index2 ); |
|
fIndexAccum.Append( index3 ); |
|
} |
|
|
|
//// AddTriangle ////////////////////////////////////////////////////////////// |
|
|
|
void plGeometrySpan::AddTriangle( hsPoint3 *vert1, hsPoint3 *vert2, hsPoint3 *vert3, UInt32 color ) |
|
{ |
|
hsVector3 twoTo1, twoTo3, normal; |
|
hsPoint3 normalPt; |
|
|
|
|
|
hsAssert( fCreating, "Calling AddTriangle() on a non-creating plGeometrySpan!" ); |
|
|
|
twoTo1.Set( vert1, vert2 ); |
|
twoTo3.Set( vert3, vert2 ); |
|
|
|
normal = twoTo1 % twoTo3; |
|
normalPt.fX = normal.fX; |
|
normalPt.fY = normal.fY; |
|
normalPt.fZ = normal.fZ; |
|
|
|
AddIndex( AddVertex( vert1, &normalPt, color, 0 ) ); |
|
AddIndex( AddVertex( vert2, &normalPt, color, 0 ) ); |
|
AddIndex( AddVertex( vert3, &normalPt, color, 0 ) ); |
|
} |
|
|
|
//// AddVertexArray /////////////////////////////////////////////////////////// |
|
|
|
void plGeometrySpan::AddVertexArray( UInt32 count, hsPoint3 *positions, hsVector3 *normals, UInt32 *colors, hsPoint3* uvws, int uvwsPerVtx ) |
|
{ |
|
hsAssert( fCreating, "Calling AddTriIndices() on a non-creating plGeometrySpan!" ); |
|
|
|
|
|
UInt32 i, dest; |
|
|
|
// This test actually does work, even if it's bad form... |
|
hsAssert( GetNumUVs() == uvwsPerVtx, "Calling wrong AddVertex() for plGeometrySpan format" ); |
|
|
|
|
|
dest = fVertAccum.GetCount(); |
|
fVertAccum.SetCount( dest + count ); |
|
for( i = 0; i < count; i++, dest++ ) |
|
{ |
|
fVertAccum[ dest ].fPosition = positions[ i ]; |
|
if( normals != nil ) |
|
{ |
|
// Stupid hsPoint3...I COULD change it, but why? |
|
fVertAccum[ dest ].fNormal.fX = normals[ i ].fX; |
|
fVertAccum[ dest ].fNormal.fY = normals[ i ].fY; |
|
fVertAccum[ dest ].fNormal.fZ = normals[ i ].fZ; |
|
} |
|
else |
|
fVertAccum[ dest ].fNormal.Set( 0, 0, 0 ); |
|
|
|
if( colors != nil ) |
|
fVertAccum[ dest ].fColor = colors[ i ]; |
|
else |
|
fVertAccum[ dest ].fColor = 0xffffffff; |
|
|
|
if( uvws && uvwsPerVtx ) |
|
{ |
|
int j; |
|
for( j = 0; j < uvwsPerVtx; j++ ) |
|
fVertAccum[ dest ].fUVs[ j ] = uvws[j]; |
|
uvws += uvwsPerVtx; |
|
} |
|
} |
|
} |
|
|
|
//// AddIndexArray //////////////////////////////////////////////////////////// |
|
|
|
void plGeometrySpan::AddIndexArray( UInt32 count, UInt16 *indices ) |
|
{ |
|
hsAssert( fCreating, "Calling AddTriIndices() on a non-creating plGeometrySpan!" ); |
|
|
|
|
|
UInt32 i, dest; |
|
|
|
|
|
dest = fIndexAccum.GetCount(); |
|
fIndexAccum.SetCount( dest + count ); |
|
for( i = 0; i < count; i++, dest++ ) |
|
fIndexAccum[ dest ] = indices[ i ]; |
|
} |
|
|
|
//// EndCreate //////////////////////////////////////////////////////////////// |
|
|
|
void plGeometrySpan::EndCreate( void ) |
|
{ |
|
hsBounds3Ext bounds; |
|
UInt32 i, size; |
|
UInt8 *tempPtr; |
|
|
|
|
|
hsAssert( fCreating, "Calling EndCreate() on a non-creating plGeometrySpan!" ); |
|
|
|
/// If we're empty, just clean up and return |
|
if( fVertAccum.GetCount() <= 0 ) |
|
{ |
|
delete [] fVertexData; |
|
fVertexData = nil; |
|
fNumVerts = 0; |
|
|
|
delete [] fIndexData; |
|
fIndexData = nil; |
|
fNumIndices = 0; |
|
fVertAccum.Reset(); |
|
fIndexAccum.Reset(); |
|
|
|
fCreating = false; |
|
return; |
|
} |
|
|
|
/// Convert vertices |
|
bounds.MakeEmpty(); |
|
size = GetVertexSize( fFormat ); |
|
|
|
if( fVertexData == nil || fNumVerts < fVertAccum.GetCount() ) |
|
{ |
|
if( fVertexData != nil ) |
|
delete [] fVertexData; |
|
|
|
fNumVerts = fVertAccum.GetCount(); |
|
fVertexData = TRACKED_NEW UInt8[ size * fNumVerts ]; |
|
|
|
delete [] fMultColor; |
|
fMultColor = TRACKED_NEW hsColorRGBA[ fNumVerts ]; |
|
|
|
delete [] fAddColor; |
|
fAddColor = TRACKED_NEW hsColorRGBA[ fNumVerts ]; |
|
|
|
delete [] fDiffuseRGBA; |
|
delete [] fSpecularRGBA; |
|
fDiffuseRGBA = TRACKED_NEW UInt32[ fNumVerts ]; |
|
fSpecularRGBA = TRACKED_NEW UInt32[ fNumVerts ]; |
|
} |
|
else |
|
fNumVerts = fVertAccum.GetCount(); |
|
|
|
for( i = 0, tempPtr = fVertexData; i < fNumVerts; i++ ) |
|
{ |
|
// Get position, normal, color |
|
memcpy( tempPtr, &fVertAccum[ i ], 6 * sizeof(float) ); |
|
tempPtr += 6 * sizeof(float); |
|
|
|
// Get Uvs |
|
int numUvs = GetNumUVs(); |
|
if( numUvs > 0 ) |
|
{ |
|
memcpy( tempPtr, &fVertAccum[ i ].fUVs[ 0 ], numUvs * 3 * sizeof(float) ); |
|
tempPtr += numUvs * 3 * sizeof(float); |
|
} |
|
|
|
// Get Weights |
|
if( fFormat & kSkinWeightMask ) |
|
{ |
|
int numWeights = 0; |
|
switch( fFormat & kSkinWeightMask ) |
|
{ |
|
case kSkin1Weight: numWeights = 1; break; |
|
case kSkin2Weights: numWeights = 2; break; |
|
case kSkin3Weights: numWeights = 3; break; |
|
default: hsAssert(false, "Garbage for weight format"); break; |
|
} |
|
memcpy( tempPtr, &fVertAccum[ i ].fWeights[ 0 ], numWeights * sizeof(float) ); |
|
tempPtr += numWeights * sizeof(float); |
|
if( fFormat & kSkinIndices ) |
|
{ |
|
memcpy( tempPtr, &fVertAccum[ i ].fIndices, sizeof(UInt32) ); |
|
tempPtr += sizeof(UInt32); |
|
} |
|
} |
|
|
|
fMultColor[i] = fVertAccum[i].fMultColor; |
|
fAddColor[i] = fVertAccum[i].fAddColor; |
|
fDiffuseRGBA[ i ] = fVertAccum[ i ].fColor; |
|
fSpecularRGBA[ i ] = fVertAccum[ i ].fSpecularColor; |
|
|
|
hsPoint3 pBnd = fLocalToOBB * fVertAccum[i].fPosition; |
|
bounds.Union(&pBnd); |
|
} |
|
bounds.Transform(&fOBBToLocal); |
|
|
|
if( fProps & kWaterHeight ) |
|
{ |
|
AdjustBounds(bounds); |
|
} |
|
|
|
fLocalBounds = bounds; |
|
fWorldBounds = fLocalBounds; |
|
fWorldBounds.Transform(&fLocalToWorld); |
|
|
|
/// Convert indices |
|
if( fIndexAccum.GetCount() == 0 ) // Allowed for patches |
|
{ |
|
delete [] fIndexData; |
|
fIndexData = nil; |
|
fNumIndices = 0; |
|
} |
|
else if( fIndexData == nil || fNumIndices < fIndexAccum.GetCount() ) |
|
{ |
|
if( fIndexData != nil ) |
|
delete [] fIndexData; |
|
|
|
fNumIndices = fIndexAccum.GetCount(); |
|
fIndexData = TRACKED_NEW UInt16[ fNumIndices ]; |
|
} |
|
else |
|
fNumIndices = fIndexAccum.GetCount(); |
|
|
|
for( i = 0; i < fNumIndices; i++ ) |
|
fIndexData[ i ] = fIndexAccum[ i ]; |
|
|
|
/// Cleanup |
|
fVertAccum.Reset(); |
|
fIndexAccum.Reset(); |
|
fCreating = false; |
|
} |
|
|
|
void plGeometrySpan::AdjustBounds(hsBounds3Ext& bnd) const |
|
{ |
|
hsBounds3Ext wBnd = bnd; |
|
wBnd.Transform(&fLocalToWorld); |
|
|
|
const hsScalar kMaxWaveHeight(5.f); |
|
hsBounds3Ext rebound; |
|
rebound.MakeEmpty(); |
|
hsPoint3 pos = wBnd.GetMins(); |
|
pos.fZ = fWaterHeight - kMaxWaveHeight; |
|
rebound.Union(&pos); |
|
pos = wBnd.GetMaxs(); |
|
pos.fZ = fWaterHeight + kMaxWaveHeight; |
|
rebound.Union(&pos); |
|
bnd = rebound; |
|
bnd.Transform(&fWorldToLocal); |
|
} |
|
|
|
//// ExtractVertex //////////////////////////////////////////////////////////// |
|
void plGeometrySpan::ExtractInitColor( UInt32 index, hsColorRGBA *multColor, hsColorRGBA *addColor) const |
|
{ |
|
if( multColor ) |
|
*multColor = fMultColor[index]; |
|
if( addColor ) |
|
*addColor = fAddColor[index]; |
|
} |
|
|
|
// Extracts the given vertex out of the vertex buffer and into the pointers |
|
// given. |
|
|
|
void plGeometrySpan::ExtractVertex( UInt32 index, hsPoint3 *pos, hsVector3 *normal, hsColorRGBA *color, hsColorRGBA *specColor ) |
|
{ |
|
UInt32 hex, spec; |
|
|
|
|
|
ExtractVertex( index, pos, normal, &hex, &spec ); |
|
color->a = ( ( hex >> 24 ) & 0xff ) / 255.0f; |
|
color->r = ( ( hex >> 16 ) & 0xff ) / 255.0f; |
|
color->g = ( ( hex >> 8 ) & 0xff ) / 255.0f; |
|
color->b = ( ( hex >> 0 ) & 0xff ) / 255.0f; |
|
|
|
if( specColor != nil ) |
|
{ |
|
specColor->a = ( ( spec >> 24 ) & 0xff ) / 255.0f; |
|
specColor->r = ( ( spec >> 16 ) & 0xff ) / 255.0f; |
|
specColor->g = ( ( spec >> 8 ) & 0xff ) / 255.0f; |
|
specColor->b = ( ( spec >> 0 ) & 0xff ) / 255.0f; |
|
} |
|
} |
|
|
|
//// ExtractVertex //////////////////////////////////////////////////////////// |
|
// Hex-color version of ExtractVertex. |
|
|
|
void plGeometrySpan::ExtractVertex( UInt32 index, hsPoint3 *pos, hsVector3 *normal, UInt32 *color, UInt32 *specColor ) |
|
{ |
|
UInt8 *basePtr; |
|
float *fPtr; |
|
|
|
|
|
/// Where? |
|
hsAssert( index < fNumVerts, "Invalid index passed to ExtractVertex()" ); |
|
basePtr = fVertexData + index * GetVertexSize( fFormat ); |
|
|
|
/// Copy over point and normal |
|
fPtr = (float *)basePtr; |
|
pos->fX = fPtr[ 0 ]; |
|
pos->fY = fPtr[ 1 ]; |
|
pos->fZ = fPtr[ 2 ]; |
|
normal->fX = fPtr[ 3 ]; |
|
normal->fY = fPtr[ 4 ]; |
|
normal->fZ = fPtr[ 5 ]; |
|
fPtr += 6; |
|
|
|
/// Diffuse color |
|
*color = fDiffuseRGBA[ index ]; |
|
if( specColor != nil ) |
|
*specColor = fSpecularRGBA[ index ]; |
|
} |
|
|
|
//// ExtractUv //////////////////////////////////////////////////////////////// |
|
|
|
void plGeometrySpan::ExtractUv( UInt32 vIdx, UInt8 uvIdx, hsPoint3 *uv ) |
|
{ |
|
UInt8 *basePtr; |
|
float *fPtr; |
|
|
|
|
|
/// Where? |
|
hsAssert( vIdx < fNumVerts, "Invalid index passed to ExtractVertex()" ); |
|
hsAssert( uvIdx < GetNumUVs(), "Invalid UV index passed to ExtractVertex()" ); |
|
basePtr = fVertexData + vIdx * GetVertexSize( fFormat ); |
|
|
|
/// Skip over point, normal, and color and specular color |
|
basePtr += 12 + 12; |
|
|
|
fPtr = (float *)basePtr; |
|
fPtr += 3 * uvIdx; |
|
uv->fX = *fPtr++; |
|
uv->fY = *fPtr++; |
|
uv->fZ = *fPtr; |
|
} |
|
|
|
//// ExtractWeights /////////////////////////////////////////////////////////// |
|
// Gets the weights out of the vertex data. |
|
|
|
void plGeometrySpan::ExtractWeights( UInt32 vIdx, float *weightArray, UInt32 *indices ) |
|
{ |
|
UInt8 *basePtr; |
|
float *fPtr; |
|
UInt32 *dPtr; |
|
int numWeights; |
|
|
|
|
|
/// Where? |
|
hsAssert( vIdx < fNumVerts, "Invalid index passed to ExtractVertex()" ); |
|
basePtr = fVertexData + vIdx * GetVertexSize( fFormat ); |
|
|
|
/// Copy over weights |
|
basePtr += sizeof( float ) * ( 6 + 3 * GetNumUVs() ); |
|
fPtr = (float *)basePtr; |
|
switch( fFormat & kSkinWeightMask ) |
|
{ |
|
case kSkinNoWeights: hsAssert( false, "ExtractWeights() called on a span with no weights" ); break; |
|
case kSkin1Weight: numWeights = 1; break; |
|
case kSkin2Weights: numWeights = 2; break; |
|
case kSkin3Weights: numWeights = 3; break; |
|
default: hsAssert( false, "Bad number of weights in ExtractWeights()" ); |
|
} |
|
|
|
memcpy( weightArray, fPtr, sizeof( float ) * numWeights ); |
|
|
|
if( fFormat & kSkinIndices ) |
|
{ |
|
fPtr += numWeights; |
|
|
|
dPtr = (UInt32 *)fPtr; |
|
*indices = *dPtr; |
|
} |
|
} |
|
|
|
//// StuffVertex ////////////////////////////////////////////////////////////// |
|
// Stuffs the given vertex data into the vertex buffer. Vertex must already |
|
// exist! |
|
|
|
void plGeometrySpan::StuffVertex( UInt32 index, hsPoint3 *pos, hsPoint3 *normal, hsColorRGBA *color, hsColorRGBA *specColor ) |
|
{ |
|
UInt8 *basePtr; |
|
float *fPtr; |
|
|
|
|
|
/// Where? |
|
hsAssert( index < fNumVerts, "Invalid index passed to StuffVertex()" ); |
|
basePtr = fVertexData + index * GetVertexSize( fFormat ); |
|
|
|
/// Copy over point and normal |
|
fPtr = (float *)basePtr; |
|
fPtr[ 0 ] = pos->fX; |
|
fPtr[ 1 ] = pos->fY; |
|
fPtr[ 2 ] = pos->fZ; |
|
|
|
fPtr[ 3 ] = normal->fX; |
|
fPtr[ 4 ] = normal->fY; |
|
fPtr[ 5 ] = normal->fZ; |
|
fPtr += 6; |
|
|
|
/// Diffuse color |
|
StuffVertex( index, color, specColor ); |
|
} |
|
|
|
void plGeometrySpan::StuffVertex( UInt32 index, hsColorRGBA *color, hsColorRGBA *specColor ) |
|
{ |
|
UInt8 r, g, b, a; |
|
|
|
|
|
/// Where? |
|
hsAssert( index < fNumVerts, "Invalid index passed to StuffVertex()" ); |
|
|
|
a = (UInt8)( color->a >= 1 ? 255 : color->a <= 0 ? 0 : color->a * 255.0 ); |
|
r = (UInt8)( color->r >= 1 ? 255 : color->r <= 0 ? 0 : color->r * 255.0 ); |
|
g = (UInt8)( color->g >= 1 ? 255 : color->g <= 0 ? 0 : color->g * 255.0 ); |
|
b = (UInt8)( color->b >= 1 ? 255 : color->b <= 0 ? 0 : color->b * 255.0 ); |
|
|
|
fDiffuseRGBA[ index ] = ( a << 24 ) | ( r << 16 ) | ( g << 8 ) | ( b ); |
|
|
|
if( specColor != nil ) |
|
{ |
|
a = (UInt8)( specColor->a >= 1 ? 255 : specColor->a <= 0 ? 0 : specColor->a * 255.0 ); |
|
r = (UInt8)( specColor->r >= 1 ? 255 : specColor->r <= 0 ? 0 : specColor->r * 255.0 ); |
|
g = (UInt8)( specColor->g >= 1 ? 255 : specColor->g <= 0 ? 0 : specColor->g * 255.0 ); |
|
b = (UInt8)( specColor->b >= 1 ? 255 : specColor->b <= 0 ? 0 : specColor->b * 255.0 ); |
|
|
|
fSpecularRGBA[ index ] = ( a << 24 ) | ( r << 16 ) | ( g << 8 ) | ( b ); |
|
} |
|
}
|
|
|