Thanks for testing that, @cybereality. Always using the second vertex is a strange default behaviour!
My barycentric coordinates are working well for allowing me to blend vertex colors, and I've added noise so that where two vertex colors meet the boundary is jagged (like a coastline of land meeting sea). However to add the noise I needed the fragment shader to have a value that is consistent across the whole triangle (but ideally unique to each triangle). I stored a vertex index in UV
, and reconstructed it in the fragment function.
However when I change the "vertex index" values I get some very strange effects, where my sharp colour boundaries start to become fuzzy. Which is not what I want!
Here's a demo I created:
My MeshInstance is created like this:
extends MeshInstance
var mdt = MeshDataTool.new()
func _ready():
build_mesh()
func build_mesh():
var st = SurfaceTool.new();
st.begin(Mesh.PRIMITIVE_TRIANGLES);
st.add_smooth_group(true)
# V1
st.add_uv(Vector2(100.0, 0.0)) # Encode vertex id in UV.x
st.add_uv2(Vector2(1, 0)) # Encode barycentric indicator in UV2
st.add_color(Color(1, 0, 0))
st.add_vertex(Vector3(-1, 0, 0))
# V2
st.add_uv(Vector2(200.0, 0.0))
st.add_uv2(Vector2(0, 1))
st.add_color(Color(0, 1, 0))
st.add_vertex(Vector3(0, sqrt(3), 0))
# V3
st.add_uv(Vector2(30000.0, 0.0))
st.add_uv2(Vector2(0, 0))
st.add_color(Color(0, 0, 1))
st.add_vertex(Vector3(1, 0, 0))
st.generate_normals();
st.generate_tangents();
var mesh = st.commit();
self.mesh = mesh
mdt.create_from_surface(mesh, 0)
And my shader looks like this:
shader_type spatial;
uniform vec3 blank_col = vec3(0, 0, 0);
varying vec3 color_one;
varying vec3 color_two;
varying vec3 color_three;
varying float v_id_one;
varying float v_id_two;
varying float v_id_three;
// barycentric coordinates
varying vec3 bc;
// Noise function by Morgan McGuire https://www.shadertoy.com/view/4dS3Wd
float hash(float p) { p = fract(p * 0.011); p *= p + 7.5; p *= p + p; return fract(p); }
float hash2(vec2 p) {vec3 p3 = fract(vec3(p.xyx) * 0.13); p3 += dot(p3, p3.yzx + 3.333); return fract((p3.x + p3.y) * p3.z); }
// Noise function by Morgan McGuire https://www.shadertoy.com/view/4dS3Wd
float noise(float x) {
float i = floor(x);
float f = fract(x);
float u = f * f * (3.0 - 2.0 * f);
return mix(hash(i), hash(i + 1.0), u);
}
// Noise function by Morgan McGuire https://www.shadertoy.com/view/4dS3Wd
float fbm(float x) {
float v = 0.0;
float a = 0.5;
float shift = float(100);
for (int i = 0; i < 5; ++i) {
v += a * noise(x);
x = x * 2.0 + shift;
a *= 0.5;
}
return v;
}
float large_wave(float x, float offset) {
float amplitude = 1.0;
float period = 4.0;
return sin(x + offset) * sin(period * 2.0 * x + offset * 2.0) * amplitude;
}
float noisy_detail(float x, float offset) {
return fbm(6.0*x + offset);
}
float mid_pass_filter(float x) {
return sin(3.0 * x) * 0.05;
}
float bias(float x, float offset) {
return mid_pass_filter(x) * (large_wave(x, offset)/2.0 + noisy_detail(2.0*x, offset-1.0));
}
void vertex() {
if (UV2.x == 1.0) {
bc = vec3(1.0, 0, 0);
color_one = COLOR.rgb;
color_two = blank_col;
color_three = blank_col;
v_id_one = UV.x;
v_id_two = 0.0;
v_id_three = 0.0;
}
else if (UV2.y == 1.0) {
bc = vec3(0, 1.0, 0);
color_one = blank_col;
color_two = COLOR.rgb;
color_three = blank_col;
v_id_one = 0.0;
v_id_two = UV.x;
v_id_three = 0.0;
}
else {
bc = vec3(0, 0, 1.0);
color_one = blank_col;
color_two = blank_col;
color_three = COLOR.rgb;
v_id_one = 0.0;
v_id_two = 0.0;
v_id_three = UV.x;
}
}
void fragment() {
// Reconstruct individual vertex colors
vec3 col_one = color_one * (1.0/bc.x);
vec3 col_two = color_two * (1.0/bc.y);
vec3 col_three = color_three * (1.0/bc.z);
// Reconstruct individual vertex IDs
float id_one = v_id_one * (1.0/bc.x);
float id_two = v_id_two * (1.0/bc.y);
float id_three = v_id_three * (1.0/bc.z);
// Act separately on each min-triangle
if (bc.x <= bc.y && bc.x <= bc.z) {
// x min, therefore blend between y and z
float half = (bc.y + bc.z)/2.0;
float t = bc.x * 3.0; // Scales boundary length to [0.0, 1.0] interval
float k = (id_two + id_three)*id_one;
float bias_yz = bias(t, k);
if (bc.y - bias_yz < half) {
ALBEDO = col_three;
}
else {
ALBEDO = col_two;
}
}
if (bc.y <= bc.x && bc.y <= bc.z) {
// y min, therefore blend between x and z
float half = (bc.x + bc.z)/2.0;
float t = bc.y * 3.0;
float k = (id_one + id_three) * id_two;
float bias_xz = bias(t, k);
if (bc.x + bias_xz < half) {
ALBEDO = col_three;
}
else {
ALBEDO = col_one;
}
}
if (bc.z <= bc.x && bc.z <= bc.y) {
// z min, therefore blend between x and y
float half = (bc.x + bc.y)/2.0;
float t = bc.z * 3.0;
float k = fbm(id_one + id_two)+TIME;
float bias_xy = bias(t, k);
if (bc.x - bias_xy < half) {
ALBEDO = col_two;
}
else {
ALBEDO = col_one;
}
}
}
In the MeshInstance
code the result looks fine if UV.x values are: 100.0, 200.0, 300.0 for example. However if vertex 3's UV.x gets too large then the result starts to look fuzzy and eventually it breaks completely. I wondered if the case was float instability when reconstructing vertex IDs, and it seems like this may well be a problem.
For example, if I add the following to the end of the fragment shader:
if (id_three != 300.0) {
ALBEDO = vec3(1.0, 1.0, 1.0);
}
Then many pixels get colored white, even though a successful reconstruction would mean that all pixels agree that id_three == 300.0.
That therefore has left me trying to figure out of Godot's shader language will let me pass per-vertex integers into the fragment function so I don't have to reconstruct them and end up with this problem.