Reputation:
Is there a way to control color between vertices with shader? Like in a classic tutorials with just a triangle being drawn on the screen - vertices have red, green and blue colors assigned accordingly. Values in between are interpolated. Is there a way to control color of that middle ground, other than modifying geometry and adding more vertices inside?
Upvotes: 1
Views: 879
Reputation: 54099
Almost anything is possible with a little imagination. However what is not possible is the conjuring of information from nowhere.
The standard triangle lacks the information needed to add new interpolated color. That information is a 2D coordinate (like a texture coordinate) and the color.
You can then use the 2D coordinate to mix the additional color.
In the example the map coordinates are added to each vertex as an attribute map
, along with the vertex color
and position (vert
2D in the example). The extra color is added as an uniform midColor
thus limits it to all triangles having the same internal color.
You can however add that color as an additional vertex attribute, or if there are only a few unique colors you can add them as a uniform array and then add an vertex attribute that you use to index into the uniform array of colors.
The most basic mixing can assume that the mapping is relative to an origin, that origin being the center of the triangle. Then just use the distance from the origin to mix the mid color with the interpolated pixel colors.
// midColor is uniform
// colV and colM are varying, and hold the color and internal color 2D mapping
gl_FragColor = vec4(mix(midColor, colV, length(colM)), 1);
The example uses the same technique but just adds a little to improve the color interpolation and moves the origin up a little visually balance the center.
attribute vec2 vert;
attribute vec3 color;
attribute vec2 map;
uniform float time;
varying vec3 colV;
varying vec2 colM;
void main() {
float z = sin(time) * vert.x;
gl_Position = vec4(cos(time) * vert.x, vert.y, z * 0.5 + 0.5, z + 1.0);
colV = color;
colM = map;
}
The mixing is controlled by the defines smoothStart
and smoothEnd
#define smoothStart 0.15
#define smoothEnd 0.5
uniform vec3 midColor;
varying vec3 colV;
varying vec2 colM;
void main() {
vec3 mixed = sqrt(
mix(
midColor * midColor,
colV * colV,
smoothstep(smoothStart, smoothEnd, length(colM - vec2(0, 1.0 / 3.0)))
)
);
gl_FragColor = vec4(mixed, 1);
}`;
const CONTEXT = "webgl";
const GL_OPTIONS = {alpha: false, depth: false, premultpliedAlpha: false, preserveDrawingBufer: true};
Math.TAU = Math.PI * 2;
Math.sinWave = (phase, period = 1, min = -1, max = 1) => Math.sin((phase * Math.TAU) / period) * (max - min) + min;
const GL_SETUP = {
get context() { return this.gl = canvas.getContext(CONTEXT, GL_OPTIONS) },
get vertexSrc() { return `${CONTEXT === "webgl2" ? "#version 300 es" : ""}
#define aspect ${(innerHeight / innerWidth).toFixed(4)}
attribute vec2 vert;
attribute vec3 color;
attribute vec2 map;
uniform float time;
varying vec3 colV;
varying vec2 colM;
void main() {
float z = sin(time) * vert.x;
gl_Position = vec4(cos(time) * vert.x * aspect, vert.y, z * 0.5 + 0.5, z + 1.0);
colV = color;
colM = map;
}`;
},
get fragmentSrc() { return `${CONTEXT === "webgl2" ? "#version 300 es" : ""}
precision highp float;
#define smoothStart 0.15
#define smoothEnd 0.5
uniform vec3 midColor;
varying vec3 colV;
varying vec2 colM;
void main() {
vec3 mixed = sqrt(mix(midColor * midColor, colV * colV, smoothstep(smoothStart, smoothEnd,length(colM - vec2(0, 1.0 / 3.0)))));
gl_FragColor = vec4(mixed, 1);
}`;
},
get locations() { return ["A_vert", "A_color", "A_map", "U_midColor", "U_time"] },
compileAndAttach(program, src, type = this.gl.VERTEX_SHADER) {
const gl = this.gl, shader = gl.createShader(type);
gl.shaderSource(shader, src);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { throw new Error("WebGL shader compile error\n" + gl.getShaderInfoLog(shader)) }
gl.attachShader(program, shader);
},
createShader() {
const gl = this.gl, locations = {}, program = gl.createProgram();
this.compileAndAttach(program, this.vertexSrc);
this.compileAndAttach(program, this.fragmentSrc, gl.FRAGMENT_SHADER);
gl.linkProgram(program);
gl.useProgram(program);
for(const desc of this.locations) {
const [type, name] = desc.split("_");
locations[name] = gl[`get${type==="A" ? "Attrib" : "Uniform"}Location`](program, name);
}
return {program, locations, gl};
},
get shader() {
const gl = this.gl;
const shader = this.createShader();
for (const [name, data] of Object.entries(this.buffers)) {
const {use = gl.STATIC_DRAW, type = gl.FLOAT, buffer, size = 2, normalize = false} = data;
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, buffer, use);
gl.enableVertexAttribArray(shader.locations[name]);
gl.vertexAttribPointer(shader.locations[name], size, type, normalize, 0, 0);
}
return shader;
},
get buffers() {
return {
vert: {buffer: this.verticies},
color: {buffer: this.vertCols, size: 3},
map: {buffer: this.mapping},
};
},
get verticies() { return new Float32Array([0,-0.5, 0.5,0.5, -0.5,0.5]) },
get vertCols() { return new Float32Array([1,0,0, 0,1,0, 0,0,1]) },
get mapping() { return new Float32Array([0,-1, 1,1, -1,1]) },
};
const gl = GL_SETUP.context;
const shader = GL_SETUP.shader;
const W = gl.canvas.width = innerWidth, H = gl.canvas.height = innerHeight;
const midColor = new Float32Array([1,0,0]);
gl.viewport(0, 0, W, H);
requestAnimationFrame(mainLoop);
function mainLoop(time) {
gl.clear(gl.COLOR_BUFFER_BIT);
midColor[0] = Math.sinWave(time / 1000, 1, 0.5, 1);
midColor[1] = Math.sinWave(time / 1000, 2, 0.5, 1);
midColor[2] = Math.sinWave(time / 1000, 3, 0.5, 1);
gl.uniform1f(shader.locations.time, time / 1000);
gl.uniform3fv(shader.locations.midColor, midColor);
gl.drawArrays(gl.TRIANGLES, 0, 3);
requestAnimationFrame(mainLoop);
}
canvas {
position: absolute;
top: 0px;
left: 0px;
}
<canvas id="canvas"></canvas>
Upvotes: 2
Reputation:
The most common way to color a triangle is to use a texture. The second most common way would be to add vertices. As @derhass pointed out you could in theory create a fragment shader that some how colors the middle different. To do that though would require giving the fragment shader more data because a fragment shader has no idea which pixel of a triangle is being drawn. So you'll end up adding more data to your geometry to accomplish that even if you don't technically add more vertices.
Further, any solution you come up with will likely be fairly inflexible where as using a texture (the most common way to color something) gives you a ton of flexibiltity. For example you might make a fragment shader that lets you pick 1 new color in the middle of the triangle.
const gl = document.querySelector('canvas').getContext('webgl');
const vs = `
attribute vec4 position;
attribute vec4 color;
attribute vec3 corner;
varying vec4 v_color;
varying vec3 v_corner;
void main() {
gl_Position = position;
v_color = color;
v_corner = corner;
}
`;
const fs = `
precision highp float;
varying vec4 v_color;
varying vec3 v_corner;
// could be a uniform
const vec4 centerColor = vec4(0, 1, 0, 1);
void main() {
vec3 center = vec3(1.0 / 3.0);
float edge = distance(center, v_corner) / 0.75;
gl_FragColor = mix(centerColor, v_color, edge);
}
`;
const prg = twgl.createProgram(gl, [vs, fs]);
const posLoc = gl.getAttribLocation(prg, 'position');
const colorLoc = gl.getAttribLocation(prg, 'color');
const cornerLoc = gl.getAttribLocation(prg, 'corner');
function createBufferAndSetupAttribute(gl, loc, data) {
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
// normally these would happen at render time
gl.enableVertexAttribArray(loc);
gl.vertexAttribPointer(loc, 3, gl.FLOAT, false, 0, 0);
}
createBufferAndSetupAttribute(gl, posLoc, [
0, 1, 0,
1, -1, 0,
-1, -1, 0,
]);
createBufferAndSetupAttribute(gl, colorLoc, [
1, 0, 0,
1, 0, 1,
0, 0, 1,
]);
createBufferAndSetupAttribute(gl, cornerLoc, [
1, 0, 0,
0, 1, 0,
0, 0, 1,
]);
gl.useProgram(prg);
gl.drawArrays(gl.TRIANGLES, 0, 3);
<script src="https://twgljs.org/dist/4.x/twgl.min.js"></script>
<canvas></canvas>
Later you decide want 2 colors, one 1/3rd of the way from the first point down the line from the first point to the mid point of the other 2 points and another 2/3rd down that line. You'll need to write a completely new shader. If you'd used a texture you'd just change the texture.
Upvotes: 1