【UE5】アウトラインを描いてみた プラグイン編 - Unreal Engine 5.3

始まり

描画エンジニアになりたい方の入門編にもちょうどよいコンテンツ。

そう、アウトライン(輪郭線)
(尚主観。異論は認めよう。)

それを幾つかの実装方法で描いていきます。

アウトライン(輪郭線)とは

読んで字の如く。

描画まわりの専門用語が一切必要の無い優しい子です。

これ以上の説明はない。

こういう感じ

これはエンジン改造でバカバカに手を加えているので、これと同等の見た目は作れないです。
(まぁまぁ大変なのよ。我ながら自信作だけど。ポストだけなのに線綺麗でしょー。えっへん。)

さあ、教えてください。誰に興味があるんですの?


???「う、うーむ……」



???「プラグイン、かなあ」


(これをやりたくて記事を書いた。)

注意点なのよ

筆者はエンジン改造特化型です。

故にプラグイン設計とそこまで親密な仲ではないのです。

ただ、エンジンを触っているとプラグインのインターフェースを見かけたり、覗いたりすることが割とあるので『あー、たまに見かけるあの子ね。』程度には知っています。

要はいつも以上に情報が適当です。

筆者のプラグインのお勉強も兼ねているので、そこんとこよろしくですわ。

プラグインとは

知らんです。プラグインという名前の方なのでしょう。

プラグインを作る

Edit > Plugins
+Add
Blankを選択して適当なプラグイン名を付ける

FSceneViewExtensionBase

今回の主役です。厳密にはISceneViewExtensionですが。

超ざっくりですが、この子は描画機能のインターフェースを提供しています。

なので描画機能にアクセスしたい場合(パス挿入とか)は、この子を使うことになります。


今回はポストプロセスでアウトラインを描画したいので、PrePostProcessPass_RenderThreadを使います。

読んで字の如く、ポストプロセスの前にパスを挿入できます。

位置的にはココです。
DeferredShadingRenderer.cpp#L4404

この後にポストプロセスマテリアルやブルーム、被写界深度などが積まれていきます。

(エンジン改造するたびに見かけていたので、地味に使ってみたかった機能なんですよねー。)

超シンプルに作ってみた

パラメータとかは全部固定で一旦描画できる状態の最小構成で作ってみます。

なんせFSceneViewExtensionBaseを使うのが初めてなので、まずは動作することを確認したいのです。

各種説明は後ほどするので、とりあえずコード直張りでサクサク進めますね。

OutlineViewExtension.h / .cpp

New C++ Class...
Noneを選択してNext
階層は先ほど作成したプラグインを選択
OutlineViewExtensionで作成
OutlineViewExtension.h
#pragma once

#include "CoreMinimal.h"
#include "SceneViewExtension.h"

/**
 * ポストプロセスなアウトライン描画機能
 */
class OUTLINERENDERPIPELINE_API FOutlineViewExtension : public FSceneViewExtensionBase
{
public:
	FOutlineViewExtension(const FAutoRegister& AutoRegister);

	virtual ~FOutlineViewExtension() = default;
	virtual void SetupViewFamily(FSceneViewFamily& InViewFamily) override {}
	virtual void SetupView(FSceneViewFamily& InViewFamily, FSceneView& InView) override {}
	virtual void BeginRenderViewFamily(FSceneViewFamily& InViewFamily) override {}

	/**
	 * Called right before Post Processing rendering begins
	 */
	virtual void PrePostProcessPass_RenderThread(FRDGBuilder& GraphBuilder, const FSceneView& View, const FPostProcessingInputs& Inputs) override;

};
OutlineViewExtension.cpp
#include "OutlineViewExtension.h"
#include "PostProcess/PostProcessing.h"
#include "PostProcess/PostProcessMaterialInputs.h"
#include "ScenePrivate.h"
#include "SystemTextures.h"
#include "SceneTextureParameters.h"
#include "DataDrivenShaderPlatformInfo.h"

DECLARE_GPU_STAT(Outline);

class FOutlinePS : public FGlobalShader
{
public:
	DECLARE_GLOBAL_SHADER(FOutlinePS);
	SHADER_USE_PARAMETER_STRUCT(FOutlinePS, FGlobalShader);

	BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )
		SHADER_PARAMETER_STRUCT_REF(FViewUniformShaderParameters, View)
		SHADER_PARAMETER_STRUCT_INCLUDE(FSceneTextureShaderParameters, SceneTextures)
		SHADER_PARAMETER(float, Radius)
		SHADER_PARAMETER(float, Bias)
		SHADER_PARAMETER(float, Intensity)
		SHADER_PARAMETER(FVector3f, Color)
		RENDER_TARGET_BINDING_SLOTS()
	END_SHADER_PARAMETER_STRUCT()

	static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters)
	{
		return IsFeatureLevelSupported(Parameters.Platform, ERHIFeatureLevel::SM5) || IsFeatureLevelSupported(Parameters.Platform, ERHIFeatureLevel::SM6);
	}
};

IMPLEMENT_GLOBAL_SHADER(FOutlinePS, "/Plugin/OutlineRenderPipeline/Private/Outline.usf", "MainPS", SF_Pixel);

FOutlineViewExtension::FOutlineViewExtension(const FAutoRegister& AutoRegister)
	: FSceneViewExtensionBase(AutoRegister)
{
}

void FOutlineViewExtension::PrePostProcessPass_RenderThread(FRDGBuilder& GraphBuilder, const FSceneView& View, const FPostProcessingInputs& Inputs)
{
	Inputs.Validate();

	FScene* Scene = View.Family->Scene->GetRenderScene();

	const FIntRect PrimaryViewRect = static_cast<const FViewInfo&>(View).ViewRect;

	// Scene color is updated incrementally through the post process pipeline.
	FScreenPassTexture SceneColor((*Inputs.SceneTextures)->SceneColorTexture, PrimaryViewRect);

	RDG_EVENT_SCOPE(GraphBuilder, "Outline");
	RDG_GPU_STAT_SCOPE(GraphBuilder, Outline);

	const FScreenPassTextureViewport InputViewport(SceneColor);
	const FScreenPassTextureViewport OutputViewport(InputViewport);

	FRDGTextureRef OutputTexture;
	{
		FRDGTextureDesc OutputTextureDesc = SceneColor.Texture->Desc;
		OutputTextureDesc.Reset();
		OutputTextureDesc.Flags |= TexCreate_RenderTargetable | TexCreate_ShaderResource;
		OutputTexture = GraphBuilder.CreateTexture(OutputTextureDesc, TEXT("Outline.Output"));
	}

	// Outline Pass
	{
		TShaderMapRef<FScreenVS> VertexShader(static_cast<const FViewInfo&>(View).ShaderMap);
		TShaderMapRef<FOutlinePS> PixelShader(static_cast<const FViewInfo&>(View).ShaderMap);

		FOutlinePS::FParameters* PassParameters = GraphBuilder.AllocParameters<FOutlinePS::FParameters>();
		PassParameters->View = View.ViewUniformBuffer;
		PassParameters->SceneTextures = GetSceneTextureShaderParameters(Inputs.SceneTextures);
		PassParameters->Radius = 1.0f;
		PassParameters->Bias = 2.0f;
		PassParameters->Intensity = 1.0f;
		PassParameters->Color = FVector3f::ZeroVector;
		PassParameters->RenderTargets[0] = FRenderTargetBinding(OutputTexture, ERenderTargetLoadAction::EClear);

		const FScreenPassTexture BlackDummy(GSystemTextures.GetBlackDummy(GraphBuilder));

		// This gets passed in whether or not it's used.
		GraphBuilder.RemoveUnusedTextureWarning(BlackDummy.Texture);

		AddDrawScreenPass(
			GraphBuilder,
			RDG_EVENT_NAME("Outline"),
			View,
			OutputViewport,
			InputViewport,
			VertexShader,
			PixelShader,
			TStaticBlendState<>::GetRHI(),
			TStaticDepthStencilState<false, CF_Always>::GetRHI(),
			PassParameters);
	}

	// Copy Pass
	{
		AddCopyTexturePass(GraphBuilder, OutputTexture, SceneColor.Texture);
	}
}

OutlineRenderPipelineModule.h / .cpp

プラグイン作成した際に自動生成されるファイルです。

