feat(render3d): a rim outline around a highlighted actor
An actor gains `outline` (metres of rim) and `ocol` (its colour). The pass draws the model a second time with its vertices pushed out along their normals and its front faces culled, so only the far side of the swollen shell survives - a silhouette exactly `outline` metres wide. It is depth-tested against the scene, so anything standing in front of the actor hides the rim too. skin.vert grows an OUTLINE path for the push; outline.frag is the flat-colour fragment shader it pairs with. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
9d1df6ec32
commit
2542cb591d
3 changed files with 70 additions and 1 deletions
|
|
@ -30,7 +30,9 @@ property Actor {
|
|||
ptint: words, # 4 per primitive: on flag, r, g, b (a part's own colour)
|
||||
hide: words, # 1 per primitive: skip it (a cap taken off)
|
||||
radius: int = 0, # float bits: bounding radius for culling (0 = never culled)
|
||||
cull: int = 0 # float bits: not drawn beyond this distance (0 = always)
|
||||
cull: int = 0, # float bits: not drawn beyond this distance (0 = always)
|
||||
outline: int = 0, # float bits: metres of rim drawn around it (0 = none)
|
||||
ocol: words # the rim's colour (null = white)
|
||||
}
|
||||
# a program and its uniform locations
|
||||
property AcProg {
|
||||
|
|
@ -44,6 +46,8 @@ property AcProg {
|
|||
l_emis: int = -1,
|
||||
l_view: int = -1,
|
||||
l_proj: int = -1,
|
||||
l_out: int = -1,
|
||||
l_ocol: int = -1,
|
||||
l_mh: int = -1,
|
||||
scene_frame: int = -1 # the frame its scene uniforms were bound
|
||||
}
|
||||
|
|
@ -52,6 +56,8 @@ var ac_lit: AcProg = null
|
|||
var ac_lit_cut: AcProg = null
|
||||
var ac_sh: AcProg = null
|
||||
var ac_sh_cut: AcProg = null
|
||||
var ac_out: AcProg = null
|
||||
var ac_out_cut: AcProg = null
|
||||
var ac_actors: []Actor = null
|
||||
var ac_next_id: int = 0
|
||||
var ac_frame: int = 0
|
||||
|
|
@ -64,6 +70,7 @@ function ac_prog_new(vs: string, fs: string, defs: string) -> AcProg {
|
|||
a.l_bones = gl_uniform(p, "u_bones[0]"); if a.l_bones < 0 { a.l_bones = gl_uniform(p, "u_bones") }
|
||||
a.l_tint = gl_uniform(p, "u_tint"); a.l_rough = gl_uniform(p, "u_rough_scale"); a.l_emis = gl_uniform(p, "u_emissive")
|
||||
a.l_view = gl_uniform(p, "u_view"); a.l_proj = gl_uniform(p, "u_proj"); a.l_mh = gl_uniform(p, "u_model_h")
|
||||
a.l_out = gl_uniform(p, "u_outline"); a.l_ocol = gl_uniform(p, "u_outline_col")
|
||||
# the samplers never move: units 0, 1, 2
|
||||
gl_use_program(p)
|
||||
gl_uniform1i(gl_uniform(p, "u_diff"), 0); gl_uniform1i(gl_uniform(p, "u_nrm"), 1); gl_uniform1i(gl_uniform(p, "u_arm"), 2)
|
||||
|
|
@ -74,6 +81,8 @@ function actor_init() -> void {
|
|||
ac_lit_cut = ac_prog_new("skin.vert", "model.frag", "#define ALPHA_TEST\n")
|
||||
ac_sh = ac_prog_new("skin.vert", "shadow.frag", "#define SHADOW_PASS\n")
|
||||
ac_sh_cut = ac_prog_new("skin.vert", "shadow.frag", "#define SHADOW_PASS\n#define ALPHA_TEST\n")
|
||||
ac_out = ac_prog_new("skin.vert", "outline.frag", "#define OUTLINE\n")
|
||||
ac_out_cut = ac_prog_new("skin.vert", "outline.frag", "#define OUTLINE\n#define ALPHA_TEST\n")
|
||||
ac_actors = new []Actor
|
||||
}
|
||||
function actor_remove(a: Actor) -> void {
|
||||
|
|
@ -188,8 +197,46 @@ function actor_draw() -> void {
|
|||
ac_bind_scene(ap)
|
||||
actor_draw_one(a, ap, false)
|
||||
}
|
||||
actor_draw_outlines()
|
||||
gl_enable(GL_CULL_FACE)
|
||||
}
|
||||
# The rim around a highlighted actor: the model again, its vertices pushed out along their
|
||||
# normals (skin.vert, OUTLINE) and its front faces culled, so only the far side of the
|
||||
# swollen shell survives — a silhouette exactly `outline` metres wide. Depth-tested
|
||||
# against the scene, so anything in front of the actor hides its rim too.
|
||||
function actor_draw_outlines() -> void {
|
||||
var any = false
|
||||
for i in 0 .. len(ac_actors) { if ac_actors[i].outline != 0 and ac_visible(ac_actors[i], false) { any = true; break } }
|
||||
if not any { return }
|
||||
gl_enable(GL_CULL_FACE)
|
||||
gl_cull_face(GL_FRONT)
|
||||
for i in 0 .. len(ac_actors) {
|
||||
let a = ac_actors[i]
|
||||
if a.outline == 0 or not ac_visible(a, false) { continue }
|
||||
var ap = ac_out
|
||||
if a.cutout { ap = ac_out_cut }
|
||||
gl_use_program(ap.prog)
|
||||
u_mat4(ap.l_view, cam_view); u_mat4(ap.l_proj, cam_proj)
|
||||
u_f(ap.l_out, f_mul(a.outline, a.scale))
|
||||
if a.ocol != null { u_v3(ap.l_ocol, a.ocol) } else { u_f3(ap.l_ocol, F_ONE, F_ONE, F_ONE) }
|
||||
actor_draw_outline_one(a, ap)
|
||||
}
|
||||
gl_cull_face(GL_BACK)
|
||||
}
|
||||
function actor_draw_outline_one(a: Actor, ap: AcProg) -> void {
|
||||
u_mat4(ap.l_model, a.mat)
|
||||
var skinned = F_ZERO
|
||||
if a.skin != null { skinned = F_ONE; gl_uniform_matrix4fv(ap.l_bones, a.skin.n_joints, 0, a.skin.bones) }
|
||||
else if a.model.skin != null { skinned = F_ONE; gl_uniform_matrix4fv(ap.l_bones, a.model.skin.n_joints, 0, a.model.skin.bones) }
|
||||
u_f(ap.l_skin, skinned)
|
||||
let model = a.model
|
||||
for i in 0 .. len(model.prims) {
|
||||
let pr = model.prims[i]
|
||||
if a.hide != null and a.hide[i] != 0 { continue }
|
||||
if a.cutout { gl_active_texture(GL_TEXTURE0); gl_bind_texture(GL_TEXTURE_2D, pr.diff) }
|
||||
mesh_draw(pr.mesh)
|
||||
}
|
||||
}
|
||||
function actor_draw_casters(light_vp: words) -> void {
|
||||
if ac_actors == null { return }
|
||||
gl_use_program(ac_sh.prog); u_mat4(ac_sh.l_lvp, light_vp)
|
||||
|
|
|
|||
14
packages/ludic.render3d/shaders/outline.frag
Normal file
14
packages/ludic.render3d/shaders/outline.frag
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// the inverted-hull shell (skin.vert with OUTLINE): a flat colour, no shading. Cut-out
|
||||
// meshes still need their shape, so the alpha test comes along.
|
||||
uniform vec3 u_outline_col;
|
||||
out vec4 o_color;
|
||||
#ifdef ALPHA_TEST
|
||||
in vec2 v_uv;
|
||||
uniform sampler2D u_diff;
|
||||
void main() {
|
||||
if (texture(u_diff, v_uv).a < 0.45) discard;
|
||||
o_color = vec4(u_outline_col, 1.0);
|
||||
}
|
||||
#else
|
||||
void main() { o_color = vec4(u_outline_col, 1.0); }
|
||||
#endif
|
||||
|
|
@ -12,6 +12,9 @@ uniform float u_skinned; // 0: a rigid model on the same path
|
|||
uniform mat4 u_view;
|
||||
uniform mat4 u_proj;
|
||||
uniform mat4 u_light_vp;
|
||||
#ifdef OUTLINE
|
||||
uniform float u_outline; // metres the shell is pushed out
|
||||
#endif
|
||||
out vec3 v_wpos;
|
||||
out vec3 v_nrm;
|
||||
out vec2 v_uv;
|
||||
|
|
@ -28,6 +31,11 @@ void main() {
|
|||
m = u_model * sk;
|
||||
}
|
||||
vec4 w = m * vec4(a_pos, 1.0);
|
||||
#ifdef OUTLINE
|
||||
// an inverted hull: the shell is pushed out along the world normal and drawn with the
|
||||
// front faces culled, so what survives is a rim of `u_outline` metres around the model
|
||||
w.xyz += normalize(mat3(m) * a_nrm) * u_outline;
|
||||
#endif
|
||||
v_wpos = w.xyz;
|
||||
v_nrm = normalize(mat3(m) * a_nrm);
|
||||
v_uv = a_uv;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue