1071 lines
37 KiB
C++
1071 lines
37 KiB
C++
/* SPDX-FileCopyrightText: 2017 Blender Authors
|
|
*
|
|
* SPDX-License-Identifier: GPL-2.0-or-later */
|
|
|
|
/** \file
|
|
* \ingroup modifiers
|
|
*/
|
|
|
|
#include <algorithm>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
|
|
#include "MEM_guardedalloc.h"
|
|
|
|
#include "BLI_listbase.h"
|
|
#include "BLI_math_base.h"
|
|
#include "BLI_math_matrix.h"
|
|
#include "BLI_math_vector.h"
|
|
#include "BLI_utildefines.h"
|
|
|
|
#include "BLT_translation.hh"
|
|
|
|
#include "DNA_defaults.h"
|
|
#include "DNA_gpencil_legacy_types.h"
|
|
#include "DNA_gpencil_modifier_types.h"
|
|
#include "DNA_meshdata_types.h"
|
|
#include "DNA_object_types.h"
|
|
#include "DNA_scene_types.h"
|
|
#include "DNA_screen_types.h"
|
|
|
|
#include "BKE_deform.hh"
|
|
#include "BKE_gpencil_geom_legacy.h"
|
|
#include "BKE_gpencil_legacy.h"
|
|
#include "BKE_gpencil_modifier_legacy.h"
|
|
#include "BKE_lib_query.hh"
|
|
#include "BKE_modifier.hh"
|
|
|
|
#include "UI_interface.hh"
|
|
#include "UI_resources.hh"
|
|
|
|
#include "RNA_access.hh"
|
|
|
|
#include "DEG_depsgraph.hh"
|
|
#include "DEG_depsgraph_query.hh"
|
|
|
|
#include "MOD_gpencil_legacy_ui_common.h"
|
|
|
|
/* Two hard-coded values for GP_BUILD_MODE_ADDITIVE with GP_BUILD_TIMEMODE_DRAWSPEED. */
|
|
|
|
/* The minimum time gap we should worry about points with no time. */
|
|
#define GP_BUILD_CORRECTGAP 0.001
|
|
/* The time for geometric strokes */
|
|
#define GP_BUILD_TIME_GEOSTROKES 1.0
|
|
|
|
static void init_data(GpencilModifierData *md)
|
|
{
|
|
BuildGpencilModifierData *gpmd = (BuildGpencilModifierData *)md;
|
|
|
|
BLI_assert(MEMCMP_STRUCT_AFTER_IS_ZERO(gpmd, modifier));
|
|
|
|
MEMCPY_STRUCT_AFTER(gpmd, DNA_struct_default_get(BuildGpencilModifierData), modifier);
|
|
}
|
|
|
|
static void copy_data(const GpencilModifierData *md, GpencilModifierData *target)
|
|
{
|
|
BKE_gpencil_modifier_copydata_generic(md, target);
|
|
}
|
|
|
|
static bool depends_on_time(GpencilModifierData * /*md*/)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
/* ******************************************** */
|
|
/* Build Modifier - Stroke generation logic
|
|
*
|
|
* There are two modes for how the strokes are sequenced (at a macro-level):
|
|
* - Sequential Mode - Strokes appear/disappear one after the other. Only a single one changes at a
|
|
* time.
|
|
* - Concurrent Mode - Multiple strokes appear/disappear at once.
|
|
*
|
|
* Assumptions:
|
|
* - Stroke points are generally equally spaced. This implies that we can just add/remove points,
|
|
* without worrying about distances between them / adding extra interpolated points between
|
|
* an visible point and one about to be added/removed (or any similar tapering effects).
|
|
*
|
|
* - All strokes present are fully visible (i.e. we don't have to ignore any)
|
|
*/
|
|
|
|
/* Remove a particular stroke */
|
|
static void clear_stroke(bGPDframe *gpf, bGPDstroke *gps)
|
|
{
|
|
BLI_remlink(&gpf->strokes, gps);
|
|
BKE_gpencil_free_stroke(gps);
|
|
}
|
|
|
|
/* Clear all strokes in frame */
|
|
static void gpf_clear_all_strokes(bGPDframe *gpf)
|
|
{
|
|
bGPDstroke *gps, *gps_next;
|
|
for (gps = static_cast<bGPDstroke *>(gpf->strokes.first); gps; gps = gps_next) {
|
|
gps_next = gps->next;
|
|
clear_stroke(gpf, gps);
|
|
}
|
|
BLI_listbase_clear(&gpf->strokes);
|
|
}
|
|
|
|
/* Reduce the number of points in the stroke
|
|
*
|
|
* NOTE: This won't be called if all points are present/removed
|
|
*/
|
|
static void reduce_stroke_points(bGPdata *gpd,
|
|
bGPDframe *gpf,
|
|
bGPDstroke *gps,
|
|
const int points_num,
|
|
const eBuildGpencil_Transition transition)
|
|
{
|
|
if ((points_num == 0) || (gps->points == nullptr)) {
|
|
clear_stroke(gpf, gps);
|
|
return;
|
|
}
|
|
bGPDspoint *new_points = static_cast<bGPDspoint *>(
|
|
MEM_callocN(sizeof(bGPDspoint) * points_num, __func__));
|
|
MDeformVert *new_dvert = nullptr;
|
|
if ((gps->dvert != nullptr) && (points_num > 0)) {
|
|
new_dvert = static_cast<MDeformVert *>(
|
|
MEM_callocN(sizeof(MDeformVert) * points_num, __func__));
|
|
}
|
|
|
|
/* Which end should points be removed from. */
|
|
switch (transition) {
|
|
case GP_BUILD_TRANSITION_GROW: /* Show in forward order =
|
|
* Remove ungrown-points from end of stroke. */
|
|
case GP_BUILD_TRANSITION_SHRINK: /* Hide in reverse order =
|
|
* Remove dead-points from end of stroke. */
|
|
{
|
|
/* copy over point data */
|
|
blender::dna::shallow_copy_array(new_points, gps->points, points_num);
|
|
if ((gps->dvert != nullptr) && (points_num > 0)) {
|
|
memcpy(new_dvert, gps->dvert, sizeof(MDeformVert) * points_num);
|
|
|
|
/* free unused point weights */
|
|
for (int i = points_num; i < gps->totpoints; i++) {
|
|
MDeformVert *dvert = &gps->dvert[i];
|
|
BKE_gpencil_free_point_weights(dvert);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
/* Hide in forward order = Remove points from start of stroke */
|
|
case GP_BUILD_TRANSITION_VANISH: {
|
|
/* points_num is the number of points left after reducing.
|
|
* We need to know how many to remove
|
|
*/
|
|
const int offset = gps->totpoints - points_num;
|
|
|
|
/* copy over point data */
|
|
blender::dna::shallow_copy_array(new_points, gps->points + offset, points_num);
|
|
if ((gps->dvert != nullptr) && (points_num > 0)) {
|
|
memcpy(new_dvert, gps->dvert + offset, sizeof(MDeformVert) * points_num);
|
|
|
|
/* free unused weights */
|
|
for (int i = 0; i < offset; i++) {
|
|
MDeformVert *dvert = &gps->dvert[i];
|
|
BKE_gpencil_free_point_weights(dvert);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
default:
|
|
printf("ERROR: Unknown transition %d in %s()\n", int(transition), __func__);
|
|
break;
|
|
}
|
|
|
|
/* replace stroke geometry */
|
|
MEM_SAFE_FREE(gps->points);
|
|
MEM_SAFE_FREE(gps->dvert);
|
|
gps->points = new_points;
|
|
gps->dvert = new_dvert;
|
|
gps->totpoints = points_num;
|
|
|
|
/* Calc geometry data. */
|
|
BKE_gpencil_stroke_geometry_update(gpd, gps);
|
|
}
|
|
|
|
static void fade_stroke_points(bGPDstroke *gps,
|
|
const int starting_index,
|
|
const int ending_index,
|
|
const float starting_weight,
|
|
const float ending_weight,
|
|
const int target_def_nr,
|
|
const eBuildGpencil_Transition transition,
|
|
const float thickness_strength,
|
|
const float opacity_strength)
|
|
{
|
|
MDeformVert *dvert;
|
|
|
|
int range = ending_index - starting_index;
|
|
if (!range) {
|
|
range = 1;
|
|
}
|
|
|
|
/* Which end should points be removed from */
|
|
switch (transition) {
|
|
/* Because starting_weight and ending_weight are set in correct order before calling this
|
|
* function, those three modes can use the same interpolation code. */
|
|
case GP_BUILD_TRANSITION_GROW:
|
|
case GP_BUILD_TRANSITION_SHRINK:
|
|
case GP_BUILD_TRANSITION_VANISH: {
|
|
for (int i = starting_index; i <= ending_index; i++) {
|
|
float weight = interpf(ending_weight, starting_weight, float(i - starting_index) / range);
|
|
if (target_def_nr >= 0) {
|
|
dvert = &gps->dvert[i];
|
|
MDeformWeight *dw = BKE_defvert_ensure_index(dvert, target_def_nr);
|
|
if (dw) {
|
|
dw->weight = weight;
|
|
CLAMP(dw->weight, 0.0f, 1.0f);
|
|
}
|
|
}
|
|
if (thickness_strength > 1e-5) {
|
|
gps->points[i].pressure *= interpf(weight, 1.0f, thickness_strength);
|
|
}
|
|
if (opacity_strength > 1e-5) {
|
|
gps->points[i].strength *= interpf(weight, 1.0f, opacity_strength);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
default:
|
|
printf("ERROR: Unknown transition %d in %s()\n", int(transition), __func__);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* --------------------------------------------- */
|
|
|
|
/* Stroke Data Table Entry - This represents one stroke being generated */
|
|
struct tStrokeBuildDetails {
|
|
bGPDstroke *gps;
|
|
|
|
/* Indices - first/last indices for the stroke's points (overall) */
|
|
size_t start_idx, end_idx;
|
|
|
|
/* Number of points - Cache for more convenient access */
|
|
int totpoints;
|
|
|
|
/* Distance to control object, used to sort the strokes if set. */
|
|
float distance;
|
|
};
|
|
|
|
static int cmp_stroke_build_details(const void *ps1, const void *ps2)
|
|
{
|
|
tStrokeBuildDetails *p1 = (tStrokeBuildDetails *)ps1;
|
|
tStrokeBuildDetails *p2 = (tStrokeBuildDetails *)ps2;
|
|
return p1->distance > p2->distance ? 1 : (p1->distance == p2->distance ? 0 : -1);
|
|
}
|
|
|
|
/* Sequential - Show strokes one after the other (includes additive mode). */
|
|
static void build_sequential(Object *ob,
|
|
BuildGpencilModifierData *mmd,
|
|
Depsgraph *depsgraph,
|
|
bGPdata *gpd,
|
|
bGPDframe *gpf,
|
|
int target_def_nr,
|
|
float fac,
|
|
const float *ctime)
|
|
{
|
|
/* Total number of strokes in this run. */
|
|
size_t tot_strokes = BLI_listbase_count(&gpf->strokes);
|
|
/* First stroke to build. */
|
|
size_t start_stroke = 0;
|
|
/* Pointer to current stroke. */
|
|
bGPDstroke *gps;
|
|
/* Recycled counter. */
|
|
size_t i;
|
|
Scene *scene = DEG_get_evaluated_scene(depsgraph);
|
|
/* Frame-rate of scene. */
|
|
const float fps = (float(scene->r.frs_sec) / scene->r.frs_sec_base);
|
|
|
|
/* 1) Determine which strokes to start with (& adapt total number of strokes to build). */
|
|
if (mmd->mode == GP_BUILD_MODE_ADDITIVE) {
|
|
if (gpf->prev) {
|
|
start_stroke = BLI_listbase_count(&gpf->runtime.gpf_orig->prev->strokes);
|
|
}
|
|
if (start_stroke <= tot_strokes) {
|
|
tot_strokes = tot_strokes - start_stroke;
|
|
}
|
|
else {
|
|
start_stroke = 0;
|
|
}
|
|
}
|
|
|
|
/* 2) Compute proportion of time each stroke should occupy. */
|
|
/* NOTE: This assumes that the total number of points won't overflow! */
|
|
tStrokeBuildDetails *table = static_cast<tStrokeBuildDetails *>(
|
|
MEM_callocN(sizeof(tStrokeBuildDetails) * tot_strokes, __func__));
|
|
/* Pointer to cache table of times for each point. */
|
|
float *idx_times;
|
|
/* Running overall time sum incrementing per point. */
|
|
float sumtime = 0;
|
|
/* Running overall point sum. */
|
|
size_t sumpoints = 0;
|
|
|
|
/* 2.1) Pass to initially tally up points. */
|
|
for (gps = static_cast<bGPDstroke *>(BLI_findlink(&gpf->strokes, start_stroke)), i = 0; gps;
|
|
gps = gps->next, i++)
|
|
{
|
|
tStrokeBuildDetails *cell = &table[i];
|
|
|
|
cell->gps = gps;
|
|
cell->totpoints = gps->totpoints;
|
|
sumpoints += cell->totpoints;
|
|
|
|
/* Compute distance to control object if set, and build according to that order. */
|
|
if (mmd->object) {
|
|
float sv1[3], sv2[3];
|
|
mul_v3_m4v3(sv1, ob->object_to_world().ptr(), &gps->points[0].x);
|
|
mul_v3_m4v3(sv2, ob->object_to_world().ptr(), &gps->points[gps->totpoints - 1].x);
|
|
float dist_l = len_v3v3(sv1, mmd->object->loc);
|
|
float dist_r = len_v3v3(sv2, mmd->object->loc);
|
|
if (dist_r < dist_l) {
|
|
BKE_gpencil_stroke_flip(gps);
|
|
cell->distance = dist_r;
|
|
}
|
|
else {
|
|
cell->distance = dist_l;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (mmd->object) {
|
|
qsort(table, tot_strokes, sizeof(tStrokeBuildDetails), cmp_stroke_build_details);
|
|
}
|
|
|
|
/* 2.2) If GP_BUILD_TIMEMODE_DRAWSPEED: Tally up point timestamps & delays to idx_times. */
|
|
if (mmd->time_mode == GP_BUILD_TIMEMODE_DRAWSPEED) {
|
|
idx_times = static_cast<float *>(MEM_callocN(sizeof(float) * sumpoints, __func__));
|
|
/* Maximum time gap between strokes in seconds. */
|
|
const float GP_BUILD_MAXGAP = mmd->speed_maxgap;
|
|
/* Running reference to overall current point. */
|
|
size_t curpoint = 0;
|
|
/* Running timestamp of last point that had data. */
|
|
float last_pointtime = 0;
|
|
|
|
for (i = 0; i < tot_strokes; i++) {
|
|
tStrokeBuildDetails *cell = &table[i];
|
|
/* Adding delay between strokes to sumtime. */
|
|
if (mmd->object == nullptr) {
|
|
/* Normal case: Delay to last stroke. */
|
|
if (i != 0 && 0 < cell->gps->inittime && 0 < (cell - 1)->gps->inittime) {
|
|
float curgps_delay = fabs(cell->gps->inittime - (cell - 1)->gps->inittime) -
|
|
last_pointtime;
|
|
if (0 < curgps_delay) {
|
|
sumtime += std::min(curgps_delay, GP_BUILD_MAXGAP);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Going through the points of the current stroke
|
|
* and filling in "zeropoints" where "time" = 0. */
|
|
|
|
/* Count of consecutive points where "time" is 0. */
|
|
int zeropoints = 0;
|
|
for (int j = 0; j < cell->totpoints; j++) {
|
|
/* Defining time for first point in stroke. */
|
|
if (j == 0) {
|
|
idx_times[curpoint] = sumtime;
|
|
last_pointtime = cell->gps->points[0].time;
|
|
}
|
|
/* Entering subsequent points */
|
|
else {
|
|
if (cell->gps->points[j].time <= 0) {
|
|
idx_times[curpoint] = sumtime;
|
|
zeropoints++;
|
|
}
|
|
/* From here current point has time data */
|
|
else {
|
|
float deltatime = fabs(cell->gps->points[j].time - last_pointtime);
|
|
/* Do we need to sanitize previous points? */
|
|
if (0 < zeropoints) {
|
|
/* Only correct if time-gap bigger than #GP_BUILD_CORRECTGAP. */
|
|
if (GP_BUILD_CORRECTGAP < deltatime) {
|
|
/* Cycling backwards through zero-points to fix them. */
|
|
for (int k = 0; k < zeropoints; k++) {
|
|
float linear_fill = interpf(
|
|
0, deltatime, (float(k) + 1) / (zeropoints + 1)); /* Factor = Proportion. */
|
|
idx_times[curpoint - k - 1] = sumtime + linear_fill;
|
|
}
|
|
}
|
|
else {
|
|
zeropoints = 0;
|
|
}
|
|
}
|
|
|
|
/* Normal behavior with time data. */
|
|
idx_times[curpoint] = sumtime + deltatime;
|
|
sumtime = idx_times[curpoint];
|
|
last_pointtime = cell->gps->points[j].time;
|
|
zeropoints = 0;
|
|
}
|
|
}
|
|
curpoint += 1;
|
|
}
|
|
|
|
/* If stroke had no time data at all, use mmd->time_geostrokes. */
|
|
if (zeropoints + 1 == cell->totpoints) {
|
|
for (int j = 0; j < cell->totpoints; j++) {
|
|
idx_times[int(curpoint) - j - 1] = float(cell->totpoints - j) *
|
|
GP_BUILD_TIME_GEOSTROKES /
|
|
float(cell->totpoints) +
|
|
sumtime;
|
|
}
|
|
last_pointtime = GP_BUILD_TIME_GEOSTROKES;
|
|
sumtime += GP_BUILD_TIME_GEOSTROKES;
|
|
}
|
|
}
|
|
|
|
float gp_build_speedfactor = mmd->speed_fac;
|
|
/* If current frame can't be built before next frame, adjust gp_build_speedfactor. */
|
|
if (gpf->next && (gpf->framenum + sumtime * fps / gp_build_speedfactor) > gpf->next->framenum)
|
|
{
|
|
gp_build_speedfactor = sumtime * fps / (gpf->next->framenum - gpf->framenum);
|
|
}
|
|
/* Apply gp_build_speedfactor to all points & to sumtime. */
|
|
for (i = 0; i < sumpoints; i++) {
|
|
float *idx_time = &idx_times[i];
|
|
*idx_time /= gp_build_speedfactor;
|
|
}
|
|
sumtime /= gp_build_speedfactor;
|
|
}
|
|
|
|
/* 2.3) Pass to compute overall indices for points (per stroke). */
|
|
for (i = 0; i < tot_strokes; i++) {
|
|
tStrokeBuildDetails *cell = &table[i];
|
|
|
|
if (i == 0) {
|
|
cell->start_idx = 0;
|
|
}
|
|
else {
|
|
cell->start_idx = (cell - 1)->end_idx + 1;
|
|
}
|
|
cell->end_idx = cell->start_idx + cell->totpoints - 1;
|
|
}
|
|
|
|
/* 3) Determine the global indices for points that should be visible. */
|
|
size_t first_visible = 0;
|
|
size_t last_visible = 0;
|
|
/* Need signed numbers because the representation of fading offset would exceed the beginning and
|
|
* the end of offsets. */
|
|
int fade_start = 0;
|
|
int fade_end = 0;
|
|
|
|
bool fading_enabled = (mmd->flag & GP_BUILD_USE_FADING);
|
|
float set_fade_fac = fading_enabled ? mmd->fade_fac : 0.0f;
|
|
float use_fac;
|
|
|
|
if (mmd->time_mode == GP_BUILD_TIMEMODE_DRAWSPEED) {
|
|
/* Recalculate equivalent of "fac" using timestamps. */
|
|
float targettime = (*ctime - float(gpf->framenum)) / fps;
|
|
fac = 0;
|
|
/* If ctime is in current frame, find last point. */
|
|
if (0 < targettime && targettime < sumtime) {
|
|
/* All except GP_BUILD_TRANSITION_SHRINK count forwards. */
|
|
if (mmd->transition != GP_BUILD_TRANSITION_SHRINK) {
|
|
for (i = 0; i < sumpoints; i++) {
|
|
if (targettime < idx_times[i]) {
|
|
fac = float(i) / sumpoints;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
for (i = 0; i < sumpoints; i++) {
|
|
if (targettime < sumtime - idx_times[sumpoints - i - 1]) {
|
|
fac = float(i) / sumpoints;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/* Don't check if ctime is beyond time of current frame. */
|
|
else if (targettime >= sumtime) {
|
|
fac = 1;
|
|
}
|
|
}
|
|
use_fac = interpf(1 + set_fade_fac, 0, fac);
|
|
float use_fade_fac = use_fac - set_fade_fac;
|
|
CLAMP(use_fade_fac, 0.0f, 1.0f);
|
|
|
|
switch (mmd->transition) {
|
|
/* Show in forward order
|
|
* - As fac increases, the number of visible points increases
|
|
*/
|
|
case GP_BUILD_TRANSITION_GROW:
|
|
first_visible = 0; /* always visible */
|
|
last_visible = size_t(roundf(sumpoints * use_fac));
|
|
fade_start = int(roundf(sumpoints * use_fade_fac));
|
|
fade_end = last_visible;
|
|
break;
|
|
|
|
/* Hide in reverse order
|
|
* - As fac increases, the number of points visible at the end decreases
|
|
*/
|
|
case GP_BUILD_TRANSITION_SHRINK:
|
|
first_visible = 0; /* always visible (until last point removed) */
|
|
last_visible = size_t(sumpoints * (1.0f + set_fade_fac - use_fac));
|
|
fade_start = int(roundf(sumpoints * (1.0f - use_fade_fac - set_fade_fac)));
|
|
fade_end = last_visible;
|
|
break;
|
|
|
|
/* Hide in forward order
|
|
* - As fac increases, the early points start getting hidden
|
|
*/
|
|
case GP_BUILD_TRANSITION_VANISH:
|
|
first_visible = size_t(sumpoints * use_fade_fac);
|
|
last_visible = sumpoints; /* i.e. visible until the end, unless first overlaps this */
|
|
fade_start = first_visible;
|
|
fade_end = int(roundf(sumpoints * use_fac));
|
|
break;
|
|
}
|
|
|
|
/* 4) Go through all strokes, deciding which to keep, and/or how much of each to keep */
|
|
for (i = 0; i < tot_strokes; i++) {
|
|
tStrokeBuildDetails *cell = &table[i];
|
|
|
|
/* Determine what portion of the stroke is visible */
|
|
if ((cell->end_idx < first_visible) || (cell->start_idx > last_visible)) {
|
|
/* Not visible at all - Either ended before */
|
|
clear_stroke(gpf, cell->gps);
|
|
}
|
|
else {
|
|
if (fade_start != fade_end && int(cell->start_idx) < fade_end &&
|
|
int(cell->end_idx) > fade_start)
|
|
{
|
|
int start_index = fade_start - cell->start_idx;
|
|
int end_index = cell->totpoints + fade_end - cell->end_idx - 1;
|
|
CLAMP(start_index, 0, cell->totpoints - 1);
|
|
CLAMP(end_index, 0, cell->totpoints - 1);
|
|
float start_weight = ratiof(fade_start, fade_end, cell->start_idx + start_index);
|
|
float end_weight = ratiof(fade_start, fade_end, cell->start_idx + end_index);
|
|
if (mmd->transition != GP_BUILD_TRANSITION_VANISH) {
|
|
start_weight = 1.0f - start_weight;
|
|
end_weight = 1.0f - end_weight;
|
|
}
|
|
fade_stroke_points(cell->gps,
|
|
start_index,
|
|
end_index,
|
|
start_weight,
|
|
end_weight,
|
|
target_def_nr,
|
|
eBuildGpencil_Transition(mmd->transition),
|
|
mmd->fade_thickness_strength,
|
|
mmd->fade_opacity_strength);
|
|
/* Calc geometry data. */
|
|
BKE_gpencil_stroke_geometry_update(gpd, cell->gps);
|
|
}
|
|
/* Some proportion of stroke is visible */
|
|
if ((first_visible <= cell->start_idx) && (last_visible >= cell->end_idx)) {
|
|
/* Do nothing - whole stroke is visible */
|
|
}
|
|
else if (first_visible > cell->start_idx) {
|
|
/* Starts partway through this stroke */
|
|
int points_num = cell->end_idx - first_visible;
|
|
reduce_stroke_points(
|
|
gpd, gpf, cell->gps, points_num, eBuildGpencil_Transition(mmd->transition));
|
|
}
|
|
else {
|
|
/* Ends partway through this stroke */
|
|
int points_num = last_visible - cell->start_idx;
|
|
reduce_stroke_points(
|
|
gpd, gpf, cell->gps, points_num, eBuildGpencil_Transition(mmd->transition));
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Free table */
|
|
MEM_freeN(table);
|
|
if (mmd->time_mode == GP_BUILD_TIMEMODE_DRAWSPEED) {
|
|
MEM_freeN(idx_times);
|
|
}
|
|
}
|
|
|
|
/* --------------------------------------------- */
|
|
|
|
/* Concurrent - Show multiple strokes at once */
|
|
static void build_concurrent(BuildGpencilModifierData *mmd,
|
|
bGPdata *gpd,
|
|
bGPDframe *gpf,
|
|
const int target_def_nr,
|
|
float fac)
|
|
{
|
|
bGPDstroke *gps, *gps_next;
|
|
int max_points = 0;
|
|
|
|
const bool reverse = (mmd->transition != GP_BUILD_TRANSITION_GROW);
|
|
|
|
/* 1) Determine the longest stroke, to figure out when short strokes should start */
|
|
/* Todo: A *really* long stroke here could dwarf everything else, causing bad timings */
|
|
LISTBASE_FOREACH (bGPDstroke *, gps, &gpf->strokes) {
|
|
if (gps->totpoints > max_points) {
|
|
max_points = gps->totpoints;
|
|
}
|
|
}
|
|
if (max_points == 0) {
|
|
printf("ERROR: Strokes are all empty (GP Build Modifier: %s)\n", __func__);
|
|
return;
|
|
}
|
|
|
|
bool fading_enabled = (mmd->flag & GP_BUILD_USE_FADING);
|
|
float set_fade_fac = fading_enabled ? mmd->fade_fac : 0.0f;
|
|
float use_fac = interpf(1 + set_fade_fac, 0, fac);
|
|
use_fac = reverse ? use_fac - set_fade_fac : use_fac;
|
|
int fade_points = set_fade_fac * max_points;
|
|
|
|
/* 2) For each stroke, determine how it should be handled */
|
|
for (gps = static_cast<bGPDstroke *>(gpf->strokes.first); gps; gps = gps_next) {
|
|
gps_next = gps->next;
|
|
|
|
/* Relative Length of Stroke - Relative to the longest stroke,
|
|
* what proportion of the available time should this stroke use
|
|
*/
|
|
const float relative_len = float(gps->totpoints) / float(max_points);
|
|
|
|
/* Determine how many points should be left in the stroke */
|
|
int points_num = 0;
|
|
|
|
switch (mmd->time_alignment) {
|
|
case GP_BUILD_TIMEALIGN_START: /* all start on frame 1 */
|
|
{
|
|
/* Scale fac to fit relative_len */
|
|
const float scaled_fac = use_fac / std::max(relative_len, PSEUDOINVERSE_EPSILON);
|
|
|
|
if (reverse) {
|
|
points_num = int(roundf((1.0f - scaled_fac) * gps->totpoints));
|
|
}
|
|
else {
|
|
points_num = int(roundf(scaled_fac * gps->totpoints));
|
|
}
|
|
|
|
break;
|
|
}
|
|
case GP_BUILD_TIMEALIGN_END: /* all end on same frame */
|
|
{
|
|
/* Build effect occurs over 1.0 - relative_len, to 1.0 (i.e. over the end of the range)
|
|
*/
|
|
const float start_fac = 1.0f - relative_len;
|
|
|
|
const float scaled_fac = (use_fac - start_fac) /
|
|
std::max(relative_len, PSEUDOINVERSE_EPSILON);
|
|
|
|
if (reverse) {
|
|
points_num = int(roundf((1.0f - scaled_fac) * gps->totpoints));
|
|
}
|
|
else {
|
|
points_num = int(roundf(scaled_fac * gps->totpoints));
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* Modify the stroke geometry */
|
|
if (points_num <= 0) {
|
|
/* Nothing Left - Delete the stroke */
|
|
clear_stroke(gpf, gps);
|
|
}
|
|
else {
|
|
int more_points = points_num - gps->totpoints;
|
|
CLAMP(more_points, 0, fade_points + 1);
|
|
float max_weight = float(points_num + more_points) / fade_points;
|
|
CLAMP(max_weight, 0.0f, 1.0f);
|
|
int starting_index = mmd->transition == GP_BUILD_TRANSITION_VANISH ?
|
|
gps->totpoints - points_num - more_points :
|
|
points_num - 1 - fade_points + more_points;
|
|
int ending_index = mmd->transition == GP_BUILD_TRANSITION_VANISH ?
|
|
gps->totpoints - points_num + fade_points - more_points :
|
|
points_num - 1 + more_points;
|
|
float starting_weight = mmd->transition == GP_BUILD_TRANSITION_VANISH ?
|
|
(float(more_points) / fade_points) :
|
|
max_weight;
|
|
float ending_weight = mmd->transition == GP_BUILD_TRANSITION_VANISH ?
|
|
max_weight :
|
|
(float(more_points) / fade_points);
|
|
CLAMP(starting_index, 0, gps->totpoints - 1);
|
|
CLAMP(ending_index, 0, gps->totpoints - 1);
|
|
fade_stroke_points(gps,
|
|
starting_index,
|
|
ending_index,
|
|
starting_weight,
|
|
ending_weight,
|
|
target_def_nr,
|
|
eBuildGpencil_Transition(mmd->transition),
|
|
mmd->fade_thickness_strength,
|
|
mmd->fade_opacity_strength);
|
|
if (points_num < gps->totpoints) {
|
|
/* Remove some points */
|
|
reduce_stroke_points(gpd, gpf, gps, points_num, eBuildGpencil_Transition(mmd->transition));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* --------------------------------------------- */
|
|
|
|
static void generate_geometry(GpencilModifierData *md,
|
|
Depsgraph *depsgraph,
|
|
Object *ob,
|
|
bGPdata *gpd,
|
|
bGPDlayer *gpl,
|
|
bGPDframe *gpf)
|
|
{
|
|
BuildGpencilModifierData *mmd = (BuildGpencilModifierData *)md;
|
|
/* Prevent incompatible options at runtime. */
|
|
if (mmd->mode == GP_BUILD_MODE_ADDITIVE) {
|
|
mmd->transition = GP_BUILD_TRANSITION_GROW;
|
|
mmd->start_delay = 0;
|
|
}
|
|
if (mmd->mode == GP_BUILD_MODE_CONCURRENT && mmd->time_mode == GP_BUILD_TIMEMODE_DRAWSPEED) {
|
|
mmd->time_mode = GP_BUILD_TIMEMODE_FRAMES;
|
|
}
|
|
|
|
const bool reverse = (mmd->transition != GP_BUILD_TRANSITION_GROW);
|
|
const bool is_percentage = (mmd->time_mode == GP_BUILD_TIMEMODE_PERCENTAGE);
|
|
|
|
const float ctime = DEG_get_ctime(depsgraph);
|
|
|
|
/* Early exit if it's an empty frame */
|
|
if (gpf->strokes.first == nullptr) {
|
|
return;
|
|
}
|
|
|
|
/* Omit layer if filter by layer */
|
|
if (mmd->layername[0] != '\0') {
|
|
if ((mmd->flag & GP_BUILD_INVERT_LAYER) == 0) {
|
|
if (!STREQ(mmd->layername, gpl->info)) {
|
|
return;
|
|
}
|
|
}
|
|
else {
|
|
if (STREQ(mmd->layername, gpl->info)) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
/* verify layer pass */
|
|
if (mmd->layer_pass > 0) {
|
|
if ((mmd->flag & GP_BUILD_INVERT_LAYERPASS) == 0) {
|
|
if (gpl->pass_index != mmd->layer_pass) {
|
|
return;
|
|
}
|
|
}
|
|
else {
|
|
if (gpl->pass_index == mmd->layer_pass) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
int target_def_nr = -1;
|
|
if (mmd->flag & GP_BUILD_USE_FADING) {
|
|
/* If there are weight output, initialize it with a default weight of 1. */
|
|
target_def_nr = BKE_object_defgroup_name_index(ob, mmd->target_vgname);
|
|
if (target_def_nr >= 0) {
|
|
LISTBASE_FOREACH (bGPDstroke *, fgps, &gpf->strokes) {
|
|
BKE_gpencil_dvert_ensure(fgps);
|
|
/* Assign a initial weight of 1, and only process those who needs additional fading. */
|
|
for (int j = 0; j < fgps->totpoints; j++) {
|
|
MDeformVert *dvert = &fgps->dvert[j];
|
|
MDeformWeight *dw = BKE_defvert_ensure_index(dvert, target_def_nr);
|
|
if (dw) {
|
|
dw->weight = 1.0f;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Early exit if outside of the frame range for this modifier
|
|
* (e.g. to have one forward, and one backwards modifier)
|
|
*/
|
|
if (mmd->flag & GP_BUILD_RESTRICT_TIME) {
|
|
if ((ctime < mmd->start_frame) || (ctime > mmd->end_frame)) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
/* Default "fac" value to call build_sequential even with
|
|
* GP_BUILD_TIMEMODE_DRAWSPEED, which uses separate logic
|
|
* in function build_sequential()
|
|
*/
|
|
float fac = 1;
|
|
|
|
if (mmd->time_mode != GP_BUILD_TIMEMODE_DRAWSPEED) {
|
|
/* Compute start and end frames for the animation effect
|
|
* By default, the upper bound is given by the "length" setting.
|
|
*/
|
|
float start_frame = is_percentage ? gpf->framenum : gpf->framenum + mmd->start_delay;
|
|
/* When use percentage don't need a limit in the upper bound, so use a maximum value for the
|
|
* last frame. */
|
|
float end_frame = is_percentage ? start_frame + 9999 : start_frame + mmd->length;
|
|
|
|
if (gpf->next) {
|
|
/* Use the next frame or upper bound as end frame, whichever is lower/closer */
|
|
end_frame = std::min(end_frame, float(gpf->next->framenum));
|
|
}
|
|
|
|
/* Early exit if current frame is outside start/end bounds */
|
|
/* NOTE: If we're beyond the next/previous frames (if existent),
|
|
* then we wouldn't have this problem anyway... */
|
|
if (ctime < start_frame) {
|
|
/* Before Start - Animation hasn't started. Display initial state. */
|
|
if (reverse) {
|
|
/* 1) Reverse = Start with all, end with nothing.
|
|
* ==> Do nothing (everything already present)
|
|
*/
|
|
}
|
|
else {
|
|
/* 2) Forward Order = Start with nothing, end with the full frame.
|
|
* ==> Free all strokes, and return an empty frame
|
|
*/
|
|
gpf_clear_all_strokes(gpf);
|
|
}
|
|
|
|
/* Early exit */
|
|
return;
|
|
}
|
|
if (ctime >= end_frame) {
|
|
/* Past End - Animation finished. Display final result. */
|
|
if (reverse) {
|
|
/* 1) Reverse = Start with all, end with nothing.
|
|
* ==> Free all strokes, and return an empty frame
|
|
*/
|
|
gpf_clear_all_strokes(gpf);
|
|
}
|
|
else {
|
|
/* 2) Forward Order = Start with nothing, end with the full frame.
|
|
* ==> Do Nothing (everything already present)
|
|
*/
|
|
}
|
|
|
|
/* Early exit */
|
|
return;
|
|
}
|
|
/* Determine how far along we are given current time, start_frame and end_frame */
|
|
fac = is_percentage ? mmd->percentage_fac : (ctime - start_frame) / (end_frame - start_frame);
|
|
}
|
|
|
|
/* Calling the correct build mode */
|
|
switch (mmd->mode) {
|
|
case GP_BUILD_MODE_SEQUENTIAL:
|
|
case GP_BUILD_MODE_ADDITIVE:
|
|
build_sequential(ob, mmd, depsgraph, gpd, gpf, target_def_nr, fac, &ctime);
|
|
break;
|
|
|
|
case GP_BUILD_MODE_CONCURRENT:
|
|
build_concurrent(mmd, gpd, gpf, target_def_nr, fac);
|
|
break;
|
|
|
|
default:
|
|
printf("Unsupported build mode (%d) for GP Build Modifier: '%s'\n",
|
|
mmd->mode,
|
|
mmd->modifier.name);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* Entry-point for Build Modifier */
|
|
static void generate_strokes(GpencilModifierData *md, Depsgraph *depsgraph, Object *ob)
|
|
{
|
|
Scene *scene = DEG_get_evaluated_scene(depsgraph);
|
|
bGPdata *gpd = (bGPdata *)ob->data;
|
|
|
|
LISTBASE_FOREACH (bGPDlayer *, gpl, &gpd->layers) {
|
|
bGPDframe *gpf = BKE_gpencil_frame_retime_get(depsgraph, scene, ob, gpl);
|
|
if (gpf == nullptr) {
|
|
continue;
|
|
}
|
|
generate_geometry(md, depsgraph, ob, gpd, gpl, gpf);
|
|
}
|
|
}
|
|
|
|
static void panel_draw(const bContext * /*C*/, Panel *panel)
|
|
{
|
|
uiLayout *layout = panel->layout;
|
|
|
|
PointerRNA ob_ptr;
|
|
PointerRNA *ptr = gpencil_modifier_panel_get_property_pointers(panel, &ob_ptr);
|
|
|
|
const int mode = RNA_enum_get(ptr, "mode");
|
|
int time_mode = RNA_enum_get(ptr, "time_mode");
|
|
|
|
uiLayoutSetPropSep(layout, true);
|
|
|
|
/* First: Build mode and build settings. */
|
|
uiItemR(layout, ptr, "mode", UI_ITEM_NONE, nullptr, ICON_NONE);
|
|
if (mode == GP_BUILD_MODE_SEQUENTIAL) {
|
|
uiItemR(layout, ptr, "transition", UI_ITEM_NONE, nullptr, ICON_NONE);
|
|
}
|
|
if (mode == GP_BUILD_MODE_CONCURRENT) {
|
|
/* Concurrent mode doesn't support GP_BUILD_TIMEMODE_DRAWSPEED, so unset it. */
|
|
if (time_mode == GP_BUILD_TIMEMODE_DRAWSPEED) {
|
|
RNA_enum_set(ptr, "time_mode", GP_BUILD_TIMEMODE_FRAMES);
|
|
time_mode = GP_BUILD_TIMEMODE_FRAMES;
|
|
}
|
|
uiItemR(layout, ptr, "transition", UI_ITEM_NONE, nullptr, ICON_NONE);
|
|
}
|
|
uiItemS(layout);
|
|
|
|
/* Second: Time mode and time settings. */
|
|
|
|
uiItemR(layout, ptr, "time_mode", UI_ITEM_NONE, nullptr, ICON_NONE);
|
|
if (mode == GP_BUILD_MODE_CONCURRENT) {
|
|
uiItemR(layout, ptr, "concurrent_time_alignment", UI_ITEM_NONE, nullptr, ICON_NONE);
|
|
}
|
|
switch (time_mode) {
|
|
case GP_BUILD_TIMEMODE_DRAWSPEED:
|
|
uiItemR(layout, ptr, "speed_factor", UI_ITEM_NONE, nullptr, ICON_NONE);
|
|
uiItemR(layout, ptr, "speed_maxgap", UI_ITEM_NONE, nullptr, ICON_NONE);
|
|
break;
|
|
case GP_BUILD_TIMEMODE_FRAMES:
|
|
uiItemR(layout, ptr, "length", UI_ITEM_NONE, IFACE_("Frames"), ICON_NONE);
|
|
if (mode != GP_BUILD_MODE_ADDITIVE) {
|
|
uiItemR(layout, ptr, "start_delay", UI_ITEM_NONE, nullptr, ICON_NONE);
|
|
}
|
|
break;
|
|
case GP_BUILD_TIMEMODE_PERCENTAGE:
|
|
uiItemR(layout, ptr, "percentage_factor", UI_ITEM_NONE, nullptr, ICON_NONE);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
uiItemS(layout);
|
|
uiItemR(layout, ptr, "object", UI_ITEM_NONE, nullptr, ICON_NONE);
|
|
|
|
/* Some housekeeping to prevent clashes between incompatible
|
|
* options */
|
|
|
|
/* Check for incompatible time modifier. */
|
|
Object *ob = static_cast<Object *>(ob_ptr.data);
|
|
GpencilModifierData *md = static_cast<GpencilModifierData *>(ptr->data);
|
|
if (BKE_gpencil_modifiers_findby_type(ob, eGpencilModifierType_Time) != nullptr) {
|
|
BKE_gpencil_modifier_set_error(md, "Build and Time Offset modifiers are incompatible");
|
|
}
|
|
|
|
gpencil_modifier_panel_end(layout, ptr);
|
|
}
|
|
|
|
static void frame_range_header_draw(const bContext * /*C*/, Panel *panel)
|
|
{
|
|
uiLayout *layout = panel->layout;
|
|
|
|
PointerRNA *ptr = gpencil_modifier_panel_get_property_pointers(panel, nullptr);
|
|
|
|
uiItemR(
|
|
layout, ptr, "use_restrict_frame_range", UI_ITEM_NONE, IFACE_("Custom Range"), ICON_NONE);
|
|
}
|
|
|
|
static void frame_range_panel_draw(const bContext * /*C*/, Panel *panel)
|
|
{
|
|
uiLayout *col;
|
|
uiLayout *layout = panel->layout;
|
|
|
|
PointerRNA *ptr = gpencil_modifier_panel_get_property_pointers(panel, nullptr);
|
|
|
|
uiLayoutSetPropSep(layout, true);
|
|
|
|
col = uiLayoutColumn(layout, false);
|
|
uiItemR(col, ptr, "frame_start", UI_ITEM_NONE, IFACE_("Start"), ICON_NONE);
|
|
uiItemR(col, ptr, "frame_end", UI_ITEM_NONE, IFACE_("End"), ICON_NONE);
|
|
}
|
|
|
|
static void fading_header_draw(const bContext * /*C*/, Panel *panel)
|
|
{
|
|
uiLayout *layout = panel->layout;
|
|
|
|
PointerRNA *ptr = gpencil_modifier_panel_get_property_pointers(panel, nullptr);
|
|
|
|
uiItemR(layout, ptr, "use_fading", UI_ITEM_NONE, IFACE_("Fade"), ICON_NONE);
|
|
}
|
|
|
|
static void fading_panel_draw(const bContext * /*C*/, Panel *panel)
|
|
{
|
|
uiLayout *col;
|
|
uiLayout *layout = panel->layout;
|
|
|
|
PointerRNA ob_ptr;
|
|
PointerRNA *ptr = gpencil_modifier_panel_get_property_pointers(panel, &ob_ptr);
|
|
|
|
uiLayoutSetPropSep(layout, true);
|
|
|
|
uiItemR(layout, ptr, "fade_factor", UI_ITEM_NONE, IFACE_("Factor"), ICON_NONE);
|
|
|
|
col = uiLayoutColumn(layout, true);
|
|
uiItemR(col, ptr, "fade_thickness_strength", UI_ITEM_NONE, IFACE_("Thickness"), ICON_NONE);
|
|
uiItemR(col, ptr, "fade_opacity_strength", UI_ITEM_NONE, IFACE_("Opacity"), ICON_NONE);
|
|
|
|
uiItemPointerR(layout,
|
|
ptr,
|
|
"target_vertex_group",
|
|
&ob_ptr,
|
|
"vertex_groups",
|
|
IFACE_("Weight Output"),
|
|
ICON_NONE);
|
|
}
|
|
|
|
static void mask_panel_draw(const bContext * /*C*/, Panel *panel)
|
|
{
|
|
gpencil_modifier_masking_panel_draw(panel, false, false);
|
|
}
|
|
|
|
static void panel_register(ARegionType *region_type)
|
|
{
|
|
PanelType *panel_type = gpencil_modifier_panel_register(
|
|
region_type, eGpencilModifierType_Build, panel_draw);
|
|
gpencil_modifier_subpanel_register(
|
|
region_type, "frame_range", "", frame_range_header_draw, frame_range_panel_draw, panel_type);
|
|
gpencil_modifier_subpanel_register(
|
|
region_type, "fading", "", fading_header_draw, fading_panel_draw, panel_type);
|
|
gpencil_modifier_subpanel_register(
|
|
region_type, "_mask", "Influence", nullptr, mask_panel_draw, panel_type);
|
|
}
|
|
|
|
static void foreach_ID_link(GpencilModifierData *md, Object *ob, IDWalkFunc walk, void *user_data)
|
|
{
|
|
BuildGpencilModifierData *mmd = (BuildGpencilModifierData *)md;
|
|
|
|
walk(user_data, ob, (ID **)&mmd->object, IDWALK_CB_NOP);
|
|
}
|
|
|
|
static void update_depsgraph(GpencilModifierData *md,
|
|
const ModifierUpdateDepsgraphContext *ctx,
|
|
const int /*mode*/)
|
|
{
|
|
BuildGpencilModifierData *lmd = (BuildGpencilModifierData *)md;
|
|
if (lmd->object != nullptr) {
|
|
DEG_add_object_relation(ctx->node, lmd->object, DEG_OB_COMP_GEOMETRY, "Build Modifier");
|
|
DEG_add_object_relation(ctx->node, lmd->object, DEG_OB_COMP_TRANSFORM, "Build Modifier");
|
|
}
|
|
DEG_add_object_relation(ctx->node, ctx->object, DEG_OB_COMP_TRANSFORM, "Build Modifier");
|
|
}
|
|
|
|
/* ******************************************** */
|
|
|
|
GpencilModifierTypeInfo modifierType_Gpencil_Build = {
|
|
/*name*/ N_("Build"),
|
|
/*struct_name*/ "BuildGpencilModifierData",
|
|
/*struct_size*/ sizeof(BuildGpencilModifierData),
|
|
/*type*/ eGpencilModifierTypeType_Gpencil,
|
|
/*flags*/ eGpencilModifierTypeFlag_NoApply,
|
|
|
|
/*copy_data*/ copy_data,
|
|
|
|
/*deform_stroke*/ nullptr,
|
|
/*generate_strokes*/ generate_strokes,
|
|
/*bake_modifier*/ nullptr,
|
|
/*remap_time*/ nullptr,
|
|
|
|
/*init_data*/ init_data,
|
|
/*free_data*/ nullptr,
|
|
/*is_disabled*/ nullptr,
|
|
/*update_depsgraph*/ update_depsgraph,
|
|
/*depends_on_time*/ depends_on_time,
|
|
/*foreach_ID_link*/ foreach_ID_link,
|
|
/*foreach_tex_link*/ nullptr,
|
|
/*panel_register*/ panel_register,
|
|
};
|