初期の名前はOutlineRenderPipelineだと思います。
OutlineRenderPipelineModuleに名前変更しました。

OutlineRenderPipelineModule.h
#pragma once

#include "CoreMinimal.h"
#include "Modules/ModuleManager.h"

class FOutlineViewExtension;

class FOutlineRenderPipelineModule : public IModuleInterface
{
public:
	virtual void StartupModule() override;
	virtual void ShutdownModule() override;

private:
	void OnPostEngineInit();

private:
	TSharedPtr<FOutlineViewExtension, ESPMode::ThreadSafe> ViewExtension;
};
OutlineRenderPipelineModule.cpp
#include "OutlineRenderPipelineModule.h"
#include "ShaderCore.h"
#include "Interfaces/IPluginManager.h"
#include "Misc/Paths.h"
#include "Modules/ModuleManager.h"
#include "OutlineViewExtension.h"

#define LOCTEXT_NAMESPACE "FOutlineRenderPipelineModule"

void FOutlineRenderPipelineModule::StartupModule()
{
	FString PluginShaderDir = FPaths::Combine(IPluginManager::Get().FindPlugin(TEXT("OutlineRenderPipeline"))->GetBaseDir(), TEXT("Shaders"));
	AddShaderSourceDirectoryMapping(TEXT("/Plugin/OutlineRenderPipeline"), PluginShaderDir);

	FCoreDelegates::OnPostEngineInit.AddRaw(this, &FOutlineRenderPipelineModule::OnPostEngineInit);
}

void FOutlineRenderPipelineModule::ShutdownModule()
{
	ViewExtension.Reset();
}

void FOutlineRenderPipelineModule::OnPostEngineInit()
{
	ViewExtension = FSceneViewExtensions::NewExtension<FOutlineViewExtension>();
}

#undef LOCTEXT_NAMESPACE
	
IMPLEMENT_MODULE(FOutlineRenderPipelineModule, OutlineRenderPipelineModule)

Plugins/OutlineRenderPipeline/Shaders/Private

Shadersフォルダを作成
ShadersフォルダにPrivateフォルダを作成して、そこにOutline.usfを作成
Outline.usf
#include "/Engine/Private/Common.ush"
#include "/Engine/Private/SceneTexturesCommon.ush"
#include "/Engine/Private/DeferredShadingCommon.ush"

static const uint   kSampleCount = 4;
static const float2 kSampleOffsetArray[kSampleCount] = {
	float2( 0.0, -1.0),
	float2( 1.0,  0.0),
	float2( 0.0,  1.0),
	float2(-1.0,  0.0),
};

float Radius;
float Bias;
float Intensity;
float3 Color;

void MainPS(
	FScreenVertexOutput Input,
	out float4 OutColor : SV_Target0)
{
	float2 BufferUV   = Input.UV;
	float2 ClampedUV  = clamp(BufferUV, View.BufferBilinearUVMinMax.xy, View.BufferBilinearUVMinMax.zw);

	float Weight         = 0.0;
	float SceneDepth     = CalcSceneDepth(ClampedUV);
	float NearSceneDepth = SceneDepth;

	UNROLL
	for (uint i = 0; i < kSampleCount; ++i)
	{
		float2 NewBufferUV   = BufferUV + ViewportUVToBufferUV(Radius * kSampleOffsetArray[i] * View.ViewSizeAndInvSize.zw);
		float2 NewClampedUV  = clamp(NewBufferUV, View.BufferBilinearUVMinMax.xy, View.BufferBilinearUVMinMax.zw);
		float  NewSceneDepth = CalcSceneDepth(NewClampedUV);
		Weight += SceneDepth - NewSceneDepth;
		NearSceneDepth = min(NewSceneDepth, NearSceneDepth);
	}

	float3 SceneColor = CalcSceneColor(ClampedUV);

	float SkyMask = step(NearSceneDepth, 1000000.0);

	float Outline = saturate(PositiveClampedPow(Weight, Bias) * Intensity) * SkyMask;

	float3 Output = lerp(SceneColor, Color, Outline);

	OutColor = float4(Output, 1.0);
}

OutlineRenderPipeline.uplugin

LoadingPhaseをDefaultからPostConfigInitに変更します。

