Add 3D pixel-art render style (pixelation + depth/normal outlines)

Full-screen URP post effect on the active PC renderer that pixelates the
scene to a fixed low-res grid and draws depth + normal edge-detection
outlines, with color posterization and tone controls, for a stylized
"3D pixel art" look. Runs via Unity's built-in Full Screen Pass feature
(no custom C#); reads camera depth/normals so it applies to DOTS entities
and GameObjects identically.

- PixelOutline.shader/.mat: pixel-snap fill + gamma-space posterize +
  brightness/contrast/saturation + depth/normal outlines. Normal
  threshold tuned high so outlines don't flood small characters.
- PC_Renderer.asset: FullScreenPassRendererFeature (Depth|Normal, after
  post-processing).
- PostFX_Daylight.asset: tonemapping ACES -> Neutral (poppier color).
- Game.unity: brighter base atmosphere (ambient + fog pushed back) and a
  stronger directional key light so the fill reads under the effect.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-06 22:30:18 -07:00
parent 401e0a9c02
commit d2203c8aa1
7 changed files with 240 additions and 39 deletions
+7 -7
View File
@@ -18,11 +18,11 @@ RenderSettings:
m_FogColor: {r: 0.07, g: 0.09, b: 0.1, a: 1}
m_FogMode: 1
m_FogDensity: 0.006
m_LinearFogStart: 26
m_LinearFogEnd: 60
m_LinearFogStart: 55
m_LinearFogEnd: 180
m_AmbientSkyColor: {r: 0.2, g: 0.24, b: 0.29, a: 1}
m_AmbientEquatorColor: {r: 0.15, g: 0.17, b: 0.19, a: 1}
m_AmbientGroundColor: {r: 0.1, g: 0.11, b: 0.12, a: 1}
m_AmbientEquatorColor: {r: 0.36, g: 0.4, b: 0.44, a: 1}
m_AmbientGroundColor: {r: 0.26, g: 0.28, b: 0.3, a: 1}
m_AmbientIntensity: 1
m_AmbientMode: 1
m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
@@ -15468,7 +15468,7 @@ Light:
serializedVersion: 13
m_Type: 1
m_Color: {r: 0.92, g: 0.94, b: 1, a: 1}
m_Intensity: 1.5
m_Intensity: 1.9
m_Range: 10
m_SpotAngle: 30
m_InnerSpotAngle: 21.80208
@@ -63364,9 +63364,9 @@ MonoBehaviour:
m_EditorClassIdentifier: ProjectM.Client::ProjectM.Client.WorldAtmosphereConfig
BoundaryX: 500
BlendHalfWidth: 80
BaseFogColor: {r: 0.07, g: 0.09, b: 0.1, a: 1}
BaseFogColor: {r: 0.4, g: 0.5, b: 0.54, a: 1}
BaseFogDensity: 0.006
BaseAmbientSky: {r: 0.2, g: 0.24, b: 0.29, a: 1}
BaseAmbientSky: {r: 0.46, g: 0.52, b: 0.6, a: 1}
ExpeditionFogColor: {r: 0.8509804, g: 0.54901963, b: 0.22745098, a: 1}
ExpeditionFogDensity: 0.01
ExpeditionAmbientSky: {r: 0.91, g: 0.769, b: 0.604, a: 1}
+40 -31
View File
@@ -1,5 +1,25 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &-3979589313966124049
MonoBehaviour:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: b00045f12942b46c698459096c89274e, type: 3}
m_Name: PixelOutline
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.FullScreenPassRendererFeature
m_Active: 1
injectionPoint: 600
fetchColorBuffer: 1
requirements: 3
passMaterial: {fileID: 2100000, guid: 13d75b55291d02c4db05da1243629fa6, type: 2}
passIndex: 0
bindDepthStencilAttachment: 0
m_Version: 1
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -13,32 +33,27 @@ MonoBehaviour:
m_Name: PC_Renderer
m_EditorClassIdentifier:
debugShaders:
debugReplacementPS: {fileID: 4800000, guid: cf852408f2e174538bcd9b7fda1c5ae7,
type: 3}
debugReplacementPS: {fileID: 4800000, guid: cf852408f2e174538bcd9b7fda1c5ae7, type: 3}
hdrDebugViewPS: {fileID: 4800000, guid: 573620ae32aec764abd4d728906d2587, type: 3}
probeVolumeSamplingDebugComputeShader: {fileID: 7200000, guid: 53626a513ea68ce47b59dc1299fe3959,
type: 3}
probeVolumeSamplingDebugComputeShader: {fileID: 7200000, guid: 53626a513ea68ce47b59dc1299fe3959, type: 3}
probeVolumeResources:
probeVolumeDebugShader: {fileID: 4800000, guid: e5c6678ed2aaa91408dd3df699057aae,
type: 3}
probeVolumeFragmentationDebugShader: {fileID: 4800000, guid: 03cfc4915c15d504a9ed85ecc404e607,
type: 3}
probeVolumeOffsetDebugShader: {fileID: 4800000, guid: 53a11f4ebaebf4049b3638ef78dc9664,
type: 3}
probeVolumeSamplingDebugShader: {fileID: 4800000, guid: 8f96cd657dc40064aa21efcc7e50a2e7,
type: 3}
probeSamplingDebugMesh: {fileID: -3555484719484374845, guid: 57d7c4c16e2765b47a4d2069b311bffe,
type: 3}
probeSamplingDebugTexture: {fileID: 2800000, guid: 24ec0e140fb444a44ab96ee80844e18e,
type: 3}
probeVolumeBlendStatesCS: {fileID: 7200000, guid: b9a23f869c4fd45f19c5ada54dd82176,
type: 3}
probeVolumeDebugShader: {fileID: 4800000, guid: e5c6678ed2aaa91408dd3df699057aae, type: 3}
probeVolumeFragmentationDebugShader: {fileID: 4800000, guid: 03cfc4915c15d504a9ed85ecc404e607, type: 3}
probeVolumeOffsetDebugShader: {fileID: 4800000, guid: 53a11f4ebaebf4049b3638ef78dc9664, type: 3}
probeVolumeSamplingDebugShader: {fileID: 4800000, guid: 8f96cd657dc40064aa21efcc7e50a2e7, type: 3}
probeSamplingDebugMesh: {fileID: -3555484719484374845, guid: 57d7c4c16e2765b47a4d2069b311bffe, type: 3}
probeSamplingDebugTexture: {fileID: 2800000, guid: 24ec0e140fb444a44ab96ee80844e18e, type: 3}
probeVolumeBlendStatesCS: {fileID: 7200000, guid: b9a23f869c4fd45f19c5ada54dd82176, type: 3}
m_RendererFeatures:
- {fileID: 7833122117494664109}
m_RendererFeatureMap: ad6b866f10d7b46c
m_UseNativeRenderPass: 1
- {fileID: -3979589313966124049}
m_RendererFeatureMap: ad6b866f10d7b46cefd31b8d9aa8c5c8
xrSystemData: {fileID: 0}
postProcessData: {fileID: 11400000, guid: 41439944d30ece34e96484bdb6645b55, type: 2}
m_AssetVersion: 2
m_AssetVersion: 3
m_PrepassLayerMask:
serializedVersion: 2
m_Bits: 4294967295
m_OpaqueLayerMask:
serializedVersion: 2
m_Bits: 4294967295
@@ -56,11 +71,14 @@ MonoBehaviour:
m_RenderingMode: 2
m_DepthPrimingMode: 0
m_CopyDepthMode: 0
m_DepthAttachmentFormat: 0
m_DepthTextureFormat: 0
m_AccurateGbufferNormals: 0
m_IntermediateTextureMode: 0
m_TileOnlyMode: 0
--- !u!114 &7833122117494664109
MonoBehaviour:
m_ObjectHideFlags: 0
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
@@ -84,12 +102,3 @@ MonoBehaviour:
BlurQuality: 0
Falloff: 100
SampleCount: -1
m_BlueNoise256Textures:
- {fileID: 2800000, guid: 36f118343fc974119bee3d09e2111500, type: 3}
- {fileID: 2800000, guid: 4b7b083e6b6734e8bb2838b0b50a0bc8, type: 3}
- {fileID: 2800000, guid: c06cc21c692f94f5fb5206247191eeee, type: 3}
- {fileID: 2800000, guid: cb76dd40fa7654f9587f6a344f125c9a, type: 3}
- {fileID: 2800000, guid: e32226222ff144b24bf3a5a451de54bc, type: 3}
- {fileID: 2800000, guid: 3302065f671a8450b82c9ddf07426f3a, type: 3}
- {fileID: 2800000, guid: 56a77a3e8d64f47b6afe9e3c95cb57d5, type: 3}
m_Shader: {fileID: 4800000, guid: 0849e84e3d62649e8882e9d6f056a017, type: 3}
@@ -15,7 +15,7 @@ MonoBehaviour:
active: 1
mode:
m_OverrideState: 1
m_Value: 2
m_Value: 1
neutralHDRRangeReductionMode:
m_OverrideState: 0
m_Value: 2
+45
View File
@@ -0,0 +1,45 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
serializedVersion: 8
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: PixelOutline
m_Shader: {fileID: 4800000, guid: ab66a5a8f176cac469b6ade5a4286ef0, type: 3}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords: []
m_InvalidKeywords: []
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap: {}
disabledShaderPasses: []
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs: []
m_Ints: []
m_Floats:
- _Brightness: 1.1
- _ColorLevels: 12
- _Contrast: 1.05
- _DepthEdgesEnabled: 1
- _DepthThreshold: 0.08
- _EdgeStrength: 1
- _NormalEdgesEnabled: 1
- _NormalThreshold: 1
- _OutlineThickness: 1
- _PixelHeight: 200
- _PixelateEnabled: 1
- _PosterizeEnabled: 1
- _Saturation: 1.35
m_Colors:
- _OutlineColorInner: {r: 0, g: 0, b: 0, a: 1}
- _OutlineColorOuter: {r: 0, g: 0, b: 0, a: 1}
m_BuildTextureStacks: []
m_AllowLocking: 1
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 13d75b55291d02c4db05da1243629fa6
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 2100000
userData:
assetBundleName:
assetBundleVariant:
+130
View File
@@ -0,0 +1,130 @@
Shader "Hidden/ProjectM/PixelOutline"
{
Properties
{
_PixelHeight ("Pixel Grid Height", Float) = 200
[ToggleUI] _PosterizeEnabled ("Posterize (banded color)", Float) = 1
_ColorLevels ("Posterize Levels / channel", Range(2,64)) = 12
_Brightness ("Brightness", Range(0.5,2)) = 1.1
_Contrast ("Contrast", Range(0.5,2)) = 1.05
_Saturation ("Saturation", Range(0,2)) = 1.35
_DepthThreshold ("Depth Threshold (relative)", Range(0,1)) = 0.08
_NormalThreshold ("Normal Threshold", Range(0,4)) = 1.0
_OutlineThickness ("Outline Thickness (texels)", Range(0.25,4)) = 1
_EdgeStrength ("Edge Strength", Range(0,1)) = 1
[HDR] _OutlineColorOuter ("Outline Color - Depth/Silhouette", Color) = (0,0,0,1)
[HDR] _OutlineColorInner ("Outline Color - Normal/Detail", Color) = (0,0,0,1)
[ToggleUI] _PixelateEnabled ("Pixelate", Float) = 1
[ToggleUI] _DepthEdgesEnabled ("Depth Edges", Float) = 1
[ToggleUI] _NormalEdgesEnabled ("Normal Edges", Float) = 1
}
SubShader
{
Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" }
ZWrite Off ZTest Always Cull Off
Pass
{
Name "PixelOutline"
HLSLPROGRAM
#pragma vertex Vert
#pragma fragment Frag
#pragma target 3.5
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareNormalsTexture.hlsl"
#include "Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl"
CBUFFER_START(UnityPerMaterial)
float _PixelHeight;
float _PosterizeEnabled;
float _ColorLevels;
float _Brightness;
float _Contrast;
float _Saturation;
float _DepthThreshold;
float _NormalThreshold;
float _OutlineThickness;
float _EdgeStrength;
float4 _OutlineColorOuter;
float4 _OutlineColorInner;
float _PixelateEnabled;
float _DepthEdgesEnabled;
float _NormalEdgesEnabled;
CBUFFER_END
float2 SnapUV(float2 uv, float2 res)
{
return (floor(uv * res) + 0.5) / res;
}
half4 Frag(Varyings input) : SV_Target
{
float2 uv = input.texcoord;
// Fixed, square-pixel low-res grid derived from a target grid height.
float aspect = _ScreenParams.x / max(_ScreenParams.y, 1.0);
float2 res = float2(max(_PixelHeight * aspect, 1.0), max(_PixelHeight, 1.0));
bool doPixelate = _PixelateEnabled > 0.5;
float2 sampleUV = doPixelate ? SnapUV(uv, res) : uv;
float2 texel = 1.0 / res;
float2 off = texel * _OutlineThickness;
// Chunky fill: point-sampled scene color at the snapped grid cell.
float3 col = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_PointClamp, sampleUV).rgb;
col = max(col, 0.0);
// ---- Tone shaping so flat pixel blocks read clearly ----
col *= _Brightness;
col = (col - 0.5) * _Contrast + 0.5;
float luma = dot(col, float3(0.2126, 0.7152, 0.0722));
col = lerp(luma.xxx, col, _Saturation);
col = saturate(col);
// ---- Posterize in perceptual (gamma) space so dark detail survives ----
if (_PosterizeEnabled > 0.5)
{
float levels = max(_ColorLevels, 2.0);
float3 g = pow(col, 1.0 / 2.2);
g = floor(g * levels + 0.5) / levels;
col = pow(saturate(g), 2.2);
}
// ---- Depth edges (outer silhouettes) ----
float d0 = LinearEyeDepth(SampleSceneDepth(sampleUV), _ZBufferParams);
float dL = LinearEyeDepth(SampleSceneDepth(sampleUV - float2(off.x, 0)), _ZBufferParams);
float dR = LinearEyeDepth(SampleSceneDepth(sampleUV + float2(off.x, 0)), _ZBufferParams);
float dD = LinearEyeDepth(SampleSceneDepth(sampleUV - float2(0, off.y)), _ZBufferParams);
float dU = LinearEyeDepth(SampleSceneDepth(sampleUV + float2(0, off.y)), _ZBufferParams);
float depthDiff = abs(d0 - dL) + abs(d0 - dR) + abs(d0 - dU) + abs(d0 - dD);
float depthEdge = step(_DepthThreshold, depthDiff / max(d0, 1e-3)) * _DepthEdgesEnabled;
// ---- Normal edges (inner detail creases) ----
float3 n0 = SampleSceneNormals(sampleUV);
float3 nL = SampleSceneNormals(sampleUV - float2(off.x, 0));
float3 nR = SampleSceneNormals(sampleUV + float2(off.x, 0));
float3 nD = SampleSceneNormals(sampleUV - float2(0, off.y));
float3 nU = SampleSceneNormals(sampleUV + float2(0, off.y));
float ne = (1.0 - saturate(dot(n0, nL))) + (1.0 - saturate(dot(n0, nR)))
+ (1.0 - saturate(dot(n0, nU))) + (1.0 - saturate(dot(n0, nD)));
float normalEdge = step(_NormalThreshold, ne) * _NormalEdgesEnabled;
// Mask inner lines out of silhouette edges to avoid double outlines.
float normalEdgeMasked = normalEdge * (1.0 - depthEdge);
float edge = saturate(depthEdge + normalEdgeMasked) * _EdgeStrength;
float3 lineCol = lerp(_OutlineColorInner.rgb, _OutlineColorOuter.rgb, depthEdge);
float3 outCol = lerp(col, lineCol, edge);
return half4(outCol, 1.0);
}
ENDHLSL
}
}
Fallback Off
}
@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: ab66a5a8f176cac469b6ade5a4286ef0
ShaderImporter:
externalObjects: {}
defaultTextures: []
nonModifiableTextures: []
userData:
assetBundleName:
assetBundleVariant: