As far as the real world is concerned, every OpenGL implementation that supports 3.2 core and above also supports 3.2 compatibility and above. Which means that the fixed function pipeline (unfortunately) hasn’t gone anywhere. A user can opt out of it if they wish, but it’s still around if you want it.
Shaders are advanced material
I keep hearing people say this, and I’ve never understand it. What is so “advanced” about shaders? If you start off explaining where shaders are in the pipeline and how they work, they’re not particularly complicated.
Complex shaders can be complex, but simple shaders are simple. Indeed, shaders make understanding certain things much easier. Take lighting for example. The basic diffuse lighting equation is this:
float lightValue = clamp(dot(normal, lightDir), 0, 1);
finalColor = lightValue * lightIntensity * diffuseColor;
You can set this up in OpenGL in a few function calls. For per-vertex lighting, at any rate. But should per-fragment lighting be considered “advanced” material? Per-fragment lighting has been commonplace for years; calling it “advanced” is doing your students a disservice.
Furthermore, look at how the equation changes when you have a texture:
vec4 diffuseColor = texture(diffuseSpl, texCoord);
float lightValue = clamp(dot(normal, lightDir), 0, 1);
finalColor = lightValue * lightIntensity * diffuseColor;
It doesn’t change. The only thing that has changed is where the diffuse color comes from. This is standard programming practice, and anyone learning how to write graphics code should be able to follow that.
In terms of fixed-function, you have to set up a glTexEnv stage to do the last multiply, which is rather different from the setup for using the per-vertex interpolated color for the last multiply.
With shaders, it’s a very minor change to the shader. It’s very obvious what you’re doing (accessing a texture and using that value as a diffuse color in the lighting equation).
How does it change when a shadow map is added?
vec4 diffuseColor = texture(diffuseSpl, texCoord);
float lightValue = clamp(dot(normal, lightDir), 0, 1);
float lightMap = textureShadow(shadowMapSpl, shadowCoord);
finalColor = lightMap * lightValue * lightIntensity * diffuseColor;
It’s really simple and obvious what’s going on. Compare that to the fixed-function code necessary to emulate this.
Fixed function is fast for throwing something on the screen. But for teaching people how to actually assemble a desired effect, it isn’t very good. It’s obtuse, inflexible, and you’ll have to abandon it anyway to do anything real.
And the last part is really the sticking point for me. See, calculus does not invalidate fractions and arithmetic. You need basic arithmetic to be able to use calculus; calculus itself requires these things. Shaders invalidates the use of glLighti, glTexEnv, and the rest of the fixed function pipeline.
Honestly, I’ve been using shaders for so long I don’t even remember how glTexEnv works. Nobody can possibly say, “I’ve been using calculus for so long I don’t even remember how addition works.”
If you think people will happily rewrite their code using shaders – they won’t. They will instead look for alternatives to OpenGL.
If by “alternatives to OpenGL” you mean Direct3D, sorry: D3D10 and above abandoned fixed functionality too. OpenGL ES 2.0 dropped it as well. Indeed, only desktop OpenGL has fixed function anymore; all that has changed is that you have to ask for it.
You are doing your students a disservice by not introducing them to a shader-based world as soon as possible. That’s the reality that they’re going to have to deal with, so it’s better for them to deal with it ASAP.