파라미터 조작

업데이트: 2022/10/06

CubismIdHandle

Cubism에서 파라미터를 궁극적으로 식별하려면 파라미터의 배열인 Index를 얻어야 합니다.
ID로 특정하는 경우에는 파라미터의 문자열인 ID를 대조해 가게 됩니다.
Native 및 Web의 Framework에서는 대조 계산 비용을 줄이기 위해 CubismIdHandle이라는 유형을 준비했습니다.

Java에서는 유형을 새롭게 정의할 수 없기 때문에, 다른 대응을 실시하고 있습니다.

Native

CubismIdHandle형의 실태는 CubismId 클래스의 포인터형으로, 관리 클래스의 함수 CubismIdManager::GetId 함수에 의해 취득할 수 있습니다.
CubismIdManager::GetId 함수는 동일한 문자열이면 동일한 포인터를 반환하기 때문에,
CubismIdHandle끼리를 비교해 같은 포인터 주소라면 같은 문자열임을 보증할 수 있습니다.

CubismIdManager 인스턴스에는 static인 CubismFramework::GetIdManager 함수로 액세스할 수 있습니다.

// C++
CubismIdHandle idA = CubismFramework::GetIdManager()->GetId("ParamAngleX")

CubismIdHandle idB = CubismFramework::GetIdManager()->GetId("ParamAngleX")

CubismIdHandle idC = CubismFramework::GetIdManager()->GetId("ParamAngleY")

csmBool ab = (idA == idB); //true
csmBool bc = (idB == idC); //false

값의 조작 함수는 Index에 의한 액세스와 CubismIdHandle에 의한 액세스의 2가지 방법이 준비되어 있습니다.

Web

CubismIdHandle 유형의 실태는 CubismId 클래스의 오브젝트 유형이며 관리 클래스의 함수 CubismIdManager.getId 함수에 의해 얻을 수 있습니다.
CubismIdManager.getId 함수는 동일한 문자열이면 동일한 인스턴스를 반환하므로,
CubismIdHandle끼리를 비교해 같은 오브젝트라면 같은 문자열임을 보증할 수 있습니다.

CubismIdManager 인스턴스는 static인 CubismFramework.getIdManager 함수로 액세스할 수 있습니다.

값의 조작 함수는 Index에 의한 액세스와 CubismIdHandle에 의한 액세스의 2가지 방법이 준비되어 있습니다.

// TypeScript
let idA: CubismIdHandle = CubismFramework.getIdManager().getId("ParamAngleX");

let idB: CubismIdHandle = CubismFramework.getIdManager().getId("ParamAngleX");

let idC: CubismIdHandle = CubismFramework.getIdManager().getId("ParamAngleY");

let ab = idA.isEqual(idB); //true
let bc = idB.isEqual(idC); //false

Java

SDK for Java에서는 상기 2개와는 달리 유형을 정의할 수 없기 때문에, CubismIdHandle 유형은 존재하지 않습니다.
관리 클래스의 함수 CubismIdManager.getId 함수는 CubismId 유형을 반환합니다.

CubismId는 equals 함수를 오버라이드하고 있기 때문에, CubismId끼리 equals 함수로 비교하면 같은 문자열임을 보증할 수 있습니다.

CubismIdManager 인스턴스는 static인 CubismFramework.getIdManager 함수로 액세스할 수 있습니다.

// Java
CubismId idA = CubismFramework.getIdManager().getId("ParamAngleX");

CubismId idB = CubismFramework.getIdManager().getId("ParamAngleX");

CubismId idC = CubismFramework.getIdManager().getId("ParamAngleY");

boolean ab = idA.equals(idB); // true
boolean bc = idB.equals(idC); // false

값의 조작 함수는 Index에 의한 액세스와 CubismId에 의한 액세스의 2가지 방법이 준비되어 있습니다.

파라미터 설정

파라미터는 일반적으로 모션에서 재생되지만 직접 값을 지정할 수도 있습니다.
파라미터를 조작하는 함수는 이하와 같이 적용하는 값의 계산 방법별로 3종류가 있습니다.

1. 값 덮어쓰기

다음 중 하나의 함수를 사용합니다.

  • Native(C++)의 CubismModel::SetParameterValue 함수
  • Web(TypeScript)의 CubismModel.setParameterValueById 함수
  • Java의 CubismModel.setParameterValue 함수