{
	"Modules": [
		{
			"Name": "OutlineRenderPipeline",
			"Type": "Runtime",
			"LoadingPhase": "PostConfigInit"
		}
	]
}

できた!!!

エンジンのインターフェースなだけあって、描画まわりの実装は流用できますね。

SceneColorはSceneTexturesで所有しているので念のため出力先のバッファを別にして後でコピーする方法にしています。


描画パス位置も想定通りですね。


エンジンと同じように仕込んだStatも使えると。


ひと区切り

描画パスの挿入が出来ることが分かり、最低限の満足度は得られました。

意外といいですね。

ここまで出来るならエンジン改造ではインターフェースを用意して、実装はプラグインに逃がしてあげた方が開発イテレーション良いかもしれませんね。

実装

さて、この後は私が飽きるまで関数とか仕様の説明を、パラメータを流し込む部分を作りながら進めていきます。

流れはこんな感じです。

  1. アウトラインの設定を纏めた構造体を作成
  2. アウトラインの設定をWorldSubsystemでやり取り
  3. アウトラインの設定を操作する適当なアクターを作成
  4. アウトラインパスとシェーダーの作成

Source/OutlineRenderPipeline/Public/OutlineSettings.h

輪郭線のパラメータは構造体に纏めておきます。
エンジンのポストプロセス設定と同じスタイルです。

個別に実装しちゃうと、パラメータをやり取りする際に怠いことになるので、纏めています。

  1. アクターのプロパティから設定を書き換えて
  2. それをサブシステムに渡して
  3. 描画機能ではサブシステムから設定を取り出す

editconditionを指定するとパラメータの有効性をチェックボックスで操作できるようになります。
後述するパラメータの上書き実装もラクにできるので、結構おすすめです。

githubで実装を見る

Source/OutlineRenderPipeline/Private/OutlineSettings.cpp

ドシンプルコンストラクタです。

githubで実装を見る

Source/OutlineRenderPipeline/Public/OutlineSubsystem.h

描画機能と輪郭線の設定はサブシステムに持たせてみました。

githubで実装を見る

Source/OutlineRenderPipeline/Private/OutlineSubsystem.cpp

普通に描画機能の実体を作成したり、アクセサを用意したりです。

OverrideOutlineSettingsは、マクロでコーディングをサボっています。

githubで実装を見る

Source/OutlineRenderPipeline/Public/OutlineControlConsoleActor.h

輪郭線の設定をプロパティからするためだけに用意したアクタークラスです。
他のやり方をシンプルに知らないというクソザコです。

githubで実装を見る

Source/OutlineRenderPipeline/Private/OutlineControlConsoleActor.cpp

エディタ上でパラメータ調整した際はOnConstructionのイベントで変更をサブシステムに伝達、ゲーム時はBeginPlayで伝達といった感じです。

githubで実装を見る

Source/OutlineRenderPipeline/Public/OutlineViewExtension.h

SetupViewFamilyとSetupViewとBeginRenderViewFamilyは抽象型なので使わなくてもこんな感じで書いておいてください。書かないと当然、怒られます。

パラメータのやりとりの理想は、取得をGameThreadのSetupで行って、取得内容を元にRenderThreadでアレコレです。UEは基本的にスレッドセーフな設計なので雑に書いても大丈夫ですが、描画まわりに関しては例外的なことがあるので、若干注意です。しかもその例外ケースが開発プラットフォームによって発生したりするのがまたネックなポイント。今回は触り程度なのでこの辺り、結構適当です。なので危ないことしてるかも。

githubで実装を見る

Source/OutlineRenderPipeline/Private/OutlineViewExtension.cpp

さぁ、メインの区間がやってきました。

端から端まで説明すると途中で飽きそうですが、とりあえず飽きるまではやってみます。

地味にいっつも説明省いてたのよね。面倒だから。


シンプルなシェーダーのフォーマットです。
UEのマテリアルを使用したい場合はFGlobalShader以外を継承する必要があるのですが、ポストエフェクトや計算用のコンピュートシェーダー系は、FGlobalShaderを使えば大体イケます。

githubで実装を見る(L14-L34)


ShouldCompilePermutationは、このシェーダーを使えるプラットフォームとかに縛りを入れられるやつです。
今回だとShadingModel5とShadingModel6だけ許可しています。つまりPC版なら大体おっけい、OpenGLやMaliのモバイル系は怪しいといった感じです。

