Skip to content

Commit 0aeb3ed

Browse files
Merge pull request #842 from fernandotonon/feat/lights-slice-i-remaining-491
feat(lights): complete Slice I extras — groups, solo, IES, area lights (#491)
2 parents 2e314a1 + 2010285 commit 0aeb3ed

28 files changed

Lines changed: 2061 additions & 21 deletions

CLAUDE.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,10 @@ Three singletons manage core state. All run on the main thread. Access via `Clas
203203
- **Intensity units**: GUI/CLI/MCP use Ogre `powerScale` (Inspector “Intensity”). glTF export writes both `qtmesh.scene.lights` (exact) and best-effort KHR punctual intensity derived from `powerScale × diffuse luminance`. FBX lights are Assimp best-effort; use the `.lights.json` sidecar for bit-exact round-trip.
204204
- **Sentry breadcrumbs**: `scene.light.create|delete|duplicate|rename|edit|shadow_toggle|apply_rig|gizmo_toggle` on core ops; `ui.action` on menu/toolbar clicks.
205205
- **Light linking (Slice I #491)**: per-light include/exclude lists map to Ogre `Light::setLightMask` / `Entity::setLightMask` (32 channel bits; bit 0 reserved; max 31 simultaneous link rules). Persisted in `qtmesh.scene.lights` JSON as `linkMode`, `linkedEntities`, `linkChannelBit`. Exclude sets excluded entities to the channel bit only (no overlap with the light's inverted mask). Combining include + exclude on the same entity is best-effort. RTSS PBR may not honour masks on every pass.
206+
- **Light collections (#491)**: user-created groups via `LightGroupLibrary` / `LightGroupController` — select 2+ lights, parent under a tagged `light_group` scene node; transform/enable/disable as one unit. Persisted in scene JSON as `rigGroups[]` with `groupKind: "collection"`.
207+
- **Viewport solo (#491)**: `ViewportLightSoloController` — per-viewport runtime toggle to show only one light during that viewport's render pass (`OgreWidget::frameEnded` begin/end hooks). Inspector picker under Scene → Lighting.
208+
- **IES profiles (#491)**: `IesProfile` LM-63 parser + `IesLightApply` maps beam/field angles to spotlight cones (approximation; not full shader 1D texture yet). Inspector Browse/Clear; polar plot overlay in `LightVisualizer`. Persisted as `iesProfilePath` in scene JSON.
209+
- **Area lights (#491, `ENABLE_AREA_LIGHTS`)**: multi-point proxy approximation on rectangle/disk/line shapes; parent light hidden while active. Inspector shape/size/samples; wire gizmo in `LightVisualizer`. Persisted as `areaShape`, `areaWidth`, `areaHeight`, `areaSampleCount`.
206210

207211
### Debug Overlays
208212

CMakeLists.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,15 @@ if(ENABLE_PS1_RIP)
324324
add_subdirectory(plugins/ps1core_stub)
325325
endif()
326326
##############################################################
327+
# Area lights (Slice I #491 — multi-point approximation)
328+
##############################################################
329+
option(ENABLE_AREA_LIGHTS "Enable area-light gizmo + multi-point approximation" ON)
330+
331+
if(ENABLE_AREA_LIGHTS)
332+
add_definitions(-DENABLE_AREA_LIGHTS)
333+
message(STATUS "Area lights enabled (multi-point approximation)")
334+
endif()
335+
##############################################################
327336
# In-app auto-updater (portable Windows/macOS/Linux tarballs)
328337
##############################################################
329338
option(ENABLE_AUTO_UPDATER "Enable in-app auto-updater UI and download path" ON)

qml/PropertiesPanel.qml

Lines changed: 364 additions & 0 deletions
Large diffs are not rendered by default.

src/AreaLight.cpp

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
#include "AreaLight.h"
2+
3+
#include "Manager.h"
4+
5+
#include <OgreSceneManager.h>
6+
#include <OgreSceneNode.h>
7+
8+
#include <QtMath>
9+
10+
#include <vector>
11+
12+
namespace
13+
{
14+
15+
struct SamplePoint
16+
{
17+
Ogre::Vector3 localPos;
18+
};
19+
20+
std::vector<SamplePoint> sampleRectangle(float width, float height, int count)
21+
{
22+
std::vector<SamplePoint> points;
23+
const int cols = qMax(1, static_cast<int>(std::ceil(std::sqrt(count))));
24+
const int rows = qMax(1, (count + cols - 1) / cols);
25+
for (int r = 0; r < rows; ++r)
26+
{
27+
for (int c = 0; c < cols; ++c)
28+
{
29+
if (static_cast<int>(points.size()) >= count)
30+
break;
31+
const float u = (cols == 1) ? 0.5f : static_cast<float>(c) / static_cast<float>(cols - 1);
32+
const float v = (rows == 1) ? 0.5f : static_cast<float>(r) / static_cast<float>(rows - 1);
33+
SamplePoint p;
34+
p.localPos = Ogre::Vector3((u - 0.5f) * width, (v - 0.5f) * height, 0.0f);
35+
points.push_back(p);
36+
}
37+
}
38+
return points;
39+
}
40+
41+
std::vector<SamplePoint> sampleDisk(float radius, int count)
42+
{
43+
std::vector<SamplePoint> points;
44+
const int n = qMax(1, count);
45+
for (int i = 0; i < n; ++i)
46+
{
47+
const float t = static_cast<float>(i) / static_cast<float>(n);
48+
const float angle = t * 2.0f * Ogre::Math::PI;
49+
const float ring = (i % 2 == 0) ? 1.0f : 0.55f;
50+
SamplePoint p;
51+
p.localPos = Ogre::Vector3(std::cos(angle) * radius * ring,
52+
std::sin(angle) * radius * ring,
53+
0.0f);
54+
points.push_back(p);
55+
}
56+
return points;
57+
}
58+
59+
std::vector<SamplePoint> sampleLine(float length, int count)
60+
{
61+
std::vector<SamplePoint> points;
62+
const int n = qMax(2, count);
63+
for (int i = 0; i < n; ++i)
64+
{
65+
const float t = static_cast<float>(i) / static_cast<float>(n - 1);
66+
SamplePoint p;
67+
p.localPos = Ogre::Vector3((t - 0.5f) * length, 0.0f, 0.0f);
68+
points.push_back(p);
69+
}
70+
return points;
71+
}
72+
73+
} // namespace
74+
75+
namespace AreaLight
76+
{
77+
78+
bool isProxyLight(const Ogre::Light* light)
79+
{
80+
if (!light)
81+
return false;
82+
const auto any = light->getUserObjectBindings().getUserAny(kProxyLightTag);
83+
return any.has_value() && Ogre::any_cast<bool>(any);
84+
}
85+
86+
QString ownerLightName(const Ogre::Light* light)
87+
{
88+
if (!light)
89+
return {};
90+
const auto any = light->getUserObjectBindings().getUserAny(kProxyOwnerKey);
91+
if (!any.has_value())
92+
return {};
93+
try
94+
{
95+
return QString::fromStdString(Ogre::any_cast<std::string>(any));
96+
}
97+
catch (...)
98+
{
99+
return {};
100+
}
101+
}
102+
103+
void removeProxies(const QString& ownerLightName)
104+
{
105+
auto* mgr = Manager::getSingletonPtr();
106+
if (!mgr || ownerLightName.isEmpty())
107+
return;
108+
109+
Ogre::SceneManager* sceneMgr = mgr->getSceneMgr();
110+
if (!sceneMgr)
111+
return;
112+
113+
const LightHandle* owner = LightManager::getSingletonPtr()
114+
? LightManager::getSingleton()->findLight(ownerLightName)
115+
: nullptr;
116+
if (!owner || !owner->sceneNode)
117+
return;
118+
119+
std::vector<Ogre::SceneNode*> toDestroy;
120+
for (const auto& child : owner->sceneNode->getChildren())
121+
{
122+
auto* childNode = static_cast<Ogre::SceneNode*>(child);
123+
for (unsigned short i = 0; i < childNode->numAttachedObjects(); ++i)
124+
{
125+
Ogre::MovableObject* obj = childNode->getAttachedObject(i);
126+
if (!obj || obj->getMovableType() != QStringLiteral("Light"))
127+
continue;
128+
auto* light = static_cast<Ogre::Light*>(obj);
129+
if (isProxyLight(light) && AreaLight::ownerLightName(light) == ownerLightName)
130+
toDestroy.push_back(childNode);
131+
}
132+
}
133+
134+
for (Ogre::SceneNode* node : toDestroy)
135+
{
136+
node->removeAndDestroyAllChildren();
137+
node->getCreator()->destroySceneNode(node);
138+
}
139+
}
140+
141+
void syncProxies(const LightHandle& owner, const LightSnapshot& snapshot)
142+
{
143+
#ifndef ENABLE_AREA_LIGHTS
144+
Q_UNUSED(owner);
145+
Q_UNUSED(snapshot);
146+
return;
147+
#else
148+
if (!owner.isValid())
149+
return;
150+
151+
removeProxies(owner.name);
152+
153+
const QString shape = snapshot.areaShape.trimmed().toLower();
154+
if (shape.isEmpty())
155+
{
156+
owner.light->setVisible(snapshot.enabled);
157+
return;
158+
}
159+
160+
std::vector<SamplePoint> samples;
161+
const int count = qBound(2, snapshot.areaSampleCount, 16);
162+
if (shape == QStringLiteral("rectangle"))
163+
samples = sampleRectangle(snapshot.areaWidth, snapshot.areaHeight, count);
164+
else if (shape == QStringLiteral("disk"))
165+
samples = sampleDisk(qMax(0.05f, snapshot.areaWidth * 0.5f), count);
166+
else if (shape == QStringLiteral("line"))
167+
samples = sampleLine(snapshot.areaWidth, count);
168+
else
169+
return;
170+
171+
owner.light->setVisible(false);
172+
173+
auto* lights = LightManager::getSingletonPtr();
174+
auto* mgr = Manager::getSingletonPtr();
175+
if (!lights || !mgr || !mgr->getSceneMgr())
176+
return;
177+
178+
Ogre::SceneManager* sceneMgr = mgr->getSceneMgr();
179+
const float weight = 1.0f / static_cast<float>(samples.size());
180+
int index = 0;
181+
for (const SamplePoint& sample : samples)
182+
{
183+
const QString proxyName = owner.name + QStringLiteral("_area%1").arg(index++);
184+
Ogre::SceneNode* node = owner.sceneNode->createChildSceneNode(proxyName.toStdString());
185+
Ogre::Light* proxyLight = sceneMgr->createLight(proxyName.toStdString());
186+
proxyLight->setType(Ogre::Light::LT_POINT);
187+
proxyLight->setDiffuseColour(owner.light->getDiffuseColour());
188+
proxyLight->setSpecularColour(owner.light->getSpecularColour());
189+
proxyLight->setPowerScale(owner.light->getPowerScale() * weight);
190+
proxyLight->setAttenuation(owner.light->getAttenuationRange(),
191+
owner.light->getAttenuationConstant(),
192+
owner.light->getAttenuationLinear(),
193+
owner.light->getAttenuationQuadric());
194+
proxyLight->setVisible(snapshot.enabled);
195+
proxyLight->setCastShadows(false);
196+
proxyLight->getUserObjectBindings().setUserAny(kProxyLightTag, Ogre::Any(true));
197+
proxyLight->getUserObjectBindings().setUserAny(kProxyOwnerKey,
198+
Ogre::Any(owner.name.toStdString()));
199+
node->attachObject(proxyLight);
200+
node->setPosition(sample.localPos);
201+
}
202+
#endif
203+
}
204+
205+
} // namespace AreaLight

src/AreaLight.h

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#pragma once
2+
3+
#include "LightManager.h"
4+
5+
#include <QString>
6+
7+
namespace AreaLight
8+
{
9+
10+
inline constexpr const char* kProxyLightTag = "area_light_proxy";
11+
inline constexpr const char* kProxyOwnerKey = "area_light_owner";
12+
13+
bool isProxyLight(const Ogre::Light* light);
14+
QString ownerLightName(const Ogre::Light* light);
15+
16+
/// Rebuild child point-light proxies for an area-shaped user light.
17+
void syncProxies(const LightHandle& owner, const LightSnapshot& snapshot);
18+
19+
/// Remove every proxy owned by @p ownerLightName.
20+
void removeProxies(const QString& ownerLightName);
21+
22+
} // namespace AreaLight

src/CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ SelectionSet.cpp
3737
LightManager.cpp
3838
LightRigLibrary.cpp
3939
LightLinking.cpp
40+
LightGroupLibrary.cpp
41+
LightGroupController.cpp
42+
ViewportLightSoloController.cpp
43+
IesProfile.cpp
44+
AreaLight.cpp
4045
LightVisualizer.cpp
4146
LightsController.cpp
4247
LightPropertiesController.cpp

0 commit comments

Comments
 (0)