제1인수에 파라미터의 ID 또는 인덱스, 제2인수에 값, 제3인수에 영향도를 설정합니다.
영향도는 생략 가능이며 그 경우 1이 됩니다.
예를 들면 0.5로 하면 이전의 값의 영향을 50% 남긴 채로 설정하게 됩니다.

// C++
_model->SetParameterValue(CubismFramework::GetIdManager()->GetId("ParamAngleX"), 30.0f, 1.0f);
// TypeScript
_model.setParameterValueById(CubismFramework.getIdManager().getId("ParamAngleX"), 30.0, 1.0);
// Java
_model.setParameterValue(CubismFramework.getIdManager().getId("ParamAngleX"), 30.0f, 1.0f);

2. 현재 값에 가산

다음 중 하나의 함수를 사용합니다.

  • Native(C++)의 CubismModel::AddParameterValue 함수
  • Web(TypeScript)의 CubismModel.addParameterValueById 함수
  • Java의 CubismModel.addParameterValue 함수

인수는 다음 중 하나의 함수와 동일합니다.

  • Native(C++)의 CubismModel::SetParameterValue 함수
  • Web(TypeScript)의 CubismModel.setParameterValueById 함수
  • Java의 CubismModel.setParameterValue 함수

이 함수로 설정한 값은 그 파라미터에 현재 설정되어 있는 값에 가산됩니다.

// C++
_model->AddParameterValue(CubismFramework::GetIdManager()->GetId("ParamAngleX"), 1.0f, 1.0f);
// TypeScript
_model.addParameterValueById(CubismFramework.getIdManager().getId("ParamAngleX"), 1.0, 1.0);
// Java
_model.addParameterValue(CubismFramework.getIdManager().getId("ParamAngleX"), 1.0f, 1.0f);

3. 현재 값에 곱하기

다음 중 하나의 함수를 사용합니다.

  • Native(C++)의 CubismModel::MultiplyParameterValue 함수
  • Web(TypeScript)의 CubismModel.multiplyParameterValueById 함수
  • Java의 CubismModel.multiplyParameterValue 함수

인수는 다음 중 하나의 함수와 동일합니다.

  • CubismModel::MultiplyParameterValue 함수
  • Web(TypeScript)의 CubismModel.multiplyParameterValueById 함수
  • Java의 CubismModel.multiplyParameterValue 함수

이 함수로 설정한 값은 그 파라미터에 현재 설정되어 있는 값에 곱해집니다.

// C++
_model->MultiplyParameterValue(CubismFramework::GetIdManager()->GetId("ParamAngleX"), 2.0f, 1.0f);
// TypeScript
_model.multiplyParameterValueById(CubismFramework.getIdManager().getId("ParamAngleX"), 2.0, 1.0);
// Java
_model.multiplyParameterValue(CubismFramework.getIdManager().getId("ParamAngleX"), 2.0f, 1.0f);

또 현재의 파라미터값을 취득하려면 다음 중 하나의 함수를 사용합니다.

  • Native(C++)의 CubismModel::GetParameterValue 함수
  • Web(TypeScript)의 CubismModel.getParameterValueById 함수
  • Java의 CubismModel.getParameterValue 함수

인덱스에 의한 파라미터 지정

파라미터의 ID 지정에는 문자열로부터 생성되는 ID형으로 지정하는 방법과 인덱스로 지정하는 방법이 있습니다.
인덱스를 미리 취득하는 등 캐쉬한 후에 인덱스를 사용하는 쪽이 고속이기 때문에, 호출 빈도가 많은 경우는 인덱스의 사용을 추천합니다.
파라미터의 인덱스는 다음 중 하나의 함수로 취득할 수 있습니다.

  • Native(C++)의 CubismModel::GetParameterIndex 함수
  • Web(TypeScript)의 CubismModel.getParameterIndex 함수
  • Java의 CubismModel.getParameterIndex 함수

// C++
// 초기화 시
csmInt32 paramAngleX;
paramAngleX = _model->GetParameterIndex(CubismFramework::GetIdManager()->GetId("ParamAngleX"));

