Softer lighting for low-poly models.

So I’m just starting to learn shaders, and I’d like to make this super-low-poly character still look good. As it is now, the edges are all blocky-looking. Is there some sort of “softer” diffuse that would blend the faces better at the edges, and make the character look less blocky overall?

I’d have to see a screen shot to be sure, but my guess is that you’re seeing normal seams. Try turning the smoothing angle up as high as it will go in your mesh’s import settings.

There’s nothing that will make the silhouette less blocky other than adding more polygons.

But clever modeling, use of smoothing groups and good texturing can hide a lot of that.

You might also want to give half-lambert lighting a go. It’ll stop the light falloff being so harsh and might help the shape look smoother in it’s actual shading.

Half-Lambert seems to be what I’m looking for. I also found something about “Wrap Diffuse” of some sort.

I’ll try playing around with those.

Half-Lambert and Wrapped Diffuse are the same thing. If my understanding of your problem is correct, though, those techniques will only reduce the contrast over hard edges in your model. To eliminate them entirely, with Half-Lambert or another lighting model, you want to smooth your normals.

I can afford to do a bit of both. Half-Lambert will probably be enough to soften a lot of edges, but my character is still only 700 triangles. Even for iStuff, that’ll probably still leave some room for perfection.

It’s kind of a stress test on my own development stills; how low my polygons can get and still look good.

If you normalize your normals in the fragment shader, you’ll get softer results, at a performance cost. Specular reflections benefit more from this than diffuse. If you don’t normalize the interpolated normals, the edges of the model will be noticeably brighter than if you do.

A ramp shader looks and runs better than Half-Lambert, which has goofy artifacts on low-poly models on the shadow side.

Jessy:

How can a ramp shader work better than half-lambert? Ramp shader IS half-lambert but with an extra texture lookup.

Also, what goofy artifacts does half-lambert have on low-poly models? Surely it’s just as accurate as regular lambert shading, just that it doesnt go into negative NdotL.

I don’t understand what you mean. You could get the same results as Half-Lambert if you use a ramp, but Half-Lambert is just one of the millions (dependent on resolution) of possibilities you have with a ramp texture.

I tried for a while just now to get a good example. I couldn’t do it, so obviously it isn’t always a problem. I’ve had a model where the round shadow blobs (which mimic the shape of the highlights, or mirror the shape if you leave out the squaring) of Half-Lambert looked broken. I’ll post here again when I see it again.

As I understand it, Half-Lambert is the same as a ramp shader. Except that because it’s so simple and uniform, it doesn’t require a ramp texture to determine the strength; it’s just a calculation added into the pass.

Ok, I get where you guys are coming from, now.

  1. You don’t need to wrap a ramp around the model. You can set the texture to clamp, and only affect the half that would be illuminated by a normal Lambertian calculation. Amongst other things, like permitting hue/saturation as well as value contrast, this allows you to bring the highlights down at a faster rate, which softens the transition into the shadow side.
  2. Otherwise, you can use a solid color for a portion of the negative side, allowing you to get just a bit of extra wraparound, while avoiding the shadow blobs I was talking about.

Well, for lambert you use

float shading = saturate(NdotL);

for half-lambert you use

float shading = NdotL * 0.5 + 0.5;

for ramp you use the half-lambert value as UV coordinates to sample the ramp texture at

float shading = tex2D(_RampTex, float2(NdotL * 0.5 + 0.5)).r;
or
float3 shading = tex2D(_RampTex, float2(NdotL * 0.5 + 0.5)).rgb;

So using a ramp texture to simulate half-lambert is actually less precise (256 colour uncompressed texture - less if compressed - rather than float) and more expensive (needs an extra texture lookup).

Granted with a ramp, you can add colours into the shading as it’s a texture lookup, but it’s more expensive if you’re after simple half-lambert.

And if you don’t wrap a ramp around the model using half-lambert, you end up with the same colour smeared across the entire unlit side of the model. Which sort of defeats the point in my eyes :stuck_out_tongue:

I just tested out the half-lambert/wrap diffuse shader.

I absolutely love shaders now.

My 700-poly character was designed -only- with bare-minimum human shape contours in mind, and it turned out looking very blocky. Now it looks smooth and beautiful!

I can only imagine how great it’ll look when I actually add a few more polygons to it.

I believe the standard is to take that and square it.

Like I said, you don’t need to; you can use a ramp over just one side, and choose your shadow color with it, avoiding the need for an ambient contribution, if you’re so inclined.

I don’t get the impression most people see through those eyes. Blinn-Phong wouldn’t ever be used otherwise.

