Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Docs/pure-base-shader-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,16 @@ Unlit returns the base surface without host direct, baked, ambient, or environme

Toon evaluates a binary direct diffuse response from the surface normal and light direction. Its `ForwardBase` direction combines the Shader-Core direct aggregate with the first-order SH direction, and its ambient result selects a fixed bright or dark SH band from that direction. Shader-Core continues to provide the lightmap input; when Shader-Core supplies the lightmap aggregate, Toon does not synthesize an additional baked-light contribution. `ForwardAdd` contributes direct light only.

### Toon direct-light visibility contract

For `PureBase/Toon`, the per-light `light.color` exposed to the `light` phase is the scene/direct light color multiplied by non-shadow distance, spot, and cookie attenuation. Unity effective visibility is published separately as `sd.shadow` before the `light` phase, so the same value is available to the `modifylight` and `shade` phases. This contract applies across the supported Unity light-kind branches, including directional, point, spot, point-cookie, and directional-cookie inputs.

`sd.shadow` is Unity effective per-light visibility: it includes realtime shadowing, baked occlusion and Shadowmask mixing, and shadow-distance fade wherever Unity enables those behaviors. It is not a raw realtime-only shadow sample. Existing Toon light modules that assumed Shader-Core had already multiplied shadow visibility into `light.color` must migrate to `sd.shadow`.

After the `light` phase, the Toon host consumes `sd.shadow` exactly once while accumulating host-managed direct radiance into `lightSum.color`. It does not apply visibility to aggregate light direction, SH, lightmap, or environment lighting, and it does not consume the value again after that direct-radiance evaluation. This Toon-only ownership adds no second `sd.shadow` consumption to PBR, Hybrid, or Unlit. A module may observe `sd.shadow` for classification or use it for an independent module-owned effect, but it must not multiply host-managed Toon direct radiance or color by `sd.shadow` again. `customlight` remains responsible for the visibility of lights it authors or changes after main-light aggregation; the Toon host does not infer or add that visibility for it.

In the exact `LIGHTMAP_ON && LIGHTMAP_SHADOW_MIXING && !SHADOWS_SHADOWMASK && SHADOWS_SCREEN` case, Shader-Core suppresses the main-light callback, `sd.shadow` remains at its initialized value `1`, and Shader-Core owns Subtractive application exactly once. This Shader-Core Mixed/Subtractive handling does not add a second Toon visibility consumption. Lightmap and SH environment lighting remain separate from direct-light visibility. `ShadowCaster` controls casting only and is unchanged by this receiving-side split; there is no material ABI, render-mode, or pass change.

### PBR

PBR evaluates a continuous metallic BRDF for direct lighting. Its `ForwardBase` also evaluates Unity Standard indirect GI and reflection probes. Its `ForwardAdd` evaluates only the additional direct BRDF contribution.
Expand Down
10 changes: 10 additions & 0 deletions Docs/technical-information.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,16 @@ Hybrid は PBR の経路の中にある既存の2値化直接拡散反射の式

この固定されたホスト動作によって、公開項目、キーワード、パス、バリアント、依存関係は増えません。公開プロパティ ABI は変わらないため、既存のマテリアルを移行する必要はなく、自動的にこの動作を受け取ります。

### Toon の直接光と可視性の契約

`PureBase/Toon` の `light` 差し込み位置へ渡す各ライトの `light.color` は、シーンの直接光の色に、影以外の距離・スポット・クッキー減衰を乗じた値です。Unity のライト単位の実効可視性は `sd.shadow` として分離して公開され、`light` の前に設定されるため、`modifylight` と `shade` からも同じ値を参照できます。この分離は、対応する方向ライト、ポイントライト、スポットライト、ポイントクッキー、方向クッキーの各ライト分岐に適用されます。

`sd.shadow` は、Unity が有効にする範囲で、リアルタイムの影、焼き込みの遮蔽と Shadowmask の混合、影距離によるフェードを含む Unity のライト単位の実効可視性です。リアルタイム影だけを取得した生の値ではありません。`light.color` に影の可視性がすでに乗っていると仮定していた既存の Toon ライトモジュールは、`sd.shadow` を使うように移行する必要があります。

`light` フェーズ後、Toon ホストはホスト管理の直接放射輝度を `lightSum.color` に集計するときに `sd.shadow` を1回だけ消費します。集計ライト方向、SH、ライトマップ、環境光には可視性を適用せず、その直接放射輝度の評価後に同じ値を再び消費しません。この Toon 専用の責務によって、PBR、Hybrid、Unlit に `sd.shadow` を再度消費する処理は追加されません。モジュールは `sd.shadow` を分類のために参照したり、モジュール自身が担当する独立した効果へ使ったりできますが、ホスト管理の Toon 直接放射輝度または色へ `sd.shadow` をもう一度乗じてはいけません。`customlight` はメインライトの集計後に動作するため、自身が作成または変更するライトの可視性を担当します。ホストはこの差し込み位置へ暗黙の可視性を追加しません。

