Unity Fogの処理を追う。builtin_shaders調査の手順メモ
UNITY_TRANSFER_FOGの処理で調査した事のメモです。また、builtin_shaders調査の手順メモ。
builtin_shaders-2019.3.9f1をDLして調査。 unity3d.com
Create > Shader > Unlitで生成される最低限のshaderコードと比較しながら調べています。 生成されたコードはこれ。
Shader "Unlit/NewUnlitShader" { Properties { _MainTex ("Texture", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag // make fog work #pragma multi_compile_fog #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; UNITY_FOG_COORDS(1) float4 vertex : SV_POSITION; }; sampler2D _MainTex; float4 _MainTex_ST; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); UNITY_TRANSFER_FOG(o,o.vertex); return o; } fixed4 frag (v2f i) : SV_Target { // sample the texture fixed4 col = tex2D(_MainTex, i.uv); // apply fog UNITY_APPLY_FOG(i.fogCoord, col); return col; } ENDCG } } }
UNITY_FOG_COORDSとは?
こんな感じで頂点データの出力に記述されている。
struct v2f { float2 uv : TEXCOORD0; UNITY_FOG_COORDS(1) float4 vertex : SV_POSITION; };
define UNITY_FOG_COORDS(
で全ファイルを検索。下記が該当箇所。
#if defined(FOG_LINEAR) || defined(FOG_EXP) || defined(FOG_EXP2) #define UNITY_FOG_COORDS(idx) UNITY_FOG_COORDS_PACKED(idx, float1)
UNITY_FOG_COORDS_PACKED
の処理はすぐ上にある。
#define UNITY_FOG_COORDS_PACKED(idx, vectype) vectype fogCoord : TEXCOORD##idx;
UNITY_FOG_COORDS(1) と記述した箇所に、
float fogCoord TEXCOORD1;
を追加するのみ。 FOG_LINEAR、FOG_EXP、FOG_EXP2のどれかのフラグが立っていない場合は、何も定義は追加されない。 フラグが立っている時だけ記述を追加する最適化のため。
UNITY_TRANSFER_FOGとは?
こんな感じで、頂点shaderで使用される。ここの UNITY_TRANSFER_FOG
を調べる。
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
define UNITY_TRANSFER_FOG(
で全ファイルから検索。コメント入れた部分の真下が該当箇所。
#if defined(FOG_LINEAR) || defined(FOG_EXP) || defined(FOG_EXP2) #define UNITY_FOG_COORDS(idx) UNITY_FOG_COORDS_PACKED(idx, float1) #if (SHADER_TARGET < 30) || defined(SHADER_API_MOBILE) // mobile or SM2.0: calculate fog factor per-vertex // shader modelが3.0より下、もしくはモバイル用。頂点単位でfogかける /* → */ #define UNITY_TRANSFER_FOG(o,outpos) UNITY_CALC_FOG_FACTOR((outpos).z); o.fogCoord.x = unityFogFactor #define UNITY_TRANSFER_FOG_COMBINED_WITH_TSPACE(o,outpos) UNITY_CALC_FOG_FACTOR((outpos).z); o.tSpace1.y = tangentSign; o.tSpace2.y = unityFogFactor #define UNITY_TRANSFER_FOG_COMBINED_WITH_WORLD_POS(o,outpos) UNITY_CALC_FOG_FACTOR((outpos).z); o.worldPos.w = unityFogFactor #define UNITY_TRANSFER_FOG_COMBINED_WITH_EYE_VEC(o,outpos) UNITY_CALC_FOG_FACTOR((outpos).z); o.eyeVec.w = unityFogFactor #else // SM3.0 and PC/console: calculate fog distance per-vertex, and fog factor per-pixel // shader model3.0以上かつpc/console用。fog distanceは頂点ごと、fog factorはpixelごと /* → */ #define UNITY_TRANSFER_FOG(o,outpos) o.fogCoord.x = (outpos).z #define UNITY_TRANSFER_FOG_COMBINED_WITH_TSPACE(o,outpos) o.tSpace2.y = (outpos).z #define UNITY_TRANSFER_FOG_COMBINED_WITH_WORLD_POS(o,outpos) o.worldPos.w = (outpos).z #define UNITY_TRANSFER_FOG_COMBINED_WITH_EYE_VEC(o,outpos) o.eyeVec.w = (outpos).z #endif #else #define UNITY_FOG_COORDS(idx) // 上記以外 /**/#define UNITY_TRANSFER_FOG(o,outpos) #define UNITY_TRANSFER_FOG_COMBINED_WITH_TSPACE(o,outpos) #define UNITY_TRANSFER_FOG_COMBINED_WITH_WORLD_POS(o,outpos) #define UNITY_TRANSFER_FOG_COMBINED_WITH_EYE_VEC(o,outpos) #endif
今回はmobileを調査。最初の箇所を更に深堀りしていく。
マクロの中にマクロがあるので、その中のUNITY_CALC_FOG_FACTOR
を調査する。
UNITY_CALC_FOG_FACTORとは?
define UNITY_CALC_FOG_FACTOR(
で検索。
#define UNITY_CALC_FOG_FACTOR(coord) UNITY_CALC_FOG_FACTOR_RAW(UNITY_Z_0_FAR_FROM_CLIPSPACE(coord))
が該当箇所。
また中にマクロがある。先にUNITY_Z_0_FAR_FROM_CLIPSPACE
を調べる。
UNITY_Z_0_FAR_FROM_CLIPSPACEとは?
define UNITY_Z_0_FAR_FROM_CLIPSPACE(
で検索。下記が該当箇所。
#if defined(UNITY_REVERSED_Z) #if UNITY_REVERSED_Z == 1 //D3d with reversed Z => z clip range is [near, 0] -> remapping to [0, far] //max is required to protect ourselves from near plane not being correct/meaningfull in case of oblique matrices. #define UNITY_Z_0_FAR_FROM_CLIPSPACE(coord) max(((1.0-(coord)/_ProjectionParams.y)*_ProjectionParams.z),0) #else //GL with reversed z => z clip range is [near, -far] -> should remap in theory but dont do it in practice to save some perf (range is close enough) #define UNITY_Z_0_FAR_FROM_CLIPSPACE(coord) max(-(coord), 0) #endif #elif UNITY_UV_STARTS_AT_TOP //D3d without reversed z => z clip range is [0, far] -> nothing to do #define UNITY_Z_0_FAR_FROM_CLIPSPACE(coord) (coord) #else //Opengl => z clip range is [-near, far] -> should remap in theory but dont do it in practice to save some perf (range is close enough) #define UNITY_Z_0_FAR_FROM_CLIPSPACE(coord) (coord) #endif
フラグらしきUNITY_REVERSED_Z
があるので調査。
UNITY_REVERSED_Z とは?
注: DX11 / 12、PS4、XboxOne、およびMetalでは、Zバッファーの範囲は1〜0で、UNITY_REVERSED_Zが定義されています。他のプラットフォームでは、範囲は0〜1です。 docs.unity3d.com
今回はMetalを前提とする。metalはUNITY_REVERSED_Zフラグが立っている。 その中の処理に進む
//D3d with reversed Z => z clip range is [near, 0] -> remapping to [0, far] //max is required to protect ourselves from near plane not being correct/meaningfull in case of oblique matrices. // 斜行行列の場合、ニアプレーンが正しくない/意味がないことから身を守るためにmaxが必要です。 #define UNITY_Z_0_FAR_FROM_CLIPSPACE(coord) max(((1.0-(coord)/_ProjectionParams.y)*_ProjectionParams.z),0)
今度は_ProjectionParamsというパラメータが出てきたので調べる。
_ProjectionParamsとは?
カメラのnear, far値が取得できるようです。
型はfloat4。xは1.0(反転投影行列でレンダリングしている場合は-1.0),yはカメラのnear plane,zはカメラの far plane,wは1 / FarPlaneです. docs.unity3d.com
UNITY_CALC_FOG_FACTOR
まで戻ると、今度はUNITY_CALC_FOG_FACTOR_RAW
マクロがあるので、これを調べる。
UNITY_CALC_FOG_FACTOR_RAWとは?
define UNITY_CALC_FOG_FACTOR_RAW(
で検索。
下記が該当。
#if defined(FOG_LINEAR) // factor = (end-z)/(end-start) = z * (-1/(end-start)) + (end/(end-start)) #define UNITY_CALC_FOG_FACTOR_RAW(coord) float unityFogFactor = (coord) * unity_FogParams.z + unity_FogParams.w #elif defined(FOG_EXP) // factor = exp(-density*z) #define UNITY_CALC_FOG_FACTOR_RAW(coord) float unityFogFactor = unity_FogParams.y * (coord); unityFogFactor = exp2(-unityFogFactor) #elif defined(FOG_EXP2) // factor = exp(-(density*z)^2) #define UNITY_CALC_FOG_FACTOR_RAW(coord) float unityFogFactor = unity_FogParams.x * (coord); unityFogFactor = exp2(-unityFogFactor*unityFogFactor) #else #define UNITY_CALC_FOG_FACTOR_RAW(coord) float unityFogFactor = 0.0 #endif
fogには3つのモードがあり、 FOG_LINEAR、FOG_EXP、FOG_EXP2がある。
今回はFOG_EXPを調べる。
// factor = exp(-density*z) #define UNITY_CALC_FOG_FACTOR_RAW(coord) float unityFogFactor = unity_FogParams.y * (coord); unityFogFactor = exp2(-unityFogFactor)
全てのマクロを展開する
UNITY_TRANSFER_FOG(o, o.vertex);
は、
float z = max(((1.0-(o.vertex.z) / _ProjectionParams.y) * _ProjectionParams.z), 0); float unityFogFactor = unity_FogParams.y * z; unityFogFactor = exp2(-unityFogFactor); o.fogCoord.x = unityFogFactor;
となる。
UNITY_TRANSFER_FOG(o, o.vertex);
を置き換えて、挙動が変化しない事を確認。 (window > Rendering > lighting settingsのfog modeもチェックする事)
PostProcessingStack v2を使い、前フレームの画像と合成するmotion blurを実装する
1フレーム前の画像を取っておき、それをレンダリング中の画像と合成するpost effectを実装します。
shader
やっている事は単純で、流し込まれた前フレームの画像をアルファ合成しているだけです。
Shader "MotionBlurLegacyHLSL" { HLSLINCLUDE #include "Packages/com.unity.postprocessing/PostProcessing/Shaders/StdLib.hlsl" TEXTURE2D_SAMPLER2D(_MainTex, sampler_MainTex); TEXTURE2D_SAMPLER2D(_PrevFrameTex, sampler_PrevFrameTex); float _Alpha; #define SAMPLES_INT 6 float4 Frag(VaryingsDefault i) : SV_Target { half4 dst = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.texcoord); half4 prevColor = SAMPLE_TEXTURE2D(_PrevFrameTex, sampler_PrevFrameTex, i.texcoord); half4 color = prevColor * _Alpha + dst * (1 - _Alpha); return color; } ENDHLSL SubShader { Cull Off ZWrite Off ZTest Always Pass { HLSLPROGRAM #pragma vertex VertDefault #pragma fragment Frag ENDHLSL } } }
c#
using System; using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.PostProcessing; namespace White { [Serializable] [PostProcess(typeof(MotionBlurLegacyRenderer), PostProcessEvent.AfterStack, "Custom/MotionBlurLegacyHLSL", false)] public sealed class MotionBlurLegacy : PostProcessEffectSettings { [Range(0f, 0.99f)] public FloatParameter alpha = new FloatParameter {value = 0f}; public override bool IsEnabledAndSupported(PostProcessRenderContext context) { if (enabled.value) { if (alpha == 0) { return false; } return true; } return false; } } internal sealed class MotionBlurLegacyRenderer : PostProcessEffectRenderer<MotionBlurLegacy> { Shader shader; private RenderTexture _prevFrameRt; private RenderTargetIdentifier _rtId; public override void Init() { float scale = 1; int width = Mathf.CeilToInt(Screen.width * scale); int height = Mathf.CeilToInt(Screen.height * scale); _prevFrameRt = new RenderTexture(width, height, 24, RenderTextureFormat.ARGBHalf); _prevFrameRt.useMipMap = false; _prevFrameRt.filterMode = FilterMode.Bilinear; _prevFrameRt.name = "prev_frame"; _prevFrameRt.Create(); _rtId = new RenderTargetIdentifier(_prevFrameRt); shader = Shader.Find("MotionBlurLegacyHLSL"); } public override void Render(PostProcessRenderContext context) { PropertySheet sheet = context.propertySheets.Get(shader); sheet.properties.SetFloat("_Alpha", settings.alpha); sheet.properties.SetTexture("_PrevFrameTex", _prevFrameRt); context.command.BlitFullscreenTriangle(context.source, context.destination, sheet, 0); context.command.Blit(null, _rtId); } } }
どうやって前フレームのキャプチャを渡すのか、というのが分かると簡単です。
まず、Initでキャプチャ用のRenderTextureを生成しておきます。 RenderTargetIdentifierを使ってcommandbufferにidで渡せるようにするのがポイント。
そして
context.command.BlitFullscreenTriangle()
した後に、
context.command.Blit(null, _rtId);
してレンダリング結果を保存します。後は
sheet.properties.SetTexture("_PrevFrameTex", _prevFrameRt);
でshaderに渡すだけです。
Timelineに設定したmarkerをプログラム上で取得する
下記コードで取得できます。
private void GetMarkers() { PlayableDirector director = GetComponent<PlayableDirector>(); IEnumerable<TrackAsset> tracks = ((TimelineAsset) director.playableAsset).GetOutputTracks(); foreach (TrackAsset trackAsset in tracks) { foreach (IMarker marker in trackAsset.GetMarkers()) { Debug.Log($"marker.time: {marker.time}"); } } }
Physical Cameraを使用した時にも使える、視錐台のサイズの求め方
下記に視錐台のサイズの求め方が載っています。 docs.unity3d.com
しかしカメラのPysical Cameraを使用している場合、上のドキュメントのやり方では求められません。 docs.unity3d.com
その場合、下記で求まります。
Vector3[] corners = new Vector3[4]; Camera.main.CalculateFrustumCorners(new Rect(0, 0, 1, 1), _distance, Camera.MonoOrStereoscopicEye.Mono, corners); float frustumHeight = (corners[0] - corners[1]).magnitude; float frustumWidth = (corners[1] - corners[2]).magnitude;
CalculateFrustumCorners
という、そのものズバリなメソッドが用意されているんですね。
これで物理カメラの使用有無に関わらず、視錐台のサイズを求める事が出来ます。
関連
常に全画面表示されるオブジェクトを実装する
カメラが動いても、常に全画面に表示される処理を書きます。
独自のUIなどを表示する時に使えるかもしれません。
コード
using UnityEngine; public class FitDisplay : MonoBehaviour { [SerializeField] private Transform _sprite; [SerializeField] private float _distance = 10; //カメラからの距離 // spriteの一変。このsampleでは500px x 500pxの画像を表示、pixel per unit: 100なので5mの大きさになっている private float _spriteHeight = 5; private float _spriteWidth = 5; private Vector3 _defaultSpriteScale = Vector3.one; void Update() { // https://docs.unity3d.com/jp/current/Manual/FrustumSizeAtDistance.html // cameraから_distance手前にある時のfrustumHeightを求める float frustumHeight = 2.0f * _distance * Mathf.Tan(Camera.main.fieldOfView * 0.5f * Mathf.Deg2Rad); float frustumWidth = frustumHeight * Camera.main.aspect; float rateX = frustumWidth / _spriteWidth; float rateY = frustumHeight / _spriteHeight; // 画面いっぱいに広がるようにscaleを調整する _sprite.localScale = new Vector3(_defaultSpriteScale.x * rateX, _defaultSpriteScale.y * rateY, 0); Matrix4x4 viewMatInv = Camera.main.worldToCameraMatrix.inverse; // cameraから見た位置 // カメラの前方は負のZ軸 // https://docs.unity3d.com/ScriptReference/Camera-worldToCameraMatrix.html Vector4 viewPos = new Vector4(0, 0, -_distance, 1); Vector4 worldPos = viewMatInv * viewPos; _sprite.transform.position = worldPos; _sprite.transform.rotation = Camera.main.transform.rotation; } }
解説
視錐台の高さを求める
まず、カメラからの距離から視錐台の高さを求めます。
求め方は公式ドキュメントにも載っています。 docs.unity3d.com
画面いっぱいになるように、scale値を調整します。
常にカメラの前に居るようにする
カメラのworldToCameraMatrixの逆変換をして、カメラの前のworld座標を求めます。
カメラ側を向くよう、回転もカメラと合わせて終わりです。
Timelineでshuriken effectを使う時、再生中にeffectが表示されなくなる問題
環境
Unity 2019.3.0f6
Timeline 1.2.11
現象
TimelineのControl Trackを使ってshuriken effectを表示します。
パフォーマンスのために、Project Settings - Timeの設定でFixed Timestepの値を大きくする事がありますが、この値を大きくしてしまうと、Timelineのeffect再生に影響が出てしまいます。
エディター停止中に再生させると問題ないので、かなりハマってしまいました。
内部的にFixedUpdateを使っているんですね。
DDD ドメイン駆動設計入門 〜 基礎的な用語を理解する
エリック・エヴァンスのドメイン駆動設計
https://www.amazon.co.jp/dp/4798121967
この書籍を理解するにあたって、基礎となる用語を理解するために纏めました。
ドメイン エキスパート
扱っているテーマについて誰よりも良く知っている人。
例えば、ゲーム開発等だと仕様について把握している人。
何かの機能を作るとした場合、その機能を考えたディレクター・プランナーがドメインエキスパート。
ユビキタス言語
ユビキタス言語とは、ドメインエキスパートやソフトウェア開発者を含めたチーム全体で作り上げる共通言語のこと。
- 実践ドメイン駆動設計p.20
ユビキタス言語やソースコードは日々変化し続けます。そのため、ユビキタス言語を用語集化しようとドキュメントを作るのは現実的ではありません。ある用語に説明が必要だと思ったときは、用語集を作るのではなくそれを表すソースコード上のモデルにコメントを書くほうが現実的です。
- わかる! ドメイン駆動設計 p13
名前のない概念やあいまいな概念をはっきりさせ、名前をつけてユビキタス言語として確立する。
ドメイン
一言で言うのは難しい。引用すると、
ドメインは「領域」の意味をもった言葉です。ソフトウェア開発におけるドメインは、「プログラムを適用する対象となる領域」を指します。重要なのはドメインが何かではなく、ドメインに含まれるものが何かです。
たとえば会計システムを例にしてみましょう。会計の世界には金銭や帳票といった概念が登場します。これらは会計システムのドメインに含まれます。物流システムであればどうでしょうか。会計システムとは打って変わって貨物や倉庫、輸送手段などの概念が存在し、それらがそのまま物流システムのドメインに含まれます。このようにドメインに含まれるものはシステムが対象とするものや領域によって大きく変化します。
成瀬 允宣. ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本 (Japanese Edition) (Kindle の位置No.465). Kindle 版.
すべてのソフトウェアプログラムは、それを使用するユーザーの何らかの活動や関心と関係がある。
ユーザーがプログラムを適用するこの対象領域が、ソフトウェアのドメインである。
組織が行う事業やそれを取り巻く世界のことだ。
事業が市場を定義して、プロダクトやサービスを販売する。組織にはそれぞれ、自分たちの対象とする領域についてのノウハウや物事の進めかたがある。
その領域、そして業務を進めていくための方法が、ドメインだ。
- 実践ドメイン駆動設計p.41
開発するアプリケーションの対象となる領域。
例えば、銀行業務システムを作るとしたら、銀行業務のドメインを理解する必要がある。
会計ソフトなら、会計業務。
コアドメイン・サブドメイン
たとえば会計ソフトを開発しているとしましょう。このソフトウェアのドメインは会計業務ですが、内部の機能をみていくと、簿記をつける機能や印刷機能、データをグラフで表示する機能などがあります。
ソフトウェアの価値をもたらす部分、ビジネスの問題を解決する部分がコアドメインですから、上記の会計ソフトなら簿記をつける機能がコアドメインとなるでしょう。
コアドメイン - ビジネスの問題を解決する部分
サブドメイン - 本質と無関係な部分
ドメインモデル
ドメインを表現したモデル。OOPなら、プログラム中のオブジェクトモデルとして表現される。
事象、あるいは概念を抽象化する作業がモデリングと呼ばれる。
ドメインモデルをコードに落とし込む段階で、考慮漏れやより良いモデル、ユビキタス言語を見つけることがあります。この場合、必ずドメインモデルやユビキタス言語にフィードバックし、常にコードが正確にドメインモデルを表現するようにします。
ドメイン駆動設計では、設計がコードであり、コードが設計なのです。
-わかる! ドメイン駆動設計 p.18
ドメインロジック
上記ドメインを表現するロジック。ビジネスロジックとも言い換えられる。
そのアプリケーション固有の処理やルールが記述される。
UIやサーバー通信、ライブラリ部分はドメインロジックでは無い。
https://wa3.i-3-i.info/word13666.html
レイヤーの概念
ユーザーインターフェイス (プレゼンテーションレイヤー)
ユーザに対して情報を表示し、ユーザの命令を解釈する。
アプリケーションレイヤー
アプリケーションの活動を調整する薄いレイヤ。ビジネスロジックを含まない。また、ビジネスオブジェクトの状態を保持しない。しかし、アプリケーションの処理の進み具合を保持することがある。
ドメインレイヤー
ドメインについての情報を含むレイヤ。業務ソフトウエアの心臓部。ビジネスオブジェクトの状態を保持する。ビジネスオブジェクトの永続化処理をインフラストラクチャ層に委託する。まれにビジネスオブジェクトの状態もインフラストラクチャ層に委託することがある。
インフラストラクチャレイヤー
他のすべてのレイヤを補助するライブラリとして働く。レイヤ間の情報のやり取りを制御し、ビジネスオブジェクトの永続化を実装する。また、ユーザインターフェイス等を補助するライブラリを含む。
エンティティ
一意性を持つものとして分類できるオブジェクトがあります。ソフトウエアの様々な状態の中で、常に同一であり続けるオブジェクトです。
(中略)
オブジェクトの存在期間はシステムの寿命に及びます。
このようなオブジェクトをエンティティと呼びます。
- domain-driven-design-quickly.pdf p26
アプリの実行中に存在する一意なオブジェクト。
一意なIDなどが付く。
バリューオブジェクト
エンティティに対して、一意ではないオブジェクト。
バリューオブジェクトは変更できないようにするべき。コンストラクタで作成された後、破棄されるまで変更できないようにする。
一時的なデータ受け渡し用オブジェクトなど。
ここに厳守すべきルールが出来上がります。つまりバリューオブジェクトが共有可能ならば、変更不可能にすること。バリューオブジェクトは小さくてシンプルなオブジェクトにすること。他のオブジェクトがバリューオブジェクトを必要とするときは、単純に値だけを渡してやるか、バリューオブジェクトそのものをコピーして渡してやること。
- domain-driven-design-quickly.pdf p29
サービス
ドメインにはどのオブジェクトにも属さないような振る舞いが幾つもある。
そのような場合、無理にエンティティやバリューオブジェクトに機能を実装せず、サービスに実装する。
ドメインにこのような振る舞いが見つかった場合、最もよい方法はこの振る舞いをサービスとして定義することです。サービスは内部に状態を保持せず、単純に機能だけを提供します。
サービスが提供する機能は重要です。サービスは特定のエンティティやバリューオブジェクトのための機能をまとめます。サービスはわかりやすく定義したほうがいいでしょう。そうすればドメイン内部の区分が明確になり、概念もカプセル化されます。サービスとして提供される機能を、エンティティやバリューオブジェクトに含めてしまえば混乱が起きるでしょう。このような機能はどのオブジェクトに含めればいいのか明らかでないからです。
- domain-driven-design-quickly.pdf p31
サービスの3つの特徴
- サービスとして作成される操作はドメインの概念をあらわしているが、エンティティやバリューオブジェクトに含めると違和感がある。
- サービスとして作成される操作はドメイン内の他のオブジェクトから参照される。
- サービスとして作成される操作は状態をもたない。
モジュール
ドメインモデルはどんどん大きくなっていく。
ドメインモデルを複数のモジュールの集まりとして再構築する。
モジュール化は関係する概念やタスクを組織して複雑性を低減する方法。
参考書籍
https://booth.pm/ja/items/392260
https://www.infoq.com/jp/minibooks/domain-driven-design-quickly/