// 파라미터 설정 시
_model->SetParameterValue( paramAngleX, 30.0f , 1.0f);
// TypeScript
// 초기화 시
let paramAngleX: number;
paramAngleX = _model.getParameterIndex(CubismFramework.getIdManager().getId("ParamAngleX"));

// 파라미터 설정 시
_model.setParameterValueByIndex(paramAngleX, 30.0, 1.0);
// Java
// 초기화 시
int paramAngleXIndex;
paramAngleXIndex = _model.getParameterIndex(CubismFramework.getIdManager().getId("ParamAngleX"));
// 파라미터 설정 시
_model.setParameterValue(paramAngleXIndex, 30.0f, 1.0f);

파라미터값 저장 및 복원

모델의 현재 파라미터값을 일시적으로 저장하려면 다음 중 하나의 함수를 사용합니다.

  • Native(C++)의 CubismModel::SaveParameters 함수
  • Web(TypeScript)의 CubismModel.saveParameters 함수
  • Java의 CubismModel.saveParameters 함수

임시 저장된 모델의 파라미터값을 복원하려면 다음 중 하나의 함수를 사용합니다.

  • Native(C++)의 CubismModel::LoadParameters 함수
  • Web(TypeScript)의 CubismModel.loadParameters 함수
  • Java의 CubismModel.loadParameters 함수

// C++
// 초기화 시
csmInt32 paramAngleX;
paramAngleX = _model->GetParameterIndex(CubismFramework::GetIdManager()->GetId("ParamAngleX"));

// 파라미터를 30으로 설정
_model->SetParameterValue( paramAngleX, 30.0f ,1.0f );

// 모든 현재 파라미터값을 임시 저장
_model->SaveParameters();

// 0으로 설정
_model->SetParameterValue( paramAngleX, 0.0f ,1.0f );

//output: value = 0
printf("value = %f",_model->GetParameterValue( paramAngleX ) );

// 마지막 saveParam 시의 상태 복원
_model->LoadParameters();

//output: value = 30
printf("value = %f",_model->GetParameterValue( paramAngleX ) );
// TypeScript
// 초기화 시
let paramAngleX: number;
paramAngleX = _model.getParameterIndex(CubismFramework.getIdManager().getId("ParamAngleX"));

// 파라미터를 30으로 설정
_model.setParameterValueByIndex(paramAngleX, 30.0, 1.0);

// 모든 현재 파라미터값을 임시 저장
_model.saveParameters();

// 0으로 설정
_model.setParameterValue(paramAngleX, 0.0, 1.0);

// output: value = 0
LAppPal.printLog("value = {0}", _model.getParameterValueByIndex(paramAngleX));

// 마지막 saveParam 시의 상태 복원
_model.loadParameters();

// output: value = 30
LAppPal.printLog("value = {0}", _model.getParameterValueByIndex(paramAngleX));
// Java
// 초기화 시
int paramAngleXIndex;
paramAngleXIndex = _model.getParameterIndex(CubismFramework.getIdManager().getId("ParamAngleX"));

// 파라미터를 30으로 설정
_model.setParameterValue(paramAngleXIndex, 30.0f, 1.0f);

// 모든 현재 파라미터값을 임시 저장
_model.saveParameters();

// 0으로 설정
_model.setParameterValue(paramAngleXIndex, 0.0f, 1.0f);

// output: value = 0
LAppPal.printLog("value = " + _model.getParametetValue(paramAngleXIndex));

// 마지막 saveParameters 시의 상태 복원
_model.loadParameters();

// output: value = 30
LAppPal.printLog("value = " + _model.getParameterValue(paramAngleXIndex));

LAppModel::Update에서 SaveParameters, LoadParameters를 사용하는 방법

API로서의 SaveParameters, LoadParameters는 값의 저장과 호출을 실시하는 것이지만,
실제 LAppModel::Update 함수는 LoadParameters를 호출하여 이전 상태로 리셋하며
모션 재생을 적용한 다음 SaveParameters를 실행하고 있습니다.