`LIGHTMAP_ON && LIGHTMAP_SHADOW_MIXING && !SHADOWS_SHADOWMASK && SHADOWS_SCREEN` の場合は Shader-Core がメインライトのコールバックを抑制し、`sd.shadow` は初期化値の `1` のままになり、Subtractive の適用は Shader-Core が1回だけ担当します。この Shader-Core の Mixed/Subtractive 処理によって、Toon の可視性がもう一度消費されることはありません。ライトマップと SH の環境光は、直接光の可視性とは分離されています。`ShadowCaster` は影を cast する処理だけを担当し、この受け側の分離によって変わりません。マテリアル ABI、描画モード、パス定義も変更しません。

## 公開の準備と実行

`package.json` が、公開名と版番号を決める唯一の情報源です。
Expand Down
10 changes: 10 additions & 0 deletions Docs/technical-information.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,16 @@ Hybrid retains its unchanged binary direct-diffuse equation inside the PBR path.

This fixed host behavior adds no public property, keyword, pass, variant, or dependency. The public property ABI is unchanged, so existing materials need no migration and receive the behavior automatically.

### Toon direct-light visibility contract

For `PureBase/Toon`, `light.color` in the `light` phase is the scene/direct light color multiplied by non-shadow distance, spot, and cookie attenuation. Unity effective per-light visibility is exposed independently through `sd.shadow` before `light`, `modifylight`, and `shade`. The same split is used for the supported directional, point, spot, point-cookie, and directional-cookie light branches.

`sd.shadow` represents Unity effective visibility, including realtime shadows, baked occlusion and Shadowmask mixing, and shadow-distance fade where enabled. It is not a raw realtime-only sample. Existing Toon light modules that assumed `light.color` was already pre-shadowed must read `sd.shadow` instead.

After the `light` phase, the Toon host consumes `sd.shadow` exactly once while accumulating host-managed direct radiance into `lightSum.color`. It does not apply visibility to aggregate light direction, SH, lightmap, or environment lighting, and it does not consume the value again after that direct-radiance evaluation. This Toon-only ownership adds no second `sd.shadow` consumption to PBR, Hybrid, or Unlit. A module may observe `sd.shadow` for classification or use it for an independent module-owned effect, but it must not multiply host-managed Toon direct radiance or color by `sd.shadow` again. `customlight` owns the visibility of its own lights after main-light aggregation; the host does not add an implicit visibility factor for that phase.

For `LIGHTMAP_ON && LIGHTMAP_SHADOW_MIXING && !SHADOWS_SHADOWMASK && SHADOWS_SCREEN`, Shader-Core suppresses the main-light callback, leaves `sd.shadow` at its initialized value `1`, and applies Subtractive shadowing exactly once. This Shader-Core Mixed/Subtractive handling does not add a second Toon visibility consumption. Lightmap and SH environment lighting remain separate from direct visibility. `ShadowCaster` remains casting-only and unchanged; this receiving-side contract does not change the material ABI, render modes, or pass declarations.

## Release preparation and publication

