あいさつ
今年はMayaのクラッシュではなく、とあるメーカーのCPUクラッシュで苦しめられた
あまぐりです
どうぞよろしくお願いします
この記事は Maya Advent Calender 2024の21日目の記事です
みなさん、年越しの準備はお済みでしょうか?
僕は今年こそはMayaのFABRIKプラグインを開発するぞと意気込んでたはずが、
Mayaのレンダリング改造で遊んでたらいつの間に年を越しそうです
さて、そういう訳で(?)今年の記事はMaya Viewport2.0のリアルタイムレンダリングパイプラインをある程度独自にカスタマイズできるMRenderOverrideをつかってポストエフェクトを自作する話を書こうかなと思います
※でもUSD利用を想定したViewportの代替が開発されているので今更感がでてきています
それでも気にせずいきましょう
本記事のMayaバージョンとか言語
Windows 11
Maya 2023.3.1
C++をメインに取り扱います
本記事を書いているときに作ったサンプルのコード達です
MRenderOverrideとは
MayaAPIにはレンダリングにまつわるモジュールが備わっており、
その名もOpenMayaRenderといいます
ドキュメントにもある通りMayaのレンダリング周りの操作やカスタマイズを行うためのAPIモジュールです
そのなかのMRenderOverrideはMayaのビューポートを独自パイプラインでオーバーライドすることができます
※オーバーライドとはいいますが、完全な置き換えではなくレンダーループの切り替えになります
Mayaのレンダーループ
レンダーループとは毎フレーム描画されるシーンの描画イテレーションのことです
MRenderOverrideを使っている場合にコールされるレンダーループは主に以下のような仕組みで構成されているそうです
公式ドキュメントから引用します
MRenderOverride override;
override.setup();
if (override.startOperationIterator())
{
--> Get operation
MRenderOperation *op = override.renderOperation();
while (op)
{
if (op)
--> Add to "list" of operations to perform
--> Iterate to next operation
if (!override.nextRenderOperation())
op = NULL;
else
op = override.renderOperation();
}
}
override.cleanup();
とてもシンプルですね(これだけ見れば
MRenderOperationはレンダーループ中にビュー内の指定の要素を描画するレンダラーで、
これをレンダリングパイプライン内でイテレーションさせてビューをレンダリングします
MRenderOperationにはいくつか種類があるのですが、そちらの解説はAreaJapan様の記事か、Mayaのドキュメントにありますので割愛します
MRenderOverrideでカスタムポストエフェクトを実装する
Mayaではカスタムポストエフェクトを実装するにはMQuadRenderを用います
といってもこれ単品をコマンド的に実行して動くものではなく自身でレンダーループを構築する必要があります
まずはループ構成のためのフロー設計を行います
レンダーループのフロー構築
今回はシーンレンダー(ジオメトリなど3D空間レンダー)の描画結果に対してスクリーン描画でポストエフェクトをかける実装を例にするため以下のようなレンダーループを構築します
途中に出てくるRenderTargetって?
なにやらRenderOparationの間に挟まっているものがありますね
RenderTargetとはなにかご存知の方はそのまま読み飛ばしていただくとして、
これらは各レンダーの描画結果をテクスチャにして確保するものです
レンダラーごとの描画はスクリーンの解像度分あるいは、
指定した解像度分の描画結果を取り置きしておき、他のレンダラーで使いまわしたりすることが求められます
これら取り置きしている描画結果をテクスチャとしてメモリ内に保存したものがRenderTargetです
MayaのAPIで公開されている標準RnderTargetの種類
Mayaでは以下の3種類のレンダーターゲットを扱うためのAPIが公開されています
- カラーターゲット
- 深度ターゲット
- ステンシルターゲット
今回の遊戯ではピクセルカラーの描画結果に対して変化を加えるため、このカラーターゲットだけで遊んでいこうと思います
レンダーターゲットがテクスチャならシェーダーで使えるやん
題目の通りでレンダーターゲットはテクスチャです
MQuadRenderはいわば2Dの四角形レンダラーで、レンダリングするためのシェーダーを持っています
このシェーダーにテクスチャとして渡すことでキャラシェーダーなどのように画面に対して描画を追加できます
MQuadRender::shader関数
MQuadRenderクラスでは、スクリーンに対する描画処理を担うシェーダーを置き換えることができる、MQuadRender::shader()という関数をオーバーライドできます
shader()関数はMShaderInstanceというシェーダー情報を持つクラスを返す必要があり、
このMShaderInstanceに独自に書いたシェーダーを渡したり、プロパティをセットすることで好きな処理を書くことが許可されています
自由度が高い分、実装する場合のメモリ管理も開発者に責任があります
今回はHLSLでポストエフェクトのシェーダーを書きたいので、
描画APIをDirectX11で行っているものとして進めていきます
DirectXならシェーダーにHLSLが使えるよ
MRenderOverrideにはどの描画APIをサポートしているのかを示す関数が用意されています
MHWRender::DrawAPI MRenderOverride::supportedDrawAPIs() const
{
return MHWRender::kDirectX11;
}
ここで指定された描画API以外で立ち上がっているMayaでは描画がなされません
ということでここを上記のようにDirectX11だけで描画しているよーと返しておきます
この状態であればMShaderInstanceにHLSLでかかれたシェーダーを渡すことができます
シェーダーのヘッダーを書く
MayaでHLSLを書くときには拡張子として.fx
が基本ですが、.fxh
も扱えます
C言語ライクらしいですが.fx
のほうをメイン処理にするとして、宣言やマクロ等を.fxh
に記載していきます
// ヘッダーを何度もincludeしないようにインクルードガードを挟みます
#ifndef _POSTEFFECT_SAMPLE_FXH_
#define _POSTEFFECT_SAMPLE_FXH_
// World-view-projection transformation.
float4x4 gWVPXf : WorldViewProjection < string UIWidget = "None"; >;
// ViewportPixelSizeセマンティクスでスクリーンの解像度を利用できます
float2 gScreenSize : ViewportPixelSize < string UIWidget = "None"; >;
// 1ピクセルあたりの大きさ(長さ)
static float2 gTexelSize = 1.0f / gScreenSize;
// Vertex shader input structure.
struct VS_INPUT_ScreenQuad
{
float3 Pos : POSITION;
float2 UV : TEXCOORD0;
};
// Vertex shader output structure.
struct VS_TO_PS_ScreenQuad
{
float4 HPos : SV_Position;
float2 UV : TEXCOORD0;
};
// Vertex shader.
VS_TO_PS_ScreenQuad VS_ScreenQuad(VS_INPUT_ScreenQuad In)
{
VS_TO_PS_ScreenQuad Out;
// Transform the position from object space to clip space for output.
Out.HPos = mul(float4(In.Pos, 1.0f), gWVPXf);
// Pass the texture coordinates.
Out.UV = In.UV;
return Out;
}
// ターゲットのピクセルをサンプリングするマクロです
#define SCREEN_SAMPLE(tex, samp, uv) tex.Sample(samp, uv)
#endif // _POSTEFFECT_SAMPLE_FXH_
シェーダーのメイン処理を書く
メインのポストエフェクト処理を書いていきます
// fxhをインクルードします
#include "PostEffectSample10.fxh"
// MQuadRenderは複数用意しますが、同じシェーダーファイルを扱うのでTexture宣言をすべて記述します
// こちらは1つ目のテクスチャの入力プロパティ
Texture2D gInputTex1 < string UIWidget = "None";
> ;
// どれも同じサンプラー情報でよいのでひとつ宣言
// UVをずらすためwrap指定してます
SamplerState gInputSampler
{
Filter = ANISOTROPIC;
AddressU = Wrap;
AddressV = Wrap;
};
// Blend State
BlendState AlphaBlend {
AlphaToCoverageEnable = false;
BlendEnable[0] = true;
SrcBlend = SRC_ALPHA;
DestBlend = INV_SRC_ALPHA;
BlendOp = ADD;
SrcBlendAlpha = ONE;
DestBlendAlpha = ZERO;
BlendOpAlpha = ADD;
};
// ------------------------------------------------------------------------------------------------
// ピクセルシェーダー
// ------------------------------------------------------------------------------------------------
// 赤色のみ抽出して描画するピクセルシェーダー
float4 PS_PostEffectSampleRedPass(VS_TO_PS_ScreenQuad i) : SV_TARGET
{
// 10ピクセル分正方向にずらしたオフセットを用意
float2 offset = i.UV + (gTexelSize * 10);
float4 col = SCREEN_SAMPLE(gInputTex1, gInputSampler, i.UV);
float4 offsetCol = SCREEN_SAMPLE(gInputTex1, gInputSampler, offset);
// ずらした分のカラーから赤色のみ抽出して描画します
return float4(
offsetCol.r,
col.g,
col.b,
0.5f
);
}
// 青色のみ抽出して描画するピクセルシェーダー
float4 PS_PostEffectSampleBluePass(VS_TO_PS_ScreenQuad i) : SV_TARGET
{
// 10ピクセル分負方向にずらしたオフセットを用意
float2 offset = i.UV - (gTexelSize * 10);
float4 col = SCREEN_SAMPLE(gInputTex1, gInputSampler, i.UV);
float4 offsetCol = SCREEN_SAMPLE(gInputTex1, gInputSampler, offset);
// ずらした分のカラーから青色のみ抽出して描画します
return float4(
col.r,
col.g,
offsetCol.b,
0.5f
);
}
// ------------------------------------------------------------------------------------------------
// テクニックを用意します
// ------------------------------------------------------------------------------------------------
// 赤と青をずらすテクニックです
technique10 Shift
{
pass P0
{
SetBlendState(AlphaBlend, float4(0, 0, 0, 0), 0xFFFFFFFF);
SetVertexShader(CompileShader(vs_4_0, VS_ScreenQuad()));
SetGeometryShader(NULL);
SetPixelShader(CompileShader(ps_4_0, PS_PostEffectSampleRedPass()));
}
pass P1
{
SetBlendState(AlphaBlend, float4(0, 0, 0, 0), 0xFFFFFFFF);
SetVertexShader(CompileShader(vs_4_0, VS_ScreenQuad()));
SetGeometryShader(NULL);
SetPixelShader(CompileShader(ps_4_0, PS_PostEffectSampleBluePass()));
}
}
なにやら.fx
のほうにだけあやしい10
の文字が含まれています
これはのちのちシェーダーファイルをMShaderInstanceへ読み込ませるとき
ファイル拡張子まで指定しない場合に特定状況下で必要になります
メッシュシェーダーとの違い
他は普段キャラシェーダー等を書かれている人には馴染み深い内容になっていますね
違いとしてはVertexShaderでPOSITIONセマンティクスにバインドされている情報がメッシュの頂点情報ではなく、スクリーンのピクセル座標が来ているのが違いになります
ですのでPixelShaderでは画面すべてを覆った4角形メッシュに対して処理を行うイメージになります
シェーダーファイルを読み込めるようにする
シェーダーファイルをMayaのShaderPathが既に通っているディレクトリ内においてもいいのですが、
MRenderOverrideはプラグインでの配布が想定されているためちょっと不便です
そこでプラグイン側から指定のパスをShaderPathに追加しちゃいましょう
ShaderPathをMayaに追加するにはMShaderManagerを利用します
// Initialize the shader
void PostEffectSample::initializeShaders()
{
MHWRender::MRenderer *renderer = MHWRender::MRenderer::theRenderer();
if (renderer)
{
const MHWRender::MShaderManager *shaderMgr = renderer->getShaderManager();
if (shaderMgr)
{
// Get the path to the shader
struct stat info;
// MStringのexpandEnvironmentVariablesAndTildeを使うと環境変数に設定された文字列を展開できます
// モジュールで環境変数を設定しておくと設定時にすでに展開されたPath文字列になっているため便利です
const MString shaderPath = MString("${EXTERNAL_SHADER_PATH}").expandEnvironmentVariablesAndTilde();
if (stat(shaderPath.asChar(), &info) != 0)
{
MDisplayError("Failed to get shader path : %s", shaderPath.asChar());
}
else
{
MDisplayInfo("Find to shader path : %s", shaderPath.asChar());
shaderMgr->addShaderPath(shaderPath);
// .fxhをインクルードする場合は追加しておきます
shaderMgr->addShaderIncludePath(shaderPath);
}
}
}
}
これは個人的なやり方ですが、
Mayaのモジュールなどを利用して環境変数を追加してMayaを起動、
その環境変数にシェーダーファイルがおいてあるディレクトリを指定しておき、プラグイン内でロードさせます
+ MAYAVERSION:2023 PostEffectSample 1.0.0 ..\.
EXTERNAL_SHADER_PATH := 2023\plug-ins\HLSL
+ MAYAVERSION:2024 PostEffectSample 1.0.0 ..\.
EXTERNAL_SHADER_PATH := 2024\plug-ins\HLSL
+ MAYAVERSION:2025 PostEffectSample 1.0.0 ..\.
EXTERNAL_SHADER_PATH := 2025\plug-ins\HLSL
モジュールはMayaのバージョンごとに読み込ませるプラグイン自体や、
スクリプトなども別々に管理できるためこの手法が一番安定した提供を可能にしている気がします
MQuadRender::shader内でMShaderInsatanceを扱う
さてシェーダーを書き終えたところでどういうふうに遊べるのでしょうか
MQaudRenderのshader関数の登場です
オーバーライドするMQuadRenderヘッダーです
// pragmaでonceを利用するとインクルードガードを簡易記述できます
#pragma once
#include <maya/MViewport2Renderer.h>
#include "RenderTargetsInterface.h"
#include "CustomRenderProcess.h"
#include "OverrideClearColorInterface.h"
class OverrideQuadRender : public MHWRender::MQuadRender, public RenderTargetsInterface, public amagly::CustomRenderProcess, public amagly::OverrideClearColorInterface
{
public:
enum EPassType
{
kRedPass,
kBluePass,
kCombainePass,
kBlitPass,
kClearPass
};
OverrideQuadRender(const MString &name, unsigned int clearMask);
~OverrideQuadRender() override;
// from MRenderOperation
MHWRender::MRenderTarget *const *targetOverrideList(unsigned int &listSize) override;
MHWRender::MClearOperation &clearOperation() override;
// from MQuadRender
const MHWRender::MShaderInstance *shader() override;
// from CustomRenderProcess
MStatus preProcess() override;
MStatus postProcess() override;
MHWRender::MRenderOperation *renderOperation() override
{
return (MHWRender::MRenderOperation*)this;
}
void setPassType(EPassType passType) { mPassType = passType; }
void setInput1Tex(MHWRender::MRenderTarget *texture) { mTexture1 = texture; }
void setInput2Tex(MHWRender::MRenderTarget *texture) { mTexture2 = texture; }
void setInput3Tex(MHWRender::MRenderTarget *texture) { mTexture3 = texture; }
protected:
MHWRender::MShaderInstance *mShaderInstance;
MHWRender::MRenderTarget *mTexture1;
MHWRender::MRenderTarget *mTexture2;
MHWRender::MRenderTarget *mTexture3;
MFloatPoint mViewRectangle;
MString mCustomOutputTargetName;
private:
MHWRender::MBlendState *mBlendState;
EPassType mPassType;
};
実装側です
const MHWRender::MShaderInstance *OverrideQuadRender::shader()
{
// printf("%s::shader\n", name().asChar());
if (mShaderInstance == nullptr)
{
MHWRender::MRenderer *renderer = MHWRender::MRenderer::theRenderer();
if (renderer)
{
const MHWRender::MShaderManager *shaderMgr = renderer->getShaderManager();
if (shaderMgr)
{
switch (mPassType)
{
case EPassType::kRedPass:
// 本来MRTやりたかったんですが時間足らず断念してここだけで描画
mShaderInstance = shaderMgr->getEffectsFileShader("PostEffectSample", "Shift", nullptr, 0, false);
break;
case EPassType::kBluePass:
mShaderInstance = shaderMgr->getEffectsFileShader("PostEffectSample", "BlueShift", nullptr, 0, false);
break;
case EPassType::kCombainePass:
mShaderInstance = shaderMgr->getEffectsFileShader("PostEffectSample", "Combine", nullptr, 0, false);
break;
case EPassType::kBlitPass:
mShaderInstance = shaderMgr->getEffectsFileShader("PostEffectSample", "Blit", nullptr, 0, false);
break;
case EPassType::kClearPass:
mShaderInstance = shaderMgr->getEffectsFileShader("PostEffectSample", "Clear", nullptr, 0, false);
return mShaderInstance;
default:
return nullptr;
}
}
}
}
if (mShaderInstance)
{
// printf("ShaderInstance : %s\n", name().asChar());
// Set the input texture
{
MHWRender::MRenderTargetAssignment assignment;
assignment.target = mTexture1;
MStatus status = mShaderInstance->setParameter("gInputTex1", assignment);
if (status != MStatus::kSuccess)
{
printf("Could not set input render target / texture parameter on invert shader\n");
return nullptr;
}
}
switch (mPassType)
{
case EPassType::kRedPass:
case EPassType::kBluePass:
{
if (mTexture2)
{
// Set Depth Texture
MHWRender::MRenderTargetAssignment assignment;
assignment.target = mTexture2;
MStatus status = mShaderInstance->setParameter("gInputTex2", assignment);
if (status != MStatus::kSuccess)
{
printf("Could not set input render target / texture parameter on invert shader\n");
return nullptr;
}
}
break;
}
case EPassType::kCombainePass:
{
// Set the second input texture
if (mTexture2)
{
MHWRender::MRenderTargetAssignment assignment;
assignment.target = mTexture2;
MStatus status = mShaderInstance->setParameter("gInputTex2", assignment);
if (status != MStatus::kSuccess)
{
printf("Could not set input render target / texture parameter on invert shader\n");
return nullptr;
}
}
// Set the third input texture
if (mTexture3)
{
MHWRender::MRenderTargetAssignment assignment;
assignment.target = mTexture3;
MStatus status = mShaderInstance->setParameter("gInputTex3", assignment);
if (status != MStatus::kSuccess)
{
printf("Could not set input render target / texture parameter on invert shader\n");
return nullptr;
}
}
break;
}
case EPassType::kBlitPass:
case EPassType::kClearPass:
default:
break;
}
}
return mShaderInstance;
}
MShaderManager->getEffectsFileShaderのハマりポイントは以下の部分です
ここでは"PostEffectSample"と渡したい場合、ファイルは"PostEffectSample10.fx"にしなければなりません
mShaderInstance = shaderMgr->getEffectsFileShader("PostEffectSample", "RedShift", nullptr, 0, true);
公式ドキュメントにも記載があります
ファイル名の引数の一部として ファイル拡張子が与えられない場合 、レンダラーが現在使用している描画APIに基づいて、以下の文字列がファイル名に追加される:
基礎となる描画APIがOpenGLの場合、「.cgfx 」が付加される。
基礎となる描画APIが DirectX 10以上の場合、「10.fx 」が付加される。
基礎となる描画APIがOpenGLCoreProfileの場合、「.ogsfx 」が付加されます。
SDKにある公式サンプルだと大抵拡張子まで引数になかったりするので、
サンプルからコード持ってきたのに自分のシェーダーファイルを指定すると動かないんだが!
とかなったりします
戒めとしてドキュメントはしっかり読み込みましょうと心得ました
ルールを守って遊びましょう
レンダーループを構築する
さてQuadRenderも用意できて、シェーダーとテクスチャも扱えるようになったので
いよいよ本番のレンダーループを構築します
SDKのサンプルにあるやり方だと
- オペレーションリスト配列をつくる
- 現在のオペレーションのindex値(int)を宣言
- nextRenderOperation()でindexを加算する
- index値がオペレーションリストのサイズを超えた場合にループをブレークする
となっています
しかし今回はちょっと実験がてらサンプルどおりには実装せず遊んで見ようと思います
レンダー定義
今回は見やすさを重視してMRenderOverrideのsetup内には定義せず、別ファイルにわけています
MStatus RenderOparationManager::setup()
{
auto *frameTexManager = static_cast<FrameTextureManager *>(FrameTextureManager::Get());
MStatus stat = frameTexManager->UpdateRenderTargets();
if (stat == MStatus::kFailure)
{
MGlobal::displayError("Failed to Update RenderTargets");
return MStatus::kFailure;
}
if (mRenderOperations.size() == 0)
{
// -----------------------------------
// inisialize the rendering oparations
// -----------------------------------
/*
ここにMRenderOparationの定義と追加処理
*/
// Set the rendering flow
// 今回はここで決めきっていますがループのセットアップ時に
// なんらか外出しのノードからの更新でフローを変更することもしやすいと思っています
std::vector<unsigned int> renderingFlow;
renderingFlow = {
RenderOparationType::BaseSceneOparation,
RenderOparationType::BlitSceneOparation,
// RenderOparationType::ClearTempRedBufferOparation,
RenderOparationType::RedPassRenderOparation,
// RenderOparationType::ClearTempBlueBufferOparation,
// RenderOparationType::BluePassRenderOparation,
// RenderOparationType::CombinePassRenderOparation,
RenderOparationType::PresentTargetOparation
};
setRenderingFlow(renderingFlow);
}
for (int i = 0; i < mRenderOperations.size(); i++)
{
if (!mRenderOperations[i])
{
return MStatus::kFailure;
}
}
// printf("setuped RenderOparationManager\n");
return MStatus::kSuccess;
}
ループ処理改造
MRenderOverrideは冒頭で軽く説明した通り、シンプルかつ開発者がループ中の処理をある程度フックしやすくしてくれています
このため今回はOparationManagerというクラスを定義してそちらにループ中の処理をフックしてあげることで、追加処理も挟めるという狙いもありました
bool vp2MayaPostEffectsSample::startOperationIterator()
{
mRenderOparationManager->start();
return true;
}
MHWRender::MRenderOperation *vp2MayaPostEffectsSample::renderOperation()
{
MStatus status = mRenderOparationManager->executePreRenderProcess();
if (status != MStatus::kSuccess)
{
printf("Could not execute pre render process\n");
return nullptr;
}
return mRenderOparationManager->renderOparation();
}
bool vp2MayaPostEffectsSample::nextRenderOperation()
{
MStatus status = mRenderOparationManager->executePostRenderProcess();
if (status != MStatus::kSuccess)
{
printf("Could not execute post render process\n");
return false;
}
mRenderOparationManager->next();
return mRenderOparationManager->isVaildNextOparation();
}
MStatus vp2MayaPostEffectsSample::setup(const MString &destination)
{
MHWRender::MRenderer *theRenderer = MHWRender::MRenderer::theRenderer();
if (!theRenderer)
{
return MStatus::kFailure;
}
return mRenderOparationManager->setup();
}
MStatus vp2MayaPostEffectsSample::cleanup()
{
return MStatus::kSuccess;
}
MRenderOparationの関数と実行順
Mayaドキュメントで見つけられなかったのですが、MRenderOparationはMUserRenderOparationやMHUDRender、MPresentTargetといったオペレーションを除いたオペレーションは、関数に実行順が指定されています
処理順を出力してみると以下のような感じで実行されていました
- clearOparationが実行
- writableTargetsが明示的にOverrideされている場合ここでターゲットのOverrideCountが処理されます
- targetOverrideListが実行
- オペレーションが実行されます
ですのでレンダーループ中、オペレーションを受け渡す前にレンダーターゲットのサイズ更新やらなんやらを操作する場合は純粋なオペレーションを定義するだけだと不可能で、MUserRenderOparationを用いてデバイスアクセス含め自分でやることになってしまうようです
clearOparationも実際はバックバッファのクリアを行っているのかなぁとおもっていたりしています
実際サンプルではMRenderOverride内で作成したターゲットのクリアは自身で行っているようです
ビルドする
具体的なC++のプラグインビルド方法はSDKや解説がございますのでそちらを見ていただくとして以下のようなCMakeファイルを用意しました
cmake_minimum_required(VERSION 3.13)
# set env variable DEVKIT_LOCATION to the location of Maya devkit
set(ENV{DEVKIT_LOCATION} C:/work/maya_devkit/devkitBase)
# include the project setting file
include($ENV{DEVKIT_LOCATION}/cmake/pluginEntry.cmake)
# specify project name
set(PROJECT_NAME vp2MayaPostEffectsSample)
# set SOURCE_FILES
set(SOURCE_FILES
Common.h
CustomRenderProcess.h
FrameTextureManager.cpp
FrameTextureManager.h
OverrideClearColorInterface.cpp
OverrideClearColorInterface.h
OverridePresentTarget.cpp
OverridePresentTarget.h
OverrideQuadRender.cpp
OverrideQuadRender.h
OverrideSceneRender.cpp
OverrideSceneRender.h
RenderOparationManager.cpp
RenderOparationManager.h
RenderTargetsInterface.cpp
RenderTargetsInterface.h
vp2MayaPostEffectsSample.cpp
vp2MayaPostEffectsSample.h
vp2MayaPostEffectsSampleMain.cpp
)
# set linking libraries
set(LIBRARIES
OpenMaya
OpenMayaRender
Foundation
)
# Build plugin
build_plugin()
Mayaを立ち上げて結果を見てみる
やりました
本来の色収差はレンズの状態などから全体にではなく、カメラからの距離などによって計算すべきかと思いますが今回はポストエフェクトをかけることが目的なので一旦良しとしましょう
おわりに
いつものツール開発くらいのノリでどんなものかと触り始めてみましたが、あーでもないこーでもないと試行錯誤していくとすぐにMayaのドキュメントではなくdirectXのドキュメントと睨めっこすることになり、改めて描画エンジニアの方を尊敬する気持ちになりました
とはいえやりごたえもありますし、さらに理解を進めればオリジナルの描画エンジンも作れそうな予感です
みなさんも是非、年末年始はMRenderoverrideで遊びながら年を越してみては如何でしょうか
それでは良いお年を!
ありがとうございました