githubで実装を見る(L30-L33)


シェーダーで扱えるパラメータは、BEGIN_SHADER_PARAMETER_STRUCTからEND_SHADER_PARAMETER_STRUCTの間で宣言します。

githubで実装を見る(L20-L28)


Viewは、ビューポートサイズやバッファサイズ、フレームインデックスとか、汎用的なパラメータが入っています。

githubで実装を見る(L21)


SceneTextureは、GBufferとSceneColorとSceneDepth(DeviceDepth)とか、バッファ系が全部入っています。地味に注意な点が、SceneColorも入っているので、RTとしてSceneColorを使用する場合は重複する危険性があります。ので今回は出力用のテクスチャを作成して、最後にコピーパス挿入といったフローを取っています。

githubで実装を見る(L22)


残りは普通のパラメータですね。
ちょっと詳しい方なら『float4で纏めなくていいの?』と思うことでしょう。
ふふふ。UEさん、その辺りはすごいんですよ。内部でパディングを合わせてくれています。むしろ人間様が勝手にレイアウト調整すると無駄が発生するので、全任せするべきです。この辺りは商業エンジンの強みというか、凄いなぁと感じますね。

githubで実装を見る(L23-L26)


上記で実装した子のシェーダーファイルのパス指定とエントリーポイントの指定、シェーダーの種類を指定しています。
FOutlinePSは/Plugin/OutlineRenderPipeline/Private/Outline.usfにあって、エントリーポイントはMainPSで、ピクセルシェーダーですよー、って感じ。

githubで実装を見る(L36)


サブシステムからパラメータを取り出す必要があるので、サブシステムの有効性チェックと輪郭線の有効性チェックをしています。

githubで実装を見る(L49-L59)


このスコープ内で発生する描画イベントはOutlineというグループで区切りますよ、という感じ。
RDG_GPU_STAT_SCOPEはご存じStatです。

githubで実装を見る(L66-L67)


前述の通りSceneTexturesにSceneColorが入っていてRenderTargetに指定すると重複状態になるので、SceneColorと同じバッファサイズで出力用のテクスチャを作っています。
厳密に言えばSceneTexturesに含まれていても、シェーダー側で宣言しなければ問題なかったはずだけど、今回はSceneColor使いたかったのでどのみちです。
コピーコストの無駄を考えるとブレンドモードでやりくりするべきですけど、今回は最適化が主軸ではないので見逃してくださいな。

ちなみにテクスチャ名の付け方の規則として、その区間の名前ドット(.)なになに、がエンジンでは一般的です。

githubで実装を見る(L72-L78)


フルスクリーンシェーダーを作りたい場合の頂点シェーダーのプリセットがFScreenVSです。
後のシェーダーファイル区間で触れますが、ビューポート座標からバッファ座標に変換した値を返してくれたりとか、基本的な変換を済ませてくれる都合のいいヤツです。

githubで実装を見る(L82)


先ほど宣言したシェーダーパラメータに値を詰めている箇所です。

githubで実装を見る(L85-L91)


アウトプットテクスチャをレンダーターゲットの0番目として指定している箇所です。
LoadActionにClearを指定しているので、クリアしてから書き込みが行われます。
クリア値は先ほどのOutputTextureDescにClearValueという要素があるので、そこで指定できます。
今回はSceneColorのクリア値をそのまま流用しているので、たぶんブラックです。

githubで実装を見る(L92)


このパスで使用しないテクスチャをBlackDummyと呼ばれる真っ黒な適当なテクスチャに置き換えている箇所です。
テクスチャスロットに前回使ったものがそのままセットされているからそれを上書きとかだったかな、これはおまじない的なアレなので、あんまり意味は深くまで追っていないです。

githubで実装を見る(L94-L97)


パスを積んでいる箇所です。
ブレンドモードの指定をしたり、深度とステンシルの設定、入出力のビューポートサイズだったり、色々ですね。

githubで実装を見る(L99-L109)


OutputTextureに書き込まれた結果をSceneColorにコピーしている箇所です。
1枚コピーするだけならこの関数を使うのがラクです。
複数枚ある場合は自分でシェーダー作ってRT複数一気に書き込むといいでしょう。