// C++
void LAppModel::Update()
{
    const csmFloat32 deltaTimeSeconds = LAppPal::GetDeltaTime();
    _userTimeSeconds += deltaTimeSeconds;

    _dragManager->Update(deltaTimeSeconds);
    _dragX = _dragManager->GetX();
    _dragY = _dragManager->GetY();

    csmBool motionUpdated = false;

    //-----------------------------------------------------------------
    _model->LoadParameters(); 
    if (_motionManager->IsFinished())
    {
        StartRandomMotion(MotionGroupIdle, PriorityIdle);
    }
    else
    {
        motionUpdated = _motionManager->UpdateMotion(_model, deltaTimeSeconds); 
    }
    _model->SaveParameters();
    //-----------------------------------------------------------------
    
    if (!motionUpdated)
    {
        if (_eyeBlink ! = NULL)
        {
            _eyeBlink->UpdateParameters(_model, deltaTimeSeconds); // Set
        }
    }

    if (_expressionManager ! = NULL)
    {
        _expressionManager->UpdateMotion(_model, deltaTimeSeconds); // Add Set Mult
    }

    _model->AddParameterValue(_idParamAngleX, _dragX * 30);
    _model->AddParameterValue(_idParamAngleY, _dragY * 30);
    _model->AddParameterValue(_idParamAngleZ, _dragX * _dragY * -30);

    _model->AddParameterValue(_idParamBodyAngleX, _dragX * 10); 
    
    _model->AddParameterValue(_idParamEyeBallX, _dragX);
    _model->AddParameterValue(_idParamEyeBallY, _dragY);

    if (_breath ! = NULL)
    {
        _breath->UpdateParameters(_model, deltaTimeSeconds); // Add
    }

    if (_physics ! = NULL)
    {
        _physics->Evaluate(_model, deltaTimeSeconds); // Set
    }

    if (_lipSync)
    {
        csmFloat32 value = 0; 

        for (csmUint32 i = 0; i < _lipSyncIds.GetSize(); ++i)
        {
            _model->AddParameterValue(_lipSyncIds[i], value, 0.8f);
        }
    }

    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._dragManager.update(deltaTimeSeconds);
    this._dragX = this._dragManager.getX();
    this._dragY = this._dragManager.getY();

    let motionUpdated = false;

    //--------------------------------------------------------------------------
    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(!motionUpdated)
    {
        if(this._eyeBlink ! = null)
        {
            this._eyeBlink.updateParameters(this._model, deltaTimeSeconds); // Set
        }
    }

    if(this._expressionManager ! = null)
    {
        this._expressionManager.updateMotion(this._model, deltaTimeSeconds); // Add Set Mult
    }

    this._model.addParameterValueById(this._idParamAngleX, this._dragX * 30);
    this._model.addParameterValueById(this._idParamAngleY, this._dragY * 30);
    this._model.addParameterValueById(this._idParamAngleZ, this._dragX * this._dragY * -30);

    this._model.addParameterValueById(this._idParamBodyAngleX, this._dragX * 10);

    this._model.addParameterValueById(this._idParamEyeBallX, this._dragX);
    this._model.addParameterValueById(this._idParamEyeBallY, this._dragY);

    if(this._breath ! = null)
    {
        this._breath.updateParameters(this._model, deltaTimeSeconds); // Add
    }

    if(this._physics ! = null)
    {
        this._physics.evaluate(this._model, deltaTimeSeconds); // Set
    }

    if(this._lipsync)
    {
        let value: number = 0;

        for(let i: number = 0; i < this._lipSyncIds.getSize(); ++i)
        {
            this._model.addParameterValueById(this._lipSyncIds.at(i), value, 0.8);
        }
    }
    
    if(this._pose ! = null)
    {
        this._pose.updateParameters(this._model, deltaTimeSeconds); // 파라미터에 대한 조작 없음
    }

    this._model.update();
}
// Java
public void update() {
    final float deltaTimeSeconds = LAppPal.getDeltaTime();
    _userTimeSeconds += deltaTimeSeconds;
  
    _dragManager.update(deltaTimeSeconds);
    _dragX = _dragManager.getX();
    _dragY = _dragManager.getY();
  
    boolean isMotionUpdated = false;
  
    //--------------------------------------------------------------
    _model.loadParameters();
  
    // 모션 재생이 없으면 대기 모션 중에서 랜덤 재생
    if (_motionManager.isFinished()) {
        startRandomMotion(LAppDefine.MotionGroup.IDLE.getId(), LAppDefine.Priority.IDLE.getPriority());
    } else {
        isMotionUpdated = _motionManager.updateMotion(_model, deltaTimeSeconds);
    }
    _model.saveParameters();
    // -------------------------------------------------------------

  // 메인 모션 업데이트가 없을 때만 깜박이기
  if (!isMotionUpdated) {
    if (_eyeBlink ! = null) {
      _eyeBlink.updateParameters(_model, deltaTimeSeconds); // Set
    }
  }
  
  if (_expressionManager ! = null) {
    _expressionManager.updateMotion(_model, deltaTimeSeconds); // Add Set Mult
  }
  
  // 드래그하여 얼굴 방향 조정
  _model.addParameterValue(_idParamAngleX, _dragX * 30);
  _model.addParameterValue(_idParamAngleY, _dragY * 30);
  _model.addParameterValue(_idParamAngleZ, _dragX * _dragY * (-30));
  
  // 드래그하여 신체 방향 조정
  _model.addParameterValue(_idParamBodyAngleX, _dragX * 10);
  
  // 드래그로 눈 방향 조정
  _model.addParameterValue(_idParamEyeBallX, _dragX);
  _model.addParameterValue(_idParamEyeBallY, _dragY);
  
  if (_breath ! = null) {
    _breath.updateParameters(_model, deltaTimeSeconds); // Add
  }
  
  if (_physics ! = null) {
    _physics.evaluate(_model, deltaTimeSeconds); // Set
  }
  
  if (_lipSync) {
    // 실시간으로 립싱크를 수행하는 경우 시스템에서 음량을 취득하여 0~1 범위의 값을 입력합니다
    float value = 0.0f;
    
    for (CubismId lipSyncId : _lipSyncIds) {
      _model.addParameterValue(lipSyncId, value, 0.8f);
    }
  }

  if (_pose ! = null) {
    _pose.updateParameters(_model, deltaTimeSeconds); // 파라미터에 대한 조작 없음
  }
  
  _model.update();
}

