Skip to content
← Back to Blog 3d-modeling

Unity Shader Graph: Build Custom PBR Shaders for Game-Ready 3D Assets

By BitSoul3D6 min read258 views

Default Unity materials are a trap. They get you moving fast, but they cap your visual ceiling and give every asset that unmistakable "stock Unity" look. If you want your game to look like yours, you need custom shaders — and Unity Shader Graph makes that achievable without writing a single line of HLSL.

Unity Shader Graph: Build Custom PBR Shaders for Game-Ready 3D Assets

This guide covers the full workflow: setting up your URP project, understanding the Shader Graph node system, building a production-ready PBR shader from scratch, and optimizing it for runtime performance.

What Is Shader Graph and Why Should You Use It?

Shader Graph is Unity's node-based visual shader editor, introduced with the Universal Render Pipeline (URP) and High Definition Render Pipeline (HDRP). Instead of writing raw shader code, you wire together nodes that represent mathematical operations, texture samples, and engine parameters.

For 3D artists working in Unity, this is transformative. You can:

Shader Graph requires URP or HDRP — it does not work with the Built-in Render Pipeline. If you're starting a new project in 2026, default to URP unless you specifically need HDRP's ray tracing or volumetric features.

Setting Up Shader Graph in a URP Project

Create a new Unity project using the URP template, or upgrade an existing project via Edit → Render Pipeline → Universal Render Pipeline → Upgrade Project Materials.

Install Shader Graph via the Package Manager if it isn't already present:

Window → Package Manager → Unity Registry → Shader Graph → Install

To create your first graph: right-click in the Project window, then Create → Shader Graph → URP → Lit Shader Graph. This opens a blank graph with a Fragment and Vertex output node wired to URP's lighting model.

Key terms to know before you start:

Setting Up Shader Graph in a URP Project — illustrated

Building a PBR Shader From Scratch

Here's a practical walkthrough for a standard rock/stone asset — the kind you'd find in any environment pack.

Step 1: Set Up Textures as Properties

In the Blackboard, add these properties:

Step 2: Wire Albedo and Tint

  1. Drag Albedo Map onto the graph → auto-creates a Sample Texture 2D node
  2. Add a Multiply node
  3. Connect: Sample Texture 2D (RGBA) → Multiply A, Tint → Multiply B
  4. Connect Multiply output → Base Color on the Master Stack

Step 3: Normal Map

  1. Drag Normal Map → Sample Texture 2D (set Type to Normal in node settings)
  2. Add a Normal Strength node
  3. Connect: texture output → Normal Strength In, Normal Strength property → Strength
  4. Connect output → Normal (Tangent Space) on Master Stack

Step 4: Roughness and Metallic

For a stone asset, metallic is 0. Wire the roughness map's Red channel directly to Smoothness with an invert node (roughness maps are inverted smoothness):

Sample Texture 2D (R) → One Minus → Smoothness

Step 5: Ambient Occlusion

Sample the AO map's Red channel → connect to Ambient Occlusion on the Master Stack.

Final Master Stack connections:

InputSource
Base ColorAlbedo × Tint
Normal (Tangent)Normal Strength node
MetallicFloat (0)
Smoothness1 - Roughness.R
Ambient OcclusionAO.R

Optimizing Shader Graph for Runtime Performance

A beautiful shader that runs at 30fps is a liability. Here's how to keep yours fast:

Use Half precision where possible. In the Graph Inspector, set your shader to Half precision. URP mobile targets benefit significantly. Switch individual nodes to Half via the Node Settings panel — avoid Half for normal map math (precision artifacts appear).

Minimize texture samples. Pack your Roughness, AO, and Metallic channels into a single texture (R=Metallic, G=AO, B=Roughness). This cuts 2 texture samples to 1 and reduces memory bandwidth.

# Blender Python: pack ORM texture via script
import bpy

def pack_orm(metallic_path, roughness_path, ao_path, output_path):
    # Load source images
    met = bpy.data.images.load(metallic_path)
    rough = bpy.data.images.load(roughness_path)
    ao = bpy.data.images.load(ao_path)
    
    # Create output image
    width, height = met.size
    orm = bpy.data.images.new("ORM", width=width, height=height)
    
    pixels = [0.0] * (width * height * 4)
    for i in range(width * height):
        pixels[i*4 + 0] = met.pixels[i*4]      # R = Metallic
        pixels[i*4 + 1] = ao.pixels[i*4]        # G = AO  
        pixels[i*4 + 2] = rough.pixels[i*4]     # B = Roughness
        pixels[i*4 + 3] = 1.0                    # A = unused
    
    orm.pixels = pixels
    orm.filepath_raw = output_path
    orm.file_format = 'PNG'
    orm.save()
    
pack_orm('/tmp/metallic.png', '/tmp/roughness.png', '/tmp/ao.png', '/tmp/ORM.png')

Avoid branching. If/else logic in shaders is expensive. Use Lerp and Step nodes to blend between states instead of conditional branches.

Check the compiled shader. In the Shader Graph, click Show Generated Code (top-right menu) to see the actual HLSL output. Count texture samples and math operations — if it's bloated, find redundant nodes.

Optimizing Shader Graph for Runtime Performance — illustrated

Exposing Parameters for Artist-Friendly Materials

One of Shader Graph's biggest wins for teams is the ability to expose controls to the Material Inspector. Any Blackboard property shows up as a tweak-able field on any material that uses your shader.

Best practices for exposed parameters:

This is especially valuable if you're selling assets on a marketplace. Well-exposed shader parameters make your asset look professional and reduce support questions. Assets on BitSoul marketplace with clean material setups consistently outperform those with locked-down, single-material packages.

Shader Graph vs. Hand-Written HLSL: When to Use Each

ScenarioUse Shader GraphUse HLSL
Standard PBR materials✅
Procedural effects (water, lava)✅
Maximum performance / mobile✅
Custom lighting models✅
Compute shaders✅
Rapid prototyping✅

For most 3D asset work — characters, props, environments — Shader Graph covers everything you need. HLSL becomes necessary when you're writing custom lighting models or squeezing every cycle on a mobile target.

Bringing It All Together

Shader Graph closes the gap between what your assets look like in Blender and what they look like in Unity. By building a reusable PBR shader template, packing your ORM textures, and exposing artist-friendly parameters, you get consistent results across every asset in your project.

If you're building an asset library or selling to other developers, a clean custom shader elevates your work above the default-material crowd. Browse environment, character, and prop packs with production-ready materials at BitSoul — and consider uploading your own optimized assets to reach Unity and Unreal developers worldwide.


This post is part of the Ultimate Guide to Free 3D Game Assets — BitSoul's complete reference for formats, texturing, rigging, optimization, and engine integration.


Need drop-in assets for this workflow? Grab the 98-model Character Pack on the BitSoul marketplace and drop them straight into your project.

Tags: characters

Skip the modeling — download it instead

A free BitSoul3D account gets you 2 GLB downloads every month for personal use plus 25 one-time AI Engine credits, no card required. PBR-textured GLB downloads with a full 3D preview before you buy, for Unreal, Unity, Godot or Blender — OBJ and 3D-printable STL come with any purchase or paid plan.

Browse 1,051 models — from $4.99 → or start free (2 downloads a month)