Drivers: add lerp and clamp functions to namespace.

Implementation of lerp without a function requires repeating one of
the arguments, which is not ideal. To avoid that, add a new function
to the driver namespace. In addition, provide a function for clamping
between 0 and 1 to support easy clamped lerp, and a smoothstep function
from GLSL that is somewhat related.

The function implementations are added to a new bl_math module.
As an aside, add the round function and two-argument log to the
pylike expression subset.

Differential Revision: https://developer.blender.org/D8205
This commit is contained in:
Alexander Gavrilov 2020-07-04 13:20:59 +03:00
parent 8369adabc0
commit f8cc01595d
7 changed files with 329 additions and 0 deletions

View File

@ -72,6 +72,8 @@ typedef enum eOpCode {
OPCODE_FUNC1,
/* 2 argument function call: (a b -> func2(a,b)) */
OPCODE_FUNC2,
/* 3 argument function call: (a b c -> func3(a,b,c)) */
OPCODE_FUNC3,
/* Parameter access: (-> params[ival]) */
OPCODE_PARAMETER,
/* Minimum of multiple inputs: (a b c... -> min); ival = arg count */
@ -92,6 +94,7 @@ typedef enum eOpCode {
typedef double (*UnaryOpFunc)(double);
typedef double (*BinaryOpFunc)(double, double);
typedef double (*TernaryOpFunc)(double, double, double);
typedef struct ExprOp {
eOpCode opcode;
@ -104,6 +107,7 @@ typedef struct ExprOp {
void *ptr;
UnaryOpFunc func1;
BinaryOpFunc func2;
TernaryOpFunc func3;
} arg;
} ExprOp;
@ -216,6 +220,11 @@ eExprPyLike_EvalStatus BLI_expr_pylike_eval(ExprPyLike_Parsed *expr,
stack[sp - 2] = ops[pc].arg.func2(stack[sp - 2], stack[sp - 1]);
sp--;
break;
case OPCODE_FUNC3:
FAIL_IF(sp < 3);
stack[sp - 3] = ops[pc].arg.func3(stack[sp - 3], stack[sp - 2], stack[sp - 1]);
sp -= 2;
break;
case OPCODE_MIN:
FAIL_IF(sp < ops[pc].arg.ival);
for (int j = 1; j < ops[pc].arg.ival; j++, sp--) {
@ -326,6 +335,35 @@ static double op_degrees(double arg)
return arg * 180.0 / M_PI;
}
static double op_log2(double a, double b)
{
return log(a) / log(b);
}
static double op_lerp(double a, double b, double x)
{
return a * (1.0 - x) + b * x;
}
static double op_clamp(double arg)
{
CLAMP(arg, 0.0, 1.0);
return arg;
}
static double op_clamp3(double arg, double minv, double maxv)
{
CLAMP(arg, minv, maxv);
return arg;
}
static double op_smoothstep(double a, double b, double x)
{
double t = (x - a) / (b - a);
CLAMP(t, 0.0, 1.0);
return t * t * (3.0 - 2.0 * t);
}
static double op_not(double a)
{
return a ? 0.0 : 1.0;
@ -390,6 +428,7 @@ static BuiltinOpDef builtin_ops[] = {
{"floor", OPCODE_FUNC1, floor},
{"ceil", OPCODE_FUNC1, ceil},
{"trunc", OPCODE_FUNC1, trunc},
{"round", OPCODE_FUNC1, round},
{"int", OPCODE_FUNC1, trunc},
{"sin", OPCODE_FUNC1, sin},
{"cos", OPCODE_FUNC1, cos},
@ -400,9 +439,14 @@ static BuiltinOpDef builtin_ops[] = {
{"atan2", OPCODE_FUNC2, atan2},
{"exp", OPCODE_FUNC1, exp},
{"log", OPCODE_FUNC1, log},
{"log", OPCODE_FUNC2, op_log2},
{"sqrt", OPCODE_FUNC1, sqrt},
{"pow", OPCODE_FUNC2, pow},
{"fmod", OPCODE_FUNC2, fmod},
{"lerp", OPCODE_FUNC3, op_lerp},
{"clamp", OPCODE_FUNC1, op_clamp},
{"clamp", OPCODE_FUNC3, op_clamp3},
{"smoothstep", OPCODE_FUNC3, op_smoothstep},
{NULL, OPCODE_CONST, NULL},
};
@ -514,6 +558,22 @@ static void parse_set_jump(ExprParseState *state, int jump)
state->ops[jump - 1].jmp_offset = state->ops_count - jump;
}
/* Returns the required argument count of the given function call code. */
static int opcode_arg_count(eOpCode code)
{
switch (code) {
case OPCODE_FUNC1:
return 1;
case OPCODE_FUNC2:
return 2;
case OPCODE_FUNC3:
return 3;
default:
BLI_assert(!"unexpected opcode");
return -1;
}
}
/* Add a function call operation, applying constant folding when possible. */
static bool parse_add_func(ExprParseState *state, eOpCode code, int args, void *funcptr)
{
@ -560,6 +620,27 @@ static bool parse_add_func(ExprParseState *state, eOpCode code, int args, void *
}
break;
case OPCODE_FUNC3:
CHECK_ERROR(args == 3);
if (jmp_gap >= 3 && prev_ops[-3].opcode == OPCODE_CONST &&
prev_ops[-2].opcode == OPCODE_CONST && prev_ops[-1].opcode == OPCODE_CONST) {
TernaryOpFunc func = funcptr;
/* volatile because some compilers overly aggressive optimize this call out.
* see D6012 for details. */
volatile double result = func(
prev_ops[-3].arg.dval, prev_ops[-2].arg.dval, prev_ops[-1].arg.dval);
if (fetestexcept(FE_DIVBYZERO | FE_INVALID) == 0) {
prev_ops[-3].arg.dval = result;
state->ops_count -= 2;
state->stack_ptr -= 2;
return true;
}
}
break;
default:
BLI_assert(false);
return false;
@ -755,6 +836,17 @@ static bool parse_unary(ExprParseState *state)
if (STREQ(state->tokenbuf, builtin_ops[i].name)) {
int args = parse_function_args(state);
/* Search for other arg count versions if necessary. */
if (args != opcode_arg_count(builtin_ops[i].op)) {
for (int j = i + 1; builtin_ops[j].name; j++) {
if (opcode_arg_count(builtin_ops[j].op) == args &&
STREQ(builtin_ops[j].name, builtin_ops[i].name)) {
i = j;
break;
}
}
}
return parse_add_func(state, builtin_ops[i].op, args, builtin_ops[i].funcptr);
}
}

View File

@ -36,12 +36,14 @@ set(SRC
bpy_threads.c
idprop_py_api.c
imbuf_py_api.c
bl_math_py_api.c
py_capi_utils.c
bgl.h
blf_py_api.h
idprop_py_api.h
imbuf_py_api.h
bl_math_py_api.h
py_capi_utils.h
# header-only

View File

@ -0,0 +1,163 @@
/*
* 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.
*/
/**
* \file
* \ingroup pygen
*
* This file defines the 'bl_math' module, a module for math utilities.
*/
#include <Python.h>
#include "BLI_math.h"
#include "BLI_utildefines.h"
#include "py_capi_utils.h"
#include "bl_math_py_api.h"
/*------------------------------------------------------------*/
/**
* \name Module doc string
* \{ */
PyDoc_STRVAR(M_Math_doc, "Miscellaneous math utilities module");
/** \} */
/*------------------------------------------------------------*/
/**
* \name Python functions
* \{ */
PyDoc_STRVAR(M_Math_clamp_doc,
".. function:: clamp(value, min=0, max=1)\n"
"\n"
" Clamps the float value between minimum and maximum. To avoid\n"
" confusion, any call must use either one or all three arguments.\n"
"\n"
" :arg value: The value to clamp.\n"
" :type value: float\n"
" :arg min: The minimum value, defaults to 0.\n"
" :type min: float\n"
" :arg max: The maximum value, defaults to 1.\n"
" :type max: float\n"
" :return: The clamped value.\n"
" :rtype: float\n");
static PyObject *M_Math_clamp(PyObject *UNUSED(self), PyObject *args)
{
double x, minv = 0.0, maxv = 1.0;
if (PyTuple_Size(args) <= 1) {
if (!PyArg_ParseTuple(args, "d:clamp", &x)) {
return NULL;
}
}
else {
if (!PyArg_ParseTuple(args, "ddd:clamp", &x, &minv, &maxv)) {
return NULL;
}
}
CLAMP(x, minv, maxv);
return PyFloat_FromDouble(x);
}
PyDoc_STRVAR(M_Math_lerp_doc,
".. function:: lerp(from, to, factor)\n"
"\n"
" Linearly interpolate between two float values based on factor.\n"
"\n"
" :arg from: The value to return when factor is 0.\n"
" :type from: float\n"
" :arg to: The value to return when factor is 1.\n"
" :type to: float\n"
" :arg factor: The interpolation value, normally in [0.0, 1.0].\n"
" :type factor: float\n"
" :return: The interpolated value.\n"
" :rtype: float\n");
static PyObject *M_Math_lerp(PyObject *UNUSED(self), PyObject *args)
{
double a, b, x;
if (!PyArg_ParseTuple(args, "ddd:lerp", &a, &b, &x)) {
return NULL;
}
return PyFloat_FromDouble(a * (1.0 - x) + b * x);
}
PyDoc_STRVAR(
M_Math_smoothstep_doc,
".. function:: smoothstep(from, to, value)\n"
"\n"
" Performs smooth interpolation between 0 and 1 as value changes between from and to.\n"
" Outside the range the function returns the same value as the nearest edge.\n"
"\n"
" :arg from: The edge value where the result is 0.\n"
" :type from: float\n"
" :arg to: The edge value where the result is 1.\n"
" :type to: float\n"
" :arg factor: The interpolation value.\n"
" :type factor: float\n"
" :return: The interpolated value in [0.0, 1.0].\n"
" :rtype: float\n");
static PyObject *M_Math_smoothstep(PyObject *UNUSED(self), PyObject *args)
{
double a, b, x;
if (!PyArg_ParseTuple(args, "ddd:smoothstep", &a, &b, &x)) {
return NULL;
}
double t = (x - a) / (b - a);
CLAMP(t, 0.0, 1.0);
return PyFloat_FromDouble(t * t * (3.0 - 2.0 * t));
}
/** \} */
/*------------------------------------------------------------*/
/**
* \name Module definition
* \{ */
static PyMethodDef M_Math_methods[] = {
{"clamp", (PyCFunction)M_Math_clamp, METH_VARARGS, M_Math_clamp_doc},
{"lerp", (PyCFunction)M_Math_lerp, METH_VARARGS, M_Math_lerp_doc},
{"smoothstep", (PyCFunction)M_Math_smoothstep, METH_VARARGS, M_Math_smoothstep_doc},
{NULL, NULL, 0, NULL},
};
static struct PyModuleDef M_Math_module_def = {
PyModuleDef_HEAD_INIT,
"bl_math", /* m_name */
M_Math_doc, /* m_doc */
0, /* m_size */
M_Math_methods, /* m_methods */
NULL, /* m_reload */
NULL, /* m_traverse */
NULL, /* m_clear */
NULL, /* m_free */
};
PyMODINIT_FUNC BPyInit_bl_math(void)
{
PyObject *submodule = PyModule_Create(&M_Math_module_def);
return submodule;
}
/** \} */

View File

@ -0,0 +1,27 @@
/*
* 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.
*/
/**
* \file
* \ingroup pygen
*/
#ifndef __BL_MATH_PY_API_H__
#define __BL_MATH_PY_API_H__
PyMODINIT_FUNC BPyInit_bl_math(void);
#endif /* __BL_MATH_PY_API_H__ */

View File

@ -114,6 +114,19 @@ int bpy_pydriver_create_dict(void)
Py_DECREF(mod);
}
/* Add math utility functions. */
mod = PyImport_ImportModuleLevel("bl_math", NULL, NULL, NULL, 0);
if (mod) {
static const char *names[] = {"clamp", "lerp", "smoothstep", NULL};
for (const char **pname = names; *pname; ++pname) {
PyObject *func = PyDict_GetItemString(PyModule_GetDict(mod), *pname);
PyDict_SetItemString(bpy_pydriver_Dict, *pname, func);
}
Py_DECREF(mod);
}
#ifdef USE_BYTECODE_WHITELIST
/* setup the whitelist */
{
@ -133,6 +146,10 @@ int bpy_pydriver_create_dict(void)
"bool",
"float",
"int",
/* bl_math */
"clamp",
"lerp",
"smoothstep",
NULL,
};

View File

@ -68,6 +68,7 @@
/* inittab initialization functions */
#include "../bmesh/bmesh_py_api.h"
#include "../generic/bgl.h"
#include "../generic/bl_math_py_api.h"
#include "../generic/blf_py_api.h"
#include "../generic/idprop_py_api.h"
#include "../generic/imbuf_py_api.h"
@ -228,6 +229,7 @@ static struct _inittab bpy_internal_modules[] = {
{"_bpy_path", BPyInit__bpy_path},
{"bgl", BPyInit_bgl},
{"blf", BPyInit_blf},
{"bl_math", BPyInit_bl_math},
{"imbuf", BPyInit_imbuf},
{"bmesh", BPyInit_bmesh},
#if 0

View File

@ -157,6 +157,32 @@ TEST_EVAL(FMod, "fmod(x, 2)", 3.5, 1.5)
TEST_CONST(Pow, "pow(4, 0.5)", 2.0)
TEST_EVAL(Pow, "pow(4, x)", 0.5, 2.0)
TEST_CONST(Log2_1, "log(4, 2)", 2.0)
TEST_CONST(Round1, "round(-0.5)", -1.0)
TEST_CONST(Round2, "round(-0.4)", 0.0)
TEST_CONST(Round3, "round(0.4)", 0.0)
TEST_CONST(Round4, "round(0.5)", 1.0)
TEST_CONST(Clamp1, "clamp(-0.1)", 0.0)
TEST_CONST(Clamp2, "clamp(0.5)", 0.5)
TEST_CONST(Clamp3, "clamp(1.5)", 1.0)
TEST_CONST(Clamp4, "clamp(0.5, 0.2, 0.3)", 0.3)
TEST_CONST(Clamp5, "clamp(0.0, 0.2, 0.3)", 0.2)
TEST_CONST(Lerp1, "lerp(-10,10,-1)", -30.0)
TEST_CONST(Lerp2, "lerp(-10,10,0.25)", -5.0)
TEST_CONST(Lerp3, "lerp(-10,10,1)", 10.0)
TEST_EVAL(Lerp1, "lerp(-10,10,x)", 0, -10.0)
TEST_EVAL(Lerp2, "lerp(-10,10,x)", 0.75, 5.0)
TEST_CONST(Smoothstep1, "smoothstep(-10,10,-20)", 0.0)
TEST_CONST(Smoothstep2, "smoothstep(-10,10,-10)", 0.0)
TEST_CONST(Smoothstep3, "smoothstep(-10,10,10)", 1.0)
TEST_CONST(Smoothstep4, "smoothstep(-10,10,20)", 1.0)
TEST_CONST(Smoothstep5, "smoothstep(-10,10,-5)", 0.15625)
TEST_EVAL(Smoothstep1, "smoothstep(-10,10,x)", 5, 0.84375)
TEST_RESULT(Min1, "min(3,1,2)", 1.0)
TEST_RESULT(Max1, "max(3,1,2)", 3.0)
TEST_RESULT(Min2, "min(1,2,3)", 1.0)