From 231045782b3b69420b4d25ec1b3ad76f96aad606 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Fri, 27 Feb 2026 17:27:33 +0100 Subject: [PATCH] Add a new center mode "Centered on Z only" for 3D model objects --- Extensions/3D/JsExtension.js | 118 ++++++++++++++---- Extensions/3D/Model3DObjectConfiguration.cpp | 5 +- Extensions/3D/Model3DRuntimeObject.ts | 23 ++-- .../3D/Model3DRuntimeObject3DRenderer.ts | 53 ++++++-- .../src/ObjectEditor/Editors/Model3DEditor.js | 106 +++------------- 5 files changed, 168 insertions(+), 137 deletions(-) diff --git a/Extensions/3D/JsExtension.js b/Extensions/3D/JsExtension.js index a1600760912c..5ba30f92b0a4 100644 --- a/Extensions/3D/JsExtension.js +++ b/Extensions/3D/JsExtension.js @@ -3071,9 +3071,9 @@ module.exports = { /** @type {number} */ _defaultDepth; - /** @type {[number, number, number] | null} */ + /** @type {[number | null, number | null, number | null]} */ _originPoint; - /** @type {[number, number, number] | null} */ + /** @type {[number | null, number | null, number | null]} */ _centerPoint; /** @type {[number, number, number]} */ @@ -3168,11 +3168,31 @@ module.exports = { } getOriginPoint() { - return this._originPoint || this._modelOriginPoint; + return [ + this._originPoint[0] === null + ? this._modelOriginPoint[0] + : this._originPoint[0], + this._originPoint[1] === null + ? this._modelOriginPoint[1] + : this._originPoint[1], + this._originPoint[2] === null + ? this._modelOriginPoint[2] + : this._originPoint[2], + ]; } getCenterPoint() { - return this._centerPoint || this._modelOriginPoint; + return [ + this._centerPoint[0] === null + ? this._modelOriginPoint[0] + : this._centerPoint[0], + this._centerPoint[1] === null + ? this._modelOriginPoint[1] + : this._centerPoint[1], + this._centerPoint[2] === null + ? this._modelOriginPoint[2] + : this._centerPoint[2], + ]; } _updateDefaultTransformation( @@ -3219,12 +3239,23 @@ module.exports = { // Center the model. const centerPoint = this._centerPoint; - if (centerPoint) { - threeObject.position.set( - -(boundingBox.min.x + modelWidth * centerPoint[0]), - // The model is flipped on Y axis. - -(boundingBox.min.y + modelHeight * (1 - centerPoint[1])), - -(boundingBox.min.z + modelDepth * centerPoint[2]) + if (centerPoint[0]) { + threeObject.position.x = -( + boundingBox.min.x + + modelWidth * centerPoint[0] + ); + } + if (centerPoint[1]) { + // The model is flipped on Y axis. + threeObject.position.y = -( + boundingBox.min.y + + modelHeight * (1 - centerPoint[1]) + ); + } + if (centerPoint[2]) { + threeObject.position.z = -( + boundingBox.min.z + + modelDepth * centerPoint[2] ); } @@ -3320,8 +3351,8 @@ module.exports = { } /** - * @param {[number, number, number] | null} point1 - * @param {[number, number, number] | null} point2 + * @param {[number | null, number | null, number | null]} point1 + * @param {[number | null, number | null, number | null]} point2 * @returns {boolean} */ const isSamePoint = (point1, point2) => { @@ -3337,14 +3368,16 @@ module.exports = { /** * @param {string} location - * @returns {[number, number, number] | null} + * @returns {[number | null, number | null, number | null]} */ const getPointForLocation = (location) => { switch (location) { case 'ModelOrigin': - return null; + return [null, null, null]; case 'ObjectCenter': return [0.5, 0.5, 0.5]; + case 'CenteredOnZ': + return [null, null, 0.5]; case 'BottomCenterZ': return [0.5, 0.5, 0]; case 'BottomCenterY': @@ -3352,7 +3385,7 @@ module.exports = { case 'TopLeft': return [0, 0, 0]; default: - return null; + return [null, null, null]; } }; @@ -3367,10 +3400,10 @@ module.exports = { _rotationY = 0; _rotationZ = 0; _keepAspectRatio = false; - /** @type {[number, number, number] | null} */ - _originPoint = null; - /** @type {[number, number, number] | null} */ - _centerPoint = null; + /** @type {[number | null, number | null, number | null]} */ + _originPoint = [null, null, null]; + /** @type {[number | null, number | null, number | null]} */ + _centerPoint = [null, null, null]; /** @type {[number, number, number]} */ _modelOriginPoint = [0, 0, 0]; @@ -3436,11 +3469,31 @@ module.exports = { } getOriginPoint() { - return this._originPoint || this._modelOriginPoint; + return [ + this._originPoint[0] === null + ? this._modelOriginPoint[0] + : this._originPoint[0], + this._originPoint[1] === null + ? this._modelOriginPoint[1] + : this._originPoint[1], + this._originPoint[2] === null + ? this._modelOriginPoint[2] + : this._originPoint[2], + ]; } getCenterPoint() { - return this._centerPoint || this._modelOriginPoint; + return [ + this._centerPoint[0] === null + ? this._modelOriginPoint[0] + : this._centerPoint[0], + this._centerPoint[1] === null + ? this._modelOriginPoint[1] + : this._centerPoint[1], + this._centerPoint[2] === null + ? this._modelOriginPoint[2] + : this._centerPoint[2], + ]; } _updateDefaultTransformation() { @@ -3493,12 +3546,23 @@ module.exports = { // Center the model. const centerPoint = this._centerPoint; - if (centerPoint) { - threeModelGroup.position.set( - -(boundingBox.min.x + modelWidth * centerPoint[0]), - // The model is flipped on Y axis. - -(boundingBox.min.y + modelHeight * (1 - centerPoint[1])), - -(boundingBox.min.z + modelDepth * centerPoint[2]) + if (centerPoint[0] !== null) { + threeModelGroup.position.x = -( + boundingBox.min.x + + modelWidth * centerPoint[0] + ); + } + if (centerPoint[1] !== null) { + // The model is flipped on Y axis. + threeModelGroup.position.y = -( + boundingBox.min.y + + modelHeight * (1 - centerPoint[1]) + ); + } + if (centerPoint[2] !== null) { + threeModelGroup.position.z = -( + boundingBox.min.z + + modelDepth * centerPoint[2] ); } diff --git a/Extensions/3D/Model3DObjectConfiguration.cpp b/Extensions/3D/Model3DObjectConfiguration.cpp index c3067342fcc3..68900235da06 100644 --- a/Extensions/3D/Model3DObjectConfiguration.cpp +++ b/Extensions/3D/Model3DObjectConfiguration.cpp @@ -22,7 +22,7 @@ using namespace std; Model3DObjectConfiguration::Model3DObjectConfiguration() : width(100), height(100), depth(100), rotationX(90), rotationY(0), rotationZ(90), modelResourceName(""), materialType("StandardWithoutMetalness"), - originLocation("ModelOrigin"), centerLocation("ModelOrigin"), + originLocation("ModelOrigin"), centerLocation("CenteredOnZ"), keepAspectRatio(true), crossfadeDuration(0.1f), isCastingShadow(true), isReceivingShadow(true) {} bool Model3DObjectConfiguration::UpdateProperty(const gd::String &propertyName, @@ -89,6 +89,8 @@ bool Model3DObjectConfiguration::UpdateProperty(const gd::String &propertyName, centerLocation = "ModelOrigin"; else if (normalizedValue == "objectcenter") centerLocation = "ObjectCenter"; + else if (normalizedValue == "centeredonz") + centerLocation = "CenteredOnZ"; else if (normalizedValue == "bottomcenterz") centerLocation = "BottomCenterZ"; else if (normalizedValue == "bottomcentery") @@ -206,6 +208,7 @@ Model3DObjectConfiguration::GetProperties() const { .SetType("choice") .AddChoice("ModelOrigin", _("Model origin")) .AddChoice("ObjectCenter", _("Object center")) + .AddChoice("CenteredOnZ", _("Centered on Z only")) .AddChoice("BottomCenterZ", _("Bottom center (Z)")) .AddChoice("BottomCenterY", _("Bottom center (Y)")) .SetLabel(_("Center point")) diff --git a/Extensions/3D/Model3DRuntimeObject.ts b/Extensions/3D/Model3DRuntimeObject.ts index 5c6aba48f9d1..300cda4f18c8 100644 --- a/Extensions/3D/Model3DRuntimeObject.ts +++ b/Extensions/3D/Model3DRuntimeObject.ts @@ -3,8 +3,8 @@ namespace gdjs { type Model3DObjectNetworkSyncDataType = { mt: number; - op: FloatPoint3D | null; - cp: FloatPoint3D | null; + op: LocationPoint | null; + cp: LocationPoint | null; anis: Model3DAnimation[]; ai: integer; ass: float; @@ -38,6 +38,7 @@ namespace gdjs { centerLocation: | 'ModelOrigin' | 'ObjectCenter' + | 'CenteredOnZ' | 'BottomCenterZ' | 'BottomCenterY'; animations: Model3DAnimation[]; @@ -47,12 +48,14 @@ namespace gdjs { }; } - type FloatPoint3D = [float, float, float]; + type LocationPoint = [float | null, float | null, float | null]; - const getPointForLocation = (location: string): FloatPoint3D | null => { + const getPointForLocation = (location: string): LocationPoint => { switch (location) { case 'ModelOrigin': - return null; + return [null, null, null]; + case 'CenteredOnZ': + return [null, null, 0.5]; case 'ObjectCenter': return [0.5, 0.5, 0.5]; case 'BottomCenterZ': @@ -62,7 +65,7 @@ namespace gdjs { case 'TopLeft': return [0, 0, 0]; default: - return null; + return [null, null, null]; } }; @@ -90,7 +93,7 @@ namespace gdjs { * configuration. * @see gdjs.Model3DRuntimeObject3DRenderer.getOriginPoint */ - _originPoint: FloatPoint3D | null; + _originPoint: LocationPoint; /** * The local point of the model that is used as rotation center. * @@ -101,7 +104,7 @@ namespace gdjs { * configuration. * @see gdjs.Model3DRuntimeObject3DRenderer.getCenterPoint */ - _centerPoint: FloatPoint3D | null; + _centerPoint: LocationPoint; _animations: Model3DAnimation[]; _currentAnimationIndex: integer = 0; @@ -275,10 +278,10 @@ namespace gdjs { this._materialType = networkSyncData.mt; } if (networkSyncData.op !== undefined) { - this._originPoint = networkSyncData.op; + this._originPoint = networkSyncData.op || [null, null, null]; } if (networkSyncData.cp !== undefined) { - this._centerPoint = networkSyncData.cp; + this._centerPoint = networkSyncData.cp || [null, null, null]; } if (networkSyncData.anis !== undefined) { this._animations = networkSyncData.anis; diff --git a/Extensions/3D/Model3DRuntimeObject3DRenderer.ts b/Extensions/3D/Model3DRuntimeObject3DRenderer.ts index 1b08ce28fcd4..1bd06bc122c7 100644 --- a/Extensions/3D/Model3DRuntimeObject3DRenderer.ts +++ b/Extensions/3D/Model3DRuntimeObject3DRenderer.ts @@ -130,12 +130,34 @@ namespace gdjs { ); } - getOriginPoint() { - return this._model3DRuntimeObject._originPoint || this._modelOriginPoint; + getOriginPoint(): FloatPoint3D { + //@ts-ignore + const point: FloatPoint3D = gdjs.staticArray( + Model3DRuntimeObject3DRenderer.prototype.getOriginPoint + ); + const originPoint = this._model3DRuntimeObject._originPoint; + point[0] = + originPoint[0] === null ? this._modelOriginPoint[0] : originPoint[0]; + point[1] = + originPoint[1] === null ? this._modelOriginPoint[1] : originPoint[1]; + point[2] = + originPoint[2] === null ? this._modelOriginPoint[2] : originPoint[2]; + return point; } - getCenterPoint() { - return this._model3DRuntimeObject._centerPoint || this._modelOriginPoint; + getCenterPoint(): FloatPoint3D { + //@ts-ignore + const point: FloatPoint3D = gdjs.staticArray( + Model3DRuntimeObject3DRenderer.prototype.getCenterPoint + ); + const centerPoint = this._model3DRuntimeObject._centerPoint; + point[0] = + centerPoint[0] === null ? this._modelOriginPoint[0] : centerPoint[0]; + point[1] = + centerPoint[1] === null ? this._modelOriginPoint[1] : centerPoint[1]; + point[2] = + centerPoint[2] === null ? this._modelOriginPoint[2] : centerPoint[2]; + return point; } /** @@ -177,12 +199,23 @@ namespace gdjs { // Center the model. const centerPoint = this._model3DRuntimeObject._centerPoint; - if (centerPoint) { - threeObject.position.set( - -(boundingBox.min.x + modelWidth * centerPoint[0]), - // The model is flipped on Y axis. - -(boundingBox.min.y + modelHeight * (1 - centerPoint[1])), - -(boundingBox.min.z + modelDepth * centerPoint[2]) + if (centerPoint[0] !== null) { + threeObject.position.x = -( + boundingBox.min.x + + modelWidth * centerPoint[0] + ); + } + if (centerPoint[1] !== null) { + // The model is flipped on Y axis. + threeObject.position.y = -( + boundingBox.min.y + + modelHeight * (1 - centerPoint[1]) + ); + } + if (centerPoint[2] !== null) { + threeObject.position.z = -( + boundingBox.min.z + + modelDepth * centerPoint[2] ); } diff --git a/newIDE/app/src/ObjectEditor/Editors/Model3DEditor.js b/newIDE/app/src/ObjectEditor/Editors/Model3DEditor.js index 554a25d67bfb..0e42e9beb6b2 100644 --- a/newIDE/app/src/ObjectEditor/Editors/Model3DEditor.js +++ b/newIDE/app/src/ObjectEditor/Editors/Model3DEditor.js @@ -32,6 +32,7 @@ import * as SkeletonUtils from 'three/examples/jsm/utils/SkeletonUtils'; import * as THREE from 'three'; import { PropertyCheckbox, PropertyField } from './PropertyFields'; import ResourceSelectorWithThumbnail from '../../ResourcesList/ResourceSelectorWithThumbnail'; +import { ChoiceProperty } from '../../BehaviorsEditor/Editors/Physics2Editor'; const gd: libGDevelop = global.gd; @@ -188,8 +189,7 @@ const Model3DEditor = ({ properties.get('originLocation').getValue() ); const onOriginLocationChange = React.useCallback( - // $FlowFixMe[missing-local-annot] - (event, index: number, newValue: string) => { + (event: any, index: number, newValue: string) => { onChangeProperty('originLocation', newValue); setOriginLocation(newValue); }, @@ -573,99 +573,27 @@ const Model3DEditor = ({ expand noColumnMargin > - - - - - - - - { + + { onChangeProperty('centerLocation', newValue); }} - fullWidth - > - - - - - + /> Lighting - { + { onChangeProperty('materialType', newValue); }} - > - - - - + /> {properties.get('materialType').getValue() !== 'Basic' && !hasLight(layout) && (