The texture lookup may or may not be an issue, depending on platform, but how much you can calculate in the vertex shader is. I made measurements of 100 overlapping Unity spheres on my iPhone 3GS, which are in the vert range that this thread is concerned with (taking up as much of the screen as possible without clipping):

Half-Lambert : 17.6 FPS

	Blend SrcColor DstColor
	GLSLPROGRAM
	varying lowp vec4 lighting;
	varying mediump vec2 mainUV;
	
	#ifdef VERTEX
	uniform mediump mat4 _World2Object;
	uniform mediump vec4 _MainTex_ST, _LightColor0;
	uniform mediump vec3 _WorldSpaceLightPos0;
	void main () {		
		mediump mat3 world2Object = mat3(
			_World2Object[0].xyz, _World2Object[1].xyz, _World2Object[2].xyz);
					
		gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
		mainUV = gl_MultiTexCoord0.xy * _MainTex_ST.xy + _MainTex_ST.zw;
		mediump float nDotL = dot(gl_Normal * world2Object, _WorldSpaceLightPos0) * .5 + .5;
		lighting = nDotL * nDotL * _LightColor0 + gl_LightModel.ambient;
	}
	#endif

	#ifdef FRAGMENT
	uniform lowp sampler2D _MainTex;
	void main () {
		gl_FragColor = texture2D(_MainTex, mainUV) * lighting;
	}
	#endif
	ENDGLSL

Ramp : 14.6 FPS

varying mediump vec2 mainUV, rampUV;
	
	#ifdef VERTEX
	uniform mediump mat4 _World2Object;
	uniform mediump vec4 _MainTex_ST;
	uniform mediump vec3 _WorldSpaceLightPos0;
	void main () {		
		mediump mat3 world2Object = mat3(
			_World2Object[0].xyz, _World2Object[1].xyz, _World2Object[2].xyz);
					
		gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
		mainUV = gl_MultiTexCoord0.xy * _MainTex_ST.xy + _MainTex_ST.zw;
		rampUV = vec2(dot(gl_Normal * world2Object, _WorldSpaceLightPos0));
	}
	#endif
	
	#ifdef FRAGMENT
	uniform lowp sampler2D _MainTex, _LightRamp;
	uniform lowp vec4 _LightColor0;
	void main () {
		gl_FragColor = texture2D(_MainTex, mainUV) * texture2D(_LightRamp, rampUV) * _LightColor0;
	}

Ramp per-pixel: 4.8 fps

varying mediump vec2 mainUV;
	varying lowp vec3 normal;
	
	#ifdef VERTEX
	uniform mediump mat4 _World2Object;
	uniform mediump vec4 _MainTex_ST;
	void main () {		
		mediump mat3 world2Object = mat3(
			_World2Object[0].xyz, _World2Object[1].xyz, _World2Object[2].xyz);
					
		gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
		mainUV = gl_MultiTexCoord0.xy * _MainTex_ST.xy + _MainTex_ST.zw;
		normal = gl_Normal * world2Object;
	}
	#endif
	
	#ifdef FRAGMENT
	uniform lowp sampler2D _MainTex, _LightRamp;
	uniform lowp vec4 _LightColor0;
	uniform lowp vec3 _WorldSpaceLightPos0;
	void main () {
		lowp vec2 rampUV = vec2(dot(normalize(normal), _WorldSpaceLightPos0));
		gl_FragColor = texture2D(_MainTex, mainUV) * texture2D(_LightRamp, rampUV) * _LightColor0;
	}
	#endif

Half-Lambert per-pixel: 3.2 FPS

varying lowp vec3 normal;
	varying lowp vec2 mainUV;
	
	#ifdef VERTEX
	uniform mediump mat4 _World2Object;
	uniform mediump vec4 _MainTex_ST;
	void main () {		
		mediump mat3 world2Object = mat3(
			_World2Object[0].xyz, _World2Object[1].xyz, _World2Object[2].xyz);
					
		gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
		mainUV = gl_MultiTexCoord0.xy * _MainTex_ST.xy + _MainTex_ST.zw;
		normal = gl_Normal * world2Object;
	}
	#endif

	#ifdef FRAGMENT
	uniform lowp sampler2D _MainTex;
	uniform lowp vec4 _LightColor0;
	uniform lowp vec3 _WorldSpaceLightPos0;
	void main () {
		lowp float nDotL = dot(normalize(normal), _WorldSpaceLightPos0) * .5 + .5;
		lowp vec4 lighting = nDotL * nDotL * _LightColor0 + gl_LightModel.ambient;
		gl_FragColor = texture2D(_MainTex, mainUV) * lighting;
	}
	#endif

687950--24732--$half-lambert.png
687950--24733--$ramp.png
687950--24734--$half-lambert, per-pixel.png
687950--24735--$ramp, per-pixel.png