`package.json` is the sole release identity and version declaration.
Expand Down
17 changes: 13 additions & 4 deletions Shaders/Common/birp_host.hlsl
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,13 @@ void SCCalculateLight(inout SCLightData lightSum, inout SCShadingData sd, inout
{
light.direction = SCModelSelectMainLightDirection(vertex, light.direction);
cd.mainLightDirection = light.direction;
if (SCModelUsesIsolatedMainLightColor())
light.color = cd.mainLightColor * cd.mainLightAttenuation;
SCModelPrepareMainLight(
light,
sd,
cd.mainLightColor,
cd.mainLightAttenuation,
cd.mainLightNonShadowAttenuation,
cd.mainLightShadowVisibility);

__SC_PHASE_light__

Expand All @@ -56,6 +61,7 @@ void SCCalculateEnvironmentLight(inout SCLightData lightSum, inout half3 env, in
}

#include "Packages/jp.lilxyzw.shadercore/ShaderLibrary/birp_lighting.hlsl"
#include "Packages/jp.penguin.purebase/Shaders/Common/birp_light_attenuation.hlsl"

/// <summary>Evaluates the selected model's ForwardBase or ForwardAdd result with all standard pixel phase insertion points.</summary>
half4 frag(v2f input, bool isFront : SV_IsFrontFace) : SV_Target
Expand All @@ -76,9 +82,12 @@ half4 frag(v2f input, bool isFront : SV_IsFrontFace) : SV_Target

SCLightData lightSum = (SCLightData)0;
half3 env = half3(0, 0, 0);
UNITY_LIGHT_ATTENUATION(mainLightAttenuation, input, vertex.position);
half mainLightShadowVisibility = UNITY_SHADOW_ATTENUATION(input, vertex.position);
half mainLightNonShadowAttenuation = PureBaseEvaluateNonShadowLightAttenuation(input, vertex.position);
cd.mainLightColor = _LightColor0.rgb;
cd.mainLightAttenuation = saturate(mainLightAttenuation);
cd.mainLightShadowVisibility = saturate(mainLightShadowVisibility);
Comment thread
PenguinDOOM marked this conversation as resolved.
cd.mainLightNonShadowAttenuation = saturate(mainLightNonShadowAttenuation);
cd.mainLightAttenuation = cd.mainLightNonShadowAttenuation * cd.mainLightShadowVisibility;
cd.mainLightDirection = half3(0, 0, 0);
SCCalculateAllLights(lightSum, env, sd, cd, vertex, input, SCModelSelectVertexLighting(SCVertexLighting(vertex.position)));

Expand Down
59 changes: 59 additions & 0 deletions Shaders/Common/birp_light_attenuation.hlsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright 2026 Penguin
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

// Defines Unity BIRP's non-shadow light attenuation terms for the PureBase fragment host.

#ifndef PUREBASE_BIRP_LIGHT_ATTENUATION_INCLUDED
#define PUREBASE_BIRP_LIGHT_ATTENUATION_INCLUDED

/// <summary>Evaluates the active Unity BIRP light's distance and cookie attenuation without visibility.</summary>
/// <param name="input">The current BIRP fragment input containing light coordinates where Unity requires them.</param>
/// <param name="worldPos">The current world-space pixel position.</param>
/// <returns>The active light's non-shadow attenuation term.</returns>
inline fixed PureBaseEvaluateNonShadowLightAttenuation(v2f input, float3 worldPos)
{
#if defined(DIRECTIONAL)
return 1;
#elif defined(POINT)
unityShadowCoord3 lightCoord = mul(unity_WorldToLight, unityShadowCoord4(worldPos, 1)).xyz;
Comment thread
PenguinDOOM marked this conversation as resolved.
return tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).r;
#elif defined(SPOT)
#if !defined(UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS)
unityShadowCoord4 lightCoord = mul(unity_WorldToLight, unityShadowCoord4(worldPos, 1));
#else
unityShadowCoord4 lightCoord = input._LightCoord;
#endif
return (lightCoord.z > 0) * UnitySpotCookie(lightCoord) * UnitySpotAttenuate(lightCoord.xyz);
#elif defined(POINT_COOKIE)
#if !defined(UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS)
unityShadowCoord3 lightCoord = mul(unity_WorldToLight, unityShadowCoord4(worldPos, 1)).xyz;
#else
unityShadowCoord3 lightCoord = input._LightCoord;
#endif
return tex2D(_LightTextureB0, dot(lightCoord, lightCoord).rr).r * texCUBE(_LightTexture0, lightCoord).w;
#elif defined(DIRECTIONAL_COOKIE)
#if !defined(UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS)
unityShadowCoord2 lightCoord = mul(unity_WorldToLight, unityShadowCoord4(worldPos, 1)).xy;
#else
unityShadowCoord2 lightCoord = input._LightCoord;
#endif
return tex2D(_LightTexture0, lightCoord).w;
#else
return 1;
#endif
}

#endif
7 changes: 7 additions & 0 deletions Shaders/Common/birp_light_attenuation.hlsl.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 8 additions & 4 deletions Shaders/Models/pbr.hlsl
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,12 @@ struct SCCustomData
half reserved;
/// <summary>Stores the Unity wrapper's unattenuated main-light color.</summary>
half3 mainLightColor;
/// <summary>Stores the Unity wrapper's main-light attenuation and shadow factor.</summary>
/// <summary>Stores the Unity wrapper's full main-light attenuation.</summary>
half mainLightAttenuation;
/// <summary>Stores the Unity wrapper's distance and cookie main-light attenuation without visibility.</summary>
half mainLightNonShadowAttenuation;
/// <summary>Stores Unity's effective visibility for the current light, including realtime shadow, mixed or baked occlusion, and fade.</summary>
half mainLightShadowVisibility;
/// <summary>Stores the normalized main-light direction before Shader-Core light-phase modifications.</summary>
half3 mainLightDirection;
};
Expand All @@ -47,10 +51,10 @@ half SCModelEvaluateDirectFactor(SCShadingData shadingData, SCLightData light)
return 1;
}

