Monday, February 07, 2011

Per Vertex Ambient Occlusion

If you want to bake ambient occlusion into your mesh, here is one way to do it.

This script modifies models on import if it has a filename that ends with "-AO". You can adjust the samples parameter to change the quality / time ratio. I find 1000 samples takes a few seconds, but still provides a good quality.

using UnityEngine;
using UnityEditor;
using System.Collections;
using System.Collections.Generic;

class AddVertexAO : AssetPostprocessor
int samples = 1000;

void OnPostprocessModel (GameObject go)
if ( ("-AO")) {
AddAO (go);

void AddAO (GameObject go)
var mf = go.GetComponent<MeshFilter> ();
mf.sharedMesh.Optimize ();
var co = go.GetComponent<MeshCollider> ();
var destoryCollider = co == null;
if (co == null)
go.AddComponent<MeshCollider> ();

var mesh = mf.sharedMesh;
var normals = mesh.normals;
var vertices = mesh.vertices;

var rotations = new Vector3[samples];

var radius = Mathf.Max(mesh.bounds.size.x, mesh.bounds.size.y, mesh.bounds.size.z);
for (var i = 0; i < samples; i++) {
rotations[i] = go.transform.position + (Random.onUnitSphere*radius);

var nVertices = new List<Vector3>();
var nColors = new List<Color>();
var nNormals = new List<Vector3>();
var nTriangles = new List<int>();
var index = 0;

foreach (var i in mesh.triangles) {
var n = normals[i];
var v = vertices[i];
var c = Color.white;
var hits = 0f;
foreach(var s in rotations) {
if(Physics.Linecast(s, go.transform.position+v)) {
hits += (1f/samples);
} else {
hits -= (1f/samples);
c *= (1-hits);
index += 1;

mesh.vertices = nVertices.ToArray();
mesh.colors = nColors.ToArray();
mesh.normals = nNormals.ToArray();
mesh.triangles = nTriangles.ToArray();


If you want to use the baked ambient occlusion colours, you need to use a shader that blends these colours in with your material colours. This is a shader I use to visualize vertex colours only.

Shader "DM/Vertex Coloured" {
Properties {
_Color ("Main Color", Color) = (0.5,0.5,0.5,1)

SubShader {
Tags { "RenderType"="Opaque" }
LOD 200

#pragma surface surf None

float4 _Color;

struct Input {
float4 color : COLOR;

half4 LightingNone (SurfaceOutput s, half3 lightDir, half atten) {
half4 c;
c.rgb = s.Albedo;
c.a = s.Alpha;
return c;

void surf (Input IN, inout SurfaceOutput o) {
half4 c = _Color * IN.color;
o.Albedo = c.rgb;
o.Alpha = c.a;

Fallback "Diffuse"

If you combine this calculated colour, with existing vertex colours on your mesh, you can get quite nice results. The image below uses no lighting, no textures and is very cheap to render. It has 700 vertices.

If you can afford the extra vertices, you can get even better results. The below screenshot shows the same model with 3000 vertices.

There is a problem with the ambient occlusion calculation. I simply use random points on a sphere when creating the samples. This is not ideal, as the set of points are not uniformly distributed on the sphere. This is fairly easy to do, and I'll show how to do this in a later post. Stay tuned!


Stephen Lavelle said...

thanks! handy to have some code I can hack from :)

Anonymous said...

thanks for this, can you please explaing a bit how to use it ? I have no idea.

mark_ffrench said...

Hi, What type of model did you use to get this working?

I tried using it for a few different meshes and - although it triggered the script correctly on import - the script either failed or didn't produce any AO maps


Simon Wittber said...

It doesn't actually generate maps, it changes the vertex colour on the model itself. You'll need to use a shader which uses this vertex colour in order to see the effect.

Popular Posts