포즈에 대해
업데이트: 2023/01/26
포즈 기능이란?
포즈란 복수의 동종 파츠 중 단일 파츠만을 모션에 근거해 전환을 페이드 표시하는 기능입니다.
복수의 동종 파츠란 오른손 A와 오른손 B 등 동시에 표시되면 표시적으로 모순을 일으키는 것을 말합니다.
모션 내의 파츠 불투명도의 조작을 최종적으로 파츠에 적용하는 처리 부분이기도 합니다.
이 페이지에 대한 이해를 위해 「모델 정보」의 「CubismModel 클래스에서 모델에 존재하지 않는 파라미터 ID 사용」을 미리 읽는 것이 좋습니다.
모션에서의 파츠 불투명도 조작 흐름
모션 재생에서의 파츠 불투명도 조작
파츠 불투명도의 정합성을 유지하기 위해 Original WorkFlow의 Framework에서의 단일 모션 재생에서는 직접 파츠 불투명도에 대해 조작을 실시하지 않습니다.
모션 재생 파츠의 불투명도 조작은 대신 파츠와 동일한 ID 파라미터에 대한 덮어쓰기로 대체됩니다.
이때 모델에 존재하지 않는 파라미터 ID는 가상 파라미터로서 값의 유지만 이루어집니다.
모션 전환에 수반되는 페이드 처리 등은 실행되지 않고, 단지 덮어쓰기만 이루어집니다.
OW SDK에서 모션에 파츠 불투명도 조작을 보간하는 방법은 스텝이 적용되는 것을 상정하고 있습니다.
// C++ void CubismMotion::DoUpdateParameters(CubismModel* model, csmFloat32 timeSeconds, csmFloat32 fadeWeight, CubismMotionQueueEntry* motionQueueEntry) { /*생략*/ for (; c < _motionData->CurveCount && curves[c].Type == CubismMotionCurveTarget_PartOpacity; ++c) { // Find parameter index. parameterIndex = model->GetParameterIndex(curves[c].Id); // Skip curve evaluation if no value in sink. if (parameterIndex == -1) { continue; } // Evaluate curve and apply value. value = EvaluateCurve(_motionData, c, time); model->SetParameterValue(parameterIndex, value);// 파츠 불투명도가 아닌 가상 파라미터에 쓴다. } /*생략*/ }
// TypeScript public doUpdateParameters(model: CubismModel, userTimeSeconds: number, fadeWeight: number, motionQueueEntry: CubismMotionQueueEntry): void { /*생략*/ for(; c < this._motionData.curveCount && curves.at(c).type == CubismMotionCurveTarget.CubismMotionCurveTarget_PartOpacity; ++c) { // Find parameter index. parameterIndex = model.getParameterIndex(curves.at(c).id); // Skip curve evaluation if no value in sink. if(parameterIndex == -1) { continue; } // Evaluate curve and apply value. value = evaluateCurve(this._motionData, c, time); model.setParameterValueByIndex(parameterIndex, value); } /*생략*/ }
// Java public void doUpdateParameters(CubismModel model, float timeSeconds, float fadeWeight, CubismMotionQueueEntry motionQueueEntry){ /*생략*/ for (CubismMotionCurve curve : curves) { if (curve.type ! = CubismMotionCurveTarget.PARAMETER) { continue; } // Findparameter index. final int parameterIndex = model.getParameterIndex(curve.id); // Skip curve evaluation if no value. if (parameterIndex == -1) { continue; } // Evaluate curve and apply value. value = evaluateCurve(curve, time); ... model.setParameterValue(parameterIndex, v); // 파츠 불투명도가 아닌 가상 파라미터에 쓴다. } /*생략*/ }
포즈 적용
모델 업데이트 프로세스의 마지막 단계에서 Pose 기능을 적용하여 .pose3.json에 기술된 정보를 바탕으로 그룹별로 파츠를 표시할지, 가상 파라미터의 값을 참조해 결정해 갑니다.
Pose를 모델에 적용하는 API는 다음과 같습니다.
// C++ CubismPose::UpdateParameters()
// TypeScript CubismPose.updateParameters()
// Java CubismPose.updateParameters();
파츠 조작 곡선은 스텝을 권장하지만 리니어 등을 선택하면, 0.001보다 커진 시점에 표시 상태로 인식합니다.
표시 파츠를 결정할 때 조작 전의 파츠 불투명도에서 차분 시간과의 비례 계산으로 새로운 불투명도가 리니어 보간으로 결정됩니다.
// C++ void CubismPose::DoFade(CubismModel* model, csmFloat32 deltaTimeSeconds, csmInt32 beginIndex, csmInt32 partGroupCount) { csmInt32 visiblePartIndex = -1; csmFloat32 newOpacity = 1.0f; const csmFloat32 Phi = 0.5f; const csmFloat32 BackOpacityThreshold = 0.15f; // 현재 표시 상태가 되어 있는 파츠를 취득 for (csmInt32 i = beginIndex; i < beginIndex + partGroupCount; ++i) { csmInt32 partIndex = _partGroups[i].PartIndex; csmInt32 paramIndex = _partGroups[i].ParameterIndex; if (model->GetParameterValue(paramIndex) > Epsilon) // const csmFloat32 Epsilon= 0.001f; { if (visiblePartIndex >= 0) { break; } visiblePartIndex = i; newOpacity = model->GetPartOpacity(partIndex); // 새로운 불투명도 계산 newOpacity += (deltaTimeSeconds / _fadeTimeSeconds); if (newOpacity > 1.0f) { newOpacity = 1.0f; } } } /*생략*/ }
// TypeScript public doFade(model: CubismModel, deltaTimeSeconds: number, beginIndex: number, partGroupCount: number): void { let visiblePartIndex: number = -1; let newOpacity: number = 1.0; const phi: number = 0.5; const backOpacityThreshold: number = 0.15; // 현재 표시 상태가 되어 있는 파츠를 취득 for(let i: number = beginIndex; i < beginIndex + partGroupCount; ++i) { const partIndex: number = this._partGroups.at(i).partIndex; const paramIndex: number = this._partGroups.at(i).parameterIndex; if(model.getParameterValueByIndex(paramIndex) > Epsilon) { if(visiblePartIndex >= 0) { break; } visiblePartIndex = i; newOpacity = model.getPartOpacityByIndex(partIndex); // 새로운 불투명도 계산 newOpacity += (deltaTimeSeconds / this._fadeTimeSeconds); if(newOpacity > 1.0) { newOpacity = 1.0; } } } /*생략*/ }
// Java public void doFade(CubismModel model, float deltaTimeSeconds, int beginIndex, int partGroupCount) { int visiblePartIndex = -1; float newOpacity = 1.0f; // 현재 표시 상태가 되어 있는 파츠를 취득 for (int i = beginIndex; i < beginIndex + partGroupCount; i++) { final int paramIndex = partGroups.get(i).parameterIndex; if (model.getParameterValue(paramIndex) > EPSILON) { if (visiblePartIndex >= 0) { break; } // 새로운 불투명도 계산 newOpacity = calculateOpacity(model, i, deltaTimeSeconds); visiblePartIndex = i; } } /*생략*/ }
표시 파츠와 그 새로운 불투명도가 결정된 후, 그룹 전체에 대해 불투명도의 덮어쓰기 처리가 실행됩니다.
숨겨진 파츠의 새로운 불투명도는 표시하는 파츠의 불투명도와의 관계상 배경이 비치지 않는 레벨에서 불투명도를 저하시켜 갑니다.
// C++ void CubismPose::DoFade(CubismModel* model, csmFloat32 deltaTimeSeconds, csmInt32 beginIndex, csmInt32 partGroupCount) { csmInt32 visiblePartIndex = -1; csmFloat32 newOpacity = 1.0f; const csmFloat32 Phi = 0.5f; const csmFloat32 BackOpacityThreshold = 0.15f; /*생략*/ if (visiblePartIndex < 0) { visiblePartIndex = 0; newOpacity = 1.0f; } // 표시 파츠, 비표시 파츠의 불투명도 설정 for (csmInt32 i = beginIndex; i < beginIndex + partGroupCount; ++i) { csmInt32 partsIndex = _partGroups[i].PartIndex; // 표시 파츠 설정 if (visiblePartIndex == i) { model->SetPartOpacity(partsIndex, newOpacity); // 먼저 설정 } // 비표시 파츠 설정 else { csmFloat32 opacity = model->GetPartOpacity(partsIndex); csmFloat32 a1; // 계산에 의해 구해지는 불투명도 if (newOpacity < Phi) { a1 = newOpacity * (Phi - 1) / Phi + 1.0f; // (0,1),(phi,phi)를 지나는 직선식 } else { a1 = (1 - newOpacity) * Phi / (1.0f - Phi); // (1,0),(phi,phi)를 지나는 직선식 } // 배경이 보이는 비율을 제한하는 경우 csmFloat32 backOpacity = (1.0f - a1) * (1.0f - newOpacity); if (backOpacity > BackOpacityThreshold) { a1 = 1.0f - BackOpacityThreshold / (1.0f - newOpacity); } if (opacity > a1) { opacity = a1; // 계산의 불투명도보다 크면(진하면) 불투명도를 높인다 } model->SetPartOpacity(partsIndex, opacity); } } }
// TypeScript public doFade(model: CubismModel, deltaTimeSeconds: number, beginIndex: number, partGroupCount: number): void { let visiblePartIndex: number = -1; let newOpacity: number = 1.0; const phi: number = 0.5; const backOpacityThreshold: number = 0.15; /*생략*/ if(visiblePartIndex < 0) { visiblePartIndex = 0; newOpacity = 1.0; } // 표시 파츠, 비표시 파츠의 불투명도 설정 for(let i: number = beginIndex; i < beginIndex + partGroupCount; ++i) { const partsIndex: number = this._partGroups.at(i).partIndex; // 표시 파츠 설정 if(visiblePartIndex == i) { model.setPartOpacityByIndex(partsIndex, newOpacity); // 먼저 설정 } // 비표시 파츠 설정 else { let opacity: number = model.getPartOpacityByIndex(partsIndex); let a1: number; // 계산에 의해 구해지는 불투명도 if(newOpacity < phi) { a1 = newOpacity * (phi - 1) / phi + 1.0; // (0,1),(phi,phi)를 지나는 직선식 } else { a1 = (1 - newOpacity) * phi / (1.0 - phi); // (1,0),(phi,phi)를 지나는 직선식 } // 배경이 보이는 비율을 제한하는 경우 const backOpacity: number = (1.0 - a1) * (1.0 - newOpacity); if(backOpacity > backOpacityThreshold) { a1 = 1.0 - backOpacityThreshold / (1.0 - newOpacity); } if(opacity > a1) { opacity = a1; // 계산의 불투명도보다 크면(진하면) 불투명도를 높인다 } model.setPartOpacityByIndex(partsIndex, opacity); } } }
// Java public void doFade(CubismModel model, float deltaTimeSeconds, int beginIndex, int partGroupCount) { int visiblePartIndex = -1; float newOpacity = 1.0f; /*생략*/ if (visiblePartIndex < 0) { visiblePartIndex = 0; newOpacity = 1.0f; } // 표시 파츠, 비표시 파츠의 불투명도 설정 for (int i = beginIndex; i < beginIndex + partGroupCount; i++) { final int partsIndex = partGroups.get(i).partIndex; // 표시 파츠 설정 if (visiblePartIndex == i) { model.setPartOpacity(partsIndex, newOpacity); } // 비표시 파츠 설정 else { final float opacity = model.getPartOpacity(partsIndex); final float result = calcNonDisplayedPartsOpacity(opacity, newOpacity); model.setPartOpacity(partsIndex, result); } } }
Pose 데이터 구조
Pose가 취급하는 정보는 파라미터, 파츠 정보에 대한 액세스를 고속으로 하기 위해 파츠 ID로 액세스했을 때의 파라미터, 파츠의 인덱스를 PartData 구조체로 통합하여 보유합니다.
또 연동하는 데이터는 Link로서 PartData의 하위 요소로서 보유합니다.
// C++ /** * @brief 파츠와 관련된 데이터 관리 * * 파츠와 관련된 다양한 데이터를 관리한다. */ struct PartData { /*생략*/ CubismIdHandle PartId; ///< 파츠 ID csmInt32 ParameterIndex; ///< 파라미터 인덱스 csmInt32 PartIndex; ///< 파츠 인덱스 csmVector<PartData> Link; ///< 연동하는 파라미터 };
// TypeScript /** * 파츠와 관련된 데이터 관리 */ export class PartData { /*생략*/ partId: CubismIdHandle; // 파츠 ID parameterIndex: number; // 파라미터 인덱스 partIndex: number; // 파츠 인덱스 link: csmVector<PartData>; // 연동하는 파라미터 }
// Java /** * 파츠와 관련된 다양한 데이터를 관리한다. */ public class PartData{ /*생략*/ CubismId partId; // 파츠 ID int parameterIndex; // 파라미터 인덱스 int partIndex; // 파츠 인덱스 List<PartData> link // 연동하는 파라미터 }
CubismPose는 그룹 정보를 1차원 PartData 배열과 각 그룹의 개수 정보 배열로 표현합니다.
// C++ class CubismPose { /*생략*/ csmVector<PartData> _partGroups; ///< 파츠 그룹 csmVector<csmInt32> _partGroupCounts; ///< 각 파츠 그룹의 개수 csmFloat32 _fadeTimeSeconds; ///< 페이드 시간[초] csmFloat32 _lastTimeSeconds; ///< 마지막으로 실행한 시간[초] CubismModel* _lastModel; ///< 마지막으로 조작한 모델 };
// TypeScript export class CubismPose { /*생략*/ _partGroups: csmVector<PartData>; // 파츠 그룹 _partGroupCounts: csmVector<number>; // 각 파츠 그룹의 개수 _fadeTimeSeconds: number; // 페이드 시간[초] _lastModel: CubismModel; // 마지막으로 조작한 모델 }
// Java public class CubismPose{ /*생략*/ List<PartData> partGroups; // 파츠 그룹 List<Integer> partGroupCounts; // 각 파츠 그룹의 개수 float fadeTimeSeconds; // 페이드 시간[초] float lastTimeSeconds; // 마지막으로 실행한 시간[초] CubismModel lastModel; // 마지막으로 조작한 모델 }
1차원 배열의 PartData는 각 그룹의 개수 정보를 바탕으로 배열상의 선두 위치와 요소 수를 DoFade에 전달하는 것으로 그룹으로서 처리가 이루어집니다.
// C++ void CubismPose::UpdateParameters(CubismModel* model, csmFloat32 deltaTimeSeconds) { // 이전 모델과 같지 않을 때는 초기화 필요 if (model ! = _lastModel) { // 파라미터 인덱스 초기화 Reset(model); } _lastModel = model; // 설정에서 시간을 변경하면 경과 시간이 마이너스가 될 수 있으므로 경과 시간 0으로 대응. if (deltaTimeSeconds < 0.0f) { deltaTimeSeconds = 0.0f; } csmInt32 beginIndex = 0; for (csmUint32 i = 0; i < _partGroupCounts.GetSize(); i++) { csmInt32 partGroupCount = _partGroupCounts[i]; DoFade(model, deltaTimeSeconds, beginIndex, partGroupCount); beginIndex += partGroupCount; } CopyPartOpacities(model); }
// TypeScript public updateParameters(model: CubismModel, deltaTimeSeconds: number): void { // 이전 모델과 같지 않은 경우 초기화 필요 if(model ! = this._lastModel) { // 파라미터 인덱스 초기화 this.reset(model); } this._lastModel = model; // 설정에서 시간을 변경하면 경과 시간이 마이너스가 될 수 있으므로 경과 시간 0으로 대응 if(deltaTimeSeconds < 0.0) { deltaTimeSeconds = 0.0; } let beginIndex: number = 0; for(let i = 0; i < this._partGroupCounts.getSize(); i++) { const partGroupCount: number = this._partGroupCounts.at(i); this.doFade(model, deltaTimeSeconds, beginIndex, partGroupCount); beginIndex += partGroupCount; } this.copyPartOpacities(model); }
// Java public void updateParameters(final CubismModel model, float deltaTimeSeconds) { // 이전 모델과 같지 않을 때는 초기화 필요 if (model ! = lastModel) { reset(model); } lastModel = model; // 설정에서 시간을 변경하면 경과 시간이 마이너스가 될 수 있으므로 경과 시간 0으로 대응. if (deltaTimeSeconds < 0.0f) { deltaTimeSeconds = 0.0f; } int beginIndex = 0; for (final int partGroupCount : partGroupCounts) { doFade(model, deltaTimeSeconds, beginIndex, partGroupCount); beginIndex += partGroupCount; } copyPartOpacities(model); }
Parent ID에 의한 불투명도 연동
Pose 적용 처리의 마지막에 호출되는 이하의 함수에 의해, Link에 지정한 파츠로 값을 전파합니다.
// C++ CubismPose::CopyPartOpacities()
// TypeScript CubismPose.copyPartOpacities()
// Java CubismPose.copyPartOpacities()
이 Link는 OWViewer에서 Parent ID를 표기한 PartID가 CubismIdHandle의 배열로 저장됩니다.
아래 그림의 경우 PartManteL001의 Link에는 PartArmLB001과 PartArmLC001의 CubismIdHandle이 저장됩니다.
Link라고 표기되어 있지만 부모자식 관계이며, 부모 파츠에 불투명도 조작이 없으면 연동이 되지 않는 점에 주의해 주세요.
Link에 표기된 자식끼리는 연동되지 않습니다.
// C++ void CubismPose::CopyPartOpacities(CubismModel* model) { for (csmUint32 groupIndex = 0; groupIndex < _partGroups.GetSize(); ++groupIndex) { PartData& partData = _partGroups[groupIndex]; if (partData.Link.GetSize() == 0) { continue; // 연동할 파라미터 없음 } csmInt32 partIndex = _partGroups[groupIndex].PartIndex; csmFloat32 opacity = model->GetPartOpacity(partIndex); for (csmUint32 linkIndex = 0; linkIndex < partData.Link.GetSize(); ++linkIndex) { PartData& linkPart = partData.Link[linkIndex]; csmInt32 linkPartIndex = linkPart.PartIndex; if (linkPartIndex < 0) { continue; } model->SetPartOpacity(linkPartIndex, opacity); } } }
// TypeScript public copyPartOpacities(model: CubismModel): void { for(let groupIndex: number = 0; groupIndex < this._partGroups.getSize(); ++groupIndex) { let partData: PartData = this._partGroups.at(groupIndex); if(partData.link.getSize() == 0) { continue; // 연동할 파라미터 없음 } const partIndex: number = this._partGroups.at(groupIndex).partIndex; const opacity: number = model.getPartOpacityByIndex(partIndex); for(let linkIndex: number = 0; linkIndex < partData.link.getSize(); ++linkIndex) { let linkPart: PartData = partData.link.at(linkIndex); const linkPartIndex: number = linkPart.partIndex; if(linkPartIndex < 0) { continue; } model.setPartOpacityByIndex(linkPartIndex, opacity); } } }
// Java private void copyPartOpacities(CubismModel model) { for (PartData partData : partGroups) { if (partData.linkedParameter == null) { continue; } final int partIndex = partData.partIndex; final float opacity = model.getPartOpacity(partIndex); for (PartData linkedPart : partData.linkedParameter) { final int linkedPartIndex = linkedPart.partIndex; if (linkedPartIndex < 0) { continue; } model.setPartOpacity(linkedPartIndex, opacity); } } }
인스턴스 생성(.pose3.json 파일 불러오기)
포즈는 .pose3.json 파일에 저장되며 CubismPose 클래스를 사용합니다.
생성에는 다음 함수를 사용합니다.
// C++ CubismPose::Create()
// TypeScript CubismPose.create()
// Java CubismPose.create()
구현 예
// C++ csmString path = _modelSetting->GetPoseFileName(); path = _modelHomeDir + path; buffer = CreateBuffer(path.GetRawString(), &size); CubismPose* pose = CubismPose::Create(buffer, size); DeleteBuffer(buffer, path.GetRawString());
// TypeScritp let path: string = _modelSetting.getPoseFileName(); path = _modelHomeDire + path; fetch(path).then( (response) => { return response.arrayBuffer(); } ).then( (arrayBuffer) => { let buffer: ArrayBuffer = arrayBuffer; let size: number = buffer.byteLength; let pose: CubismPose = CubismPose.create(buffer, size); deleteBuffer(buffer, path); } );
// Java String path = modelSetting.getPoseFileName(); path = modelHomeDir + path; buffer = CreateBuffer(path); CubismPose pose = CubismPose.create(buffer);
포즈 적용
포즈를 적용하려면 다음 함수를 사용합니다.
// C++ CubismPose::UpdateParameters()
// TypeScript CubismPose.updateParameters()
// Java CubismPose.updateParameters()
사전에 모션의 적용 등 가상적인 파라미터의 계산을 완료해 둘 필요가 있습니다.
// C++ void LAppModel::Update() { const csmFloat32 deltaTimeSeconds = LAppPal::GetDeltaTime(); _userTimeSeconds += deltaTimeSeconds; /*생략*/ //----------------------------------------------------------------- _model->LoadParameters(); // 마지막으로 저장된 상태를 로드 if (_motionManager->IsFinished()) { // 모션 재생이 없으면 대기 모션 중에서 랜덤 재생 StartRandomMotion(MotionGroupIdle, PriorityIdle); } else { motionUpdated = _motionManager->UpdateMotion(_model, deltaTimeSeconds); // 모션 업데이트 } _model->SaveParameters(); // 상태 저장 //----------------------------------------------------------------- /*생략*/ // 포즈 설정 if (_pose ! = NULL) { _pose->UpdateParameters(_model, deltaTimeSeconds); } _model->Update(); }
// TypeScript /** * 업데이트 */ public update(): void { if(this._state ! = LoadStep.CompleteSetup) return; const deltaTimeSeconds: number = LAppPal.getDeltaTime(); this._userTimeSeconds += deltaTimeSeconds; /*생략*/ //-------------------------------------------------------------------------- this._model.loadParameters(); // 마지막으로 저장된 상태를 로드 if(this._motionManager.isFinished()) { // 모션 재생이 없으면 대기 모션 중에서 랜덤 재생 this.startRandomMotion(LAppDefine.MotionGroupIdle, LAppDefine.PriorityIdle); } else { motionUpdated = this._motionManager.updateMotion(this._model, deltaTimeSeconds); // 모션 업데이트 } this._model.saveParameters(); // 상태 저장 //-------------------------------------------------------------------------- /*생략*/ // 포즈 설정 if(this._pose ! = null) { this._pose.updateParameters(this._model, deltaTimeSeconds); } this._model.update(); }
// Java public void update() { final float deltaTimeSeconds = LAppPal.getDeltaTime(); userTimeSeconds += deltaTimeSeconds; /*생략*/ // ------------------------------------------------- model.loadParameters(); // 마지막으로 저장된 상태를 로드 if (motionManager.isFinished()) { // 모션 재생이 없으면 대기 모션 중에서 랜덤 재생 startRandomMotion(LAppDefine.MotionGroup.IDLE.getId(), LAppDefine.Priority.IDLE.getPriority()); } else { isMotionUpdated = motionManager.updateMotion(model, deltaTimeSeconds); } model.saveParameters(); // 상태 저장 // ------------------------------------------------- /*생략*/ if (pose ! = null) { pose.updateParameters(model, deltaTimeSeconds); } model.update(); }
파기
모델이 해제되는 타이밍에 CubismPose 인스턴스도 파기해야 합니다.
// C++ CubismPose::Delete(pose);
// TypeScript CubismPose.delete(pose);
SDK for Java의 경우는 가비지 콜렉션에 해방을 맡기고 있으므로 파기할 필요는 없습니다.