/// <summary>Requests main-light color replacement because Unity Standard owns the public _LightColor0 declaration.</summary>
bool SCModelUsesIsolatedMainLightColor()
/// <summary>Prepares the Unity Standard main light while keeping Shader-Core visibility neutral.</summary>
void SCModelPrepareMainLight(inout SCLightData light, inout SCShadingData sd, half3 mainLightColor, half mainLightAttenuation, half mainLightNonShadowAttenuation, half mainLightShadowVisibility)
{
return true;
light.color = mainLightColor * mainLightAttenuation;
}

/// <summary>Selects a normalized per-pixel Unity main-light direction before Shader-Core's light phase.</summary>
Expand Down
17 changes: 11 additions & 6 deletions Shaders/Models/toon.hlsl
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,12 @@ struct SCCustomData
half reserved;
/// <summary>Stores the Unity wrapper's unattenuated main-light color.</summary>
half3 mainLightColor;
/// <summary>Stores the Unity wrapper's main-light attenuation and shadow factor.</summary>
/// <summary>Stores the Unity wrapper's full main-light attenuation.</summary>
half mainLightAttenuation;
/// <summary>Stores the Unity wrapper's distance and cookie main-light attenuation without visibility.</summary>
half mainLightNonShadowAttenuation;
/// <summary>Stores Unity's effective visibility for the current light, including realtime shadow, mixed or baked occlusion, and fade.</summary>
half mainLightShadowVisibility;
/// <summary>Stores the normalized main-light direction before Shader-Core light-phase modifications.</summary>
half3 mainLightDirection;
};
Expand All @@ -41,16 +45,17 @@ void SCModelInitializeTangentNormal(inout SCShadingData shadingData)
shadingData.N_detail = shadingData.N;
}

/// <summary>Returns the quantized per-light Toon response after the Shader-Core light phase.</summary>
/// <summary>Returns the quantized per-light Toon response with Unity effective visibility applied once after the Shader-Core light phase.</summary>
half SCModelEvaluateDirectFactor(SCShadingData shadingData, SCLightData light)
{
return PureBaseToonEvaluateDirectFactor(shadingData.N, light.direction);
return PureBaseToonEvaluateDirectFactor(shadingData.N, light.direction) * shadingData.shadow;
}

/// <summary>Retains Shader-Core's light color because the Toon wrapper does not isolate Unity Standard declarations.</summary>
bool SCModelUsesIsolatedMainLightColor()
/// <summary>Prepares the Toon main light so direct radiance remains independent from directional visibility.</summary>
void SCModelPrepareMainLight(inout SCLightData light, inout SCShadingData sd, half3 mainLightColor, half mainLightAttenuation, half mainLightNonShadowAttenuation, half mainLightShadowVisibility)
{
return false;
light.color = mainLightColor * mainLightNonShadowAttenuation;
sd.shadow = mainLightShadowVisibility;
Comment thread
PenguinDOOM marked this conversation as resolved.
}

/// <summary>Preserves the Shader-Core light direction for the Toon quantization response.</summary>
Expand Down
11 changes: 7 additions & 4 deletions Shaders/Models/unlit.hlsl
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,12 @@ struct SCCustomData
half reserved;
/// <summary>Stores the Unity wrapper's unattenuated main-light color.</summary>
half3 mainLightColor;
/// <summary>Stores the Unity wrapper's main-light attenuation and shadow factor.</summary>
/// <summary>Stores the Unity wrapper's full main-light attenuation.</summary>
half mainLightAttenuation;
/// <summary>Stores the Unity wrapper's distance and cookie main-light attenuation without visibility.</summary>
half mainLightNonShadowAttenuation;
/// <summary>Stores Unity's effective visibility for the current light, including realtime shadow, mixed or baked occlusion, and fade.</summary>
half mainLightShadowVisibility;
/// <summary>Stores the normalized main-light direction before Shader-Core light-phase modifications.</summary>
half3 mainLightDirection;
};
Expand All @@ -45,10 +49,9 @@ half SCModelEvaluateDirectFactor(SCShadingData shadingData, SCLightData light)
return 1;
}

/// <summary>Retains Shader-Core's light color because the Unlit wrapper does not isolate Unity Standard declarations.</summary>
bool SCModelUsesIsolatedMainLightColor()
/// <summary>Leaves the Shader-Core main light and visibility unchanged for the lighting-independent Unlit model.</summary>
void SCModelPrepareMainLight(inout SCLightData light, inout SCShadingData sd, half3 mainLightColor, half mainLightAttenuation, half mainLightNonShadowAttenuation, half mainLightShadowVisibility)
{
return false;
}

/// <summary>Preserves the Shader-Core light direction for the lighting-independent Unlit model.</summary>
Expand Down
Loading