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.
569 lines
17 KiB
569 lines
17 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/>. |
|
|
|
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==*/ |
|
#include "hsTypes.h" |
|
#include "hsUtils.h" |
|
#include "hsResMgr.h" |
|
#include "../pnMessage/plRefMsg.h" |
|
#include "../plMessage/plParticleUpdateMsg.h" |
|
#include "plParticleGenerator.h" |
|
#include "plParticleEmitter.h" |
|
#include "plParticleSystem.h" |
|
#include "plParticle.h" |
|
#include "plParticleEffect.h" |
|
#include "../CoreLib/hsColorRGBA.h" |
|
#include "../plInterp/plController.h" |
|
#include "../plSurface/hsGMaterial.h" |
|
#include "../plSurface/plLayerInterface.h" |
|
#include "plProfile.h" |
|
#include "hsFastMath.h" |
|
|
|
plProfile_CreateTimer("Update", "Particles", ParticleUpdate); |
|
plProfile_CreateTimer("Generate", "Particles", ParticleGenerate); |
|
|
|
plParticleEmitter::plParticleEmitter() |
|
{ |
|
fParticleCores = nil; |
|
fParticleExts = nil; |
|
fGenerator = nil; |
|
fLocalToWorld.Reset(); |
|
fTimeToLive = 0; |
|
} |
|
|
|
void plParticleEmitter::Init(plParticleSystem *system, UInt32 maxParticles, UInt32 spanIndex, UInt32 miscFlags, |
|
plParticleGenerator *gen /* = nil */) |
|
{ |
|
IClear(); |
|
fSystem = system; |
|
plLayerInterface *layer = system->fTexture->GetLayer(0)->BottomOfStack(); |
|
fMiscFlags = miscFlags | kNeedsUpdate; |
|
if( fMiscFlags & kOnReserve ) |
|
fTimeToLive = -1.f; // Wait for someone to give us a spurt of life. |
|
if( layer->GetShadeFlags() & hsGMatState::kShadeEmissive ) |
|
{ |
|
fMiscFlags |= kMatIsEmissive; |
|
fColor = layer->GetAmbientColor(); |
|
} |
|
else |
|
{ |
|
fColor = layer->GetRuntimeColor(); |
|
} |
|
fColor.a = layer->GetOpacity(); |
|
fGenerator = gen; |
|
fMaxParticles = maxParticles; |
|
fSpanIndex = spanIndex; |
|
ISetupParticleMem(); |
|
} |
|
|
|
void plParticleEmitter::Clone(plParticleEmitter* src, UInt32 spanIndex) |
|
{ |
|
Init(src->fSystem, |
|
src->fMaxParticles, |
|
spanIndex, |
|
src->fMiscFlags, |
|
src->fGenerator); |
|
|
|
fMiscFlags |= kBorrowedGenerator; |
|
fTimeToLive = -1.f; |
|
} |
|
|
|
void plParticleEmitter::OverrideLocalToWorld(const hsMatrix44& l2w) |
|
{ |
|
fLocalToWorld = l2w; |
|
fMiscFlags |= kOverrideLocalToWorld; |
|
} |
|
|
|
plParticleEmitter::~plParticleEmitter() |
|
{ |
|
IClear(); |
|
} |
|
|
|
void plParticleEmitter::IClear() |
|
{ |
|
delete [] fParticleCores; |
|
fParticleCores = nil; |
|
delete [] fParticleExts; |
|
fParticleExts = nil; |
|
if( !(fMiscFlags & kBorrowedGenerator) ) |
|
delete fGenerator; |
|
fGenerator = nil; |
|
} |
|
|
|
void plParticleEmitter::ISetupParticleMem() |
|
{ |
|
fNumValidParticles = 0; |
|
|
|
fParticleCores = TRACKED_NEW plParticleCore[fMaxParticles]; |
|
fParticleExts = TRACKED_NEW plParticleExt[fMaxParticles]; |
|
|
|
fTargetInfo.fPos = (UInt8 *)fParticleCores; |
|
fTargetInfo.fColor = (UInt8 *)fParticleCores + sizeof(hsPoint3); |
|
fTargetInfo.fPosStride = fTargetInfo.fColorStride = sizeof(plParticleCore); |
|
|
|
fTargetInfo.fVelocity = (UInt8 *)fParticleExts; |
|
fTargetInfo.fInvMass = fTargetInfo.fVelocity + sizeof(hsVector3); |
|
fTargetInfo.fAcceleration = fTargetInfo.fInvMass + sizeof(hsScalar); |
|
fTargetInfo.fMiscFlags = (UInt8 *)&(fParticleExts[0].fMiscFlags); |
|
fTargetInfo.fRadsPerSec = (UInt8 *)&(fParticleExts[0].fRadsPerSec); |
|
fTargetInfo.fVelocityStride |
|
= fTargetInfo.fInvMassStride |
|
= fTargetInfo.fAccelerationStride |
|
= fTargetInfo.fRadsPerSecStride |
|
= fTargetInfo.fMiscFlagsStride |
|
= sizeof(plParticleExt); |
|
} |
|
|
|
UInt32 plParticleEmitter::GetNumTiles() const |
|
{ |
|
return fSystem->GetNumTiles(); |
|
} |
|
|
|
const hsMatrix44 &plParticleEmitter::GetLocalToWorld() const |
|
{ |
|
return fMiscFlags & kOverrideLocalToWorld ? fLocalToWorld : fSystem->GetLocalToWorld(); |
|
} |
|
|
|
void plParticleEmitter::AddParticle(hsPoint3 &pos, hsVector3 &velocity, UInt32 tileIndex, |
|
hsScalar hSize, hsScalar vSize, hsScalar scale, hsScalar invMass, hsScalar life, |
|
hsPoint3 &orientation, UInt32 miscFlags, hsScalar radsPerSec) |
|
{ |
|
plParticleCore *core; |
|
plParticleExt *ext; |
|
UInt32 currParticle; |
|
|
|
if (fNumValidParticles == fMaxParticles) |
|
return; // No more room... you lose! |
|
else |
|
currParticle = fNumValidParticles++; |
|
|
|
core = &fParticleCores[currParticle]; |
|
core->fPos = pos; |
|
core->fOrientation = orientation; |
|
core->fColor = CreateHexColor(fColor); |
|
core->fHSize = hSize * scale; |
|
core->fVSize = vSize * scale; |
|
|
|
// even if the kNormalUp flag isn't there, we should initialize it to something sane. |
|
//if (core->fMiscFlags & kNormalUp != 0) |
|
core->fNormal.Set(0, 0, 1); |
|
|
|
hsScalar xOff = (tileIndex % fSystem->fXTiles) / (float)fSystem->fXTiles; |
|
hsScalar yOff = (tileIndex / fSystem->fXTiles) / (float)fSystem->fYTiles; |
|
|
|
core->fUVCoords[0].fX = xOff; |
|
core->fUVCoords[0].fY = yOff + 1.0f / fSystem->fYTiles; |
|
core->fUVCoords[0].fZ = 1.0f; |
|
core->fUVCoords[1].fX = xOff + 1.0f / fSystem->fXTiles; |
|
core->fUVCoords[1].fY = yOff + 1.0f / fSystem->fYTiles; |
|
core->fUVCoords[1].fZ = 1.0f; |
|
core->fUVCoords[2].fX = xOff + 1.0f / fSystem->fXTiles; |
|
core->fUVCoords[2].fY = yOff; |
|
core->fUVCoords[2].fZ = 1.0f; |
|
core->fUVCoords[3].fX = xOff; |
|
core->fUVCoords[3].fY = yOff; |
|
core->fUVCoords[3].fZ = 1.0f; |
|
|
|
ext = &fParticleExts[currParticle]; |
|
ext->fVelocity = velocity; |
|
ext->fInvMass = invMass; |
|
ext->fLife = ext->fStartLife = life; |
|
ext->fMiscFlags = miscFlags; // Is this ever NOT zero? |
|
if (life <= 0) |
|
ext->fMiscFlags |= plParticleExt::kImmortal; |
|
|
|
ext->fRadsPerSec = radsPerSec; |
|
ext->fAcceleration.Set(0, 0, 0); |
|
ext->fScale = scale; |
|
} |
|
|
|
void plParticleEmitter::WipeExistingParticles() |
|
{ |
|
fNumValidParticles = 0; // That was easy. |
|
} |
|
|
|
// This method is called from a network received message. Don't trust the args without checking. |
|
void plParticleEmitter::KillParticles(hsScalar num, hsScalar timeToDie, UInt8 flags) |
|
{ |
|
if (flags & plParticleKillMsg::kParticleKillPercentage) |
|
{ |
|
if (num < 0) |
|
num = 0; |
|
if (num > 1) |
|
num = 1; |
|
|
|
num *= fNumValidParticles; |
|
} |
|
else |
|
{ |
|
if (num < 0) |
|
num = 0; |
|
} |
|
|
|
if (timeToDie < 0) |
|
timeToDie = 0; |
|
|
|
int i; |
|
for (i = 0; i < fNumValidParticles && num > 0; i++) |
|
{ |
|
if ((flags & plParticleKillMsg::kParticleKillImmortalOnly) && !(fParticleExts[i].fMiscFlags & plParticleExt::kImmortal)) |
|
continue; |
|
|
|
fParticleExts[i].fLife = fParticleExts[i].fStartLife = timeToDie; |
|
fParticleExts[i].fMiscFlags &= ~plParticleExt::kImmortal; |
|
num--; |
|
} |
|
} |
|
|
|
void plParticleEmitter::UpdateGenerator(UInt32 paramID, hsScalar paramValue) |
|
{ |
|
if (fGenerator != nil) |
|
fGenerator->UpdateParam(paramID, paramValue); |
|
} |
|
|
|
UInt16 plParticleEmitter::StealParticlesFrom(plParticleEmitter *victim, UInt16 num) |
|
{ |
|
UInt16 spaceAvail = (UInt16)(fMaxParticles - fNumValidParticles); |
|
UInt16 numToCopy = (UInt16)(hsMinimum(num, (victim ? victim->fNumValidParticles : 0))); |
|
if (spaceAvail < numToCopy) |
|
numToCopy = spaceAvail; |
|
|
|
if (numToCopy > 0) |
|
{ |
|
// copy them over |
|
memcpy(&(fParticleCores[fNumValidParticles]), &(victim->fParticleCores[victim->fNumValidParticles - numToCopy]), numToCopy * sizeof(plParticleCore)); |
|
memcpy(&(fParticleExts[fNumValidParticles]), &(victim->fParticleExts[victim->fNumValidParticles- numToCopy]), numToCopy * sizeof(plParticleExt)); |
|
|
|
fNumValidParticles += numToCopy; |
|
victim->fNumValidParticles -= numToCopy; |
|
} |
|
|
|
return numToCopy; |
|
} |
|
|
|
void plParticleEmitter::TranslateAllParticles(hsPoint3 &amount) |
|
{ |
|
int i; |
|
for (i = 0; i < fNumValidParticles; i++) |
|
fParticleCores[i].fPos += amount; |
|
} |
|
|
|
hsBool plParticleEmitter::IUpdate(hsScalar delta) |
|
{ |
|
if (fMiscFlags & kNeedsUpdate) |
|
{ |
|
plProfile_BeginTiming(ParticleUpdate); |
|
|
|
IUpdateParticles(delta); |
|
IUpdateBoundsAndNormals(delta); |
|
|
|
plProfile_EndTiming(ParticleUpdate); |
|
} |
|
|
|
if (fGenerator == nil && fNumValidParticles <= 0) |
|
return false; // no generator, no particles, let the system decide if this emitter is done |
|
else |
|
return true; |
|
} |
|
|
|
void plParticleEmitter::IUpdateParticles(hsScalar delta) |
|
{ |
|
|
|
int i, j; |
|
|
|
// Have to remove particles before adding new ones, or we can run out of room. |
|
for (i = 0; i < fNumValidParticles; i++) |
|
{ |
|
fParticleExts[i].fLife -= delta; |
|
if (fParticleExts[i].fLife <= 0 && !(fParticleExts[i].fMiscFlags & plParticleExt::kImmortal)) |
|
{ |
|
IRemoveParticle(i); |
|
i--; // so that we hit this index again on the next iteration |
|
continue; |
|
} |
|
} |
|
|
|
fTargetInfo.fFirstNewParticle = fNumValidParticles; |
|
|
|
if ((fGenerator != nil) && (fTimeToLive >= 0)) |
|
{ |
|
plProfile_BeginLap(ParticleGenerate, fSystem->GetKeyName()); |
|
if (!fGenerator->AddAutoParticles(this, delta)) |
|
{ |
|
delete fGenerator; |
|
fGenerator = nil; |
|
} |
|
if( (fTimeToLive > 0) && ((fTimeToLive -= delta) <= 0) ) |
|
fTimeToLive = -1.f; |
|
plProfile_EndLap(ParticleGenerate, fSystem->GetKeyName()); |
|
} |
|
|
|
fTargetInfo.fContext = fSystem->fContext; |
|
fTargetInfo.fNumValidParticles = fNumValidParticles; |
|
const hsVector3 up(0, 0, 1.0f); |
|
hsVector3 currDirection; |
|
hsPoint3 *currPos; |
|
hsVector3 *currVelocity; |
|
hsVector3 *currAccel; |
|
hsPoint3 color(fColor.r, fColor.g, fColor.b); |
|
hsScalar alpha = fColor.a; |
|
plController *colorCtl; |
|
|
|
// Allow effects a chance to cache any upfront calculations |
|
// that will apply to all particles. |
|
for (j = 0; j < fSystem->fForces.GetCount(); j++) |
|
{ |
|
fSystem->fForces[j]->PrepareEffect(fTargetInfo); |
|
} |
|
for (j = 0; j < fSystem->fEffects.GetCount(); j++) |
|
{ |
|
fSystem->fEffects[j]->PrepareEffect(fTargetInfo); |
|
} |
|
for (j = 0; j < fSystem->fConstraints.GetCount(); j++) |
|
{ |
|
fSystem->fConstraints[j]->PrepareEffect(fTargetInfo); |
|
} |
|
|
|
for (i = 0; i < fNumValidParticles; i++) |
|
{ |
|
if (!( fParticleExts[i].fMiscFlags & plParticleExt::kImmortal )) |
|
{ |
|
hsScalar percent = (1.0f - fParticleExts[i].fLife / fParticleExts[i].fStartLife); |
|
colorCtl = (fMiscFlags & kMatIsEmissive ? fSystem->fAmbientCtl : fSystem->fDiffuseCtl); |
|
if (colorCtl != nil) |
|
colorCtl->Interp(colorCtl->GetLength() * percent, &color); |
|
|
|
if (fSystem->fOpacityCtl != nil) |
|
{ |
|
fSystem->fOpacityCtl->Interp(fSystem->fOpacityCtl->GetLength() * percent, &alpha); |
|
alpha /= 100.0f; |
|
if (alpha < 0) |
|
alpha = 0; |
|
else if (alpha > 1.f) |
|
alpha = 1.f; |
|
} |
|
|
|
if (fSystem->fWidthCtl != nil) |
|
{ |
|
fSystem->fWidthCtl->Interp(fSystem->fWidthCtl->GetLength() * percent, |
|
&fParticleCores[i].fHSize); |
|
fParticleCores[i].fHSize *= fParticleExts[i].fScale; |
|
} |
|
if (fSystem->fHeightCtl != nil) |
|
{ |
|
fSystem->fHeightCtl->Interp(fSystem->fHeightCtl->GetLength() * percent, |
|
&fParticleCores[i].fVSize); |
|
fParticleCores[i].fVSize *= fParticleExts[i].fScale; |
|
} |
|
|
|
fParticleCores[i].fColor = CreateHexColor(color.fX, color.fY, color.fZ, alpha); |
|
} |
|
|
|
for (j = 0; j < fSystem->fForces.GetCount(); j++) |
|
{ |
|
fSystem->fForces[j]->ApplyEffect(fTargetInfo, i); |
|
} |
|
|
|
currPos = (hsPoint3 *)(fTargetInfo.fPos + i * fTargetInfo.fPosStride); |
|
currVelocity = (hsVector3 *)(fTargetInfo.fVelocity + i * fTargetInfo.fVelocityStride); |
|
//currAccel = (hsVector3 *)(fTargetInfo.fAcceleration + i * fTargetInfo.fAccelerationStride); |
|
currAccel = &fSystem->fAccel; // Nothing accellerates on a per-particle basis (yet) |
|
|
|
*currPos += *currVelocity * delta; |
|
|
|
// This is the only orientation option (so far) that requires an update here |
|
if (fMiscFlags & (kOrientationVelocityBased | kOrientationVelocityStretch | kOrientationVelocityFlow)) |
|
fParticleCores[i].fOrientation.Set(&(*currVelocity * delta)); // mf - want the orientation to be a delposition |
|
else if( fParticleExts[i].fRadsPerSec != 0 ) |
|
{ |
|
hsScalar sinX, cosX; |
|
hsFastMath::SinCos(fParticleExts[i].fLife * fParticleExts[i].fRadsPerSec * 2.f * hsScalarPI, sinX, cosX); |
|
fParticleCores[i].fOrientation.Set(sinX, -cosX, 0); |
|
} |
|
|
|
// Viscous force F(t) = -k V(t) |
|
// Integral S from t0 to t1 of F(t) is |
|
// = S(-kV(t))[t1..t0] |
|
// = -k(P(t1) - P(t0)) |
|
// = -k*(currVelocity * delta) |
|
// or |
|
// V = V + -k*(V * delta) |
|
// V *= (1 + -k * delta) |
|
// Giving the change in velocity. |
|
hsScalar drag = 1.f + fSystem->fDrag * delta; |
|
// Clamp it at 0. Drag should never cause a reversal in velocity direction. |
|
if( drag < 0.f ) |
|
drag = 0.f; |
|
*currVelocity *= drag; |
|
|
|
*currVelocity += *currAccel * delta; |
|
|
|
for (j = 0; j < fSystem->fEffects.GetCount(); j++) |
|
{ |
|
fSystem->fEffects[j]->ApplyEffect(fTargetInfo, i); |
|
} |
|
|
|
// We may need to do more than one iteration through the constraints. It's a trade-off |
|
// between accurracy and speed (what's new?) but I'm going to go with just one |
|
// for now until we decide things don't "look right" |
|
for (j = 0; j < fSystem->fConstraints.GetCount(); j++) |
|
{ |
|
if( fSystem->fConstraints[j]->ApplyEffect(fTargetInfo, i) ) |
|
{ |
|
IRemoveParticle(i); |
|
i--; // so that we hit this index again on the next iteration |
|
break; |
|
// break will break us out of loop over constraints, |
|
// and since we're last, we move onto next particle. |
|
} |
|
} |
|
} |
|
|
|
// Notify the effects that they are done for now. |
|
for (j = 0; j < fSystem->fForces.GetCount(); j++) |
|
{ |
|
fSystem->fForces[j]->EndEffect(fTargetInfo); |
|
} |
|
for (j = 0; j < fSystem->fEffects.GetCount(); j++) |
|
{ |
|
fSystem->fEffects[j]->EndEffect(fTargetInfo); |
|
} |
|
for (j = 0; j < fSystem->fConstraints.GetCount(); j++) |
|
{ |
|
fSystem->fConstraints[j]->EndEffect(fTargetInfo); |
|
} |
|
} |
|
|
|
plProfile_CreateTimer("Bound", "Particles", ParticleBound); |
|
plProfile_CreateTimer("Normal", "Particles", ParticleNormal); |
|
|
|
void plParticleEmitter::IUpdateBoundsAndNormals(hsScalar delta) |
|
{ |
|
plProfile_BeginTiming(ParticleBound); |
|
fBoundBox.MakeEmpty(); |
|
int i; |
|
for (i = 0; i < fNumValidParticles; i++) |
|
fBoundBox.Union(&fParticleCores[i].fPos); |
|
|
|
hsPoint3 center; |
|
if (fNumValidParticles > 0) |
|
center = fBoundBox.GetCenter(); |
|
plProfile_EndTiming(ParticleBound); |
|
|
|
plProfile_BeginTiming(ParticleNormal); |
|
hsVector3 normal; |
|
if (fMiscFlags & kNormalVelUpVel) |
|
{ |
|
for (i = fNumValidParticles - 1; i >=0; i--) |
|
{ |
|
//currDirection.Set(&fParticleCores[i].fPos, &fParticleExts[i].fOldPos); |
|
//normal = (currDirection % up % currDirection); |
|
normal.Set(-fParticleExts[i].fVelocity.fX * fParticleExts[i].fVelocity.fZ, |
|
-fParticleExts[i].fVelocity.fY * fParticleExts[i].fVelocity.fZ, |
|
(fParticleExts[i].fVelocity.fX * fParticleExts[i].fVelocity.fX + |
|
fParticleExts[i].fVelocity.fY * fParticleExts[i].fVelocity.fY)); |
|
if (!normal.IsEmpty()) // zero length check |
|
{ |
|
normal.Normalize(); |
|
fParticleCores[i].fNormal = normal; |
|
} |
|
} |
|
} |
|
else if (fMiscFlags & kNormalFromCenter) |
|
{ |
|
for (i = fNumValidParticles - 1; i >=0; i--) |
|
{ |
|
normal.Set(&fParticleCores[i].fPos, ¢er); |
|
if (!normal.IsEmpty()) // zero length check |
|
{ |
|
normal.Normalize(); |
|
fParticleCores[i].fNormal = normal; |
|
} |
|
} |
|
} |
|
// otherwise we just keep the last normal. |
|
plProfile_EndTiming(ParticleNormal); |
|
} |
|
|
|
void plParticleEmitter::IRemoveParticle(UInt32 index) |
|
{ |
|
hsAssert(index < fNumValidParticles, "Trying to remove an invalid particle"); |
|
|
|
fNumValidParticles--; |
|
if (fNumValidParticles <= 0) |
|
{ |
|
fNumValidParticles = 0; |
|
return; |
|
} |
|
|
|
fParticleCores[index] = fParticleCores[fNumValidParticles]; |
|
fParticleExts[index] = fParticleExts[fNumValidParticles]; |
|
} |
|
|
|
// Reading and writing doesn't transfer individual particle info. We assume those are expendable. |
|
// Only the configuration info necessary to began spawning particles again is saved. |
|
// The particle system that owns this emitter is responsible for setting the pointer back to itself |
|
void plParticleEmitter::Read(hsStream *s, hsResMgr *mgr) |
|
{ |
|
plCreatable::Read(s, mgr); |
|
|
|
fGenerator = plParticleGenerator::ConvertNoRef(mgr->ReadCreatable(s)); |
|
fSpanIndex = s->ReadSwap32(); |
|
fMaxParticles = s->ReadSwap32(); |
|
fMiscFlags = s->ReadSwap32(); |
|
fColor.Read(s); |
|
|
|
if( fMiscFlags & kOnReserve ) |
|
fTimeToLive = -1.f; // Wait for someone to give us a spurt of life. |
|
|
|
ISetupParticleMem(); |
|
} |
|
|
|
void plParticleEmitter::Write(hsStream *s, hsResMgr *mgr) |
|
{ |
|
plCreatable::Write(s, mgr); |
|
|
|
mgr->WriteCreatable(s, fGenerator); |
|
s->WriteSwap32(fSpanIndex); |
|
s->WriteSwap32(fMaxParticles); |
|
s->WriteSwap32(fMiscFlags); |
|
fColor.Write(s); |
|
} |
|
|
|
UInt32 plParticleEmitter::CreateHexColor(const hsColorRGBA &color) |
|
{ |
|
return CreateHexColor(color.r, color.g, color.b, color.a); |
|
} |
|
|
|
UInt32 plParticleEmitter::CreateHexColor(const hsScalar r, const hsScalar g, const hsScalar b, const hsScalar a) |
|
{ |
|
UInt32 ru, gu, bu, au; |
|
|
|
au = (UInt32)(a * 255.0f); |
|
ru = (UInt32)(r * 255.0f); |
|
gu = (UInt32)(g * 255.0f); |
|
bu = (UInt32)(b * 255.0f); |
|
return ( au << 24 ) | ( ru << 16 ) | ( gu << 8 ) | ( bu ); |
|
} |
|
|
|
|