diff --git a/doc/mesh/images/uploader-pivot.png b/doc/mesh/images/uploader-pivot.png new file mode 100644 index 0000000000..a609c185af Binary files /dev/null and b/doc/mesh/images/uploader-pivot.png differ diff --git a/doc/mesh/object-origins.md b/doc/mesh/object-origins.md new file mode 100644 index 0000000000..5a78ecb371 --- /dev/null +++ b/doc/mesh/object-origins.md @@ -0,0 +1,61 @@ +# Object Origins for Prim-Animated Doors, Hinges and Joints + +## Why the origin matters + +A prim in Second Life turns about its own position, and a mesh prim's position is the object origin it was uploaded with. Put the origin on the hinge, and a script that turns the door with `llSetLocalRot` swings it on its hinge. Leave it in the middle, and the door spins about its centre. + +Until now the uploader always moved the origin to the middle of the object's bounding box. Doors were built with an invisible extra half, or a script had to move the prim as it turned it. The uploader's **Pivot** option keeps the origin you set in Blender instead. + +## Setting the origin in Blender + +1. Apply rotation and scale first: select the object, **Ctrl+A > Rotation & Scale**. +2. Put the 3D cursor on the hinge or joint: in Edit Mode select the vertices along the hinge line, then **Shift+S > Cursor to Selected**. +3. Back in Object Mode, **Object > Set Origin > Origin to 3D Cursor**. +4. Check it: rotate the object (R, then an axis key). It should turn about the hinge, and the orange dot should sit on it. + +- **The origin must lie inside or on the object's bounding box.** The uploader stores it as a point within the box, so an origin outside is moved back to the centre. +- **Turn about a local axis.** Model the part so the hinge runs along X, Y or Z, so a script needs only one angle. +- Each part of a linkset has its own origin. An arm needs one per joint: the turret on its vertical axis, each limb on its elbow. + +## The uploader's Pivot + +The **Pivot** row is on the **Upload options** tab. It has three modes, X/Y/Z spinners beside them, and a status line under the spinners. + +![The Upload options tab: Pivot on From file, the spinners showing the upper arm's origin in percent, and the Origin arrows drawn on the preview at its elbow](images/uploader-pivot.png) + +| Mode | The origin goes | Works for | +| --- | --- | --- | +| From file (default) | where Blender's origin was, for each object | one object or many | +| Centre | to the middle of the bounding box, as uploads always did | one object or many | +| Custom | where you type it in the spinners | a single object only | + +- The spinners show the pivot along the object's own axes, in **percent** of its half size (−100 to 100, 0 = centre) or in **metres**; the dropdown beside them switches the unit. +- They are editable only in Custom, which starts from the pivot shown when you switch to it. +- Tick **Origin** under the preview to see the pivot: coloured arrows (X red, Y green, Z blue) drawn at each object's pivot. +- In a multi-object upload each part keeps its own origin, and its position in the linkset moves with it, so every part lands where it sat in Blender. + +The status line says when the pivot is held at the centre, or when the collision stays centred: + +| Status | Why | Fix | +| --- | --- | --- | +| Rigged mesh | rigged mesh is placed by its skeleton | none needed | +| Physics not analyzed | the sim fits a mesh physics shape to the box until it's analyzed | click **Analyze** on the Physics tab | +| No physics shape | the part moves to its pivot, but collides with a default hull centred on itself | fine for phantom parts or physics type None; otherwise add a physics shape and analyze it | + +## Examples and common mistakes + +| Part | Where the origin goes | Turns about | +| --- | --- | --- | +| Door | on the hinge edge, halfway up | its vertical axis | +| Chest lid | on the back edge, along the hinge | the hinge's axis | +| Arm turret | on the base's centre line | its vertical axis | +| Arm limb | in the joint at the limb's near end | the joint's axis | + +A limb's joint is often at the very end of the part. That's fine: the origin may sit on the box's edge, just not outside it. + +| You see | Likely cause | +| --- | --- | +| The part spins about its middle | the pivot is on Centre, the origin lies outside the box, or the physics isn't analyzed | +| The spinners read 0 0 0 on From file | Blender's origin was at the middle, or outside the box | +| The part rotates about the wrong axis | rotation wasn't applied before export | +| Custom is greyed out | the upload has several objects; Custom is for a single one | diff --git a/indra/llprimitive/llmodel.cpp b/indra/llprimitive/llmodel.cpp index 9521c82269..c0ec5d69dd 100644 --- a/indra/llprimitive/llmodel.cpp +++ b/indra/llprimitive/llmodel.cpp @@ -627,7 +627,8 @@ LLSD LLModel::writeModel( bool lock_scale_if_joint_position, EWriteModelMode write_mode, bool as_slm, - int submodel_id) + int submodel_id, + const LLVector3& position_offset) // Mesh uploader pivot { LLSD mdl; @@ -665,6 +666,15 @@ LLSD LLModel::writeModel( model[LLModel::LOD_PHYSICS] = NULL; } + // Mesh uploader pivot: the sim keeps analyzed hulls in their domain but fits the bounding hull to the box + if (!decomp.mHull.empty() && mdl.has("physics_convex")) + { + LLSD& convex = mdl["physics_convex"]; + convex["Min"] = (LLVector3(convex["Min"]) + position_offset).getValue(); + convex["Max"] = (LLVector3(convex["Max"]) + position_offset).getValue(); + } + // + if (as_slm) { //save material list names for (U32 i = 0; i < high->mMaterialList.size(); ++i) @@ -813,8 +823,10 @@ LLSD LLModel::writeModel( } //write out face data - mdl[model_names[idx]][i]["PositionDomain"]["Min"] = min_pos.getValue(); - mdl[model_names[idx]][i]["PositionDomain"]["Max"] = max_pos.getValue(); + // Mesh uploader pivot: the quantized positions stay, only their domain moves + mdl[model_names[idx]][i]["PositionDomain"]["Min"] = (min_pos + position_offset).getValue(); + mdl[model_names[idx]][i]["PositionDomain"]["Max"] = (max_pos + position_offset).getValue(); + // mdl[model_names[idx]][i]["NormalizedScale"] = face.mNormalizedScale.getValue(); mdl[model_names[idx]][i]["Position"] = verts; diff --git a/indra/llprimitive/llmodel.h b/indra/llprimitive/llmodel.h index 55adbe7412..99e6f93c60 100644 --- a/indra/llprimitive/llmodel.h +++ b/indra/llprimitive/llmodel.h @@ -186,7 +186,8 @@ class alignas(16) LLModel : public LLVolume bool lock_scale_if_joint_position, EWriteModelMode write_mode = WRITE_BINARY, bool as_slm = false, - int submodel_id = 0); + int submodel_id = 0, + const LLVector3& position_offset = LLVector3::zero); // Mesh uploader pivot: shifts the stored domains off the prim's origin static LLSD writeModelToStream( std::ostream& ostr, diff --git a/indra/newview/llfloatermodelpreview.cpp b/indra/newview/llfloatermodelpreview.cpp index 4057890113..ef5d78cab4 100644 --- a/indra/newview/llfloatermodelpreview.cpp +++ b/indra/newview/llfloatermodelpreview.cpp @@ -31,6 +31,8 @@ #include "llfloatermodelpreview.h" +#include "llradiogroup.h" // Mesh uploader pivot + #include "llfilepicker.h" #include "llimagebmp.h" #include "llimagetga.h" @@ -191,6 +193,16 @@ bool LLFloaterModelPreview::postBuild() childSetCommitCallback("import_scale", onImportScaleCommit, this); childSetCommitCallback("pelvis_offset", onPelvisOffsetCommit, this); + // Mesh uploader pivot + for (const char* axis : { "pivot_x", "pivot_y", "pivot_z" }) + { + getChild(axis)->setCommitCallback(boost::bind(&LLFloaterModelPreview::onPivotCommit, this)); + } + childSetValue("pivot_units", "percent"); + getChild("pivot_units")->setCommitCallback(boost::bind(&LLFloaterModelPreview::onPivotUnitsCommit, this)); + childSetValue("pivot_mode", "file"); + getChild("pivot_mode")->setCommitCallback(boost::bind(&LLFloaterModelPreview::onPivotModeCommit, this)); + // getChild("description_form")->setKeystrokeCallback(boost::bind(&LLFloaterModelPreview::onDescriptionKeystroke, this, _1), NULL); @@ -200,6 +212,7 @@ bool LLFloaterModelPreview::postBuild() getChild("show_skin_weight")->setCommitCallback(boost::bind(&LLFloaterModelPreview::onShowSkinWeightChecked, this, _1)); getChild("show_joint_overrides")->setCommitCallback(boost::bind(&LLFloaterModelPreview::onViewOptionChecked, this, _1)); getChild("show_joint_positions")->setCommitCallback(boost::bind(&LLFloaterModelPreview::onViewOptionChecked, this, _1)); + getChild("show_origin")->setCommitCallback(boost::bind(&LLFloaterModelPreview::onViewOptionChecked, this, _1)); // Mesh uploader pivot childDisable("upload_skin"); childDisable("upload_joints"); @@ -495,6 +508,7 @@ void LLFloaterModelPreview::onClickCalculateBtn() clearLogTab(); addStringToLog("Calculating model data.", false); mModelPreview->rebuildUploadData(); + mModelPreview->updateStatusMessages(); // Mesh uploader pivot: whether a pivot is allowed follows the rebuilt data bool upload_skinweights = childGetValue("upload_skin").asBoolean(); bool upload_joint_positions = childGetValue("upload_joints").asBoolean(); @@ -510,7 +524,8 @@ void LLFloaterModelPreview::onClickCalculateBtn() childGetValue("upload_textures").asBoolean(), upload_skinweights, upload_joint_positions, lock_scale_if_joint_position, mUploadModelUrl, mDestinationFolderId, false, - getWholeModelFeeObserverHandle()); + getWholeModelFeeObserverHandle(), + LLHandle(), mModelPreview->getUploadPositionOffsets()); // Mesh uploader pivot toggleCalculateButton(false); mUploadBtn->setEnabled(false); @@ -675,6 +690,120 @@ void LLFloaterModelPreview::onImportScaleCommit(LLUICtrl*,void* userdata) fp->mModelPreview->refresh(); } + +// Mesh uploader pivot +static const char* const PIVOT_AXES[3] = { "pivot_x", "pivot_y", "pivot_z" }; + +LLVector3 LLFloaterModelPreview::getPivotHalfExtents() const +{ + if (!mModelPreview) + { + return LLVector3::zero; + } + if (mModelPreview->isSingleModel()) + { + // The pivot moves along the mesh's own axes, so a rotated instance measures along them. + // The transform carries the import scale it was built with; a scale changed since is applied here. + const LLMatrix4& mat = mModelPreview->mUploadData[0].mTransform; + const F32 built = mModelPreview->mPivotScale; + const F32 rescale = built > 0.f ? (F32)childGetValue("import_scale").asReal() / built : 1.f; + LLVector3 half; + for (S32 i = 0; i < 3; ++i) + { + half.mV[i] = LLVector3(mat.mMatrix[i][0], mat.mMatrix[i][1], mat.mMatrix[i][2]).length() * 0.5f * rescale; + } + return half; + } + return mModelPreview->mPreviewScale * (F32)childGetValue("import_scale").asReal(); +} + +void LLFloaterModelPreview::onPivotCommit() +{ + if (!mModelPreview) + { + return; + } + + const bool metres = childGetValue("pivot_units").asString() == "metres"; + const LLVector3 half = getPivotHalfExtents(); + LLVector3 pivot; + for (S32 i = 0; i < 3; ++i) + { + const F32 shown = (F32)childGetValue(PIVOT_AXES[i]).asReal(); + if (half.mV[i] <= 0.f) + { + pivot.mV[i] = 0.f; // a flat axis has nowhere to put it + } + else + { + pivot.mV[i] = metres ? shown / half.mV[i] : shown / 100.f; + } + pivot.mV[i] = llclamp(pivot.mV[i], -1.f, 1.f); + } + + mModelPreview->setCustomPivot(pivot); + mModelPreview->updateStatusMessages(); + toggleCalculateButton(true); + mModelPreview->refresh(); +} + +void LLFloaterModelPreview::onPivotUnitsCommit() +{ + updatePivotControls(); +} + +void LLFloaterModelPreview::onPivotModeCommit() +{ + if (!mModelPreview) + { + return; + } + const std::string mode = childGetValue("pivot_mode").asString(); + mModelPreview->setPivotMode(mode == "custom" ? LLModelPreview::PIVOT_CUSTOM + : mode == "centre" ? LLModelPreview::PIVOT_CENTRE + : LLModelPreview::PIVOT_FILE); + updatePivotControls(); + mModelPreview->updateStatusMessages(); + toggleCalculateButton(true); + mModelPreview->refresh(); +} + +void LLFloaterModelPreview::updatePivotControls() +{ + if (!mModelPreview) + { + return; + } + + // Custom is for one model: several each take their own origin from the file, or the centre. + const bool single = mModelPreview->isSingleModel(); + if (LLRadioGroup* modes = getChild("pivot_mode")) + { + if (!single && mModelPreview->getPivotMode() == LLModelPreview::PIVOT_CUSTOM) + { + mModelPreview->setPivotMode(LLModelPreview::PIVOT_FILE); + } + modes->setIndexEnabled(2, single); + const LLModelPreview::EPivotMode mode = mModelPreview->getPivotMode(); + modes->setValue(mode == LLModelPreview::PIVOT_CUSTOM ? "custom" : mode == LLModelPreview::PIVOT_CENTRE ? "centre" : "file"); + } + + const bool metres = childGetValue("pivot_units").asString() == "metres"; + const LLVector3 half = getPivotHalfExtents(); + const LLVector3 pivot = single ? mModelPreview->getWantedPivot(mModelPreview->mUploadData[0].mModel) : LLVector3::zero; + for (S32 i = 0; i < 3; ++i) + { + LLSpinCtrl* spinner = getChild(PIVOT_AXES[i]); + const F32 range = metres ? half.mV[i] : 100.f; + spinner->setMinValue(-range); + spinner->setMaxValue(range); + spinner->setIncrement(metres ? 0.01f : 50.f); + spinner->setPrecision(metres ? 3 : 1); + spinner->setValue(pivot.mV[i] * range); + } +} +// + //static void LLFloaterModelPreview::onPelvisOffsetCommit( LLUICtrl*, void* userdata ) { @@ -1722,6 +1851,7 @@ void LLFloaterModelPreview::onUpload(void* user_data) mp->mUploadBtn->setEnabled(false); mp->mModelPreview->rebuildUploadData(); + mp->mModelPreview->updateStatusMessages(); // Mesh uploader pivot: whether a pivot is allowed follows the rebuilt data bool upload_skinweights = mp->childGetValue("upload_skin").asBoolean(); bool upload_joint_positions = mp->childGetValue("upload_joints").asBoolean(); @@ -1739,7 +1869,8 @@ void LLFloaterModelPreview::onUpload(void* user_data) mp->childGetValue("upload_textures").asBoolean(), upload_skinweights, upload_joint_positions, lock_scale_if_joint_position, mp->mUploadModelUrl, mp->mDestinationFolderId, - true, LLHandle(), mp->getWholeModelUploadObserverHandle()); + true, LLHandle(), mp->getWholeModelUploadObserverHandle(), + mp->mModelPreview->getUploadPositionOffsets()); // Mesh uploader pivot } @@ -1873,6 +2004,15 @@ void LLFloaterModelPreview::resetDisplayOptions() void LLFloaterModelPreview::resetUploadOptions() { childSetValue("import_scale", 1); + // Mesh uploader pivot + childSetValue("pivot_units", "percent"); + if (mModelPreview) + { + mModelPreview->setPivotMode(LLModelPreview::PIVOT_FILE); + mModelPreview->setCustomPivot(LLVector3::zero); + } + updatePivotControls(); + // childSetValue("pelvis_offset", 0); childSetValue("physics_explode", 0); childSetValue("physics_file", ""); diff --git a/indra/newview/llfloatermodelpreview.h b/indra/newview/llfloatermodelpreview.h index 39df7448f0..347d215a9e 100644 --- a/indra/newview/llfloatermodelpreview.h +++ b/indra/newview/llfloatermodelpreview.h @@ -144,6 +144,13 @@ class LLFloaterModelPreview final : public LLFloaterModelUploadBase void onDescriptionKeystroke(LLUICtrl*); static void onImportScaleCommit(LLUICtrl*, void*); + // Mesh uploader pivot: shown in percent of the half-extent or in metres from the centre + void onPivotCommit(); + void onPivotUnitsCommit(); + void onPivotModeCommit(); + void updatePivotControls(); + LLVector3 getPivotHalfExtents() const; + // static void onPelvisOffsetCommit(LLUICtrl*, void*); static void onPreviewLODCommit(LLUICtrl*,void*); diff --git a/indra/newview/llmeshrepository.cpp b/indra/newview/llmeshrepository.cpp index e2620c6fc5..7f81db1e7c 100644 --- a/indra/newview/llmeshrepository.cpp +++ b/indra/newview/llmeshrepository.cpp @@ -2699,7 +2699,8 @@ LLMeshUploadThread::LLMeshUploadThread(LLMeshUploadThread::instance_list_t& data bool upload_skin, bool upload_joints, bool lock_scale_if_joint_position, const std::string & upload_url, LLUUID destination_folder_id, bool do_upload, LLHandle fee_observer, - LLHandle upload_observer) + LLHandle upload_observer, + const position_offsets_t& position_offsets) // Mesh uploader pivot : LLThread("mesh upload"), LLCore::HttpHandler(), mDiscarded(false), @@ -2715,6 +2716,7 @@ LLMeshUploadThread::LLMeshUploadThread(LLMeshUploadThread::instance_list_t& data mUploadSkin = upload_skin; mUploadJoints = upload_joints; mLockScaleIfJointPosition = lock_scale_if_joint_position; + mPositionOffsets = position_offsets; // Mesh uploader pivot mMutex = new LLMutex(); mPendingUploads = 0; mFinished = false; @@ -2825,6 +2827,14 @@ LLSD llsd_from_file(std::string filename) return result; } +// Mesh uploader pivot +LLVector3 LLMeshUploadThread::getPositionOffset(const LLModel* model) const +{ + auto it = mPositionOffsets.find(model); + return it != mPositionOffsets.end() ? it->second : LLVector3::zero; +} +// + void LLMeshUploadThread::packModelIntance( LLModel* model, LLMeshUploadThread::instance_list_t& instance_list, @@ -2879,7 +2889,8 @@ void LLMeshUploadThread::packModelIntance( mLockScaleIfJointPosition, LLModel::WRITE_BINARY, false, - data.mBaseModel->mSubmodelID); + data.mBaseModel->mSubmodelID, + getPositionOffset(data.mBaseModel)); // Mesh uploader pivot data.mAssetData = ostr.str(); std::string str = ostr.str(); @@ -2907,6 +2918,9 @@ void LLMeshUploadThread::packModelIntance( LLQuaternion rot; LLMatrix4 transformation = instance.mTransform; decomposeMeshMatrix(transformation, pos, rot, scale); + // Mesh uploader pivot: the prim stands on its pivot, so the mesh stays where it was placed + pos = (-getPositionOffset(data.mBaseModel)) * transformation + mOrigin; + // instance_entry["position"] = ll_sd_from_vector3(pos); instance_entry["rotation"] = ll_sd_from_quaternion(rot); instance_entry["scale"] = ll_sd_from_vector3(scale); @@ -5556,11 +5570,13 @@ void LLMeshRepository::uploadModel(std::vector& data, const std LLVector3& scale, bool upload_textures, bool upload_skin, bool upload_joints, bool lock_scale_if_joint_position, std::string upload_url, const LLUUID& destination_folder_id, bool do_upload, - LLHandle fee_observer, LLHandle upload_observer) + LLHandle fee_observer, LLHandle upload_observer, + const LLMeshUploadThread::position_offsets_t& position_offsets) // Mesh uploader pivot { LLMeshUploadThread* thread = new LLMeshUploadThread(data, lod_sources, scale, upload_textures, upload_skin, upload_joints, lock_scale_if_joint_position, - upload_url, destination_folder_id, do_upload, fee_observer, upload_observer); + upload_url, destination_folder_id, do_upload, fee_observer, upload_observer, + position_offsets); // Mesh uploader pivot mUploadWaitList.push_back(thread); } diff --git a/indra/newview/llmeshrepository.h b/indra/newview/llmeshrepository.h index 5060e55d05..5e5ebc7993 100644 --- a/indra/newview/llmeshrepository.h +++ b/indra/newview/llmeshrepository.h @@ -801,6 +801,11 @@ class LLMeshUploadThread : public LLThread, public LLCore::HttpHandler bool mUploadSkin; bool mUploadJoints; bool mLockScaleIfJointPosition; + // Mesh uploader pivot: added to each base model's stored domains + typedef boost::unordered_map position_offsets_t; + position_offsets_t mPositionOffsets; + LLVector3 getPositionOffset(const LLModel* model) const; + // volatile bool mDiscarded; LLHost mHost; @@ -815,7 +820,8 @@ class LLMeshUploadThread : public LLThread, public LLCore::HttpHandler const LLUUID destination_folder_id = LLUUID::null, bool do_upload = true, LLHandle fee_observer = (LLHandle()), - LLHandle upload_observer = (LLHandle())); + LLHandle upload_observer = (LLHandle()), + const position_offsets_t& position_offsets = position_offsets_t()); // Mesh uploader pivot ~LLMeshUploadThread(); bool finished() const { return mFinished; } @@ -1008,7 +1014,8 @@ class LLMeshRepository const LLUUID& destination_folder_id = LLUUID::null, bool do_upload = true, LLHandle fee_observer= (LLHandle()), - LLHandle upload_observer = (LLHandle())); + LLHandle upload_observer = (LLHandle()), + const LLMeshUploadThread::position_offsets_t& position_offsets = LLMeshUploadThread::position_offsets_t()); // Mesh uploader pivot S32 getMeshSize(const LLUUID& mesh_id, S32 lod) const; diff --git a/indra/newview/llmodelpreview.cpp b/indra/newview/llmodelpreview.cpp index 374821d3e4..242bd99e84 100644 --- a/indra/newview/llmodelpreview.cpp +++ b/indra/newview/llmodelpreview.cpp @@ -279,6 +279,13 @@ void LLModelPreview::updateDimentionsAndOffsets() mDetailsSignal((F32)(mPreviewScale[0] * scale), (F32)(mPreviewScale[1] * scale), (F32)(mPreviewScale[2] * scale)); + // Mesh uploader pivot: metres follow the model's size + if (LLFloaterModelPreview::sInstance && LLFloaterModelPreview::sInstance->mModelPreview == this) + { + LLFloaterModelPreview::sInstance->updatePivotControls(); + } + // + updateStatusMessages(); } @@ -297,6 +304,7 @@ void LLModelPreview::rebuildUploadData() LLSpinCtrl* scale_spinner = mFMP->getChild("import_scale"); F32 scale = (F32)scale_spinner->getValue().asReal(); + mPivotScale = scale; // Mesh uploader pivot: the scale the transforms below carry LLMatrix4 scale_mat; scale_mat.initScale(LLVector3(scale, scale, scale)); @@ -1839,6 +1847,83 @@ void LLModelPreview::genMeshOptimizerLODs(S32 which_lod, S32 meshopt_mode, U32 d << ", failures: " << meshes_fail_count << LL_ENDL; } +// Mesh uploader pivot +void LLModelPreview::setPivotMode(EPivotMode mode) +{ + if (mode == PIVOT_CUSTOM && mPivotMode != PIVOT_CUSTOM && isSingleModel()) + { + mCustomPivot = getWantedPivot(mUploadData[0].mModel); + } + mPivotMode = mode; +} + +// static +LLVector3 LLModelPreview::getFileOriginPivot(const LLModel* model) +{ + if (!model) + { + return LLVector3::zero; + } + // The loader centred the model on its box; the file's origin is where it moved the centre from. + LLVector3 size, centre; + model->getNormalizedScaleTranslation(size, centre); + LLVector3 pivot; + for (S32 i = 0; i < 3; ++i) + { + pivot.mV[i] = size.mV[i] > 0.f ? -2.f * centre.mV[i] / size.mV[i] : 0.f; + // An origin outside the box is a scene's, from a file that placed its vertices in the scene: centre it. + if (fabsf(pivot.mV[i]) > 1.001f) + { + return LLVector3::zero; + } + pivot.mV[i] = llclamp(pivot.mV[i], -1.f, 1.f); + } + return pivot; +} + +LLVector3 LLModelPreview::getWantedPivot(const LLModel* model) const +{ + switch (mPivotMode) + { + case PIVOT_FILE: + return getFileOriginPivot(model); + case PIVOT_CUSTOM: + return isSingleModel() ? mCustomPivot : LLVector3::zero; + default: + return LLVector3::zero; + } +} + +bool LLModelPreview::uploadsRigged(const LLModelInstance& instance) const +{ + // The upload takes its skin from the High model, which may carry weights its base does not. + if (!mFMP || !mFMP->childGetValue("upload_skin").asBoolean()) + { + return false; + } + const LLModel* high = instance.mLOD[LLModel::LOD_HIGH]; + return (instance.mModel && !instance.mModel->mSkinWeights.empty()) || (high && !high->mSkinWeights.empty()); +} + +bool LLModelPreview::isSingleModel() const +{ + // A model with more than eight materials uploads as a root and its submodels, sharing one transform. + return std::count_if(mUploadData.begin(), mUploadData.end(), + [](const LLModelInstance& instance) { return instance.mModel && !instance.mModel->mSubmodelID; }) == 1; +} + +LLMeshUploadThread::position_offsets_t LLModelPreview::getUploadPositionOffsets() const +{ + LLMeshUploadThread::position_offsets_t offsets; + for (const LLModelInstance& instance : mUploadData) + { + // Rigged mesh is placed by its skeleton and keeps the default position domain, whatever the pivot. + offsets[instance.mModel.get()] = uploadsRigged(instance) ? LLVector3::zero : getUploadPivot(instance.mModel) * -0.5f; + } + return offsets; +} +// + void LLModelPreview::updateStatusMessages() { // bit mask values for physics errors. used to prevent overwrite of single line status @@ -2182,6 +2267,77 @@ void LLModelPreview::updateStatusMessages() } } + // Mesh uploader pivot: the sim keeps analyzed hulls where the pivot puts them but fits + // any other physics shape to the box, so a pivot needs analyzed physics or none at all. + { + // Each part is judged on its own, so one rigged or unanalyzed part holds only itself at the centre. + bool any_rigged = false; + bool any_unanalyzed = false; + bool pivot_without_physics = false; // a pivoted part that collides with a default hull centred on itself + mPivotHeld.clear(); + for (const LLModelInstance& instance : mUploadData) + { + const LLModel* model = instance.mModel; + if (!model) + { + continue; + } + // Skin weights left out of the upload make a static mesh, which can take a pivot. + const bool rigged = uploadsRigged(instance); + // The hulls uploaded are the physics model's, or the base model's without one. + const LLModel* physics = instance.mLOD[LLModel::LOD_PHYSICS]; + bool instance_physics = false; + bool unanalyzed = false; + if (physics && physics->getNumVolumeFaces() > 0) + { + instance_physics = true; + unanalyzed = physics->mPhysics.mHull.empty(); + } + else if (!physics && !model->mPhysics.mHull.empty()) + { + instance_physics = true; + } + if (rigged || unanalyzed) + { + mPivotHeld.insert(model); + } + any_rigged |= rigged; + any_unanalyzed |= unanalyzed && !rigged; + pivot_without_physics |= !rigged && !instance_physics && !getWantedPivot(model).isExactlyZero(); + } + + // Some instance must be free of the hold: several instances of one held model are all held. + mUploadPivotAllowed = std::any_of(mUploadData.begin(), mUploadData.end(), [this](const LLModelInstance& instance) + { return instance.mModel && !mPivotHeld.count(instance.mModel); }); + // Every reason that applies is shown, since each concerns different parts. + std::string pivot_status; + auto add_status = [&](const char* name) + { + pivot_status += (pivot_status.empty() ? "" : " ") + mFMP->getString(name); + }; + if (any_rigged) + { + add_status("pivot_status_rigged"); + } + if (any_unanalyzed) + { + add_status("pivot_status_analyze"); + } + if (pivot_without_physics) + { + add_status("pivot_status_no_physics"); + } + + const bool custom = mFMP->childGetValue("pivot_mode").asString() == "custom"; + for (const char* axis : { "pivot_x", "pivot_y", "pivot_z" }) + { + mFMP->childSetEnabled(axis, mUploadPivotAllowed && custom); + } + mFMP->childSetEnabled("pivot_mode", mUploadPivotAllowed); + mFMP->childSetValue("pivot_status", pivot_status); + } + // + if (!mModelNoErrors || mHasDegenerate) { mFMP->childDisable("ok_btn"); @@ -3641,6 +3797,82 @@ bool LLModelPreview::render() } } + // Mesh uploader pivot: the object's origin, drawn through the model + if (mViewOption["show_origin"] && !mViewOption["show_skin_weight"]) // rigged mesh is drawn on the avatar + { + LLGLSLShader* shader = LLGLSLShader::sCurBoundShaderPtr; + gUIProgram.bind(); // takes the vertex colours, which gDebugProgram replaces with one uniform + gGL.getTextureSlot(0)->unbind(); + LLGLDepthTest gls_depth(GL_FALSE); + LLGLDisable no_cull(GL_CULL_FACE); + gGL.setLineWidth(2.f); + + for (const LLModelInstance& instance : mUploadData) + { + const LLMatrix4& mat = instance.mTransform; + const LLVector3 origin = (getUploadPivot(instance.mModel) * 0.5f) * mat; + + LLVector3 axes[3]; + F32 reach = 0.f; + for (S32 i = 0; i < 3; ++i) + { + axes[i].set(mat.mMatrix[i][0], mat.mMatrix[i][1], mat.mMatrix[i][2]); + reach = llmax(reach, axes[i].normalize()); + } + reach *= 0.6f; + + // Arrows as the translate gizmo draws them: a shaft and a solid cone per axis. + static const LLColor4 colors[3] = { LLColor4::red, LLColor4::green, LLColor4::blue }; + const F32 head_length = reach * 0.2f; + const F32 head_radius = reach * 0.06f; + constexpr S32 HEAD_SEGMENTS = 12; + + gGL.begin(LLRender::LINES); + for (S32 i = 0; i < 3; ++i) + { + gGL.color4fv(colors[i].mV); + gGL.vertex3fv(origin.mV); + gGL.vertex3fv((origin + axes[i] * (reach - head_length)).mV); + } + gGL.end(); + + gGL.begin(LLRender::TRIANGLES); + for (S32 i = 0; i < 3; ++i) + { + const LLVector3 tip = origin + axes[i] * reach; + const LLVector3 base = origin + axes[i] * (reach - head_length); + const LLVector3 u = axes[(i + 1) % 3] * head_radius; + const LLVector3 v = axes[(i + 2) % 3] * head_radius; + for (S32 s = 0; s < HEAD_SEGMENTS; ++s) + { + const F32 a0 = F_TWO_PI * s / HEAD_SEGMENTS; + const F32 a1 = F_TWO_PI * (s + 1) / HEAD_SEGMENTS; + const LLVector3 r0 = base + u * cosf(a0) + v * sinf(a0); + const LLVector3 r1 = base + u * cosf(a1) + v * sinf(a1); + // Shade the side by its angle so the cone reads as solid. + const F32 shade = 0.65f + 0.35f * cosf(a0); + gGL.color4f(colors[i].mV[VRED] * shade, colors[i].mV[VGREEN] * shade, colors[i].mV[VBLUE] * shade, 1.f); + gGL.vertex3fv(tip.mV); + gGL.vertex3fv(r0.mV); + gGL.vertex3fv(r1.mV); + gGL.color4f(colors[i].mV[VRED] * 0.5f, colors[i].mV[VGREEN] * 0.5f, colors[i].mV[VBLUE] * 0.5f, 1.f); + gGL.vertex3fv(base.mV); + gGL.vertex3fv(r1.mV); + gGL.vertex3fv(r0.mV); + } + } + gGL.end(); + } + gGL.flush(); + gGL.setLineWidth(1.f); + + if (shader) + { + shader->bind(); + } + } + // + gObjectPreviewProgram.unbind(); gGL.popMatrix(); diff --git a/indra/newview/llmodelpreview.h b/indra/newview/llmodelpreview.h index b62030a349..10a465845b 100644 --- a/indra/newview/llmodelpreview.h +++ b/indra/newview/llmodelpreview.h @@ -172,6 +172,25 @@ class LLModelPreview : public LLViewerDynamicTexture, public LLMutex, public LLH void setHasPivot(bool val) { mHasPivot = val; } void setModelPivot(const LLVector3& pivot) { mModelPivot = pivot; } + // Mesh uploader pivot: each model's origin within its bounding box, -1 to 1 per axis + enum EPivotMode { PIVOT_FILE, PIVOT_CENTRE, PIVOT_CUSTOM }; + // Custom starts from the pivot the last mode gave a single model. + void setPivotMode(EPivotMode mode); + EPivotMode getPivotMode() const { return mPivotMode; } + void setCustomPivot(const LLVector3& pivot) { mCustomPivot = pivot; } + // The origin the file gave the model, where its vertices' origin sits in its box. + static LLVector3 getFileOriginPivot(const LLModel* model); + // The pivot the mode asks for, and the one in effect: centred while the physics or rigging rules it out. + LLVector3 getWantedPivot(const LLModel* model) const; + LLVector3 getUploadPivot(const LLModel* model) const { return mPivotHeld.count(model) ? LLVector3::zero : getWantedPivot(model); } + // What each model's stored domains are shifted by so that its origin lands on its pivot. + LLMeshUploadThread::position_offsets_t getUploadPositionOffsets() const; + // One model to upload, counting a root and its submodels once: what Custom and metres are for. + bool isSingleModel() const; + // Whether this instance uploads skinned, from its base or its High model: such mesh takes no pivot. + bool uploadsRigged(const LLModelInstance& instance) const; + // + //Is a rig valid so that it can be used as a criteria for allowing for uploading of joint positions //Accessors for joint position upload friendly rigs const bool isRigValidForJointPositionUpload(void) const { return mRigValidJointUpload; } @@ -271,6 +290,13 @@ class LLModelPreview : public LLViewerDynamicTexture, public LLMutex, public LLH bool mResetJoints; bool mModelNoErrors; bool mLookUpLodFiles; + // Mesh uploader pivot + EPivotMode mPivotMode{PIVOT_FILE}; + LLVector3 mCustomPivot; + bool mUploadPivotAllowed{true}; // some part may take a pivot + std::set mPivotHeld; // parts held at the centre: rigged, or with physics not yet analyzed + F32 mPivotScale{1.f}; // the import scale the upload data was last built with + // std::map mViewOption; diff --git a/indra/newview/skins/default/xui/en/floater_model_preview.xml b/indra/newview/skins/default/xui/en/floater_model_preview.xml index dee158eaa9..ac7527fdb2 100644 --- a/indra/newview/skins/default/xui/en/floater_model_preview.xml +++ b/indra/newview/skins/default/xui/en/floater_model_preview.xml @@ -35,6 +35,9 @@ LOD materials are not a subset of reference model. Some physical hulls exceed vertex limitations. The physics mesh is too dense or contains degenerate triangles. Use Analyze/Simplify to resolve. + A pivot is for unrigged mesh; rigged mesh is placed by its skeleton. + Analyze the physics to set a pivot: unanalyzed physics stays centred on the object. + No physics shape is defined, so the object collides with its default hull, centred on itself. Fine for a phantom piece or physics type None. All Analyzing... Simplifying... @@ -1365,6 +1368,119 @@ label_text.text_color="White" left="20" top_pad="20"/> + + + Pivot: + + + + + + + + + + + + + + + + height="340"/> @@ -1934,8 +2048,18 @@ Analysed: label_text.text_color="White" layout="topleft" name="show_joint_positions" - top_pad="21"> + top_pad="10"> + + + +