Files
2025-09-18 17:55:52 -04:00

453 lines
12 KiB
C++

#include "stdafx.h"
#include "LightObject.h"
#include "v8datamodel/Light.h"
#include "v8datamodel/PartInstance.h"
#include "SceneUpdater.h"
#include "Util.h"
#include "VisualEngine.h"
namespace RBX
{
namespace Graphics
{
// http://www.gamedev.net/topic/418582-aabb-boundary-of-a-round-cone/
static Extents computeConeExtents(const Vector3& position, float radius, const Vector3& direction, float angle)
{
static const Vector3 axes[6] =
{
Vector3(-1, 0, 0), Vector3(0, -1, 0), Vector3(0, 0, -1),
Vector3(+1, 0, 0), Vector3(0, +1, 0), Vector3(0, 0, +1),
};
Vector3 E[6];
Vector3 C = position;
Vector3 A = direction;
float r = radius;
float s = G3D::toRadians(angle / 2);
float coss = cosf(s), sins = sinf(s);
for (int axis = 0; axis < 6; ++axis)
{
Vector3 D = axes[axis];
// extreme point is on spherical cap: dot(D, A) >= cos(s)
if (dot(D, A) >= coss)
{
E[axis] = C + r*D;
}
// extreme point is the apex: dot(D, A) <= cos(s+pi/2)
else if (dot(D, A) <= -sins)
{
E[axis] = C;
}
// extreme point is on the circle
else
{
Matrix3 AAT =
Matrix3(A.x * A.x, A.x * A.y, A.x * A.z,
A.y * A.x, A.y * A.y, A.y * A.z,
A.z * A.x, A.z * A.y, A.z * A.z);
E[axis] = C + (r * coss) * A + (r * sins) * normalize(D - AAT * D);
}
}
return Extents(Vector3(E[0].x, E[1].y, E[2].z), Vector3(E[3].x, E[4].y, E[5].z));
}
static Extents computeLightExtents(const Vector3& position, const Vector3& direction, const Vector4& axisU, const Vector4& axisV, Light* light)
{
if (PointLight* pointLight = light->fastDynamicCast<PointLight>())
{
return Extents::fromCenterRadius(position, pointLight->getRange());
}
else if (SurfaceLight* surfaceLight = light->fastDynamicCast<SurfaceLight>())
{
Vector3 u = axisU.xyz() * axisU.w;
Vector3 v = axisV.xyz() * axisV.w;
Extents e;
for (int i = 0; i < 4; ++i)
{
Vector3 c = position + (i & 1 ? 1 : -1) * u + (i & 2 ? 1 : -1) * v;
Extents ec = computeConeExtents(c, surfaceLight->getRange(), direction, surfaceLight->getAngle());
e.expandToContain(ec);
}
return e;
}
else if (SpotLight* spotLight = light->fastDynamicCast<SpotLight>())
{
return computeConeExtents(position, spotLight->getRange(), direction, spotLight->getAngle());
}
else
{
RBXASSERT(false);
return Extents();
}
}
static float getLightRange(Light* light)
{
if (PointLight* pointLight = light->fastDynamicCast<PointLight>())
{
return pointLight->getRange();
}
else if (SpotLight* spotLight = light->fastDynamicCast<SpotLight>())
{
return spotLight->getRange();
}
else if (SurfaceLight* surfaceLight = light->fastDynamicCast<SurfaceLight>())
{
return surfaceLight->getRange();
}
else
{
RBXASSERT(false);
return 0;
}
}
static void resizeShadowProjection(boost::scoped_array<unsigned char>& data, unsigned int oldSize, unsigned int newSize)
{
RBXASSERT(newSize > 0);
RBXASSERT(newSize % 2 == 1); // all resize requests are odd; this simplifies the copy logic below
unsigned char* newData = new unsigned char[newSize * newSize];
// during resizing, we assume that the center part of the shadow map is preserved
// we need to copy that over and clear everything else
memset(newData, 0, newSize * newSize);
if (oldSize != 0)
{
unsigned int sharedSize = std::min(oldSize, newSize);
RBXASSERT(sharedSize % 2 == 1);
const unsigned char* oldData = data.get();
// positive by construction
unsigned int sharedStartOld = (oldSize - sharedSize) / 2;
unsigned int sharedStartNew = (newSize - sharedSize) / 2;
for (unsigned int y = 0; y < sharedSize; ++y)
{
memcpy(newData + (y + sharedStartNew) * newSize + sharedStartNew, oldData + (y + sharedStartOld) * oldSize + sharedStartOld, sharedSize);
}
}
data.reset(newData);
}
LightObject::LightObject(VisualEngine* visualEngine)
: Super(visualEngine, CullMode_SpatialHash, Flags_LightObject)
, type(Type_None)
, brightness(0)
, range(0)
, angle(0)
, dirty(false)
{
}
LightObject::~LightObject()
{
unbind();
// notify scene updater about destruction so that the pointer to LightObject is no longer stored
getVisualEngine()->getSceneUpdater()->notifyDestroyed(this);
}
void LightObject::onSleepingChangedEx(bool sleeping)
{
if (sleeping)
{
getVisualEngine()->getSceneUpdater()->notifySleeping(this);
}
else
{
getVisualEngine()->getSceneUpdater()->notifyAwake(this);
}
}
void LightObject::onParentSizeChangedEx(const RBX::Reflection::PropertyDescriptor* pd)
{
if (pd == &PartInstance::prop_Size)
{
invalidateEntity();
}
}
static void getBasis(NormalId face, const Matrix3& rotation, const Vector3& halfSize, Vector4& axis, Vector4& axisU, Vector4& axisV)
{
switch (face)
{
case NORM_X:
case NORM_X_NEG:
axis = Vector4(Math::getWorldNormal(face, rotation), halfSize.x);
axisU = Vector4(Math::getColumn(rotation, 1), halfSize.y);
axisV = Vector4(Math::getColumn(rotation, 2), halfSize.z);
break;
case NORM_Y:
case NORM_Y_NEG:
axis = Vector4(Math::getWorldNormal(face, rotation), halfSize.y);
axisU = Vector4(Math::getColumn(rotation, 2), halfSize.z);
axisV = Vector4(Math::getColumn(rotation, 0), halfSize.x);
break;
case NORM_Z:
case NORM_Z_NEG:
axis = Vector4(Math::getWorldNormal(face, rotation), halfSize.z);
axisU = Vector4(Math::getColumn(rotation, 0), halfSize.x);
axisV = Vector4(Math::getColumn(rotation, 1), halfSize.y);
break;
default:
axisU = Vector4();
axisV = Vector4();
}
}
void LightObject::updateCoordinateFrame(bool recalcLocalBounds)
{
if (!light) return;
Extents oldWorldBB = getWorldBounds();
CoordinateFrame frame = part ? part->calcRenderingCoordinateFrame() : CoordinateFrame();
if (!dirty && transform.fuzzyEq(frame))
{
// Nothing to update
return;
}
transform = frame;
if (part)
{
// Update basis
position = transform.translation;
direction = Vector3();
axisU = Vector4();
axisV = Vector4();
if (SurfaceLight* surfaceLight = light->fastDynamicCast<SurfaceLight>())
{
Vector4 axis;
getBasis(surfaceLight->getFace(), transform.rotation, part->getPartSizeUi() * 0.5f, axis, axisU, axisV);
position = transform.translation + axis.xyz() * axis.w;
direction = axis.xyz();
}
else if (SpotLight* spotLight = light->fastDynamicCast<SpotLight>())
{
direction = Math::getWorldNormal(spotLight->getFace(), transform.rotation);
}
// Update world-space light extents
Extents extents = computeLightExtents(position, direction, axisU, axisV, light.get());
updateWorldBounds(extents);
}
else
{
// Reset world-space light extents
updateWorldBounds(Extents());
// Make sure the node is not in the spatial hash
RBXASSERT(!IsInSpatialHash());
}
invalidateLighting(oldWorldBB);
invalidateLighting(getWorldBounds());
}
void LightObject::onCombinedSignalEx(Instance::CombinedSignalType type, const Instance::ICombinedSignalData* data)
{
switch (type)
{
case Instance::PROPERTY_CHANGED:
onPropertyChangedEx(boost::polymorphic_downcast<const Instance::PropertyChangedSignalData*>(data)->propertyDescriptor);
break;
case Instance::ANCESTRY_CHANGED:
onAncestorChangedEx();
break;
default:
break;
}
}
void LightObject::onPropertyChangedEx(const RBX::Reflection::PropertyDescriptor* descriptor)
{
invalidateEntity();
}
void LightObject::onAncestorChangedEx()
{
shared_ptr<Light> lightCopy = light;
// Remove me from the scene if I am being removed from the Workspace
if (!isInWorkspace(lightCopy.get()))
{
// will cause a delete on next updateEntity()
zombify();
}
else
{
unbind();
RBX::PartInstance* parent = RBX::Instance::fastDynamicCast<RBX::PartInstance>(lightCopy->getParent());
shared_ptr<RBX::PartInstance> part = shared_from(parent);
bind(part, lightCopy);
}
}
void LightObject::bind(const shared_ptr<RBX::PartInstance>& part, const shared_ptr<RBX::Light>& light)
{
RBXASSERT(!this->part && !this->light);
RBXASSERT(light);
this->part = part;
this->light = light;
connections.push_back(light->combinedSignal.connect(boost::bind(&LightObject::onCombinedSignalEx, this, _1, _2)));
if (part)
{
connections.push_back(part->onDemandWrite()->sleepingChangedSignal.connect(boost::bind(&LightObject::onSleepingChangedEx, this, _1)));
if (light->fastDynamicCast<SurfaceLight>())
connections.push_back(part->propertyChangedSignal.connect( boost::bind(&LightObject::onParentSizeChangedEx, this, _1)));
// we just connected, so sync up the state.
onSleepingChangedEx(part->getSleeping());
}
invalidateEntity();
}
void LightObject::unbind()
{
Super::unbind();
part.reset();
light.reset();
}
void LightObject::invalidateEntity()
{
if (!dirty)
{
dirty = true;
getVisualEngine()->getSceneUpdater()->queueInvalidatePart(this);
}
}
void LightObject::updateEntity(bool assetsUpdated)
{
if (connections.empty()) // zombified.
{
invalidateLighting(getWorldBounds());
getVisualEngine()->getSceneUpdater()->destroyAttachment(this);
return;
}
updateCoordinateFrame(true);
if (light && light->getEnabled())
{
color = light->getColor();
brightness = light->getBrightness();
if (SurfaceLight* surfaceLight = light->fastDynamicCast<SurfaceLight>())
{
type = Type_Surface;
range = surfaceLight->getRange();
angle = surfaceLight->getAngle();
}
else if (SpotLight* spotLight = light->fastDynamicCast<SpotLight>())
{
type = Type_Spot;
range = spotLight->getRange();
angle = spotLight->getAngle();
}
else if (PointLight* pointLight = light->fastDynamicCast<PointLight>())
{
type = Type_Point;
range = pointLight->getRange();
angle = 0;
}
else
{
RBXASSERT(false);
type = Type_None;
}
}
else
{
type = Type_None;
}
if (light && light->getShadows())
{
// create an empty shadow map if necessary
if (!shadowMap)
shadowMap.reset(new LightShadowMap);
// get new light size; note that we always use the full radius even for spot lights
// this is necessary to minimize changes on light rotation
// we need to ceil and add 1 to account for worst-case in terms of number of intersected voxels
unsigned int lightSize = G3D::iCeil(getLightRange(light.get()) / 4.f) * 2 + 1;
// clamp max size to handle FP issues
if (lightSize > 31)
{
RBXASSERT(false);
lightSize = 31;
}
// resize each projection of shadow map
if (lightSize != shadowMap->size)
{
resizeShadowProjection(shadowMap->sliceX.data, shadowMap->size, lightSize);
resizeShadowProjection(shadowMap->sliceY0.data, shadowMap->size, lightSize);
resizeShadowProjection(shadowMap->sliceY1.data, shadowMap->size, lightSize);
resizeShadowProjection(shadowMap->sliceZ.data, shadowMap->size, lightSize);
shadowMap->size = lightSize;
}
}
else
{
shadowMap.reset();
}
dirty = false;
}
void LightObject::invalidateLighting(const Extents& bbox)
{
if (!bbox.isNull())
{
getVisualEngine()->getSceneUpdater()->lightingInvalidateLocal(bbox);
}
}
const Extents& LightObject::getExtents() const
{
return getWorldBounds();
}
}
}