/* * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * * The Original Code is Copyright (C) 2020 Blender Foundation. * All rights reserved. */ /** \file * \ingroup edsculpt */ #include "MEM_guardedalloc.h" #include "BLI_blenlib.h" #include "BLI_dial_2d.h" #include "BLI_ghash.h" #include "BLI_gsqueue.h" #include "BLI_hash.h" #include "BLI_math.h" #include "BLI_task.h" #include "BLI_utildefines.h" #include "BLT_translation.h" #include "DNA_brush_types.h" #include "DNA_customdata_types.h" #include "DNA_mesh_types.h" #include "DNA_meshdata_types.h" #include "DNA_node_types.h" #include "DNA_object_types.h" #include "DNA_scene_types.h" #include "BKE_brush.h" #include "BKE_ccg.h" #include "BKE_colortools.h" #include "BKE_context.h" #include "BKE_image.h" #include "BKE_kelvinlet.h" #include "BKE_key.h" #include "BKE_lib_id.h" #include "BKE_main.h" #include "BKE_mesh.h" #include "BKE_mesh_mapping.h" #include "BKE_mesh_mirror.h" #include "BKE_modifier.h" #include "BKE_multires.h" #include "BKE_node.h" #include "BKE_object.h" #include "BKE_paint.h" #include "BKE_particle.h" #include "BKE_pbvh.h" #include "BKE_pointcache.h" #include "BKE_report.h" #include "BKE_scene.h" #include "BKE_screen.h" #include "BKE_subdiv_ccg.h" #include "BKE_subsurf.h" #include "DEG_depsgraph.h" #include "WM_api.h" #include "WM_message.h" #include "WM_toolsystem.h" #include "WM_types.h" #include "ED_object.h" #include "ED_screen.h" #include "ED_sculpt.h" #include "ED_view3d.h" #include "paint_intern.h" #include "sculpt_intern.h" #include "RNA_access.h" #include "RNA_define.h" #include "GPU_draw.h" #include "GPU_immediate.h" #include "GPU_immediate_util.h" #include "GPU_matrix.h" #include "GPU_state.h" #include "UI_interface.h" #include "UI_resources.h" #include "bmesh.h" #include "bmesh_tools.h" #include #include #include #define CLOTH_LENGTH_CONSTRAINTS_BLOCK 100000 #define CLOTH_SIMULATION_ITERATIONS 5 #define CLOTH_MAX_CONSTRAINTS_PER_VERTEX 1024 #define CLOTH_SIMULATION_TIME_STEP 0.01f static void cloth_brush_add_length_constraint(SculptSession *ss, const int v1, const int v2) { SculptClothSimulation *cloth_sim = ss->cache->cloth_sim; cloth_sim->length_constraints[cloth_sim->tot_length_constraints].v1 = v1; cloth_sim->length_constraints[cloth_sim->tot_length_constraints].v2 = v2; cloth_sim->length_constraints[cloth_sim->tot_length_constraints].length = len_v3v3( SCULPT_vertex_co_get(ss, v1), SCULPT_vertex_co_get(ss, v2)); cloth_sim->tot_length_constraints++; /* Reallocation if the array capacity is exceeded. */ if (cloth_sim->tot_length_constraints >= cloth_sim->capacity_length_constraints) { cloth_sim->capacity_length_constraints += CLOTH_LENGTH_CONSTRAINTS_BLOCK; cloth_sim->length_constraints = MEM_reallocN_id(cloth_sim->length_constraints, cloth_sim->capacity_length_constraints * sizeof(SculptClothLengthConstraint), "length constraints"); } } static void do_cloth_brush_build_constraints_task_cb_ex( void *__restrict userdata, const int n, const TaskParallelTLS *__restrict UNUSED(tls)) { SculptThreadedTaskData *data = userdata; SculptSession *ss = data->ob->sculpt; PBVHVertexIter vd; const float radius = ss->cache->initial_radius; const float limit = radius + (radius * data->brush->cloth_sim_limit); BKE_pbvh_vertex_iter_begin(ss->pbvh, data->nodes[n], vd, PBVH_ITER_UNIQUE) { if (len_squared_v3v3(vd.co, ss->cache->initial_location) < limit * limit) { SculptVertexNeighborIter ni; int build_indices[CLOTH_MAX_CONSTRAINTS_PER_VERTEX]; int tot_indices = 0; build_indices[tot_indices] = vd.index; tot_indices++; SCULPT_VERTEX_NEIGHBORS_ITER_BEGIN (ss, vd.index, ni) { build_indices[tot_indices] = ni.index; tot_indices++; } SCULPT_VERTEX_NEIGHBORS_ITER_END(ni); /* As we don't know the order of the neighbor vertices, we create all possible combinations * between the neighbor and the original vertex as length constraints. */ /* This results on a pattern that contains structural, shear and bending constraints for all * vertices, but constraints are repeated taking more memory than necessary. */ for (int c_i = 0; c_i < tot_indices; c_i++) { for (int c_j = 0; c_j < tot_indices; c_j++) { if (c_i != c_j) { cloth_brush_add_length_constraint(ss, build_indices[c_i], build_indices[c_j]); } } } } } BKE_pbvh_vertex_iter_end; } static float cloth_brush_simulation_falloff_get(const Brush *brush, const float radius, const float location[3], const float co[3]) { const float distance = len_v3v3(location, co); const float limit = radius + (radius * brush->cloth_sim_limit); const float falloff = radius + (radius * brush->cloth_sim_limit * brush->cloth_sim_falloff); if (distance > limit) { /* Outiside the limits. */ return 0.0f; } else if (distance < falloff) { /* Before the falloff area. */ return 1.0f; } else { /* Do a smoothstep transition inside the falloff area. */ float p = 1.0f - ((distance - falloff) / (limit - falloff)); return 3.0f * p * p - 2.0f * p * p * p; } } static void cloth_brush_apply_force_to_vertex(SculptSession *ss, const float force[3], const int vertex_index) { SculptClothSimulation *cloth_sim = ss->cache->cloth_sim; madd_v3_v3fl(cloth_sim->acceleration[vertex_index], force, 1.0f / cloth_sim->mass); } static void do_cloth_brush_apply_forces_task_cb_ex(void *__restrict userdata, const int n, const TaskParallelTLS *__restrict tls) { SculptThreadedTaskData *data = userdata; SculptSession *ss = data->ob->sculpt; const Brush *brush = data->brush; SculptClothSimulation *cloth_sim = ss->cache->cloth_sim; const float *offset = data->offset; const float *grab_delta = data->grab_delta; float(*imat)[4] = data->mat; const bool use_falloff_plane = brush->cloth_force_falloff_type == BRUSH_CLOTH_FORCE_FALLOFF_PLANE; PBVHVertexIter vd; const float bstrength = ss->cache->bstrength; SculptBrushTest test; SculptBrushTestFn sculpt_brush_test_sq_fn = SCULPT_brush_test_init_with_falloff_shape( ss, &test, data->brush->falloff_shape); /* For Pich Perpendicular Deform Type. */ float x_object_space[3]; float z_object_space[3]; if (brush->cloth_deform_type == BRUSH_CLOTH_DEFORM_PINCH_PERPENDICULAR) { normalize_v3_v3(x_object_space, imat[0]); normalize_v3_v3(z_object_space, imat[2]); } /* For Plane Force Falloff. */ float deform_plane[4]; float plane_normal[3]; if (use_falloff_plane) { normalize_v3_v3(plane_normal, grab_delta); plane_from_point_normal_v3(deform_plane, data->area_co, plane_normal); } /* Gravity */ float gravity[3] = {0.0f}; if (ss->cache->supports_gravity) { madd_v3_v3fl( gravity, ss->cache->gravity_direction, -ss->cache->radius * data->sd->gravity_factor); } BKE_pbvh_vertex_iter_begin(ss->pbvh, data->nodes[n], vd, PBVH_ITER_UNIQUE) { float force[3]; const float sim_factor = cloth_brush_simulation_falloff_get( brush, ss->cache->radius, ss->cache->initial_location, cloth_sim->init_pos[vd.index]); /* When using the plane falloff mode the falloff is not constrained by the brush radius. */ if (sculpt_brush_test_sq_fn(&test, vd.co) || use_falloff_plane) { float dist = sqrtf(test.dist); if (use_falloff_plane) { dist = dist_to_plane_v3(vd.co, deform_plane); } const float fade = sim_factor * bstrength * SCULPT_brush_strength_factor(ss, brush, vd.co, dist, vd.no, vd.fno, vd.mask ? *vd.mask : 0.0f, vd.index, tls->thread_id); float brush_disp[3]; float normal[3]; if (vd.no) { normal_short_to_float_v3(normal, vd.no); } else { copy_v3_v3(normal, vd.fno); } switch (brush->cloth_deform_type) { case BRUSH_CLOTH_DEFORM_DRAG: sub_v3_v3v3(brush_disp, ss->cache->location, ss->cache->last_location); normalize_v3(brush_disp); mul_v3_v3fl(force, brush_disp, fade); break; case BRUSH_CLOTH_DEFORM_PUSH: /* Invert the fade to push inwards. */ mul_v3_v3fl(force, offset, -fade); break; case BRUSH_CLOTH_DEFORM_GRAB: mul_v3_v3fl(force, grab_delta, fade); break; case BRUSH_CLOTH_DEFORM_PINCH_POINT: if (use_falloff_plane) { float distance = dist_signed_to_plane_v3(vd.co, deform_plane); copy_v3_v3(brush_disp, plane_normal); mul_v3_fl(brush_disp, -distance); } else { sub_v3_v3v3(brush_disp, ss->cache->location, vd.co); } normalize_v3(brush_disp); mul_v3_v3fl(force, brush_disp, fade); break; case BRUSH_CLOTH_DEFORM_PINCH_PERPENDICULAR: { float disp_center[3]; float x_disp[3]; float z_disp[3]; sub_v3_v3v3(disp_center, ss->cache->location, vd.co); normalize_v3(disp_center); mul_v3_v3fl(x_disp, x_object_space, dot_v3v3(disp_center, x_object_space)); mul_v3_v3fl(z_disp, z_object_space, dot_v3v3(disp_center, z_object_space)); add_v3_v3v3(disp_center, x_disp, z_disp); mul_v3_v3fl(force, disp_center, fade); } break; case BRUSH_CLOTH_DEFORM_INFLATE: mul_v3_v3fl(force, normal, fade); break; case BRUSH_CLOTH_DEFORM_EXPAND: cloth_sim->length_constraint_tweak[vd.index] += fade * 0.1f; zero_v3(force); break; } madd_v3_v3fl(force, gravity, fade); cloth_brush_apply_force_to_vertex(ss, force, vd.index); } } BKE_pbvh_vertex_iter_end; } static SculptClothSimulation *cloth_brush_simulation_create(SculptSession *ss, Brush *brush) { const int totverts = SCULPT_vertex_count_get(ss); SculptClothSimulation *cloth_sim; cloth_sim = MEM_callocN(sizeof(SculptClothSimulation), "cloth constraints"); cloth_sim->length_constraints = MEM_callocN(sizeof(SculptClothLengthConstraint) * CLOTH_LENGTH_CONSTRAINTS_BLOCK, "cloth length constraints"); cloth_sim->capacity_length_constraints = CLOTH_LENGTH_CONSTRAINTS_BLOCK; cloth_sim->acceleration = MEM_callocN(sizeof(float) * 3 * totverts, "cloth sim acceleration"); cloth_sim->pos = MEM_callocN(sizeof(float) * 3 * totverts, "cloth sim pos"); cloth_sim->prev_pos = MEM_callocN(sizeof(float) * 3 * totverts, "cloth sim prev pos"); cloth_sim->init_pos = MEM_callocN(sizeof(float) * 3 * totverts, "cloth sim init pos"); cloth_sim->length_constraint_tweak = MEM_callocN(sizeof(float) * totverts, "cloth sim length tweak"); cloth_sim->mass = brush->cloth_mass; cloth_sim->damping = brush->cloth_damping; return cloth_sim; } static void do_cloth_brush_solve_simulation_task_cb_ex( void *__restrict userdata, const int n, const TaskParallelTLS *__restrict UNUSED(tls)) { SculptThreadedTaskData *data = userdata; SculptSession *ss = data->ob->sculpt; const Brush *brush = data->brush; PBVHVertexIter vd; SculptClothSimulation *cloth_sim = ss->cache->cloth_sim; const float time_step = data->cloth_time_step; BKE_pbvh_vertex_iter_begin(ss->pbvh, data->nodes[n], vd, PBVH_ITER_UNIQUE) { const float sim_factor = cloth_brush_simulation_falloff_get( brush, ss->cache->radius, ss->cache->initial_location, cloth_sim->init_pos[vd.index]); if (sim_factor > 0.0f) { int i = vd.index; float temp[3]; copy_v3_v3(temp, cloth_sim->pos[i]); mul_v3_fl(cloth_sim->acceleration[i], time_step); float pos_diff[3]; sub_v3_v3v3(pos_diff, cloth_sim->pos[i], cloth_sim->prev_pos[i]); mul_v3_fl(pos_diff, (1.0f - cloth_sim->damping)); const float mask_v = (1.0f - (vd.mask ? *vd.mask : 0.0f)) * SCULPT_automasking_factor_get(ss, vd.index); madd_v3_v3fl(cloth_sim->pos[i], pos_diff, mask_v); madd_v3_v3fl(cloth_sim->pos[i], cloth_sim->acceleration[i], mask_v); copy_v3_v3(cloth_sim->prev_pos[i], temp); copy_v3_fl(cloth_sim->acceleration[i], 0.0f); copy_v3_v3(vd.co, ss->cache->cloth_sim->pos[vd.index]); if (vd.mvert) { vd.mvert->flag |= ME_VERT_PBVH_UPDATE; } } } BKE_pbvh_vertex_iter_end; } static void cloth_brush_build_nodes_constraints(Sculpt *sd, Object *ob, PBVHNode **nodes, int totnode) { Brush *brush = BKE_paint_brush(&sd->paint); /* TODO: Multi-threaded needs to be disabled for this task until implementing the optimization of * storing the constraints per node. */ /* Currently all constrains are added to the same global array which can't be accessed from * different threads. */ PBVHParallelSettings settings; BKE_pbvh_parallel_range_settings(&settings, false, totnode); SculptThreadedTaskData build_constraints_data = { .sd = sd, .ob = ob, .brush = brush, .nodes = nodes, }; BKE_pbvh_parallel_range( 0, totnode, &build_constraints_data, do_cloth_brush_build_constraints_task_cb_ex, &settings); } static void cloth_brush_satisfy_constraints(SculptSession *ss, Brush *brush, SculptClothSimulation *cloth_sim) { for (int constraint_it = 0; constraint_it < CLOTH_SIMULATION_ITERATIONS; constraint_it++) { for (int i = 0; i < cloth_sim->tot_length_constraints; i++) { const SculptClothLengthConstraint *constraint = &cloth_sim->length_constraints[i]; const int v1 = constraint->v1; const int v2 = constraint->v2; float v1_to_v2[3]; sub_v3_v3v3(v1_to_v2, cloth_sim->pos[v2], cloth_sim->pos[v1]); const float current_distance = len_v3(v1_to_v2); float correction_vector[3]; float correction_vector_half[3]; const float constraint_distance = constraint->length + (cloth_sim->length_constraint_tweak[v1] * 0.5f) + (cloth_sim->length_constraint_tweak[v2] * 0.5f); if (current_distance > 0.0f) { mul_v3_v3fl(correction_vector, v1_to_v2, 1.0f - (constraint_distance / current_distance)); } else { copy_v3_v3(correction_vector, v1_to_v2); } mul_v3_v3fl(correction_vector_half, correction_vector, 0.5f); const float mask_v1 = (1.0f - SCULPT_vertex_mask_get(ss, v1)) * SCULPT_automasking_factor_get(ss, v1); const float mask_v2 = (1.0f - SCULPT_vertex_mask_get(ss, v2)) * SCULPT_automasking_factor_get(ss, v2); const float sim_factor_v1 = cloth_brush_simulation_falloff_get( brush, ss->cache->radius, ss->cache->initial_location, cloth_sim->init_pos[v1]); const float sim_factor_v2 = cloth_brush_simulation_falloff_get( brush, ss->cache->radius, ss->cache->initial_location, cloth_sim->init_pos[v2]); madd_v3_v3fl(cloth_sim->pos[v1], correction_vector_half, 1.0f * mask_v1 * sim_factor_v1); madd_v3_v3fl(cloth_sim->pos[v2], correction_vector_half, -1.0f * mask_v2 * sim_factor_v2); } } } static void cloth_brush_do_simulation_step(Sculpt *sd, Object *ob, PBVHNode **nodes, int totnode) { SculptSession *ss = ob->sculpt; Brush *brush = BKE_paint_brush(&sd->paint); SculptClothSimulation *cloth_sim = ss->cache->cloth_sim; /* Update the constraints. */ cloth_brush_satisfy_constraints(ss, brush, cloth_sim); /* Solve the simulation and write the final step to the mesh. */ SculptThreadedTaskData solve_simulation_data = { .sd = sd, .ob = ob, .brush = brush, .nodes = nodes, .cloth_time_step = CLOTH_SIMULATION_TIME_STEP, }; PBVHParallelSettings settings; BKE_pbvh_parallel_range_settings(&settings, (sd->flags & SCULPT_USE_OPENMP), totnode); BKE_pbvh_parallel_range( 0, totnode, &solve_simulation_data, do_cloth_brush_solve_simulation_task_cb_ex, &settings); } static void cloth_brush_apply_brush_foces(Sculpt *sd, Object *ob, PBVHNode **nodes, int totnode) { SculptSession *ss = ob->sculpt; Brush *brush = BKE_paint_brush(&sd->paint); float grab_delta[3]; float mat[4][4]; float area_no[3]; float area_co[3]; float imat[4][4]; float offset[3]; SculptThreadedTaskData apply_forces_data = { .sd = sd, .ob = ob, .brush = brush, .nodes = nodes, .area_no = area_no, .area_co = area_co, .mat = imat, }; BKE_curvemapping_initialize(brush->curve); /* Init the grab delta. */ copy_v3_v3(grab_delta, ss->cache->grab_delta_symmetry); normalize_v3(grab_delta); apply_forces_data.grab_delta = grab_delta; if (is_zero_v3(ss->cache->grab_delta_symmetry)) { return; } /* Calcuate push offset. */ if (brush->cloth_deform_type == BRUSH_CLOTH_DEFORM_PUSH) { mul_v3_v3fl(offset, ss->cache->sculpt_normal_symm, ss->cache->radius); mul_v3_v3(offset, ss->cache->scale); mul_v3_fl(offset, 2.0f); apply_forces_data.offset = offset; } if (brush->cloth_deform_type == BRUSH_CLOTH_DEFORM_PINCH_PERPENDICULAR || brush->cloth_force_falloff_type == BRUSH_CLOTH_FORCE_FALLOFF_PLANE) { SCULPT_calc_brush_plane(sd, ob, nodes, totnode, area_no, area_co); /* Init stroke local space matrix. */ cross_v3_v3v3(mat[0], area_no, ss->cache->grab_delta_symmetry); mat[0][3] = 0.0f; cross_v3_v3v3(mat[1], area_no, mat[0]); mat[1][3] = 0.0f; copy_v3_v3(mat[2], area_no); mat[2][3] = 0.0f; copy_v3_v3(mat[3], ss->cache->location); mat[3][3] = 1.0f; normalize_m4(mat); apply_forces_data.area_co = area_co; apply_forces_data.area_no = area_no; apply_forces_data.mat = mat; /* Update matrix for the cursor preview. */ if (ss->cache->mirror_symmetry_pass == 0) { copy_m4_m4(ss->cache->stroke_local_mat, mat); } } PBVHParallelSettings settings; BKE_pbvh_parallel_range_settings(&settings, (sd->flags & SCULPT_USE_OPENMP), totnode); BKE_pbvh_parallel_range( 0, totnode, &apply_forces_data, do_cloth_brush_apply_forces_task_cb_ex, &settings); } /* Public functions. */ /* Main Brush Function. */ void SCULPT_do_cloth_brush(Sculpt *sd, Object *ob, PBVHNode **nodes, int totnode) { SculptSession *ss = ob->sculpt; Brush *brush = BKE_paint_brush(&sd->paint); const int totverts = SCULPT_vertex_count_get(ss); /* In the first brush step of each symmetry pass, build the constraints for the vertices in all * nodes inside the simulation's limits. */ /* Brush stroke types that restore the mesh on each brush step also need the cloth sim data to be * created on each step. */ if (ss->cache->first_time || !ss->cache->cloth_sim) { /* The simulation structure only needs to be created on the first symmetry pass. */ if (ss->cache->mirror_symmetry_pass == 0) { ss->cache->cloth_sim = cloth_brush_simulation_create(ss, brush); for (int i = 0; i < totverts; i++) { copy_v3_v3(ss->cache->cloth_sim->prev_pos[i], SCULPT_vertex_co_get(ss, i)); copy_v3_v3(ss->cache->cloth_sim->init_pos[i], SCULPT_vertex_co_get(ss, i)); } } /* Build the constraints. */ cloth_brush_build_nodes_constraints(sd, ob, nodes, totnode); return; } /* Store the initial state in the simulation. */ for (int i = 0; i < totverts; i++) { copy_v3_v3(ss->cache->cloth_sim->pos[i], SCULPT_vertex_co_get(ss, i)); } /* Apply forces to the vertices. */ cloth_brush_apply_brush_foces(sd, ob, nodes, totnode); /* Update and write the simulation to the nodes. */ cloth_brush_do_simulation_step(sd, ob, nodes, totnode); return; } void SCULPT_cloth_simulation_free(struct SculptClothSimulation *cloth_sim) { MEM_SAFE_FREE(cloth_sim->pos); MEM_SAFE_FREE(cloth_sim->prev_pos); MEM_SAFE_FREE(cloth_sim->acceleration); MEM_SAFE_FREE(cloth_sim->length_constraints); MEM_SAFE_FREE(cloth_sim->length_constraint_tweak); MEM_SAFE_FREE(cloth_sim->init_pos); MEM_SAFE_FREE(cloth_sim); } /* Cursor drawing function. */ void SCULPT_cloth_simulation_limits_draw(const uint gpuattr, const Brush *brush, const float obmat[4][4], const float location[3], const float normal[3], const float rds, const float line_width, const float outline_col[3], const float alpha) { float cursor_trans[4][4], cursor_rot[4][4]; float z_axis[4] = {0.0f, 0.0f, 1.0f, 0.0f}; float quat[4]; copy_m4_m4(cursor_trans, obmat); translate_m4(cursor_trans, location[0], location[1], location[2]); rotation_between_vecs_to_quat(quat, z_axis, normal); quat_to_mat4(cursor_rot, quat); GPU_matrix_mul(cursor_trans); GPU_matrix_mul(cursor_rot); GPU_line_width(line_width); immUniformColor3fvAlpha(outline_col, alpha * 0.5f); imm_draw_circle_dashed_3d( gpuattr, 0, 0, rds + (rds * brush->cloth_sim_limit * brush->cloth_sim_falloff), 320); immUniformColor3fvAlpha(outline_col, alpha * 0.7f); imm_draw_circle_wire_3d(gpuattr, 0, 0, rds + rds * brush->cloth_sim_limit, 80); } void SCULPT_cloth_plane_falloff_preview_draw(const uint gpuattr, SculptSession *ss, const float outline_col[3], float outline_alpha) { float local_mat_inv[4][4]; invert_m4_m4(local_mat_inv, ss->cache->stroke_local_mat); GPU_matrix_mul(ss->cache->stroke_local_mat); const float dist = ss->cache->radius; const float arrow_x = ss->cache->radius * 0.2f; const float arrow_y = ss->cache->radius * 0.1f; immUniformColor3fvAlpha(outline_col, outline_alpha); GPU_line_width(2.0f); immBegin(GPU_PRIM_LINES, 2); immVertex3f(gpuattr, dist, 0.0f, 0.0f); immVertex3f(gpuattr, -dist, 0.0f, 0.0f); immEnd(); immBegin(GPU_PRIM_TRIS, 6); immVertex3f(gpuattr, dist, 0.0f, 0.0f); immVertex3f(gpuattr, dist - arrow_x, arrow_y, 0.0f); immVertex3f(gpuattr, dist - arrow_x, -arrow_y, 0.0f); immVertex3f(gpuattr, -dist, 0.0f, 0.0f); immVertex3f(gpuattr, -dist + arrow_x, arrow_y, 0.0f); immVertex3f(gpuattr, -dist + arrow_x, -arrow_y, 0.0f); immEnd(); }