I knew about the when constructor but never had any need to use it.
Can anyone hint me on how to make it work without the handleAt attribute?
Personally I would use an expression controller to do these things purely in math because that way the rig will be max script free which gives much faster results and it will still be pure math as well.
Here goes:
(
max create mode
local numSegs = 10
local start = point name:"start" pos:[0,0,0] size:10 box:true cross:false wirecolor:green
local end = point name:"end" pos:[100,0,0] size:10 box:true cross:false wirecolor:green
local outStart = point name:"out-start" pos:[100.0 / 3,0,0] size:2.5 box:true cross:false wirecolor:green
local inEnd = point name:"in-end" pos:[200.0 / 3,0,0] size:2.5 box:true cross:false wirecolor:green
local ctrls = #(start, outStart, inEnd, end)
setTransformLockFlags #(outStart, inEnd) #{2..9}
outStart.parent = start
inEnd.parent = end
local ghosts = for i = 1 to numSegs collect
point name:("ghost" + i as string) axistripod:on box:on cross:off size:10 wirecolor:red
setTransformLockFlags ghosts #all
fn getBezierInterpolation p t =
(
(1. - t) ^ 3 * p[1] + 3 * (1. - t) ^ 2 * t * p[2] + 3 * (1. - t) * t ^ 2 * p[3] + t ^ 3 * p[4]
)
when transform ctrls changes handleAt:#redrawViews do (
with redraw off (
with animate off (
local pts = for o in ctrls collect o.pos
for i = 1 to numSegs do (
local t = (1. * i / (numSegs + 1))
local p = getBezierInterpolation pts t
ghosts[i].pos = p
local x, y, z
if i == 1 then
x = normalize (ghosts[i].pos - start.pos)
else
x = normalize (ghosts[i].pos - ghosts[i - 1].pos)
z = normalize [0,0,1]
y = normalize (cross z x)
z = normalize (cross x y)
local m = matrix3 x y z p
local a = start.rotation.controller.x_rotation * (1. - t) + end.rotation.controller.x_rotation * t
preRotateX m a
ghosts[i].transform = m
)
)))
move start [0,0,0]
forceCompleteRedraw()
)
Great! One thing is missed. In my rig the ghost objects distributed by length (not by path). See the path constraint’s constantVel param…
PS. there is a trick:
-- instead of
for k=1 to 10 do d_float = 1.*k/10
-- do
for k=1. to 10 do d_float = k/10
another thing is to try not use the start’s and end’s rotation controllers. try to use their transforms.
in this case we will not be limited by type of assigned controllers. Our control nodes (start, end, out_start, and in_end) might be driven by script controller, or orientation_constraint for example…
here is an improved version, still haven’t managed to figure out how to do the constant velocity yet. I tried Catmull-Rom and Hermite spline interpolations but it doesn’t help my situation…
(
max create mode
local numSegs = 10
local start = point name:"start" pos:[0,0,0] size:10 box:true cross:false wirecolor:green
local end = point name:"end" pos:[100,0,0] size:10 box:true cross:false wirecolor:green
local outStart = point name:"out-start" pos:[100.0 / 3,0,0] size:2.5 box:true cross:false wirecolor:green
local inEnd = point name:"in-end" pos:[200.0 / 3,0,0] size:2.5 box:true cross:false wirecolor:green
local ctrls = #(start, outStart, inEnd, end)
setTransformLockFlags #(outStart, inEnd) #{2..9}
outStart.parent = start
inEnd.parent = end
local ghosts = for i = 1 to numSegs collect
point name:("ghost" + i as string) axistripod:on box:on cross:off size:10 wirecolor:red
setTransformLockFlags ghosts #all
fn getBezierInterpolation p t =
(
(1. - t) ^ 3 * p[1] + 3 * (1. - t) ^ 2 * t * p[2] + 3 * (1. - t) * t ^ 2 * p[3] + t ^ 3 * p[4]
)
when transform ctrls changes handleAt:#redrawViews do (
with redraw off (
with animate off (
local pts = for o in ctrls collect o.pos
for i = 1. to numSegs do (
local t = i / (numSegs + 1)
local p1 = getBezierInterpolation pts t
local p2 = getBezierInterpolation pts (t + 0.0001)
local x, y, z
x = normalize (p2 - p1)
z = normalize [0,0,1]
y = normalize (cross z x)
z = normalize (cross x y)
local m = matrix3 x y z p1
local startXRot = (quatToEuler start.transform.rotationpart).x
local endXRot = (quatToEuler end.transform.rotationpart).x
local a = startXRot * (1. - t) + endXRot * t
preRotateX m a
ghosts[i].transform = m
)
)))
move start [0,0,0]
forceCompleteRedraw()
)
local startXRot = (quatToEuler start.transform.rotationpart).x
local endXRot = (quatToEuler end.transform.rotationpart).x
this is not right. Object transform is absolute (in world space coordinate system). we have to interpolate in relative (local) space. The best representation is Angle-Axis. Where the axis is object’s local space and the angle is spin (x-rotation in our case).
the tangent is accurate now.
constant velocity… it’s kinda expensive to get. but … look how to calculate bezier spline length.
i’ve thought a little… only thing that we need to do is to find the right angles application order.
no constant param yet, but
something very inconsistent, happens with max reference system:
the callback is getting transform changes from the tangents when these are not being transformed .
this does not happen by transforming the cont5rol trhough mxs: $ruh002.pos+=20
uncommenting line 33 to force a CTRL node update, avoids this redundancy
another problem is the undo that is unable to capture all changes depending on what node was updated last,
some of these issues may be fixed if I link all through the ref system… but it’s not clean.
I’ve had issues like this with the ref system in max, but can’t understand where is the origin…
( --mxs curves test cgt=985724
if ::ru_call==undefined then
(
::ru_call=on
local n=9.--param
local prop=#([-70,50,0],[-30,-80,0],[-30,50,0],[-70,-80,0],yellow,yellow,orange,orange)
local nodes=for n in 1 to 4 collect (point name:(uniqueName "ruh") box:true size:5 pos:prop[n] wirecolor:(prop[n+4]))
local ctrls=for o in nodes collect NodeTransformMonitor node:o forwardTransformChangeMsgs:true
local subs=for o=1 to n collect point wirecolor:white size:5 cross:false box:true name:#ruhs
for c=1 to 4 do custAttributes.add nodes[c] (
attributes linkage(
parameters main
(
num type:#integer
offset type:#point3
)
)
)
for n=1 to 4 do nodes[n].num=n
nodes[3].offset=[ 40,0,0]
nodes[4].offset=[-40,0,0]
--
fn bspline subs ctrls nodes obj=
(
-- print obj.num
if obj.num<3 then
(
format "%: CONTROL is dragged
" (timestamp())
ctrls[obj.num+2].node.transform=(transMatrix ctrls[obj.num+2].node.offset)*obj.transform
-- ctrls[obj.num].node.transform=ctrls[obj.num].node.transform -- HACK to force UNDO
)
else
(
format "%: TANGENT is dragged
" (timestamp())
--MAX gizmo/controllers // ref messages?
ctrls[obj.num-2].node.offset=(obj.pos*inverse ctrls[obj.num-2].node.transform)*[1,0,0]
ctrls[obj.num].node.transform=ctrls[obj.num-2].node.transform*(transMatrix ctrls[obj.num-2].node.offset)
-- ctrls[obj.num-2].node.transform=ctrls[obj.num-2].node.transform -- HACK to force UNDO
)
for s=1. to n do subs[s].pos=
(
t=s/(n+1)
ctrls[1].node.pos * (1-T)^3 +
ctrls[3].node.pos * 3*(1-T)^2*T +
ctrls[4].node.pos * 3*(1-T)*T^2 +
ctrls[2].node.pos * T^3
)
)
bspline subs ctrls nodes nodes[1]
when transform nodes change obj do bspline subs ctrls nodes obj id:#ru_call
)
else
(
deleteAllChangeHandlers id:#ru_call
globalVars.remove #ru_call
delete $ruh*
)
)
here it is w constant length stuff added| not optimized.
I tried to modify one of the controls without triggering the callback again, but it seems there is no way, it’s not evident buy annoys me.
conding using only Change Handlers doesn’t sound like a good idea IMO…
( --mxs curves test cgt=985724
if ::ru_call==undefined then
(
local n=9.--param
local prop=#([-70,50,0],[-30,-80,0],[-30,50,0],[-70,-80,0],yellow,yellow,orange,orange)
::ru_call=#(
for n in 1 to 4 collect (point name:(uniqueName "ruh") box:true size:5 pos:prop[n] wirecolor:(prop[n+4])),
for o=1 to n collect point wirecolor:white size:5 cross:false box:true name:#ruhs
)
for c=1 to 4 do custAttributes.add ru_call[1][c] (
attributes linkage(
parameters main
(
num type:#integer
offset type:#point3
)
)
)
for n=1 to 4 do ru_call[1][n].num=n
ru_call[1][3].offset=[ 40,0,0]
ru_call[1][4].offset=[-40,0,0]
--
fn bspline subs nodes obj =
(
--format "num:% time: %
" obj.num (timestamp())
if obj.num<3 then nodes[obj.num+2].transform=(transMatrix nodes[obj.num+2].offset)*obj.transform
else
(
nodes[obj.num].offset=(obj.pos*inverse nodes[obj.num-2].transform)*[1,0,0]
nodes[obj.num].transform=(transMatrix nodes[obj.num].offset)*nodes[obj.num-2].transform
)
leng=0
prev=ru_call[1][1].pos
local a=nodes[1].pos,b=nodes[3].pos,c=nodes[4].pos,d=nodes[2].pos
for t=0. to 1 by 1e-2 do
(
new=
(
a * (1-T)^3 +
b * 3*(1-T)^2*T +
c * 3*(1-T)*T^2 +
d * T^3
)
leng+=distance prev new
prev=new
)
leng/=10.
leng2=0
idx=1
prev=ru_call[1][1].pos
for t=0 to 1 by 1e-3 do
(
new=
(
a * (1-T)^3 +
b * 3*(1-T)^2*T +
c * 3*(1-T)*T^2 +
d * T^3
)
leng2+=distance prev new
prev=new
if leng2 >= leng and idx<10 do
(--format "leng: % leng2:%
" leng leng2
::ru_call[2][idx].pos=new
idx+=1
prev=new
leng2=0
)
)
)
bspline ::ru_call[2] ::ru_call[1] ru_call[1][1]
when transform ::ru_call[1] change id:#ru_call obj do bspline ::ru_call[2] ::ru_call[1] obj
)
else
(
deleteAllChangeHandlers id:#ru_call
globalVars.remove #ru_call
delete $ruh*
)
)
Wow, you don’t check this forum for one day, and this is what happens.
Looks really fun, great work everyone.
TzMtn: I get weird results without handleAt:#redrawViews as well, on a new computer. I am not sure why it happens though.
Seems you are right, previously tested on max 2009 and it didn’t work. On max 2012 trial it works fine.