githubで実装を見る(L114)


ちょっと巻き戻る。
OutputTextureは書き込み先であるレンダーターゲットに指定したり、コピーパスでフェッチしたりするから、TexCreate_RenderTargetableTexCreate_ShaderResourceの両方指定が必要です。
仮に忘れても親切設計で実行時にフラグ指定よろしく♪とエラーの案内出るから説明不要かもだけど一応。

githubで実装を見る(L76)

Shaders/Private/Outline.usf

汎用関数とSceneTexturesで使用できる各種テクスチャとサンプラの宣言がされているushをインクルードしています。

githubで実装を見る(L7-L9)


パラメータ宣言のシェーダー側ですね。
たしか変数名で判別していたので、順番は不定でも大丈夫なはずです。
可読性の観点から普通は合わせるけどね。

githubで実装を見る(L19-L22)


FScreenVertexOutputから取得するUVはバッファ座標です。
ビューポート座標じゃないので注意です。

githubで実装を見る(L28)


クランプする座標は端っこから1ピクセル内側にしています。
これは輪郭線の書き方次第なので、必要に応じて変えればいいと思います。

githubで実装を見る(L29)


SceneDepthを取ってきてくれています。
バイス深度が欲しい場合は、LookupDeviceZSceneDepthTextureを直に使ってフェッチしてください。

githubで実装を見る(L32)


ここバッファとビューポート座標混在しているので注意です。
オフセット幅はビューポート基準で計算しているので変換を挟んでいて、基準となる座標はバッファ基準だから変換不要です。
オフセット幅の変換をサボると画面サイズが変わるたびに輪郭線の太さが変わるというバグが起きるので、変換忘れずにです。

githubで実装を見る(L38)


私の環境だとSceneColorのAlphaを見ることないから1.0固定だけど、使う場合はちゃんとSceneColorを継続にしてあげてください。
CalcSceneColorはRGBなので、CalcFullSceneColorでRGBA取ってきてください。

githubで実装を見る(L53)

Source/OutlineRenderPipeline/Public/OutlineRenderPipelineModule.h

描画機能の所有権をサブシステムに譲渡したので、中身すっからかんのモジュールちゃんです。

githubで実装を見る

Source/OutlineRenderPipeline/Private/OutlineRenderPipelineModule.cpp

特筆すべき点は、シェーダーフォルダを追加しているぐらいですね。

githubで実装を見る

Source/OutlineRenderPipeline/OutlineRenderPipeline.Build.cs

モジュールの依存関係部分ですね。

#include "PostProcess/PostProcessing.h"とあともう1つはゴメン忘れた、これはRendererフォルダに入っているヤツなので、Path.Combine(GetModuleDirectory("Renderer"), "Private")を追加しないとダメです。

githubで実装を見る

OutlineRenderPipeline.uplugin

シェーダーの読込が発生するため、LoadingPhaseをDefaultからPostConfigInitに変更しています。

なので理想はシェーダーまわりとランタイムでモジュール分割して、読込タイミングをそれぞれにするべきです。今回は面倒ね。

githubで実装を見る

パラメータ調整してみる

BP作って
パラメータ調整できた
無意味に線の色変えたり

おわり!!!

プラグインで描画機能にアクセスするの検証以外では久々だったので結構楽しめました。

パラメータのやり取りをもう少しスマートに出来たらより楽しかったですね。

実装はサブモジュールで見れるはずです。たぶん。


ん-、疲れた。

次回はエンジン改造編、と言いたいところですが、せっかくやるならアウトラインマスクをしたいですね。

ちょっと遠回りになりますが、間に3本挟みます。

  1. シェーディングモデルの追加
  2. アウトラインマスクを格納するマテリアルピンの追加
  3. 空いているGBufferにアウトラインマスクを格納する

この前提実装を終えてようやくアウトライン エンジン改造編に突入です。

恋愛アドベンチャーゲームに比べたら少ない選択肢ですけどねー。

雑談

ここから先は年齢制限のかかっている作品を取り扱うページとなります。


表示する

