mirror of
https://github.com/3b1b/manim.git
synced 2025-11-14 15:47:44 +00:00
Added glossiness to VMobjects
This commit is contained in:
parent
725a7e3121
commit
38cc0a7174
14 changed files with 284 additions and 206 deletions
|
|
@ -1,5 +1,3 @@
|
|||
from functools import reduce
|
||||
import operator as op
|
||||
import moderngl
|
||||
from colour import Color
|
||||
|
||||
|
|
@ -9,6 +7,7 @@ import itertools as it
|
|||
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.mobject.mobject import Point
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.iterables import batch_by_property
|
||||
from manimlib.utils.simple_functions import fdiv
|
||||
|
|
@ -22,8 +21,6 @@ from manimlib.utils.space_ops import quaternion_from_angle_axis
|
|||
from manimlib.utils.space_ops import quaternion_mult
|
||||
|
||||
|
||||
# TODO, think about how to incorporate perspective,
|
||||
# and change get_height, etc. to take orientation into account
|
||||
class CameraFrame(Mobject):
|
||||
CONFIG = {
|
||||
"width": FRAME_WIDTH,
|
||||
|
|
@ -46,7 +43,7 @@ class CameraFrame(Mobject):
|
|||
self.set_rotation_quaternion([1, 0, 0, 0])
|
||||
return self
|
||||
|
||||
def get_transform_to_screen_space(self):
|
||||
def get_inverse_camera_position_matrix(self):
|
||||
# Map from real space into camera space
|
||||
result = np.identity(4)
|
||||
# First shift so that origin of real space coincides with camera origin
|
||||
|
|
@ -83,6 +80,7 @@ class CameraFrame(Mobject):
|
|||
self.rotate(dtheta, OUT)
|
||||
|
||||
def increment_phi(self, dphi):
|
||||
# TODO, this seems clunky
|
||||
camera_point = rotation_matrix_transpose_from_quaternion(self.rotation_quaternion)[2]
|
||||
axis = cross(OUT, camera_point)
|
||||
axis = normalize(axis, fall_back=RIGHT)
|
||||
|
|
@ -135,7 +133,7 @@ class Camera(object):
|
|||
"image_mode": "RGBA",
|
||||
"n_channels": 4,
|
||||
"pixel_array_dtype": 'uint8',
|
||||
"line_width_multiple": 0.01,
|
||||
"light_source_position": [-10, 10, 10],
|
||||
}
|
||||
|
||||
def __init__(self, ctx=None, **kwargs):
|
||||
|
|
@ -145,6 +143,7 @@ class Camera(object):
|
|||
self.init_context(ctx)
|
||||
self.init_shaders()
|
||||
self.init_textures()
|
||||
self.light_source = Point(self.light_source_position)
|
||||
|
||||
def init_frame(self):
|
||||
self.frame = CameraFrame(**self.frame_config)
|
||||
|
|
@ -329,11 +328,15 @@ class Camera(object):
|
|||
# TODO, think about how uniforms come from mobjects as well.
|
||||
pw, ph = self.get_pixel_shape()
|
||||
|
||||
transform = self.frame.get_inverse_camera_position_matrix()
|
||||
light = self.light_source.get_location()
|
||||
transformed_light = np.dot(transform, [*light, 1])[:3]
|
||||
mapping = {
|
||||
'to_screen_space': tuple(self.frame.get_transform_to_screen_space().T.flatten()),
|
||||
'to_screen_space': tuple(transform.T.flatten()),
|
||||
'aspect_ratio': (pw / ph), # AR based on pixel shape
|
||||
'focal_distance': self.frame.get_focal_distance(),
|
||||
'anti_alias_width': 3 / ph, # 1.5 Pixel widths
|
||||
'light_source_position': tuple(transformed_light),
|
||||
}
|
||||
for key, value in mapping.items():
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -58,12 +58,15 @@ class VMobject(Mobject):
|
|||
"fill_frag_shader_file": "quadratic_bezier_fill_frag.glsl",
|
||||
# Could also be Bevel, Miter, Round
|
||||
"joint_type": "auto",
|
||||
# Positive gloss up to 1 makes it reflect the light.
|
||||
"gloss": 0.0,
|
||||
"render_primative": moderngl.TRIANGLES,
|
||||
"triangulation_locked": False,
|
||||
"fill_dtype": [
|
||||
('point', np.float32, (3,)),
|
||||
('color', np.float32, (4,)),
|
||||
('fill_all', np.float32, (1,)),
|
||||
('gloss', np.float32, (1,)),
|
||||
],
|
||||
"stroke_dtype": [
|
||||
("point", np.float32, (3,)),
|
||||
|
|
@ -72,6 +75,7 @@ class VMobject(Mobject):
|
|||
("stroke_width", np.float32, (1,)),
|
||||
("color", np.float32, (4,)),
|
||||
("joint_type", np.float32, (1,)),
|
||||
("gloss", np.float32, (1,)),
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -227,6 +231,14 @@ class VMobject(Mobject):
|
|||
super().fade(darkness, family)
|
||||
return self
|
||||
|
||||
def set_gloss(self, gloss, family=True):
|
||||
if family:
|
||||
for sm in self.get_family():
|
||||
sm.gloss = gloss
|
||||
else:
|
||||
self.gloss = gloss
|
||||
return self
|
||||
|
||||
def get_fill_rgbas(self):
|
||||
try:
|
||||
return self.fill_rgbas
|
||||
|
|
@ -896,14 +908,15 @@ class VMobject(Mobject):
|
|||
stroke_width = self.stretched_style_array_matching_points(stroke_width)
|
||||
|
||||
data = self.get_blank_shader_data_array(len(self.points), "stroke_data")
|
||||
data['point'] = self.points
|
||||
data['prev_point'][:3] = self.points[-3:]
|
||||
data['prev_point'][3:] = self.points[:-3]
|
||||
data['next_point'][:-3] = self.points[3:]
|
||||
data['next_point'][-3:] = self.points[:3]
|
||||
data['stroke_width'][:, 0] = stroke_width
|
||||
data['color'] = rgbas
|
||||
data['joint_type'] = joint_type_to_code[self.joint_type]
|
||||
data["point"] = self.points
|
||||
data["prev_point"][:3] = self.points[-3:]
|
||||
data["prev_point"][3:] = self.points[:-3]
|
||||
data["next_point"][:-3] = self.points[3:]
|
||||
data["next_point"][-3:] = self.points[:3]
|
||||
data["stroke_width"][:, 0] = stroke_width
|
||||
data["color"] = rgbas
|
||||
data["joint_type"] = joint_type_to_code[self.joint_type]
|
||||
data["gloss"] = self.gloss
|
||||
return data
|
||||
|
||||
def lock_triangulation(self, family=True):
|
||||
|
|
@ -1003,6 +1016,7 @@ class VMobject(Mobject):
|
|||
# are on the boundary, and the rest are in the interior
|
||||
data["fill_all"][:len(points)] = 0
|
||||
data["fill_all"][len(points):] = 1
|
||||
data["gloss"] = self.gloss
|
||||
# Always send points in a positively oriented way
|
||||
if orientation < 0:
|
||||
data["point"][:len(points)] = points[::-1]
|
||||
|
|
|
|||
15
manimlib/shaders/add_light.glsl
Normal file
15
manimlib/shaders/add_light.glsl
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
vec4 add_light(vec4 raw_color, vec3 point, vec3 unit_normal, vec3 light_coords, float gloss){
|
||||
if(gloss == 0.0) return raw_color;
|
||||
|
||||
float camera_distance = 6;
|
||||
// Assume everything has already been rotated such that camera is in the z-direction
|
||||
vec3 to_camera = vec3(0, 0, camera_distance) - point;
|
||||
vec3 to_light = light_coords - point;
|
||||
vec3 light_reflection = -to_light + 2 * unit_normal * dot(to_light, unit_normal);
|
||||
float dot_prod = dot(normalize(light_reflection), normalize(to_camera));
|
||||
float shine = gloss * exp(-2 * pow(1 - dot_prod, 2));
|
||||
return vec4(
|
||||
mix(raw_color.rgb, vec3(1.0), shine),
|
||||
raw_color.a
|
||||
);
|
||||
}
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
// Assumes the following uniforms exist in the surrounding context:
|
||||
// uniform float aspect_ratio;
|
||||
// uniform float focal_distance;
|
||||
|
||||
vec4 get_gl_Position(vec3 point){
|
||||
// Extremely minimal modification, but that might change later...
|
||||
point.x /= aspect_ratio;
|
||||
point.z /= focal_distance;
|
||||
point.xy /= max(1 - point.z, 0);
|
||||
// Todo, does this discontinuity add weirdness? Theoretically, by this point,
|
||||
// the z-coordiante of gl_Position only matter for z-indexing. The reason
|
||||
// for thie line is to avoid agressive clipping of distant points.
|
||||
|
|
|
|||
15
manimlib/shaders/get_unit_normal.glsl
Normal file
15
manimlib/shaders/get_unit_normal.glsl
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
|
||||
vec3 get_unit_normal(in vec3 point0, in vec3 point1, in vec3 point2){
|
||||
vec3 cp = cross(point1 - point0, point2 - point1);
|
||||
if(length(cp) == 0){
|
||||
return vec3(0.0, 0.0, 1.0);
|
||||
}else{
|
||||
if(cp.z < 0){
|
||||
// After re-orienting, camera will always sit in the positive
|
||||
// z-direction. We always want normal vectors pointing towards
|
||||
// the camera.
|
||||
cp *= -1;
|
||||
}
|
||||
return normalize(cp);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,11 +3,7 @@
|
|||
// uniform float focal_distance;
|
||||
|
||||
vec3 position_point_into_frame(vec3 point){
|
||||
// Most of the heavy lifting is done by the pre-computed
|
||||
// to_screen_space matrix; here's there just a little added
|
||||
// perspective morphing.
|
||||
// Apply the pre-computed to_screen_space matrix.
|
||||
vec4 new_point = to_screen_space * vec4(point, 1);
|
||||
new_point.z /= focal_distance;
|
||||
new_point.xy /= max(1 - new_point.z, 0);
|
||||
return new_point.xyz;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,9 +70,9 @@ int cubic_solve(float a, float b, float c, float d, out float roots[3]){
|
|||
float dist_to_line(vec2 p, vec2 b2){
|
||||
float t = clamp(p.x / b2.x, 0, 1);
|
||||
float dist;
|
||||
if(t == 0) dist = length(p);
|
||||
if(t == 0) dist = length(p);
|
||||
else if(t == 1) dist = distance(p, b2);
|
||||
else dist = abs(p.y);
|
||||
else dist = abs(p.y);
|
||||
|
||||
return modify_distance_for_endpoints(p, dist, t);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,18 @@
|
|||
#version 330
|
||||
|
||||
uniform vec3 light_source_position;
|
||||
uniform mat4 to_screen_space;
|
||||
|
||||
in vec4 color;
|
||||
in float fill_all; // Either 0 or 1e
|
||||
in float uv_anti_alias_width;
|
||||
|
||||
in vec3 xyz_coords;
|
||||
in vec3 unit_normal;
|
||||
in vec2 uv_coords;
|
||||
in vec2 uv_b2;
|
||||
in float bezier_degree;
|
||||
in float gloss;
|
||||
|
||||
out vec4 frag_color;
|
||||
|
||||
|
|
@ -19,13 +25,15 @@ float modify_distance_for_endpoints(vec2 p, float dist, float t){
|
|||
// so to share functionality between this and others, the caller
|
||||
// replaces this line with the contents of quadratic_bezier_sdf.glsl
|
||||
#INSERT quadratic_bezier_distance.glsl
|
||||
#INSERT add_light.glsl
|
||||
|
||||
|
||||
float sdf(){
|
||||
// For really flat curves, just take the distance to the curve
|
||||
if(bezier_degree < 2 || abs(uv_b2.y / uv_b2.x) < uv_anti_alias_width){
|
||||
return min_dist_to_curve(uv_coords, uv_b2, bezier_degree);
|
||||
}
|
||||
// This converts uv_coords to a space where the bezier points sit on
|
||||
// This converts uv_coords to yet another space where the bezier points sit on
|
||||
// (0, 0), (1/2, 0) and (1, 1), so that the curve can be expressed implicityly
|
||||
// as y = x^2.
|
||||
float u2 = uv_b2.x;
|
||||
|
|
@ -36,10 +44,11 @@ float sdf(){
|
|||
);
|
||||
vec2 p = to_simple_space * uv_coords;
|
||||
|
||||
// Sign takes care of whether we should be filling the inside or outside of curve.
|
||||
float Fp = sign(v2) * (p.x * p.x - p.y);
|
||||
|
||||
vec2 grad = vec2(
|
||||
- 2 * p.x * v2, // del C / del u
|
||||
-2 * p.x * v2, // del C / del u
|
||||
4 * v2 - 4 * p.x * (2 - u2) // del C / del v
|
||||
);
|
||||
return Fp / length(grad);
|
||||
|
|
@ -48,8 +57,7 @@ float sdf(){
|
|||
|
||||
void main() {
|
||||
if (color.a == 0) discard;
|
||||
frag_color = color;
|
||||
// TODO, Add shading based on normal vector, light position and gloss
|
||||
frag_color = add_light(color, xyz_coords, unit_normal, light_source_position, gloss);
|
||||
if (fill_all == 1.0) return;
|
||||
frag_color.a *= smoothstep(1, 0, sdf() / uv_anti_alias_width);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,32 +3,40 @@
|
|||
layout (triangles) in;
|
||||
layout (triangle_strip, max_vertices = 5) out;
|
||||
|
||||
uniform float anti_alias_width;
|
||||
// Needed for get_gl_Position
|
||||
uniform float aspect_ratio;
|
||||
uniform float anti_alias_width;
|
||||
uniform float focal_distance;
|
||||
|
||||
in vec3 bp[3];
|
||||
in vec4 v_color[3];
|
||||
in float v_fill_all[3];
|
||||
in float v_gloss[3];
|
||||
|
||||
out vec4 color;
|
||||
out float fill_all;
|
||||
out float uv_anti_alias_width;
|
||||
|
||||
out vec3 xyz_coords;
|
||||
out vec3 unit_normal;
|
||||
// uv space is where b0 = (0, 0), b1 = (1, 0), and transform is orthogonal
|
||||
out vec2 uv_coords;
|
||||
out vec2 uv_b2;
|
||||
|
||||
out float bezier_degree;
|
||||
out float gloss;
|
||||
|
||||
// To my knowledge, there is no notion of #include for shaders,
|
||||
// so to share functionality between this and others, the caller
|
||||
// in manim replaces this line with the contents of named file
|
||||
#INSERT quadratic_bezier_geometry_functions.glsl
|
||||
#INSERT get_gl_Position.glsl
|
||||
#INSERT get_unit_normal.glsl
|
||||
|
||||
void emit_simple_triangle(){
|
||||
for(int i = 0; i < 3; i++){
|
||||
color = v_color[i];
|
||||
gloss = v_gloss[i];
|
||||
xyz_coords = bp[i];
|
||||
gl_Position = get_gl_Position(bp[i]);
|
||||
EmitVertex();
|
||||
}
|
||||
|
|
@ -36,46 +44,48 @@ void emit_simple_triangle(){
|
|||
}
|
||||
|
||||
|
||||
void emit_pentagon(vec2 bp0, vec2 bp1, vec2 bp2){
|
||||
void emit_pentagon(vec3[3] points, vec3 normal){
|
||||
vec3 p0 = points[0];
|
||||
vec3 p1 = points[1];
|
||||
vec3 p2 = points[2];
|
||||
// Tangent vectors
|
||||
vec2 t01 = normalize(bp1 - bp0);
|
||||
vec2 t12 = normalize(bp2 - bp1);
|
||||
// Normal vectors
|
||||
// Rotate tangent vector 90-degrees clockwise
|
||||
vec2 n01 = vec2(t01.y, -t01.x);
|
||||
vec2 n12 = vec2(t12.y, -t12.x);
|
||||
vec3 t01 = normalize(p1 - p0);
|
||||
vec3 t12 = normalize(p2 - p1);
|
||||
// Vectors normal to the curve in the plane of the curve
|
||||
vec3 n01 = cross(t01, normal);
|
||||
vec3 n12 = cross(t12, normal);
|
||||
|
||||
float c_orient = sign(cross(t01, t12));
|
||||
bool fill_in = (c_orient > 0);
|
||||
// Assume you always fill in to the left of the curve
|
||||
float orient = sign(dot(cross(t01, t12), normal));
|
||||
bool fill_in = (orient > 0);
|
||||
|
||||
float aaw = anti_alias_width;
|
||||
vec2 nudge1 = fill_in ? 0.5 * aaw * (n01 + n12) : vec2(0);
|
||||
vec2 corners[5] = vec2[5](
|
||||
bp0 + aaw * n01,
|
||||
bp0,
|
||||
bp1 + nudge1,
|
||||
bp2,
|
||||
bp2 + aaw * n12
|
||||
float aaw = anti_alias_width / normal.z;
|
||||
vec3 nudge1 = fill_in ? 0.5 * aaw * (n01 + n12) : vec3(0);
|
||||
vec3 corners[5] = vec3[5](
|
||||
p0 + aaw * n01,
|
||||
p0,
|
||||
p1 + nudge1,
|
||||
p2,
|
||||
p2 + aaw * n12
|
||||
);
|
||||
|
||||
int coords_index_map[5] = int[5](0, 1, 2, 3, 4);
|
||||
if(!fill_in) coords_index_map = int[5](1, 0, 2, 4, 3);
|
||||
|
||||
mat3 xy_to_uv = get_xy_to_uv(bp0, bp1);
|
||||
uv_b2 = (xy_to_uv * vec3(bp2, 1)).xy;
|
||||
uv_anti_alias_width = anti_alias_width / length(bp1 - bp0);
|
||||
mat4 xyz_to_uv = get_xyz_to_uv(p0, p1, normal);
|
||||
uv_b2 = (xyz_to_uv * vec4(p2, 1)).xy;
|
||||
uv_anti_alias_width = anti_alias_width / length(p1 - p0);
|
||||
|
||||
int nearest_bp_index_map[5] = int[5](0, 0, 1, 2, 2);
|
||||
for(int i = 0; i < 5; i++){
|
||||
vec2 corner = corners[coords_index_map[i]];
|
||||
float z = bp[nearest_bp_index_map[i]].z;
|
||||
uv_coords = (xy_to_uv * vec3(corner, 1)).xy;
|
||||
vec3 corner = corners[coords_index_map[i]];
|
||||
xyz_coords = corner;
|
||||
uv_coords = (xyz_to_uv * vec4(corner, 1)).xy;
|
||||
// I haven't a clue why an index map doesn't work just
|
||||
// as well here, but for some reason it doesn't.
|
||||
if(i < 2) color = v_color[0];
|
||||
else if(i == 2) color = v_color[1];
|
||||
else color = v_color[2];
|
||||
gl_Position = get_gl_Position(vec3(corner, z));
|
||||
int j = int(sign(i - 1) + 1); // Maps 0, 1, 2, 3, 4 onto 0, 0, 1, 2, 2
|
||||
color = v_color[j];
|
||||
gloss = v_gloss[j];
|
||||
gl_Position = get_gl_Position(corner);
|
||||
EmitVertex();
|
||||
}
|
||||
EndPrimitive();
|
||||
|
|
@ -84,29 +94,16 @@ void emit_pentagon(vec2 bp0, vec2 bp1, vec2 bp2){
|
|||
|
||||
void main(){
|
||||
fill_all = v_fill_all[0];
|
||||
unit_normal = get_unit_normal(bp[0], bp[1], bp[2]);
|
||||
|
||||
if(fill_all == 1){
|
||||
emit_simple_triangle();
|
||||
return;
|
||||
}
|
||||
|
||||
vec2 new_bp[3];
|
||||
int n = get_reduced_control_points(bp[0].xy, bp[1].xy, bp[2].xy, new_bp);
|
||||
bezier_degree = float(n);
|
||||
vec2 bp0, bp1, bp2;
|
||||
if(n == 0){
|
||||
return; // Don't emit any vertices
|
||||
}
|
||||
else if(n == 1){
|
||||
bp0 = new_bp[0];
|
||||
bp2 = new_bp[1];
|
||||
bp1 = 0.5 * (bp0 + bp2);
|
||||
}else{
|
||||
bp0 = new_bp[0];
|
||||
bp1 = new_bp[1];
|
||||
bp2 = new_bp[2];
|
||||
}
|
||||
|
||||
emit_pentagon(bp0, bp1, bp2);
|
||||
vec3 new_bp[3];
|
||||
bezier_degree = get_reduced_control_points(vec3[3](bp[0], bp[1], bp[2]), new_bp);
|
||||
if(bezier_degree == 0) return; // Don't emit any vertices
|
||||
emit_pentagon(new_bp, unit_normal);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
#version 330
|
||||
|
||||
uniform mat4 to_screen_space;
|
||||
uniform float focal_distance;
|
||||
|
||||
in vec3 point;
|
||||
in vec4 color;
|
||||
in float fill_all; // Either 0 or 1
|
||||
in float gloss;
|
||||
|
||||
out vec3 bp; // Bezier control point
|
||||
out vec4 v_color;
|
||||
out float v_fill_all;
|
||||
out float v_gloss;
|
||||
|
||||
// To my knowledge, there is no notion of #include for shaders,
|
||||
// so to share functionality between this and others, the caller
|
||||
|
|
@ -20,4 +21,5 @@ void main(){
|
|||
bp = position_point_into_frame(point);
|
||||
v_color = color;
|
||||
v_fill_all = fill_all;
|
||||
v_gloss = gloss;
|
||||
}
|
||||
|
|
@ -1,26 +1,33 @@
|
|||
// This file is not a shader, it's just a set of
|
||||
// functions meant to be inserted into other shaders.
|
||||
|
||||
float cross(vec2 v, vec2 w){
|
||||
float cross2d(vec2 v, vec2 w){
|
||||
return v.x * w.y - w.x * v.y;
|
||||
}
|
||||
|
||||
// Matrix to convert to a uv space defined so that
|
||||
// Orthogonal matrix to convert to a uv space defined so that
|
||||
// b0 goes to [0, 0] and b1 goes to [1, 0]
|
||||
mat3 get_xy_to_uv(vec2 b0, vec2 b1){
|
||||
vec2 T = b1 - b0;
|
||||
|
||||
mat3 shift = mat3(
|
||||
1, 0, 0,
|
||||
0, 1, 0,
|
||||
-b0.x, -b0.y, 1
|
||||
mat4 get_xyz_to_uv(vec3 b0, vec3 b1, vec3 unit_normal){
|
||||
mat4 shift = mat4(
|
||||
1, 0, 0, 0,
|
||||
0, 1, 0, 0,
|
||||
0, 0, 1, 0,
|
||||
-b0.x, -b0.y, -b0.z, 1
|
||||
);
|
||||
mat3 rotate_and_scale = mat3(
|
||||
T.x, -T.y, 0,
|
||||
T.y, T.x, 0,
|
||||
0, 0, 1
|
||||
) / dot(T, T);
|
||||
return rotate_and_scale * shift;
|
||||
|
||||
float scale_factor = length(b1 - b0);
|
||||
vec3 I = (b1 - b0) / scale_factor;
|
||||
vec3 K = unit_normal;
|
||||
vec3 J = cross(K, I);
|
||||
// Transpose (hence inverse) of matrix taking
|
||||
// i-hat to I, k-hat to unit_normal, and j-hat to their cross
|
||||
mat4 rotate = mat4(
|
||||
I.x, J.x, K.x, 0,
|
||||
I.y, J.y, K.y, 0,
|
||||
I.z, J.z, K.z, 0,
|
||||
0, 0, 0 , 1
|
||||
);
|
||||
return (1 / scale_factor) * rotate * shift;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -29,12 +36,17 @@ mat3 get_xy_to_uv(vec2 b0, vec2 b1){
|
|||
// which for quadratics will be the same, but for linear and null
|
||||
// might change. The idea is to inform the caller of the degree,
|
||||
// while also passing tangency information in the linear case.
|
||||
int get_reduced_control_points(vec2 b0, vec2 b1, vec2 b2, out vec2 new_points[3]){
|
||||
// float get_reduced_control_points(vec3 b0, vec3 b1, vec3 b2, out vec3 new_points[3]){
|
||||
float get_reduced_control_points(in vec3 points[3], out vec3 new_points[3]){
|
||||
float length_threshold = 1e-6;
|
||||
float angle_threshold = 1e-3;
|
||||
vec2 v01 = (b1 - b0);
|
||||
vec2 v12 = (b2 - b1);
|
||||
// bool aligned = abs(cross(normalize(v01), normalize(v12))) < angle_threshold;
|
||||
|
||||
vec3 p0 = points[0];
|
||||
vec3 p1 = points[1];
|
||||
vec3 p2 = points[2];
|
||||
vec3 v01 = (p1 - p0);
|
||||
vec3 v12 = (p2 - p1);
|
||||
|
||||
bool aligned = acos(dot(normalize(v01), normalize(v12))) < angle_threshold;
|
||||
bool distinct_01 = length(v01) > length_threshold; // v01 is considered nonzero
|
||||
bool distinct_12 = length(v12) > length_threshold; // v12 is considered nonzero
|
||||
|
|
@ -44,16 +56,19 @@ int get_reduced_control_points(vec2 b0, vec2 b1, vec2 b2, out vec2 new_points[3]
|
|||
bool linear = (n_uniques == 1) || ((n_uniques == 2) && aligned);
|
||||
bool constant = (n_uniques == 0);
|
||||
if(quadratic){
|
||||
new_points[0] = b0;
|
||||
new_points[1] = b1;
|
||||
new_points[2] = b2;
|
||||
return 2;
|
||||
new_points[0] = p0;
|
||||
new_points[1] = p1;
|
||||
new_points[2] = p2;
|
||||
return 2.0;
|
||||
}else if(linear){
|
||||
new_points[0] = b0;
|
||||
new_points[1] = b2;
|
||||
return 1;
|
||||
new_points[0] = p0;
|
||||
new_points[1] = (p0 + p2) / 2.0;
|
||||
new_points[2] = p2;
|
||||
return 1.0;
|
||||
}else{
|
||||
new_points[0] = b0;
|
||||
return 0;
|
||||
new_points[0] = p0;
|
||||
new_points[1] = p0;
|
||||
new_points[2] = p0;
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,16 @@
|
|||
#version 330
|
||||
|
||||
uniform mat4 to_screen_space;
|
||||
uniform vec3 light_source_position;
|
||||
|
||||
in vec3 xyz_coords;
|
||||
in vec3 unit_normal;
|
||||
in vec2 uv_coords;
|
||||
in vec2 uv_b2;
|
||||
|
||||
in float uv_stroke_width;
|
||||
in vec4 color;
|
||||
in float gloss;
|
||||
in float uv_anti_alias_width;
|
||||
|
||||
in float has_prev;
|
||||
|
|
@ -19,7 +25,7 @@ in float bezier_degree;
|
|||
out vec4 frag_color;
|
||||
|
||||
|
||||
float cross(vec2 v, vec2 w){
|
||||
float cross2d(vec2 v, vec2 w){
|
||||
return v.x * w.y - w.x * v.y;
|
||||
}
|
||||
|
||||
|
|
@ -64,8 +70,8 @@ float modify_distance_for_endpoints(vec2 p, float dist, float t){
|
|||
);
|
||||
vec2 v21_unit = v21 / length(v21);
|
||||
float bevel_d = max(
|
||||
abs(cross(p - uv_b2, v21_unit)),
|
||||
abs(cross((rot * (p - uv_b2)), v21_unit))
|
||||
abs(cross2d(p - uv_b2, v21_unit)),
|
||||
abs(cross2d((rot * (p - uv_b2)), v21_unit))
|
||||
);
|
||||
return min(dist, bevel_d);
|
||||
}
|
||||
|
|
@ -78,12 +84,15 @@ float modify_distance_for_endpoints(vec2 p, float dist, float t){
|
|||
// so to share functionality between this and others, the caller
|
||||
// replaces this line with the contents of named file
|
||||
#INSERT quadratic_bezier_distance.glsl
|
||||
#INSERT add_light.glsl
|
||||
|
||||
|
||||
void main() {
|
||||
if (uv_stroke_width == 0) discard;
|
||||
|
||||
frag_color = color;
|
||||
// Add lighting if needed
|
||||
frag_color = add_light(color, xyz_coords, unit_normal, light_source_position, gloss);
|
||||
|
||||
float dist_to_curve = min_dist_to_curve(uv_coords, uv_b2, bezier_degree);
|
||||
// An sdf for the region around the curve we wish to color.
|
||||
float signed_dist = abs(dist_to_curve) - 0.5 * uv_stroke_width;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ layout (triangle_strip, max_vertices = 5) out;
|
|||
|
||||
// Needed for get_gl_Position
|
||||
uniform float aspect_ratio;
|
||||
uniform float focal_distance;
|
||||
uniform float anti_alias_width;
|
||||
|
||||
in vec3 bp[3];
|
||||
|
|
@ -14,9 +15,11 @@ in vec3 next_bp[3];
|
|||
in vec4 v_color[3];
|
||||
in float v_stroke_width[3];
|
||||
in float v_joint_type[3];
|
||||
in float v_gloss[3];
|
||||
|
||||
out vec4 color;
|
||||
out float uv_stroke_width;
|
||||
out float gloss;
|
||||
out float uv_anti_alias_width;
|
||||
|
||||
out float has_prev;
|
||||
|
|
@ -28,6 +31,8 @@ out float angle_to_next;
|
|||
|
||||
out float bezier_degree;
|
||||
|
||||
out vec3 xyz_coords;
|
||||
out vec3 unit_normal;
|
||||
out vec2 uv_coords;
|
||||
out vec2 uv_b2;
|
||||
|
||||
|
|
@ -43,35 +48,36 @@ const float MITER_JOINT = 3;
|
|||
// replaces this line with the contents of named file
|
||||
#INSERT quadratic_bezier_geometry_functions.glsl
|
||||
#INSERT get_gl_Position.glsl
|
||||
#INSERT get_unit_normal.glsl
|
||||
|
||||
|
||||
float angle_between_vectors(vec2 v1, vec2 v2){
|
||||
vec2 nv1 = normalize(v1);
|
||||
vec2 nv2 = normalize(v2);
|
||||
float angle_between_vectors(vec3 v1, vec3 v2, vec3 normal){
|
||||
vec3 nv1 = normalize(v1);
|
||||
vec3 nv2 = normalize(v2);
|
||||
float unsigned_angle = acos(clamp(dot(nv1, nv2), -1, 1));
|
||||
float sn = sign(cross(nv1, nv2));
|
||||
float sn = sign(dot(cross(nv1, nv2), normal));
|
||||
return sn * unsigned_angle;
|
||||
}
|
||||
|
||||
|
||||
bool find_intersection(vec2 p0, vec2 v0, vec2 p1, vec2 v1, out vec2 intersection){
|
||||
bool find_intersection(vec3 p0, vec3 v0, vec3 p1, vec3 v1, vec3 normal, out vec3 intersection){
|
||||
// Find the intersection of a line passing through
|
||||
// p0 in the direction v0 and one passing through p1 in
|
||||
// the direction p1.
|
||||
// That is, find a solutoin to p0 + v0 * t = p1 + v1 * s
|
||||
// float det = -v0.x * v1.y + v1.x * v0.y;
|
||||
float det = cross(v1, v0);
|
||||
float det = dot(cross(v1, v0), normal);
|
||||
if(det == 0){
|
||||
// intersection = p0;
|
||||
return false;
|
||||
}
|
||||
float t = cross(p0 - p1, v1) / det;
|
||||
float t = dot(cross(p0 - p1, v1), normal) / det;
|
||||
intersection = p0 + v0 * t;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
bool is_between(vec2 p, vec2 a, vec2 b){
|
||||
bool is_between(vec3 p, vec3 a, vec3 b){
|
||||
// Assumes three points fall on a line, returns whether
|
||||
// or not p sits between a and b.
|
||||
float d_pa = distance(p, a);
|
||||
|
|
@ -83,18 +89,18 @@ bool is_between(vec2 p, vec2 a, vec2 b){
|
|||
|
||||
// Tries to detect if one of the corners defined by the buffer around
|
||||
// b0 and b2 should be modified to form a better convex hull
|
||||
bool should_motify_corner(vec2 c, vec2 from_c, vec2 o1, vec2 o2, vec2 from_o, float buff){
|
||||
vec2 int1;
|
||||
vec2 int2;
|
||||
find_intersection(c, from_c, o1, from_o, int1);
|
||||
find_intersection(c, from_c, o2, from_o, int2);
|
||||
bool should_motify_corner(vec3 c, vec3 from_c, vec3 o1, vec3 o2, vec3 from_o, vec3 normal, float buff){
|
||||
vec3 int1;
|
||||
vec3 int2;
|
||||
find_intersection(c, from_c, o1, from_o, normal, int1);
|
||||
find_intersection(c, from_c, o2, from_o, normal, int2);
|
||||
return !is_between(int2, c + 1 * from_c * buff, int1);
|
||||
}
|
||||
|
||||
|
||||
void create_joint(float angle, vec2 unit_tan, float buff, float should_bevel,
|
||||
vec2 static_c0, out vec2 changing_c0,
|
||||
vec2 static_c1, out vec2 changing_c1){
|
||||
void create_joint(float angle, vec3 unit_tan, float buff, float should_bevel,
|
||||
vec3 static_c0, out vec3 changing_c0,
|
||||
vec3 static_c1, out vec3 changing_c1){
|
||||
float shift;
|
||||
float joint_type = v_joint_type[0];
|
||||
bool miter = (
|
||||
|
|
@ -118,38 +124,37 @@ void create_joint(float angle, vec2 unit_tan, float buff, float should_bevel,
|
|||
// This function is responsible for finding the corners of
|
||||
// a bounding region around the bezier curve, which can be
|
||||
// emitted as a triangle fan
|
||||
int get_corners(vec2 controls[3], int degree, out vec2 corners[5]){
|
||||
int get_corners(vec3 controls[3], vec3 normal, int degree, out vec3 corners[5]){
|
||||
vec3 p0 = controls[0];
|
||||
vec3 p1 = controls[1];
|
||||
vec3 p2 = controls[2];
|
||||
|
||||
// Unit vectors for directions between
|
||||
// Various control points
|
||||
vec2 v02, v20, v10, v01, v12, v21;
|
||||
|
||||
vec2 p0 = controls[0];
|
||||
vec2 p2 = controls[degree];
|
||||
v02 = normalize(p2 - p0);
|
||||
v20 = -v02;
|
||||
if(degree == 2){
|
||||
v10 = normalize(p0 - controls[1]);
|
||||
v12 = normalize(p2 - controls[1]);
|
||||
}else{
|
||||
v10 = v20;
|
||||
v12 = v02;
|
||||
}
|
||||
v01 = -v10;
|
||||
v21 = -v12;
|
||||
vec3 v02 = normalize(p2 - p0);
|
||||
vec3 v10 = normalize(p0 - p1);
|
||||
vec3 v12 = normalize(p2 - p1);
|
||||
vec3 v20 = -v02;
|
||||
vec3 v01 = -v10;
|
||||
vec3 v21 = -v12;
|
||||
|
||||
// Find bounding points around ends
|
||||
vec2 p0_perp = vec2(-v01.y, v01.x);
|
||||
vec2 p2_perp = vec2(-v21.y, v21.x);
|
||||
vec3 p0_perp = cross(normal, v01);
|
||||
vec3 p2_perp = cross(normal, v21);
|
||||
|
||||
float buff0 = 0.5 * v_stroke_width[0] + anti_alias_width;
|
||||
float buff2 = 0.5 * v_stroke_width[2] + anti_alias_width;
|
||||
float aaw0 = (1 - has_prev) * anti_alias_width;
|
||||
float aaw2 = (1 - has_next) * anti_alias_width;
|
||||
// aaw is the added width given around the polygon for antialiasing.
|
||||
// In case the normal is faced away from (0, 0, 1), the vector to the
|
||||
// camera, this is scaled up.
|
||||
float aaw = anti_alias_width / normal.z;
|
||||
float buff0 = 0.5 * v_stroke_width[0] + aaw;
|
||||
float buff2 = 0.5 * v_stroke_width[2] + aaw;
|
||||
float aaw0 = (1 - has_prev) * aaw;
|
||||
float aaw2 = (1 - has_next) * aaw;
|
||||
|
||||
vec2 c0 = p0 - buff0 * p0_perp + aaw0 * v10;
|
||||
vec2 c1 = p0 + buff0 * p0_perp + aaw0 * v10;
|
||||
vec2 c2 = p2 - p2_perp * buff2 + aaw2 * v12;
|
||||
vec2 c3 = p2 + p2_perp * buff2 + aaw2 * v12;
|
||||
vec3 c0 = p0 - buff0 * p0_perp + aaw0 * v10;
|
||||
vec3 c1 = p0 + buff0 * p0_perp + aaw0 * v10;
|
||||
vec3 c2 = p2 - buff2 * p2_perp + aaw2 * v12;
|
||||
vec3 c3 = p2 + buff2 * p2_perp + aaw2 * v12;
|
||||
|
||||
// Account for previous and next control points
|
||||
if(has_prev == 1){
|
||||
|
|
@ -163,56 +168,57 @@ int get_corners(vec2 controls[3], int degree, out vec2 corners[5]){
|
|||
if(degree == 1){
|
||||
// Swap between 2 and 3 is deliberate, the order of corners
|
||||
// should be for a triangle_strip. Last entry is a dummy
|
||||
corners = vec2[5](c0, c1, c3, c2, vec2(0.0));
|
||||
corners = vec3[5](c0, c1, c3, c2, vec3(0.0));
|
||||
return 4;
|
||||
}
|
||||
|
||||
// Some admitedly complicated logic to (hopefully efficiently)
|
||||
// make sure corners forms a convex hull around the curve.
|
||||
if(cross(v10, v12) > 0){
|
||||
if(dot(cross(v10, v12), normal) > 0){
|
||||
bool change_c0 = (
|
||||
// has_prev == 0 &&
|
||||
dot(v21, v20) > 0 &&
|
||||
should_motify_corner(c0, v01, c2, c3, v21, buff0)
|
||||
should_motify_corner(c0, v01, c2, c3, v21, normal, buff0)
|
||||
);
|
||||
if(change_c0) c0 = p0 + p2_perp * buff0;
|
||||
|
||||
bool change_c3 = (
|
||||
// has_next == 0 &&
|
||||
dot(v01, v02) > 0 &&
|
||||
should_motify_corner(c3, v21, c1, c0, v01, buff2)
|
||||
should_motify_corner(c3, v21, c1, c0, v01, normal, buff2)
|
||||
);
|
||||
if(change_c3) c3 = p2 - p0_perp * buff2;
|
||||
|
||||
vec2 i12;
|
||||
find_intersection(c1, v01, c2, v21, i12);
|
||||
corners = vec2[5](c1, c0, i12, c3, c2);
|
||||
vec3 i12;
|
||||
find_intersection(c1, v01, c2, v21, normal, i12);
|
||||
corners = vec3[5](c1, c0, i12, c3, c2);
|
||||
}else{
|
||||
bool change_c1 = (
|
||||
// has_prev == 0 &&
|
||||
dot(v21, v20) > 0 &&
|
||||
should_motify_corner(c1, v01, c3, c2, v21, buff0)
|
||||
should_motify_corner(c1, v01, c3, c2, v21, normal, buff0)
|
||||
);
|
||||
if(change_c1) c1 = p0 - p2_perp * buff0;
|
||||
|
||||
bool change_c2 = (
|
||||
// has_next == 0 &&
|
||||
dot(v01, v02) > 0 &&
|
||||
should_motify_corner(c2, v21, c0, c1, v01, buff2)
|
||||
should_motify_corner(c2, v21, c0, c1, v01, normal, buff2)
|
||||
);
|
||||
if(change_c2) c2 = p2 + p0_perp * buff2;
|
||||
|
||||
vec2 i03;
|
||||
find_intersection(c0, v01, c3, v21, i03);
|
||||
corners = vec2[5](c0, c1, i03, c2, c3);
|
||||
vec3 i03;
|
||||
find_intersection(c0, v01, c3, v21, normal, i03);
|
||||
corners = vec3[5](c0, c1, i03, c2, c3);
|
||||
}
|
||||
return 5;
|
||||
}
|
||||
|
||||
|
||||
void set_adjascent_info(vec2 c0, vec2 tangent,
|
||||
int degree, int mult, int flip,
|
||||
vec2 adj[3],
|
||||
void set_adjascent_info(vec3 c0, vec3 tangent,
|
||||
int degree,
|
||||
vec3 normal,
|
||||
vec3 adj[3],
|
||||
out float has,
|
||||
out float bevel,
|
||||
out float angle
|
||||
|
|
@ -223,17 +229,15 @@ void set_adjascent_info(vec2 c0, vec2 tangent,
|
|||
bevel = 0;
|
||||
angle = 0;
|
||||
|
||||
vec2 new_adj[3];
|
||||
int adj_degree = get_reduced_control_points(
|
||||
adj[0], adj[1], adj[2], new_adj
|
||||
);
|
||||
vec3 new_adj[3];
|
||||
float adj_degree = get_reduced_control_points(adj, new_adj);
|
||||
has = float(adj_degree > 0);
|
||||
if(has == 1){
|
||||
vec2 adj = new_adj[mult * adj_degree - flip];
|
||||
angle = flip * angle_between_vectors(c0 - adj, tangent);
|
||||
vec3 adj = new_adj[1];
|
||||
angle = angle_between_vectors(c0 - adj, tangent, normal);
|
||||
}
|
||||
// Decide on joint type
|
||||
bool one_linear = (degree == 1 || adj_degree == 1);
|
||||
bool one_linear = (degree == 1 || adj_degree == 1.0);
|
||||
bool should_bevel = (
|
||||
(joint_type == AUTO_JOINT && one_linear) ||
|
||||
joint_type == BEVEL_JOINT
|
||||
|
|
@ -242,22 +246,22 @@ void set_adjascent_info(vec2 c0, vec2 tangent,
|
|||
}
|
||||
|
||||
|
||||
void set_previous_and_next(vec2 controls[3], int degree){
|
||||
void set_previous_and_next(vec3 controls[3], int degree, vec3 normal){
|
||||
float a_tol = 1e-10;
|
||||
|
||||
if(distance(prev_bp[2], bp[0]) < a_tol){
|
||||
vec2 tangent = controls[1] - controls[0];
|
||||
vec3 tangent = controls[1] - controls[0];
|
||||
set_adjascent_info(
|
||||
controls[0], tangent, degree, 1, 1,
|
||||
vec2[3](prev_bp[0].xy, prev_bp[1].xy, prev_bp[2].xy),
|
||||
controls[0], tangent, degree, normal,
|
||||
vec3[3](prev_bp[0], prev_bp[1], prev_bp[2]),
|
||||
has_prev, bevel_start, angle_from_prev
|
||||
);
|
||||
}
|
||||
if(distance(next_bp[0], bp[2]) < a_tol){
|
||||
vec2 tangent = controls[degree - 1] - controls[degree];
|
||||
vec3 tangent = controls[1] - controls[2];
|
||||
set_adjascent_info(
|
||||
controls[degree], tangent, degree, 0, -1,
|
||||
vec2[3](next_bp[0].xy, next_bp[1].xy, next_bp[2].xy),
|
||||
controls[2], tangent, degree, normal,
|
||||
vec3[3](next_bp[0], next_bp[1], next_bp[2]),
|
||||
has_next, bevel_end, angle_to_next
|
||||
);
|
||||
}
|
||||
|
|
@ -265,47 +269,42 @@ void set_previous_and_next(vec2 controls[3], int degree){
|
|||
|
||||
|
||||
void main() {
|
||||
vec2 controls[3];
|
||||
int degree = get_reduced_control_points(bp[0].xy, bp[1].xy, bp[2].xy, controls);
|
||||
bezier_degree = float(degree);
|
||||
unit_normal = get_unit_normal(bp[0], bp[1], bp[2]);
|
||||
// unit_normal = vec3(0, 0, 1);
|
||||
|
||||
// Null curve or linear with higher index than needed
|
||||
vec3 controls[3];
|
||||
bezier_degree = get_reduced_control_points(vec3[3](bp[0], bp[1], bp[2]), controls);
|
||||
int degree = int(bezier_degree);
|
||||
|
||||
// Null curve
|
||||
if(degree == 0) return;
|
||||
|
||||
set_previous_and_next(controls, degree);
|
||||
set_previous_and_next(controls, degree, unit_normal);
|
||||
|
||||
// Find uv conversion matrix
|
||||
mat3 xy_to_uv = get_xy_to_uv(controls[0], controls[1]);
|
||||
mat4 xyz_to_uv = get_xyz_to_uv(controls[0], controls[1], unit_normal);
|
||||
float scale_factor = length(controls[1] - controls[0]);
|
||||
uv_anti_alias_width = anti_alias_width / scale_factor;
|
||||
uv_b2 = (xy_to_uv * vec3(controls[degree], 1.0)).xy;
|
||||
uv_anti_alias_width = anti_alias_width / scale_factor / unit_normal.z;
|
||||
uv_b2 = (xyz_to_uv * vec4(controls[2], 1.0)).xy;
|
||||
|
||||
// Corners of a bounding region around curve
|
||||
vec2 corners[5];
|
||||
int n_corners = get_corners(controls, degree, corners);
|
||||
vec3 corners[5];
|
||||
int n_corners = get_corners(controls, unit_normal, degree, corners);
|
||||
|
||||
// Get style info aligned to the corners
|
||||
float stroke_widths[5];
|
||||
vec4 stroke_colors[5];
|
||||
float z_values[5];
|
||||
int index_map[5];
|
||||
if(n_corners == 4) index_map = int[5](0, 0, 2, 2, 2);
|
||||
else index_map = int[5](0, 0, 1, 2, 2);
|
||||
for(int i = 0; i < 5; i++){
|
||||
stroke_widths[i] = v_stroke_width[index_map[i]];
|
||||
stroke_colors[i] = v_color[index_map[i]];
|
||||
z_values[i] = bp[index_map[i]].z; // TODO, seems clunky
|
||||
}
|
||||
int index_map[5] = int[5](0, 0, 1, 2, 2);
|
||||
if(n_corners == 4) index_map[2] = 2;
|
||||
|
||||
// Emit each corner
|
||||
for(int i = 0; i < n_corners; i++){
|
||||
vec2 corner = corners[i];
|
||||
uv_coords = (xy_to_uv * vec3(corner, 1.0)).xy;
|
||||
|
||||
uv_stroke_width = stroke_widths[i] / scale_factor;
|
||||
color = stroke_colors[i];
|
||||
|
||||
gl_Position = get_gl_Position(vec3(corner, z_values[i]));
|
||||
xyz_coords = corners[i];
|
||||
uv_coords = (xyz_to_uv * vec4(xyz_coords, 1.0)).xy;
|
||||
uv_stroke_width = v_stroke_width[index_map[i]] / scale_factor;
|
||||
color = v_color[index_map[i]];
|
||||
gloss = v_gloss[index_map[i]];
|
||||
gl_Position = get_gl_Position(xyz_coords);
|
||||
EmitVertex();
|
||||
}
|
||||
EndPrimitive();
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ in vec3 next_point;
|
|||
in float stroke_width;
|
||||
in vec4 color;
|
||||
in float joint_type;
|
||||
in float gloss;
|
||||
|
||||
// Bezier control point
|
||||
out vec3 bp;
|
||||
|
|
@ -19,6 +20,7 @@ out vec3 next_bp;
|
|||
out float v_stroke_width;
|
||||
out vec4 v_color;
|
||||
out float v_joint_type;
|
||||
out float v_gloss;
|
||||
|
||||
const float STROKE_WIDTH_CONVERSION = 0.0025;
|
||||
|
||||
|
|
@ -33,7 +35,8 @@ void main(){
|
|||
next_bp = position_point_into_frame(next_point);
|
||||
|
||||
v_stroke_width = STROKE_WIDTH_CONVERSION * stroke_width;
|
||||
v_stroke_width /= (1 - bp.z); // Change stroke width by perspective
|
||||
// v_stroke_width /= (1 - bp.z / focal_distance); // Change stroke width by perspective
|
||||
v_color = color;
|
||||
v_joint_type = joint_type;
|
||||
v_gloss = gloss;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue