
Unity Book Viewer Setup Guide | free unity asset | Unity Book Asset
August 4, 2026
How to Make NPCs Look at the Player Only When Close (Unity Guide)
August 11, 2026Neon Sketch Outline Effect in Unity URP
In this tutorial, we’re going to build a colorful Neon Sketch / Edge Detection effect in Unity using URP.
The final result is a dark scene where only the outlines and important surface details of your objects are visible. Every object can have its own neon color, and Bloom turns those lines into glowing strokes.
This effect works especially well for stylized games, horror scenes, dream sequences, sci-fi environments, magical worlds, or simply giving a normal 3D scene a completely different visual identity.
—
What We’re Building
By the end of this tutorial, you will have:
- A fully black background
- Only selected objects visible in the camera
- Neon outlines around object silhouettes
- Internal geometry lines and surface details
- Different neon colors for different objects
- Bloom-based glow
- A reusable URP Full Screen Pass setup
The nice part is that this is not just an inverted hull outline. It uses depth and normal edge detection, so it can also reveal internal edges, hard surface details, and changes in the shape of the model.
—
How It Works
The effect is built from a few simple parts:
- We use a dedicated Unity Layer to decide which objects are visible.
- The camera only renders that layer, so everything else disappears completely.
- Each visible object uses a colored
URP/Unlitmaterial. - A custom full-screen shader compares the scene depth and normals to find edges.
- The shader reads the object color from the camera color texture.
- Bloom adds the neon glow.
This means a pink character can have pink outlines, a green prop can have green outlines, and an orange floor can have orange outlines, all in the same scene.
—
Step 1: Create a Visible Layer
Create a new Layer called something like:
NeonLayer
Assign this layer only to the objects that should appear in the final effect.
Then select your real rendering camera, usually Main Camera, and set its Culling Mask so it only renders NeonLayer.
Now every object outside that layer is completely removed from the final image. It does not render, write depth, create normals, or produce outlines.
—
Step 2: Set the Camera Background to Black
On the Main Camera, set:
Environment > Background Type: Solid Color Background: Black
Your scene should now look almost empty or completely black before the edge effect is added.
—
Step 3: Give Objects Their Neon Colors
Create separate materials for your objects using:
Universal Render Pipeline/Unlit
For example:
M_Character_Neon → Pink / Red M_Lantern_Neon → Cyan / Blue M_Ground_Neon → Orange M_Props_Neon → Green
Use Unlit materials so lighting and shadows do not change the colors. The material color becomes the source color for the neon outline shader.
—
Step 4: Enable Depth and Opaque Textures
In your URP settings, make sure these options are enabled:
Depth Texture: On Opaque Texture: On
The depth texture helps the shader detect shape changes and object silhouettes.
The opaque color texture lets the shader read the color of each object, which is why every object can have a different neon outline color.
—
Step 5: Add a Full Screen Pass Renderer Feature
Open the active URP Renderer Data asset used by your project. (Common names: ForwardRenderer, URP_Renderer, UniversalRendererData.)
In the Inspector, click:
Add Renderer Feature → Full Screen Pass Renderer Feature
Rename it to something clear, such as:
Neon Edge Detection
This feature runs the edge detection shader across the entire camera image.
—
Step 5.1: Create a Material for the Edge Shader
After creating the Neon Edge Detection shader, you need to create a material that uses it.
In the Project window:
Right-click → Create → Material
Name it something like:
M_NeonEdge
Then, with the material selected, set its Shader to:
Custom/Neon Edge Detection
This material contains the settings for the full-screen effect, such as line thickness, edge sensitivity, and neon intensity.
Important: Do not assign M_NeonEdge to any 3D object in your scene. This is a full-screen effect material, not an object material.
Instead, assign it here:
URP Renderer Data → Full Screen Pass Renderer Feature → Pass Material → M_NeonEdge
Then use these Full Screen Pass settings:
Injection Point: Before Rendering Post Processing Requirements: - Depth - Normal - Color
Color is important because it lets the shader read the color of every rendered object and use that color for its neon outline.
Your actual 3D objects should still use separate colored materials, preferably:
Universal Render Pipeline/Unlit
Example:
Character → Pink Unlit Material Lantern → Cyan Unlit Material Ground → Orange Unlit Material Props → Green Unlit Material
The edge shader reads those object colors from the camera color texture, detects the edges using Depth and Normals, and then draws the glowing neon lines in the correct color.
—
Step 5.2: Neon Edge Detection Shader (Full Code)
Paste this into a new shader file, for example:
Assets/Shaders/NeonEdgeDetection.shader
Shader code:
Shader "Custom/Neon Edge Detection"
{
Properties
{
_DepthSensitivity ("Depth Sensitivity", Range(0.001, 2)) = 0.08
_NormalSensitivity ("Normal Sensitivity", Range(0.001, 1)) = 0.35
_Thickness ("Line Thickness", Range(0.5, 5)) = 1.2
_Intensity ("Neon Intensity", Range(0, 20)) = 5
}
SubShader
{
Tags { "RenderPipeline" = "UniversalPipeline" }
ZWrite Off
ZTest Always
Cull Off
Pass
{
Name "NeonEdgeDetection"
HLSLPROGRAM
#pragma vertex Vert
#pragma fragment Frag
#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"
struct FSAttributes
{
uint vertexID : SV_VertexID;
};
struct FSVaryings
{
float4 positionCS : SV_POSITION;
float2 uv : TEXCOORD0;
};
float _DepthSensitivity;
float _NormalSensitivity;
float _Thickness;
float _Intensity;
FSVaryings Vert(FSAttributes input)
{
FSVaryings output;
output.positionCS = GetFullScreenTriangleVertexPosition(input.vertexID);
output.uv = GetFullScreenTriangleTexCoord(input.vertexID);
return output;
}
float IsBackgroundDepth(float rawDepth)
{
#if UNITY_REVERSED_Z
return step(rawDepth, 0.0001);
#else
return step(0.9999, rawDepth);
#endif
}
float GetEdgeAtOffset(float2 uv, float2 offset)
{
float centerRawDepth = SampleSceneDepth(uv);
float sampleRawDepth = SampleSceneDepth(uv + offset);
float centerIsBackground = IsBackgroundDepth(centerRawDepth);
float sampleIsBackground = IsBackgroundDepth(sampleRawDepth);
if (centerIsBackground > 0.5)
return 0.0;
float centerDepth = LinearEyeDepth(centerRawDepth, _ZBufferParams);
float sampleDepth = LinearEyeDepth(sampleRawDepth, _ZBufferParams);
float depthDifference = abs(centerDepth - sampleDepth);
float depthThreshold = _DepthSensitivity * max(centerDepth, 0.001);
float depthEdge = step(depthThreshold, depthDifference);
float normalEdge = 0.0;
if (sampleIsBackground < 0.5)
{
float3 centerNormal = SampleSceneNormals(uv);
float3 sampleNormal = SampleSceneNormals(uv + offset);
float normalDifference = 1.0 - saturate(dot(centerNormal, sampleNormal));
normalEdge = step(_NormalSensitivity, normalDifference);
}
return saturate(depthEdge + normalEdge);
}
float3 GetObjectColor(float2 uv, float2 offsetX, float2 offsetY)
{
float3 c0 = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv).rgb;
float3 c1 = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv + offsetX).rgb;
float3 c2 = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv - offsetX).rgb;
float3 c3 = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv + offsetY).rgb;
float3 c4 = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv - offsetY).rgb;
float l0 = dot(c0, float3(0.2126, 0.7152, 0.0722));
float l1 = dot(c1, float3(0.2126, 0.7152, 0.0722));
float l2 = dot(c2, float3(0.2126, 0.7152, 0.0722));
float l3 = dot(c3, float3(0.2126, 0.7152, 0.0722));
float l4 = dot(c4, float3(0.2126, 0.7152, 0.0722));
float3 best = c0;
float bestL = l0;
if (l1 > bestL) { best = c1; bestL = l1; }
if (l2 > bestL) { best = c2; bestL = l2; }
if (l3 > bestL) { best = c3; bestL = l3; }
if (l4 > bestL) { best = c4; }
return best;
}
float4 Frag(FSVaryings input) : SV_Target
{
float2 pixelSize = 1.0 / _ScreenParams.xy;
float2 offsetX = float2(pixelSize.x * _Thickness, 0.0);
float2 offsetY = float2(0.0, pixelSize.y * _Thickness);
float edge = 0.0;
edge += GetEdgeAtOffset(input.uv, offsetX);
edge += GetEdgeAtOffset(input.uv, -offsetX);
edge += GetEdgeAtOffset(input.uv, offsetY);
edge += GetEdgeAtOffset(input.uv, -offsetY);
edge = saturate(edge);
float3 objectColor = GetObjectColor(input.uv, offsetX, offsetY);
float3 neonColor = objectColor * edge * _Intensity;
return float4(neonColor, 1.0);
}
ENDHLSL
}
}
}Because the Full Screen Pass runs before post-processing, Bloom can see the bright neon colors and create the glow effect.
—
Recommended Shader Values
These values are a good balanced starting point:
Neon Intensity: 4 to 7 Line Thickness: 0.9 to 1.2 Depth Sensitivity: 0.08 Normal Sensitivity: 0.45 to 0.65
If the image has too many noisy internal lines, increase Normal Sensitivity.
If the outlines are too thin, increase Line Thickness.
If the glow is too strong, reduce Bloom Intensity or Neon Intensity.
—
Important Setup Rules
Keep these rules in mind:
- Use colored
URP/Unlitmaterials on your visible 3D objects. - Put visible objects on
NeonLayer. - Let the Main Camera render only that layer.
- Keep
M_NeonEdgeassigned only to the Full Screen Pass feature. - Set the camera background to pure black.
- Enable HDR and Post Processing for Bloom.
—
Final Result
You now have a flexible neon sketch effect that can turn a normal 3D Unity scene into a colorful glowing line-art world.
You can use it as a full visual style, a special gameplay mode, a horror “vision mode”, a scanning mode, an enemy highlight system, or a transition effect between normal and surreal environments.
Because the color comes from each object’s material, the effect is easy to expand. Just give a new object a new Unlit color material, place it on NeonLayer, and it automatically gets its own glowing outline.
Here is the video about this lesson:





![Lighting in Unity: A Practical Guide for Game Developers There’s something satisfying about watching a flat, boring plane turn into a mountain range. That’s exactly what a height map does in Unity. With a simple grayscale image, you can shape landscapes, add depth to materials, and create worlds that feel real instead of flat. It’s one of those tools that looks technical at first, but once you understand it, it becomes surprisingly simple and powerful. In this guide, we’ll break down what a height map is, how it works in Unity, how to use it for terrain, how it differs from normal maps, and how to control it with C#. What Is a Height Map? A height map is a grayscale image where each pixel represents elevation. Black = lowest height White = highest height Gray = values in between Think of it like a topographic map, but simplified into brightness levels. Unity reads this image and uses the brightness values to push parts of a surface up or down. The result? Hills, valleys, cliffs, and surface details created from a simple image. Height Maps in Unity Terrain The most common use of height maps in Unity is terrain generation. Unity’s Terrain system allows you to import a height map and automatically generate a 3D landscape from it. How to Import a Height Map into Terrain Create a Terrain: GameObject → 3D Object → Terrain Select the Terrain object. Open the Terrain Inspector. Choose Import Raw under the heightmap settings. Select your grayscale RAW file. Once imported, Unity converts the grayscale values into elevation data. If your height map is smooth, you’ll get rolling hills. If it has sharp contrast, you’ll get steep cliffs. Height Map Resolution Matters Resolution affects how detailed your terrain will be. A low-resolution height map creates blocky terrain. A high-resolution height map creates smoother, more detailed landscapes. However, higher resolution also increases memory usage and processing cost. If you're building for mobile, balance detail with performance. Height Maps vs Normal Maps This is where many beginners get confused. Height Map Actually changes geometry (in terrain or displacement). Creates real depth. More performance cost if geometry changes. Normal Map Does NOT change geometry. Fakes lighting to simulate bumps. Much cheaper performance-wise. If you need real terrain shape, use a height map. If you just want surface detail like cracks or scratches, a normal map is usually better. Using Height Maps in Materials (Parallax & Displacement) Height maps are not limited to terrain. You can also use them in materials. In Unity’s Standard Shader (or URP/HDRP equivalents), height maps can be used for: Parallax Mapping – creates depth illusion without changing geometry. Displacement Mapping – actually modifies mesh vertices (HDRP). For example, if you apply a brick texture, adding a height map can make the mortar appear recessed and bricks raised. Creating Height Maps You can create height maps using: Photoshop or GIMP (grayscale images) Blender (baked displacement maps) World Machine or Gaea (terrain generation tools) Procedural generation with code The key is keeping it grayscale and avoiding compression artifacts. Generating a Height Map with Code You can also generate terrain procedurally using Perlin Noise. This is common in open-world or survival games. Here’s a simple example: [csharp] using UnityEngine; public class TerrainGenerator : MonoBehaviour { public Terrain terrain; public int depth = 20; public int width = 256; public int height = 256; public float scale = 20f; void Start() { terrain.terrainData = GenerateTerrain(terrain.terrainData); } TerrainData GenerateTerrain(TerrainData terrainData) { terrainData.heightmapResolution = width + 1; terrainData.size = new Vector3(width, depth, height); terrainData.SetHeights(0, 0, GenerateHeights()); return terrainData; } float[,] GenerateHeights() { float[,] heights = new float[width, height]; for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { heights[x, y] = Mathf.PerlinNoise(x / scale, y / scale); } } return heights; } } [/csharp] This script generates a terrain using Perlin Noise, which creates natural-looking hills and variation. Controlling Height Strength Sometimes height maps look too extreme. Other times they look flat. In terrain settings, you can adjust: Terrain height (Y scale) Brush strength (when sculpting manually) In materials, you can adjust height intensity inside the shader settings. Small adjustments make a big difference. Subtle depth often looks more realistic than exaggerated displacement. Common Problems and Fixes Terrain Looks Blocky Increase heightmap resolution or smooth the terrain. Edges Look Stretched Make sure your height map is square and uses proper dimensions (like 512x512 or 1024x1024). Lighting Looks Strange Check your normal settings and ensure lighting is baked or set correctly. When to Use Height Maps Use height maps when: You need large landscapes. You want realistic terrain shaping. You’re building procedural worlds. You need true geometric depth. Skip them when: You only need small surface detail. Performance is extremely limited. Final Thoughts Height maps are one of those tools that feel technical at first, but once you use them, they become creative tools. You’re not just editing numbers. You’re sculpting mountains. Carving valleys. Designing the shape of a world. Start simple. Import a grayscale image. Adjust the scale. Play with noise. Watch how small changes affect the landscape. Once you understand height maps, Unity stops feeling like a flat engine. It starts feeling like a world builder.](http://unityqueen.com/wp-content/uploads/2026/02/Lighting-in-Unity-150x150.jpg)




