Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlexander Gavrilov <angavrilov@gmail.com>2020-07-04 13:20:59 +0300
committerAlexander Gavrilov <angavrilov@gmail.com>2020-07-21 19:01:50 +0300
commitf8cc01595d1181b9a8adcb6aa930d4cbfebdc8bf (patch)
tree881dd5079bb2e7d674980a5e80d3ea7d16dd8375
parent8369adabc0ec7a1fce248b688bf20860ae0434bb (diff)
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
-rw-r--r--source/blender/blenlib/intern/expr_pylike_eval.c92
-rw-r--r--source/blender/python/generic/CMakeLists.txt2
-rw-r--r--source/blender/python/generic/bl_math_py_api.c163
-rw-r--r--source/blender/python/generic/bl_math_py_api.h27
-rw-r--r--source/blender/python/intern/bpy_driver.c17
-rw-r--r--source/blender/python/intern/bpy_interface.c2
-rw-r--r--tests/gtests/blenlib/BLI_expr_pylike_eval_test.cc26
7 files changed, 329 insertions, 0 deletions
diff --git a/source/blender/blenlib/intern/expr_pylike_eval.c b/source/blender/blenlib/intern/expr_pylike_eval.c
index d1d84dab3f7..f8618c54ea4 100644
--- a/source/blender/blenlib/intern/expr_pylike_eval.c
+++ b/source/blender/blenlib/intern/expr_pylike_eval.c
@@ -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);
}
}
diff --git a/source/blender/python/generic/CMakeLists.txt b/source/blender/python/generic/CMakeLists.txt
index 822f05bad90..785c1d66407 100644
--- a/source/blender/python/generic/CMakeLists.txt
+++ b/source/blender/python/generic/CMakeLists.txt
@@ -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
diff --git a/source/blender/python/generic/bl_math_py_api.c b/source/blender/python/generic/bl_math_py_api.c
new file mode 100644
index 00000000000..4d5a63ffba3
--- /dev/null
+++ b/source/blender/python/generic/bl_math_py_api.c
@@ -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;
+}
+
+/** \} */
diff --git a/source/blender/python/generic/bl_math_py_api.h b/source/blender/python/generic/bl_math_py_api.h
new file mode 100644
index 00000000000..9183573abfc
--- /dev/null
+++ b/source/blender/python/generic/bl_math_py_api.h
@@ -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__ */
diff --git a/source/blender/python/intern/bpy_driver.c b/source/blender/python/intern/bpy_driver.c
index 3d83eb90da6..5e2162c9e2d 100644
--- a/source/blender/python/intern/bpy_driver.c
+++ b/source/blender/python/intern/bpy_driver.c
@@ -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,
};
diff --git a/source/blender/python/intern/bpy_interface.c b/source/blender/python/intern/bpy_interface.c
index ed5e505176c..a880d2cd285 100644
--- a/source/blender/python/intern/bpy_interface.c
+++ b/source/blender/python/intern/bpy_interface.c
@@ -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
diff --git a/tests/gtests/blenlib/BLI_expr_pylike_eval_test.cc b/tests/gtests/blenlib/BLI_expr_pylike_eval_test.cc
index 449577401d7..1b17e6d839e 100644
--- a/tests/gtests/blenlib/BLI_expr_pylike_eval_test.cc
+++ b/tests/gtests/blenlib/BLI_expr_pylike_eval_test.cc
@@ -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)