feat: more rotation gizmo improvements

This commit is contained in:
Nick Fisher
2024-12-12 22:00:41 +08:00
parent 62cd85c148
commit 7cf1468f38
9 changed files with 7697 additions and 7948 deletions

Binary file not shown.

View File

@@ -8,9 +8,15 @@ class _Gizmo {
final ThermionViewer viewer; final ThermionViewer viewer;
final GizmoAsset _gizmo; final GizmoAsset _gizmo;
ThermionEntity? _attachedTo; ThermionEntity? _attachedTo;
final GizmoType type;
_Gizmo(this._gizmo, this.viewer, this.type); final GizmoType _gizmoType;
_Gizmo(this._gizmo, this.viewer, this._gizmoType);
static Future<_Gizmo> forType(ThermionViewer viewer, GizmoType type) async {
final view = await viewer.getViewAt(0);
return _Gizmo(await viewer.createGizmo(view, type), viewer, type);
}
final _onEntityTransformUpdated = StreamController< final _onEntityTransformUpdated = StreamController<
({ThermionEntity entity, Matrix4 transform})>.broadcast(); ({ThermionEntity entity, Matrix4 transform})>.broadcast();
@@ -18,7 +24,6 @@ class _Gizmo {
Axis? _active; Axis? _active;
Axis? get active => _active; Axis? get active => _active;
double _getAngleBetweenVectors(Vector2 v1, Vector2 v2) { double _getAngleBetweenVectors(Vector2 v1, Vector2 v2) {
// Normalize vectors to ensure consistent rotation regardless of distance from center // Normalize vectors to ensure consistent rotation regardless of distance from center
v1.normalize(); v1.normalize();
@@ -87,9 +92,9 @@ class _Gizmo {
return; return;
} }
if (type == GizmoType.translation) { if (_gizmoType == GizmoType.translation) {
await _updateTranslation(currentPosition, delta); await _updateTranslation(currentPosition, delta);
} else if (type == GizmoType.rotation) { } else if (_gizmoType == GizmoType.rotation) {
await _updateRotation(currentPosition, delta); await _updateRotation(currentPosition, delta);
} }
@@ -97,7 +102,8 @@ class _Gizmo {
.add((entity: _attachedTo!, transform: gizmoTransform!)); .add((entity: _attachedTo!, transform: gizmoTransform!));
} }
Future<void> _updateTranslation(Vector2 currentPosition, Vector2 delta) async { Future<void> _updateTranslation(
Vector2 currentPosition, Vector2 delta) async {
var view = await viewer.getViewAt(0); var view = await viewer.getViewAt(0);
var camera = await viewer.getActiveCamera(); var camera = await viewer.getActiveCamera();
var viewport = await view.getViewport(); var viewport = await view.getViewport();
@@ -115,17 +121,13 @@ class _Gizmo {
var gizmoNdc = gizmoClipSpace / gizmoClipSpace.w; var gizmoNdc = gizmoClipSpace / gizmoClipSpace.w;
var gizmoScreenSpace = Vector2( var gizmoScreenSpace = Vector2(((gizmoNdc.x / 2) + 0.5) * viewport.width,
((gizmoNdc.x / 2) + 0.5) * viewport.width,
viewport.height - (((gizmoNdc.y / 2) + 0.5) * viewport.height)); viewport.height - (((gizmoNdc.y / 2) + 0.5) * viewport.height));
gizmoScreenSpace += delta; gizmoScreenSpace += delta;
gizmoNdc = Vector4( gizmoNdc = Vector4(((gizmoScreenSpace.x / viewport.width) - 0.5) * 2,
((gizmoScreenSpace.x / viewport.width) - 0.5) * 2, (((gizmoScreenSpace.y / viewport.height)) - 0.5) * -2, gizmoNdc.z, 1.0);
(((gizmoScreenSpace.y / viewport.height)) - 0.5) * -2,
gizmoNdc.z,
1.0);
var gizmoViewSpace = inverseProjectionMatrix * gizmoNdc; var gizmoViewSpace = inverseProjectionMatrix * gizmoNdc;
gizmoViewSpace /= gizmoViewSpace.w; gizmoViewSpace /= gizmoViewSpace.w;
@@ -142,7 +144,6 @@ class _Gizmo {
} }
Future<void> _updateRotation(Vector2 currentPosition, Vector2 delta) async { Future<void> _updateRotation(Vector2 currentPosition, Vector2 delta) async {
var view = await viewer.getViewAt(0); var view = await viewer.getViewAt(0);
var camera = await viewer.getActiveCamera(); var camera = await viewer.getActiveCamera();
var viewport = await view.getViewport(); var viewport = await view.getViewport();
@@ -157,8 +158,7 @@ class _Gizmo {
gizmoPositionWorldSpace.z, 1.0); gizmoPositionWorldSpace.z, 1.0);
var gizmoNdc = gizmoClipSpace / gizmoClipSpace.w; var gizmoNdc = gizmoClipSpace / gizmoClipSpace.w;
var gizmoScreenSpace = Vector2( var gizmoScreenSpace = Vector2(((gizmoNdc.x / 2) + 0.5) * viewport.width,
((gizmoNdc.x / 2) + 0.5) * viewport.width,
viewport.height - (((gizmoNdc.y / 2) + 0.5) * viewport.height)); viewport.height - (((gizmoNdc.y / 2) + 0.5) * viewport.height));
// Calculate vectors from gizmo center to previous and current mouse positions // Calculate vectors from gizmo center to previous and current mouse positions
@@ -207,23 +207,48 @@ class _Gizmo {
// Apply rotation to the current transform // Apply rotation to the current transform
gizmoTransform = rotationMatrix * gizmoTransform!; gizmoTransform = rotationMatrix * gizmoTransform!;
await viewer.setTransform(_attachedTo!, gizmoTransform!); await viewer.setTransform(_attachedTo!, gizmoTransform!);
} }
} }
class GizmoInputHandler extends InputHandler { class GizmoInputHandler extends InputHandler {
final InputHandler wrapped; final InputHandler wrapped;
final ThermionViewer viewer; final ThermionViewer viewer;
late final _Gizmo translationGizmo;
late final _Gizmo rotationGizmo;
_Gizmo get active =>
type == GizmoType.translation ? translationGizmo : rotationGizmo;
GizmoType type = GizmoType.translation;
late final _gizmos = <GizmoType, _Gizmo>{};
_Gizmo? active;
StreamSubscription? _entityTransformUpdatedListener;
GizmoType? getGizmoType() {
return active?._gizmoType;
}
Future setGizmoType(GizmoType? type) async {
if (type == null) {
await active?.detach();
active = null;
return;
}
var target = _gizmos[type]!;
if (target != active) {
await _entityTransformUpdatedListener?.cancel();
if (active?._attachedTo != null) {
var attachedTo = active!._attachedTo!;
await active!.detach();
await target.attach(attachedTo);
}
active = target;
_entityTransformUpdatedListener =
active!._onEntityTransformUpdated.stream.listen((event) {
_transformUpdatedController.add(event);
});
}
}
final _transformUpdatedController =
StreamController<({ThermionEntity entity, Matrix4 transform})>();
Stream<({ThermionEntity entity, Matrix4 transform})> Stream<({ThermionEntity entity, Matrix4 transform})>
get onEntityTransformUpdated => get onEntityTransformUpdated => _transformUpdatedController.stream;
translationGizmo._onEntityTransformUpdated.stream;
GizmoInputHandler({required this.wrapped, required this.viewer}) { GizmoInputHandler({required this.wrapped, required this.viewer}) {
initialize(); initialize();
@@ -236,13 +261,12 @@ class GizmoInputHandler extends InputHandler {
throw Exception("Already initialized"); throw Exception("Already initialized");
} }
await viewer.initialized; await viewer.initialized;
final view = await viewer.getViewAt(0);
var tg = await viewer.createGizmo(view, GizmoType.translation);
this.translationGizmo = _Gizmo(tg, viewer, GizmoType.translation);
var rg = await viewer.createGizmo(view, GizmoType.rotation);
this.rotationGizmo = _Gizmo(rg, viewer, GizmoType.rotation);
_gizmos[GizmoType.translation] =
await _Gizmo.forType(viewer, GizmoType.translation);
_gizmos[GizmoType.rotation] =
await _Gizmo.forType(viewer, GizmoType.rotation);
await setGizmoType(GizmoType.translation);
_initialized.complete(true); _initialized.complete(true);
} }
@@ -251,8 +275,8 @@ class GizmoInputHandler extends InputHandler {
@override @override
Future dispose() async { Future dispose() async {
await viewer.removeEntity(rotationGizmo._gizmo); await viewer.removeEntity(_gizmos[GizmoType.rotation]!._gizmo);
await viewer.removeEntity(translationGizmo._gizmo); await viewer.removeEntity(_gizmos[GizmoType.translation]!._gizmo);
} }
@override @override
@@ -288,13 +312,13 @@ class GizmoInputHandler extends InputHandler {
await viewer.pick(localPosition.x.toInt(), localPosition.y.toInt(), await viewer.pick(localPosition.x.toInt(), localPosition.y.toInt(),
(result) async { (result) async {
if (active._gizmo.isNonPickable(result.entity) || if (active?._gizmo.isNonPickable(result.entity) == true ||
result.entity == FILAMENT_ENTITY_NULL) { result.entity == FILAMENT_ENTITY_NULL) {
await active.detach(); await active!.detach();
return; return;
} }
if (!active._gizmo.isGizmoEntity(result.entity)) { if (active?._gizmo.isGizmoEntity(result.entity) != true) {
active.attach(result.entity); active!.attach(result.entity);
} }
}); });
} }
@@ -304,19 +328,18 @@ class GizmoInputHandler extends InputHandler {
if (!_initialized.isCompleted) { if (!_initialized.isCompleted) {
return; return;
} }
active.checkHover( active?.checkHover(localPosition.x.floor(), localPosition.y.floor());
localPosition.x.floor(), localPosition.y.floor());
} }
@override @override
Future<void> onPointerMove( Future<void> onPointerMove(
Vector2 localPosition, Vector2 delta, bool isMiddle) async { Vector2 localPosition, Vector2 delta, bool isMiddle) async {
if (!isMiddle && active._active != null) { if (!isMiddle && active?._active != null) {
final scaledDelta = Vector2( final scaledDelta = Vector2(
delta.x, delta.x,
delta.y, delta.y,
); );
active._updateTransform(localPosition, scaledDelta); active!._updateTransform(localPosition, scaledDelta);
return; return;
} }
return wrapped.onPointerMove(localPosition, delta, isMiddle); return wrapped.onPointerMove(localPosition, delta, isMiddle);
@@ -364,14 +387,14 @@ class GizmoInputHandler extends InputHandler {
} }
Future detach(ThermionAsset asset) async { Future detach(ThermionAsset asset) async {
if (active._attachedTo == asset.entity) { if (active?._attachedTo == asset.entity) {
await active.detach(); await active!.detach();
return; return;
} }
final childEntities = await asset.getChildEntities(); final childEntities = await asset.getChildEntities();
for (final childEntity in childEntities) { for (final childEntity in childEntities) {
if (active._attachedTo == childEntity) { if (active?._attachedTo == childEntity) {
await active.detach(); await active!.detach();
return; return;
} }
} }

View File

@@ -8,5 +8,5 @@ ROTATION_GIZMO_GLB_PACKAGE:
ROTATION_GIZMO_GLB_ROTATION_GIZMO_OFFSET: ROTATION_GIZMO_GLB_ROTATION_GIZMO_OFFSET:
.int 0 .int 0
ROTATION_GIZMO_GLB_ROTATION_GIZMO_SIZE: ROTATION_GIZMO_GLB_ROTATION_GIZMO_SIZE:
.int 163152 .int 156056

View File

@@ -8,5 +8,5 @@ _ROTATION_GIZMO_GLB_PACKAGE:
_ROTATION_GIZMO_GLB_ROTATION_GIZMO_OFFSET: _ROTATION_GIZMO_GLB_ROTATION_GIZMO_OFFSET:
.int 0 .int 0
_ROTATION_GIZMO_GLB_ROTATION_GIZMO_SIZE: _ROTATION_GIZMO_GLB_ROTATION_GIZMO_SIZE:
.int 163152 .int 156056

File diff suppressed because it is too large Load Diff

View File

@@ -201,10 +201,13 @@ namespace thermion
} }
else else
{ {
if(entity == axis->getEntity()) { if (entity == axis->getEntity())
{
TRACE("MATCHED AXIS HEAD ENTITY"); TRACE("MATCHED AXIS HEAD ENTITY");
result = GizmoPickResultType(axisIndex); result = GizmoPickResultType(axisIndex);
} else { }
else
{
for (int entityIndex = 0; entityIndex < axis->getChildEntityCount(); entityIndex++) for (int entityIndex = 0; entityIndex < axis->getChildEntityCount(); entityIndex++)
{ {
auto childEntity = axis->getChildEntities()[entityIndex]; auto childEntity = axis->getChildEntities()[entityIndex];

View File

@@ -25,14 +25,25 @@ namespace thermion
View *view, View *view,
Scene *scene, Scene *scene,
Material *material) noexcept : _source(sceneAsset), Material *material) noexcept : _source(sceneAsset),
_engine(engine), _engine(engine),
_view(view), _view(view),
_scene(scene), _scene(scene),
_material(material) _material(material)
{ {
auto &entityManager = _engine->getEntityManager(); auto &entityManager = _engine->getEntityManager();
_parent = entityManager.create(); _parent = entityManager.create();
RenderableManager::Builder(1) // 1 primitive
.boundingBox({{-1, -1, -1}, {1, 1, 1}}) // Set a basic bounding box
.culling(false) // Disable culling since this is a UI element
.castShadows(false) // UI elements typically don't cast shadows
.receiveShadows(false)
.build(*_engine, _parent); // Bu
auto &tm = _engine->getTransformManager();
auto parentTransformInstance = tm.getInstance(_parent);
tm.setTransform(parentTransformInstance, math::mat4f());
TRACE("Created Gizmo parent entity %d", _parent); TRACE("Created Gizmo parent entity %d", _parent);
_entities.push_back(_parent); _entities.push_back(_parent);
@@ -71,7 +82,6 @@ namespace thermion
auto instance = _source->createInstance(&materialInstance, 1); auto instance = _source->createInstance(&materialInstance, 1);
TRACE("Created Gizmo axis glTF instance with head entity %d", instance->getEntity()); TRACE("Created Gizmo axis glTF instance with head entity %d", instance->getEntity());
materialInstance->setParameter("baseColorFactor", inactiveColors[axis]); materialInstance->setParameter("baseColorFactor", inactiveColors[axis]);
materialInstance->setParameter("scale", _scale); materialInstance->setParameter("scale", _scale);
@@ -109,7 +119,15 @@ namespace thermion
tm.setTransform(transformInstance, transform); tm.setTransform(transformInstance, transform);
// parent this entity's transform to the Gizmo _parent entity // parent this entity's transform to the Gizmo _parent entity
tm.setParent(transformInstance, tm.getInstance(_parent)); auto parentTransformInstance = tm.getInstance(_parent);
if (parentTransformInstance.isValid())
{
tm.setParent(transformInstance, parentTransformInstance);
}
else
{
TRACE("WARNING: parent transform instance not valid.");
}
_entities.push_back(instance->getEntity()); _entities.push_back(instance->getEntity());
@@ -120,6 +138,11 @@ namespace thermion
auto entity = instance->getChildEntities()[i]; auto entity = instance->getChildEntities()[i];
_entities.push_back(entity); _entities.push_back(entity);
TRACE("Added entity %d for axis %d", entity, axis); TRACE("Added entity %d for axis %d", entity, axis);
auto renderable = rm.getInstance(entity);
if (renderable.isValid())
{
rm.setPriority(renderable, 7);
}
} }
_axes.push_back(instance); _axes.push_back(instance);

View File

@@ -9,27 +9,81 @@ void main() async {
final testHelper = TestHelper("gizmo"); final testHelper = TestHelper("gizmo");
group("gizmo tests", () { group("gizmo tests", () {
test('add gizmo', () async { test('add/remove translation gizmo', () async {
await testHelper.withViewer((viewer) async { await testHelper.withViewer((viewer) async {
var cameraPos = Vector3(1.5, 1.5, 3);
var modelMatrix = var modelMatrix =
makeViewMatrix(Vector3(0.5, 0.5, 0.5), Vector3.zero(), Vector3(0, 1, 0)); makeViewMatrix(cameraPos, Vector3.zero(), Vector3(0, 1, 0));
modelMatrix.invert(); modelMatrix.invert();
await viewer.setCameraModelMatrix4(modelMatrix); await viewer.setCameraModelMatrix4(modelMatrix);
final view = await viewer.getViewAt(0); final view = await viewer.getViewAt(0);
await viewer.showGridOverlay();
final gizmo = await viewer.createGizmo(view, GizmoType.translation); final gizmo = await viewer.createGizmo(view, GizmoType.translation);
await viewer.setLayerVisibility(VisibilityLayers.OVERLAY, true); await viewer.setLayerVisibility(VisibilityLayers.OVERLAY, true);
await gizmo.addToScene(); await gizmo.addToScene();
await testHelper.capture( await testHelper.capture(viewer, "translation_gizmo_near");
viewer, "gizmo_added_to_scene_unattached_close");
modelMatrix = modelMatrix = makeViewMatrix(
makeViewMatrix(Vector3(0.5, 0.5, 0.5).scaled(10), Vector3.zero(), Vector3(0, 1, 0)); cameraPos.scaled(10), Vector3.zero(), Vector3(0, 1, 0));
modelMatrix.invert(); modelMatrix.invert();
await viewer.setCameraModelMatrix4(modelMatrix); await viewer.setCameraModelMatrix4(modelMatrix);
// gizmo occupies same viewport size no matter the camera position // gizmo occupies same viewport size no matter the camera position
await testHelper.capture(viewer, "gizmo_added_to_scene_unattached_far"); await testHelper.capture(viewer, "translation_gizmo_far");
await gizmo.removeFromScene();
await testHelper.capture(viewer, "translation_gizmo_removed");
}, postProcessing: true, bg: kWhite);
});
test('add/remove rotation gizmo', () async {
await testHelper.withViewer((viewer) async {
var cameraPos = Vector3(1.5, 1.5, 3);
var modelMatrix =
makeViewMatrix(cameraPos, Vector3.zero(), Vector3(0, 1, 0));
modelMatrix.invert();
await viewer.setCameraModelMatrix4(modelMatrix);
final view = await viewer.getViewAt(0);
await viewer.showGridOverlay();
final gizmo = await viewer.createGizmo(view, GizmoType.rotation);
await viewer.setLayerVisibility(VisibilityLayers.OVERLAY, true);
await gizmo.addToScene();
await testHelper.capture(viewer, "rotation_gizmo_near");
modelMatrix = makeViewMatrix(
cameraPos.scaled(10), Vector3.zero(), Vector3(0, 1, 0));
modelMatrix.invert();
await viewer.setCameraModelMatrix4(modelMatrix);
// gizmo occupies same viewport size no matter the camera position
await testHelper.capture(viewer, "rotation_gizmo_far");
await gizmo.removeFromScene();
await testHelper.capture(viewer, "rotation_gizmo_removed");
}, postProcessing: true, bg: kWhite);
});
test('set gizmo transform', () async {
await testHelper.withViewer((viewer) async {
var cameraPos = Vector3(1.5, 1.5, 3);
var modelMatrix =
makeViewMatrix(cameraPos, Vector3.zero(), Vector3(0, 1, 0));
modelMatrix.invert();
await viewer.setCameraModelMatrix4(modelMatrix);
final view = await viewer.getViewAt(0);
await viewer.showGridOverlay();
final gizmo = await viewer.createGizmo(view, GizmoType.translation);
await viewer.setLayerVisibility(VisibilityLayers.OVERLAY, true);
await gizmo.addToScene();
await viewer.setTransform(gizmo.entity, Matrix4.translation(Vector3(0,2,0)));
await testHelper.capture(viewer, "translation_gizmo_transformed");
}, postProcessing: true, bg: kWhite); }, postProcessing: true, bg: kWhite);
}); });
@@ -60,7 +114,7 @@ void main() async {
}, postProcessing: true, bg: kWhite); }, postProcessing: true, bg: kWhite);
}); });
test('pick gizmo when added to scene', () async { test('pick translation gizmo when added to scene', () async {
await testHelper.withViewer((viewer) async { await testHelper.withViewer((viewer) async {
await viewer.setCameraPosition(0, 0, 1); await viewer.setCameraPosition(0, 0, 1);
final view = await viewer.getViewAt(0); final view = await viewer.getViewAt(0);
@@ -73,7 +127,7 @@ void main() async {
await testHelper.capture(viewer, "gizmo_before_pick_no_highlight"); await testHelper.capture(viewer, "gizmo_before_pick_no_highlight");
await gizmo.pick(viewport.width ~/ 2, viewport.height ~/ 2 + 1, await gizmo.pick(viewport.width ~/ 2 + 100, viewport.height ~/ 2,
handler: (resultType, coords) async { handler: (resultType, coords) async {
completer.complete(resultType); completer.complete(resultType);
}); });
@@ -86,17 +140,18 @@ void main() async {
} }
assert(completer.isCompleted); assert(completer.isCompleted);
expect(await completer.future, GizmoPickResultType.AxisX);
}, postProcessing: true, bg: kWhite); }, postProcessing: true, bg: kWhite);
}); });
test('highlight/unhighlight gizmo', () async { test('highlight/unhighlight gizmo', () async {
await testHelper.withViewer((viewer) async { await testHelper.withViewer((viewer) async {
final modelMatrix = final modelMatrix = makeViewMatrix(
makeViewMatrix(Vector3(0.5, 0.5, 0.5), Vector3.zero(), Vector3(0, 1, 0)); Vector3(0.5, 0.5, 0.5), Vector3.zero(), Vector3(0, 1, 0));
modelMatrix.invert(); modelMatrix.invert();
await viewer.setCameraModelMatrix4(modelMatrix); await viewer.setCameraModelMatrix4(modelMatrix);
final view = await viewer.getViewAt(0); final view = await viewer.getViewAt(0);
final viewport = await view.getViewport();
final gizmo = await viewer.createGizmo(view, GizmoType.translation); final gizmo = await viewer.createGizmo(view, GizmoType.translation);
await gizmo.addToScene(); await gizmo.addToScene();
await viewer.setLayerVisibility(VisibilityLayers.OVERLAY, true); await viewer.setLayerVisibility(VisibilityLayers.OVERLAY, true);