이 방법의 목적은 모션이 재생되지 않았거나 모션 재생에 의해 지정되지 않은 파라미터에
Update의 다른 조작이 들어가기 전의 값으로 덮어쓰기하여 가산·곱하기 계산의 베이스를 만들어 내는 것에 있습니다.

이 기능이 없는 경우는 모션으로 지정이 없는 파라미터에 Add를 실시하면, 갱신마다 값이 Add되어 범위 외로 값이 나오게 됩니다.

모션 재생 전의 Load만이라도 가산·곱셈의 베이스는 작성할 수 있지만,
모션 재생 후 Save가 들어가면 모션의 마지막 상태를 유지할 수 있게 됩니다.

파츠 불투명도 취득/설정

파츠의 불투명도 설정은 다음 중 하나의 함수를 사용합니다.

  • Native(C++)의 CubismModel::SetPartOpacity 함수
  • Web(TypeScript)의 CubismModel.setPartOpacityById 함수
  • Java의 CubismModel.setPartOpactiy 함수

파츠의 불투명도 취득은 다음 중 하나의 함수를 사용합니다.

  • Native(C++)의 CubismModel::GetPartOpacity 함수
  • Web(TypeScript)의 CubismModel.getPartOpacityById 함수
  • Java의 CubismModel.getPartOpacity 함수

// C++
// 얼굴 파츠 불투명도를 0.5로 설정
_model->SetPartOpacity( CubismFramework::GetIdManager()->GetId("PartArmLB001"),0.5f );
// TypeScript
// 얼굴 파츠 불투명도를 0.5로 설정
_model.setPartOpacityById(CubismFramework.getIdManager().getId("PartArmLB001"), 0.5);
// Java
// 얼굴 파츠 불투명도를 0.5로 설정
_model.setPartOpacity(CubismFramework.getIdManager().getId("PartArmLB001"), 0.5f);

파라미터 적용 타이밍 및 비용

파라미터 설정 시에는 파라미터값을 재기록하기만 하고 정점 계산은 실시하지 않습니다.
파라미터 변경 후 다음 중 하나의 함수로 정점을 계산합니다.

  • Native(C++)의 CubismModel::Updatel 함수
  • Web(TypeScript)의 CubismModel.updatel 함수
  • Java의 CubismModel.updatel 함수

