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
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@
*.exr binary
*.png binary
Tests/Fixtures/Scenes/PureBaseValidation/LightingData.asset binary
Tests/Fixtures/Scenes/PureBaseValidation/OwnerLightingData.asset binary
28 changes: 25 additions & 3 deletions .github/scripts/New-PureBaseCiProject.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,28 @@ if ([string]$shaderCoreJson.name -ne 'jp.lilxyzw.shadercore' -or [string]$shader
throw "The CI workspace requires jp.lilxyzw.shadercore exactly 0.1.9."
}

$ownerLightingDataRelativePath = 'Tests/Fixtures/Scenes/PureBaseValidation/OwnerLightingData.asset'
$ownerLightingDataAssetPath = Join-Path $packageRoot $ownerLightingDataRelativePath
if (-not (Test-Path -LiteralPath $ownerLightingDataAssetPath -PathType Leaf)) {
throw "Owner LightingData fixture is missing: '$ownerLightingDataRelativePath'."
}

$ownerLightingDataMetaRelativePath = "$ownerLightingDataRelativePath.meta"
$ownerLightingDataMetaPath = Join-Path $packageRoot $ownerLightingDataMetaRelativePath
if (-not (Test-Path -LiteralPath $ownerLightingDataMetaPath -PathType Leaf)) {
throw "Owner LightingData metadata is missing: '$ownerLightingDataMetaRelativePath'."
}

$ownerLightingDataGuidLines = @([regex]::Matches((Get-Content -LiteralPath $ownerLightingDataMetaPath -Raw), '(?m)^guid:\s*(\S+)\s*$'))
if ($ownerLightingDataGuidLines.Count -ne 1) {
throw "Owner LightingData metadata must contain exactly one GUID: '$ownerLightingDataMetaRelativePath'."
}

$ownerLightingDataGuid = $ownerLightingDataGuidLines[0].Groups[1].Value
if ($ownerLightingDataGuid -notmatch '^[0-9a-fA-F]{32}$') {
throw "Owner LightingData metadata contains a malformed GUID: '$ownerLightingDataMetaRelativePath'."
}

$assetsRoot = Join-Path $projectRootFullPath 'Assets'
$projectSettingsRoot = Join-Path $projectRootFullPath 'ProjectSettings'
$packagesRoot = Join-Path $projectRootFullPath 'Packages'
Expand Down Expand Up @@ -79,7 +101,7 @@ $manifestText = ($manifest | ConvertTo-Json -Depth 4) + "`n"
[System.Text.UTF8Encoding]::new($false)
)

$ownerSceneText = @'
$ownerSceneText = @"
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!29 &1
Expand Down Expand Up @@ -179,7 +201,7 @@ LightmapSettings:
m_ExportTrainingData: 0
m_TrainingDataDestination: TrainingData
m_LightProbeSampleCountMultiplier: 4
m_LightingDataAsset: {fileID: 0}
m_LightingDataAsset: {fileID: 112000000, guid: $ownerLightingDataGuid, type: 2}
m_LightingSettings: {fileID: 0}
--- !u!196 &4
NavMeshSettings:
Expand Down Expand Up @@ -209,7 +231,7 @@ NavMeshSettings:
SceneRoots:
m_ObjectHideFlags: 0
m_Roots: []
'@
"@
[System.IO.File]::WriteAllText(
(Join-Path $assetsRoot 'Pure-Base.unity'),
$ownerSceneText.Replace("`r`n", "`n") + "`n",
Expand Down
46 changes: 45 additions & 1 deletion .github/tests/New-PureBaseCiProject.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@ Describe 'Pure-Base CI Unity project generation' {
$pureBaseRoot = Join-Path $projectRoot 'Packages/jp.penguin.purebase'
$shaderCoreRoot = Join-Path $projectRoot 'Packages/jp.lilxyzw.shadercore'
$consumerSettings = Join-Path $pureBaseRoot 'Tests/Release/ConsumerProject/ProjectSettings'
New-Item -ItemType Directory -Path $pureBaseRoot,$shaderCoreRoot,$consumerSettings -Force | Out-Null
$ownerLightingDataDirectory = Join-Path $pureBaseRoot 'Tests/Fixtures/Scenes/PureBaseValidation'
$ownerLightingDataAssetPath = Join-Path $ownerLightingDataDirectory 'OwnerLightingData.asset'
$ownerLightingDataMetaPath = "$ownerLightingDataAssetPath.meta"
$ownerLightingDataGuid = [guid]::NewGuid().ToString('N')
New-Item -ItemType Directory -Path $pureBaseRoot,$shaderCoreRoot,$consumerSettings,$ownerLightingDataDirectory -Force | Out-Null
[IO.File]::WriteAllText(
(Join-Path $pureBaseRoot 'package.json'),
'{"name":"jp.penguin.purebase","version":"0.1.0"}',
Expand Down Expand Up @@ -75,6 +79,16 @@ QualitySettings:
$qualitySettingsFixture + "`n",
[Text.UTF8Encoding]::new($false)
)
[IO.File]::WriteAllText(
$ownerLightingDataAssetPath,
"Owner LightingData test fixture`n",
[Text.UTF8Encoding]::new($false)
)
[IO.File]::WriteAllText(
$ownerLightingDataMetaPath,
"fileFormatVersion: 2`nguid: $ownerLightingDataGuid`n",
[Text.UTF8Encoding]::new($false)
)
}

It 'keeps the tracked VRChat-project QualitySettings source fixture under the reviewed contract' {
Expand Down Expand Up @@ -109,6 +123,14 @@ QualitySettings:
$ownerScene = Get-Content -LiteralPath $ownerScenePath -Raw
Assert-CiProjectHarness -Condition ($ownerScene -match 'SceneRoots:') -Message 'Generated owner scene is not a serialized Unity scene.'
Assert-CiProjectHarness -Condition ($ownerScene -match 'm_Roots: \[\]') -Message 'Generated owner scene must remain empty.'
Assert-CiProjectHarness -Condition (Test-Path -LiteralPath $ownerLightingDataAssetPath -PathType Leaf) -Message 'Temporary package fixture is missing the owner LightingData asset.'
Assert-CiProjectHarness -Condition (Test-Path -LiteralPath $ownerLightingDataMetaPath -PathType Leaf) -Message 'Temporary package fixture is missing the owner LightingData metadata.'
$ownerLightingDataMeta = Get-Content -LiteralPath $ownerLightingDataMetaPath -Raw
$ownerLightingDataGuidMatch = [regex]::Match($ownerLightingDataMeta, '(?m)^guid:\s*([0-9a-f]{32})\s*$')
Assert-CiProjectHarness -Condition $ownerLightingDataGuidMatch.Success -Message 'Temporary owner LightingData metadata must contain a GUID.'
$ownerSceneLightingDataGuidMatch = [regex]::Match($ownerScene, '(?m)^\s*m_LightingDataAsset: \{fileID: 112000000, guid: ([0-9a-f]{32}), type: 2\}\s*$')
Assert-CiProjectHarness -Condition $ownerSceneLightingDataGuidMatch.Success -Message 'Generated owner scene must reference a LightingData asset.'
Assert-CiProjectHarness -Condition ($ownerSceneLightingDataGuidMatch.Groups[1].Value -eq $ownerLightingDataGuidMatch.Groups[1].Value) -Message 'Generated owner scene LightingData GUID must match the owner fixture metadata GUID.'

$qualitySettingsPath = Join-Path $projectRoot 'ProjectSettings/QualitySettings.asset'
Assert-CiProjectHarness -Condition (Test-Path -LiteralPath $qualitySettingsPath -PathType Leaf) -Message 'Generated CI project is missing the reviewed VRChat-project QualitySettings snapshot.'
Expand All @@ -133,4 +155,26 @@ QualitySettings:
catch { $failure = $_ }
Assert-CiProjectHarness -Condition ($null -ne $failure -and $failure.Exception.Message -like '*exactly 0.1.9*') -Message 'The CI project builder accepted an unexpected Shader-Core version.'
}

It 'rejects a missing owner LightingData fixture' {
Remove-Item -LiteralPath $ownerLightingDataAssetPath -Force

$failure = $null
try { & $projectBuilder -ProjectRoot $projectRoot }
catch { $failure = $_ }
Assert-CiProjectHarness -Condition ($null -ne $failure -and $failure.Exception.Message -like '*Owner LightingData fixture is missing*') -Message 'The CI project builder accepted a missing owner LightingData fixture.'
}

It 'rejects malformed owner LightingData metadata GUIDs' {
[IO.File]::WriteAllText(
$ownerLightingDataMetaPath,
"fileFormatVersion: 2`nguid: malformed`n",
[Text.UTF8Encoding]::new($false)
)

$failure = $null
try { & $projectBuilder -ProjectRoot $projectRoot }
catch { $failure = $_ }
Assert-CiProjectHarness -Condition ($null -ne $failure -and $failure.Exception.Message -like '*malformed GUID*') -Message 'The CI project builder accepted malformed owner LightingData metadata.'
}
}
10 changes: 10 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
2026/08/08
Ver. 0.2.0-beta.1
https://github.com/Penguin-Repository/Pure-Base/releases#release-0.2.0-beta.1