目次


  • ユイちゃんが悪い顔して悪いことしてる!!! - 2024/08/12 (月)
  • アクリルジオラマメロンブックス限定版 - 2024/08/12 (月)
  • 抱き枕の本体をいつか買いたいかも - 2024/08/12 (月)
  • シーラブを遊ぶ宣言 - 2024/08/12 (月)
  • 5本ぐらい記事を書きたい気持ち - 2024/08/13 (火)
  • 履行失敗 - 2024/08/17 (土)


ユイちゃんが悪い顔して悪いことしてる!!! - 2024/08/12 (月)


ギャラリーの2列目の5行目のCG絵です。


ああああああああああああああああああ。

みなt、じゃなくて、ユイちゃん!!

そんな悪そうな表情するんですね!!

素敵です!!


既視感あるなと思ったら湊くんと風莉さんも夜の白鈴学園で窓(夜空)を背景にえっちしてましたね。

この赤枠で囲んだシーンのことね。

ぱれっと利用規約バカ厳しいので全モザで保険。というか厳密にはこれも確かダメなんだっけなぁ。

一応非営利ブログという最終保険があるので、法廷で合うことになっても、微々たる金額で済むことを願いたい。


アクリルジオラマメロンブックス限定版 - 2024/08/12 (月)


う~む。

買おうと思えば当然変えるけど、流石に要らねぇかなぁ。

いや欲しいんだけど、コスパがなぁ。

好きなものにコスパを求めるのは、愛が弱いと思いますが、労働力という対価を払って得た微々たる報酬なので、使える額に限りがあるんですよ。

発売まで数か月もあるので、気が変わることもあるでしょう。

そしたらその日の気分に従って散財しましょうね。ねぇ~。わたし~。


抱き枕の本体をいつか買いたいかも - 2024/08/12 (月)


お引越ししたら買いたいなと思っているわたくし。

https://www.a-and-j.co.jp/item/cate13/20181105-46/

ず~っとブックマークに居座り続けている抱き枕本体ちゃん。


シーラブを遊ぶ宣言 - 2024/08/12 (月)


3連休という絶好の機会をマジで度忘れしてしまった罪人の筆者さん。

コード書くのは結構なことですが、それよりも遊んでください。いい加減に。

せっかく買ったんだから。ねぇ?

そうね、来週というか今週は、プラグイン編を書くことでしょうから、その時に読み上げツールを片手に作業しましょう。

ということで未来の予約をしておきます。

さぁ、書いたんだから、やれよな。わたし。

んじゃ、今日の私はこれでお休み。


あ、待って。

やるべきことが落ち着いたらこれ見といて、わたし。

よし、おやすみ。


5本ぐらい記事を書きたい気持ち - 2024/08/13 (火)


現状は5本構成ですわ。

ブログを書きたい欲求が大爆発中なの。

あるよね、欲を抑えられない時。


履行失敗 - 2024/08/17 (土)


文章を書きながらエロゲを遊べますか?

答えはノーですわ。

まぁいいですわ。

今はエロゲ欲よりもブログ欲が勝っているの。

この欲がいつまで持つか知らんけど、欲求には素直に従うべきと思う筆者さん、直近の攻略は諦めました。

というかセレオブの脳内再生機能が暫く動作する関係で、新規プレイで物語を取り込んじゃうと多分記憶パンクしちゃう。

筆者さん現実世界の人間が関連する記憶力はゴミカス過ぎるんだけど、コーディングとエロゲに関しては割と誇れる。

えっへん。


買うかも買わんかも - 2024/08/17 (土)


組み合わせのメモ。

店舗特典情報 - 夢幻のティル・ナ・ノーグ

ソフマップは特別限定版。

Amazonは通常版、絵がめっちゃエッチで好き。

駿河屋は通常版、構図が好き、ただ駿河屋は発売日にどうせ届かないから、ココ単体で買うのは絶対ダメ。

大体4万くらい。

オトメきの店舗特典次第なところだから、保留かな。

ただなー、別に、タペストリー買ったところで、飾らんのよな。

なんのために買ってるんだろう。

まー好きなことに対して意味を問いて合理性を持たせたところでしょうもないので、別にいいか。

理由なんて好きだから&趣味の一言で片付くことだし。

ソフマップの予約一覧見れば新規タイトルの把握ラクかも。新発見。