그 후 다음 중 하나의 함수로 파라미터 적용 후의 모델이 렌더링됩니다.

  • Native(C++)의 CubismRenderer::DrawModel 함수
  • Web(TypeScript)의 CubismRenderer.drawModel 함수
  • Java의 CubismRenderer.drawModel 함수

파라미터 계산 순서의 중요성

파라미터 조작에는 덮어쓰기, 가산, 곱셈의 3종류가 있습니다.
곱셈을 먼저 계산해도 나중에 가산이나 덮어쓰기에 영향을 주지 않습니다.
덮어쓰기를 마지막에 실행하면 이전의 모든 계산 결과가 무시됩니다.
덮어쓰기, 가산, 곱셈 순으로 적용하는 것이 일반적입니다.

프로그램 컴포넌트 등으로 계산 내용을 확인하기 어려운 상황에서는
어떤 작업이 어떤 계산을 하는지 파악하고 순서대로 컴포넌트의 적용 순서를 설정해야 합니다.

위의 Gif에 표시된 눈 열림의 차이가 어떤 코드의 차이로 발생하는지 살펴보겠습니다.
SDK에 포함된 샘플 모델인 haru에 대해서 눈 깜빡임과 표정을 적용한 예입니다.
표정 설정에 관해서는 눈의 개폐를 2배로 여는 f06.exp3.json을 재생하고 있는 상태입니다.
계산 순서만 문제이므로 C ++ 코드만 게재합니다.

덮어쓰기 동작인 자동 눈 깜빡임 후 곱셈 동작인 표정 설정을 수행하는 A의 Update

// C++
	// 눈 깜빡임
    if (!motionUpdated)
    {
        if (_eyeBlink ! = NULL)
        {
            // 메인 모션 업데이트가 없을 때
            _eyeBlink->UpdateParameters(_model, deltaTimeSeconds); // 눈 끔뻑
        }
    }

    if (_expressionManager ! = NULL)
    {
        _expressionManager->UpdateMotion(_model, deltaTimeSeconds); // 표정으로 파라미터 업데이트(상대 변화) 
    }
// TypeScript
	// 눈 깜빡임
    if (!motionUpdated)
    {
        if (this._eyeBlink ! = NULL)
        {
            // 메인 모션 업데이트가 없을 때
            this._eyeBlink->UpdateParameters(this._model, deltaTimeSeconds); // 눈 끔뻑
        }
    }

    if (this._expressionManager ! = NULL)
    {
        this._expressionManager.updateMotion(this._model, deltaTimeSeconds); // 표정으로 파라미터 업데이트(상대 변화)
    }

곱셈 동작인 표정 설정 후 덮어쓰기 동작인 자동 눈 깜빡임을 하는 B의 Update

B의 Update (Native)

// C++
	if (_expressionManager ! = NULL)
    {
        _expressionManager->UpdateMotion(_model, deltaTimeSeconds); // 표정으로 파라미터 업데이트(상대 변화) 
    }

    // 눈 깜빡임
    if (!motionUpdated)
    {
        if (_eyeBlink ! = NULL)
        {
            // 메인 모션 업데이트가 없을 때
            _eyeBlink->UpdateParameters(_model, deltaTimeSeconds); // 눈 끔뻑
        }
    }
// TypeScript
	if (this._expressionManager ! = NULL)
    {
        this._expressionManager.updateMotion(this._model, deltaTimeSeconds); // 표정으로 파라미터 업데이트(상대 변화)
    }

    // 눈 깜빡임
    if (!motionUpdated)
    {
        if (this._eyeBlink ! = NULL)
        {
            // 메인 모션 업데이트가 없을 때
            this._eyeBlink.updateParameters(this._model, deltaTimeSeconds); // 눈 끔뻑
        }
    }

A에서는 눈 깜빡임 후에 표정의 효과로 눈이 크게 떠진 것을 확인할 수 있습니다.
한편 B의 계산에서는 눈 깜빡임에 덮어쓰기되어 표준 눈 개폐에 억제되어 버렸습니다.
계산 순서가 다르기만 해도 모델이 표현하는 뉘앙스는 크게 달라집니다.

모션이나 표정, 기능의 계산 순서는 디자이너와도 공유해 둡시다.

이 기사에 관한 의견 및 요청사항을 보내 주시기 바랍니다.