- Added the public `_RenderingMode` ABI with Opaque, Cutout, and Transparent states.
- Added explicit editor synchronization through `PureBaseMaterialRenderingMode.Apply(Material)` and `Assets/PureBase/Resync Rendering Mode`.
- Added documented render-state behavior for queue, blend, depth writing, coverage, and Transparent pass enablement.
- Preserved four source pass declarations while allowing Transparent mode to disable `ShadowCaster` and `Meta`.
- Updated the package version and release download identity to `0.2.0-beta.1`.

2026/08/06
Ver. 0.1.0
https://github.com/Penguin-Repository/Pure-Base/releases#release-0.1.0
Expand Down
31 changes: 24 additions & 7 deletions Docs/pure-base-shader-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ This document defines the stable public contract of the Pure-Base shader package
- Integration test graphics API: D3D11, forced by the harness.
- Shader-Core dependency: exactly `jp.lilxyzw.shadercore` `0.1.9`.
- Pure-Base does not automatically allow future `0.1.x` releases. Shader-Core upstream has not declared compatibility across `0.x` releases, and importer, ProjectSettings, and method-shape contracts are sensitive.
- Transparent material blending and URP are outside the supported contract.
- Opaque, Cutout, and Transparent rendering modes are supported. URP is outside the supported contract.

## Stable Shader Paths

Expand All @@ -42,16 +42,32 @@ Each shader is independently usable without an optional module.

## Material and Pass Contract

Every product shader has the fixed tags `RenderType=TransparentCutout` and `Queue=AlphaTest`. Each exposes exactly four passes:
Every product shader source retains exactly four passes:

| Pass | Ownership and restrictions |
| --- | --- |
| `ForwardBase` | Builds the normal surface and lighting result. PBR and Hybrid own Unity Standard indirect GI and reflection-probe evaluation here. |
| `ForwardAdd` | Additional direct-light contribution only, with black fog semantics. PBR and Hybrid must not duplicate indirect GI or reflection-probe lighting here. |
| `ShadowCaster` | Applies Cutout coverage after the Shader-Core `base` phase, so module changes to `sd.albedoAlpha.a` affect casting. |
| `Meta` | Uses the host base-texture Cutout coverage for Meta/lightmap workflows. This dedicated pass does not execute the standard phase ABI. |
| `ShadowCaster` | When enabled for Cutout, applies coverage after the Shader-Core `base` phase, so module changes to `sd.albedoAlpha.a` affect casting. |
| `Meta` | Uses the host base-texture Cutout coverage for Meta/lightmap workflows when enabled. This dedicated pass does not execute the standard phase ABI. |

The Cutout contract is not transparent blending support. The `ForwardAdd` additive blend state represents an additional direct-light pass, not a transparent material mode.
The effective tags, queue, blend state, depth writing, and pass enablement are selected by the rendering-mode ABI below. The `ForwardAdd` additive blend state in Opaque and Cutout is an additional direct-light pass, not transparent blending.

## Rendering-mode ABI

`_RenderingMode` is a ShaderLab `Integer` backed by `SC_uint` with these values:

| Value | Mode | Contract |
| ---: | --- | --- |
| `0` | Opaque | Uses `RenderType=Opaque`, queue `2000`, blend `One Zero`, and `ZWrite 1`. Opaque rendering is uncut and unblended; lighting contributions remain enabled. |
| `1` | Cutout (default) | Clears the material queue override to `-1`, resolving `RenderType=TransparentCutout` and the `AlphaTest` queue at `2450`. It uses no mode keyword, clips coverage, and keeps lighting contributions enabled. |
| `2` | Transparent | Uses `RenderType=Transparent`, queue `3000`, base blend `SrcAlpha OneMinusSrcAlpha`, additional-light blend `SrcAlpha One`, and `ZWrite 0`. `ShadowCaster` and `Meta` are disabled. |

Cutout is the keyword-free state. Opaque and Transparent use only local rendering-mode keywords. All source shaders retain their four pass declarations even when Transparent disables `ShadowCaster` and `Meta`.

Coverage behavior is part of the public contract: Opaque is uncut and unblended, Cutout clips coverage, and Transparent alpha-blends without writing depth. The final alpha produced by `postpixel` controls the `ForwardBase` and `ForwardAdd` source alpha.

The explicit editor action is `PureBaseMaterialRenderingMode.Apply(Material)`. The selected-material menu is `Assets/PureBase/Resync Rendering Mode`. Opening or refreshing the Inspector does not migrate or dirty a legacy material. Runtime switching is not guaranteed. An explicit mode change or Resync resets the standard queue and synchronizes derived state; a user custom queue remains until the next explicit mode edit or Resync.

## Public Property ABI

Expand All @@ -65,6 +81,7 @@ All four shaders expose exactly these common properties:
| `_SharedGradients` | All shaders |
| `_Cutoff` | All shaders |
| `_Cull` | All shaders |
| `_RenderingMode` | All shaders |

The model-specific properties are:

Expand All @@ -85,9 +102,9 @@ The standard insertion points are shared by the product hosts in this order:

`morph` -> `postvertex` -> `base` -> `light` -> `customlight` -> `modifylight` -> `shade` -> `reflection` -> `add` -> `postpixel`

External modules may target these standard phases. The `base` phase runs before Cutout coverage is finalized. The host saturates only `sd.albedoAlpha.a` before the alpha test; `sd.albedoAlpha.rgb` remains unclamped so HDR base color and module color adjustments are preserved. The host finalizes output alpha and applies fog before `postpixel`; no host color mutation occurs after `postpixel` before returning the fragment result.
External modules may target these standard phases. The `base` phase runs before Cutout coverage is finalized. The host saturates only `sd.albedoAlpha.a` before the alpha test; `sd.albedoAlpha.rgb` remains unclamped so HDR base color and module color adjustments are preserved. The host finalizes output alpha and applies fog before `postpixel`; no host color mutation occurs after `postpixel` before returning the fragment result. The final alpha from `postpixel` is the source alpha for both `ForwardBase` and `ForwardAdd`.

`Meta` is not a standard-phase execution path. Pass ownership remains fixed: `ForwardBase` builds the normal surface and lighting result, `ForwardAdd` is additional direct light only, `ShadowCaster` honors base-phase Cutout changes, and `Meta` retains host-owned Cutout coverage.
`Meta` is not a standard-phase execution path. Pass ownership remains fixed: `ForwardBase` builds the normal surface and lighting result, `ForwardAdd` is additional direct light only, `ShadowCaster` honors base-phase Cutout changes when enabled, and `Meta` retains host-owned Cutout coverage when enabled.

## Model Semantics

Expand Down
Loading