commit
caf9f41ee0
2 changed files with 6798 additions and 0 deletions
@ -0,0 +1,611 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-or-later */ |
||||
/* |
||||
This js file is meant to be used in Cycling'74 Max with the [v8] object. |
||||
It provides a way to control cameras in a typical first-person fashion, |
||||
and move and turn objects in view-space. |
||||
|
||||
Copyright (C) 2025 Théophile Clet <contact@tflcl.xyz> - https://tflcl.xyz
|
||||
|
||||
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 3 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: |
||||
<https://www.gnu.org/licenses/gpl-3.0.txt>.
|
||||
*/ |
||||
|
||||
autowatch = 1; |
||||
|
||||
outlets = 2; |
||||
|
||||
var drawto = ""; |
||||
declareattribute({ name: "drawto", setter: "setdrawto" }); |
||||
|
||||
var ease = 0.1; |
||||
declareattribute({ |
||||
name: "ease", |
||||
setter: "set_ease", |
||||
label: "Ease", |
||||
type: "float", |
||||
min: 0.0, |
||||
default: 0.1, |
||||
}); |
||||
|
||||
var camera_node = ""; |
||||
declareattribute({ |
||||
name: "camera_node", |
||||
setter: "camera", |
||||
label: "Camera anim node", |
||||
type: "string", |
||||
}); |
||||
|
||||
var flymode = 0; |
||||
declareattribute({ |
||||
name: "flymode", |
||||
setter: "set_flymode", |
||||
style: "onoff", |
||||
default: 0, |
||||
}); |
||||
|
||||
var show_bounds = 1; |
||||
declareattribute({ |
||||
name: "show_bounds", |
||||
setter: "set_show_bounds", |
||||
style: "onoff", |
||||
default: 1, |
||||
}); |
||||
|
||||
const ANIMABLE_GL_OBJECTS = [ |
||||
"jit_gl_node", |
||||
"jit_gl_mesh", |
||||
"jit_gl_gridshape", |
||||
"jit_gl_model", |
||||
"jit_gl_plato", |
||||
"jit_gl_nurbs", |
||||
"jit_gl_text", |
||||
"jit_gl_videoplane", |
||||
"jit_gl_graph", |
||||
"jit_gl_sketch", |
||||
"jit_gl_path", |
||||
"jit_gl_lua", |
||||
"jit_gl_isosurf", |
||||
"jit_gl_cornerpin", |
||||
"jit_gl_skybox", |
||||
"jit_gl_volume", |
||||
"jit_gl_multiple", |
||||
"jit_gl_light", |
||||
"jit_gl_camera", |
||||
]; |
||||
|
||||
let controlled_gl_objects = []; // Used for enabling.disabling drawbounds on object(s) animated by the controller
|
||||
let controllable_objects = []; // Used for listing all controllable objects in a patch
|
||||
|
||||
let top_level_patcher; |
||||
let ctlr; |
||||
|
||||
class Controller { |
||||
constructor(drawto) { |
||||
this.ease = 0.1; // anim.drive easing
|
||||
this.flymode = false; |
||||
this.flymode_applyed = false; // Flag
|
||||
this.control_mode; // 'cam' if the camera is the target, 'obj' otherwise
|
||||
|
||||
this.world_up = [0, 1, 0]; |
||||
this.camera_node = new JitterObject("jit.proxy"); |
||||
this.cam_direction, this.cam_up, this.cam_right; |
||||
|
||||
this.camera_base_matrix; |
||||
this.get_cam_base_matrix(); |
||||
|
||||
// main_node always has its Y axis aligned with world_up direction
|
||||
// It is used with main_drive for up/down movements, Y rotation (yaw) and X-Y movements (accross an horizontal plan)
|
||||
this.main_node = new JitterObject("jit.anim.node"); |
||||
this.main_node.turnmode = "local"; |
||||
this.main_node.movemode = "local"; |
||||
this.main_node.tripod = 1; // for flymode
|
||||
|
||||
this.main_drive = new JitterObject("jit.anim.drive"); |
||||
this.main_drive.drawto = drawto; |
||||
this.main_drive.ease = this.ease; |
||||
this.main_drive.targetname = this.main_node.name; |
||||
|
||||
// pitch_node used with pitch_drive for pitch rotation (over the objects local x axis)
|
||||
this.pitch_node = new JitterObject("jit.anim.node"); |
||||
this.pitch_node.anim = this.main_node.name; |
||||
this.pitch_node.turnmode = "parent"; |
||||
this.pitch_node.movemode = "parent"; |
||||
|
||||
this.pitch_drive = new JitterObject("jit.anim.drive"); |
||||
this.pitch_drive.drawto = drawto; |
||||
this.pitch_drive.ease = this.ease; |
||||
this.pitch_drive.targetname = this.pitch_node.name; |
||||
|
||||
this.target = new JitterObject("jit.proxy"); // Stores the object to control
|
||||
} |
||||
|
||||
// Taking control over a new target
|
||||
control(obj_name, ctrl_mode) { |
||||
if (this.camera_node.class == "") { |
||||
error("No camera defined. Cannot control object.\n"); |
||||
return; |
||||
} |
||||
|
||||
let new_target; |
||||
if (obj_name != undefined) { |
||||
new_target = get_anim_node(obj_name, this.pitch_node.name); |
||||
} |
||||
|
||||
if ( |
||||
new_target != undefined && |
||||
(this.target.name != "" || this.target.name != new_target) |
||||
) { |
||||
this.control_mode = ctrl_mode; |
||||
|
||||
if (this.target.name != "") { |
||||
const transform = this.target.send("getworldtransform"); // Get the current targets tranfsorm,
|
||||
this.target.send("anim"); // Unbind it (makes it loose its transform part coming from Controller nodes)
|
||||
this.target.send("anim_reset"); // Reset it
|
||||
this.target.send("transform", transform); // And re-apply the transform so it stays in place
|
||||
} |
||||
|
||||
this.target.name = new_target; // Bind to new target
|
||||
this.reset(); // Reset this.Controller anim nodes
|
||||
this.main_node.position = this.target.send("getworldpos"); // Take the target position, rotatexyz and scale and pass them to this.Controller anim nodes
|
||||
let target_rotatexyz = this.target.send("getrotatexyz"); |
||||
this.main_node.rotatexyz = [0, target_rotatexyz[1], 0]; |
||||
this.pitch_node.rotatexyz = [target_rotatexyz[0], 0, target_rotatexyz[2]]; |
||||
this.pitch_node.scale = this.target.send("getscale"); |
||||
|
||||
// And select the new target
|
||||
this.target.send("anim_reset"); |
||||
this.target.send("anim", this.pitch_node.name); |
||||
|
||||
// Apply specific rules depending on if controlling the camera or an object
|
||||
if (this.control_mode == "cam") { |
||||
this.main_node.movemode = "local"; |
||||
this.main_node.turnmode = "local"; |
||||
this.pitch_node.turnmode = "parent"; |
||||
this.has_rotated = false; |
||||
this.set_flymode(this.flymode); |
||||
} else { |
||||
this.flymode_applyed = false; |
||||
// When controlling an object, we convert the translation vector
|
||||
// from screen space to world space in this.move
|
||||
// So we need the cam base matrix
|
||||
this.main_node.movemode = "world"; |
||||
this.main_node.turnmode = "world"; |
||||
this.pitch_node.turnmode = "world"; |
||||
this.get_cam_base_matrix(); |
||||
} |
||||
|
||||
// No target
|
||||
} else if (obj_name == undefined && this.target.name != "") { |
||||
this.target.send("anim"); |
||||
this.target.send("anim_reset"); |
||||
this.target.send("transform", this.pitch_node.worldtransform); |
||||
this.target.name = ""; |
||||
} |
||||
outlet(0, "control", obj_name, this.target.name); |
||||
} |
||||
|
||||
camera(obj_name) { |
||||
const cam_anim_node_name = get_anim_node(obj_name, this.pitch_node.name); |
||||
if (cam_anim_node_name != undefined) { |
||||
this.camera_node.name = cam_anim_node_name; |
||||
camera_node = this.camera_node.name; |
||||
this.camera_node.send("animmode", "parent"); |
||||
this.get_cam_base_matrix(); |
||||
} |
||||
} |
||||
|
||||
get_cam_base_matrix() { |
||||
if (this.camera_node.name != "") { |
||||
this.cam_direction = this.camera_node.send("getdirection"); |
||||
this.cam_direction[1] = 0; |
||||
this.cam_direction = normalize(this.cam_direction); |
||||
this.cam_right = normalize(cross(this.cam_direction, this.world_up)); |
||||
this.cam_up = normalize(cross(this.cam_right, this.cam_direction)); |
||||
this.camera_base_matrix = [ |
||||
this.cam_right, |
||||
this.cam_up, |
||||
this.cam_direction, |
||||
]; |
||||
} |
||||
} |
||||
|
||||
move(x, y, z) { |
||||
let translat_vec = [x, y, z]; |
||||
if (this.control_mode == "obj") { |
||||
let cam_x_rot = this.camera_node.send("getrotatexyz")[0]; |
||||
// Inverse translation direction if cam is upside down
|
||||
if (cam_x_rot < -90 || cam_x_rot > 90) { |
||||
x *= -1; |
||||
z *= -1; |
||||
} |
||||
translat_vec = multiplyMatrixVector(this.camera_base_matrix, [x, 0, -z]); |
||||
} |
||||
this.main_drive.move(translat_vec[0], y, translat_vec[2]); |
||||
} |
||||
|
||||
turn(x, y, z) { |
||||
let rot_vec = [x, y, z]; |
||||
if (this.control_mode == "obj") { |
||||
rot_vec = multiplyMatrixVector(this.camera_base_matrix, [-x, 0, 0]); |
||||
rot_vec[1] = -y; |
||||
} |
||||
if (this.flymode && this.control_mode == "cam") { |
||||
// When flymode is enabled, we apply all rotations to the main_node
|
||||
// so that the cam can move toward the direction it is pointing to
|
||||
this.main_drive.turn(rot_vec[0], rot_vec[1], 0); |
||||
this.pitch_drive.turn(0, 0, 0); |
||||
} else { |
||||
this.main_drive.turn(0, rot_vec[1], 0); |
||||
this.pitch_drive.turn(rot_vec[0], 0, rot_vec[2]); |
||||
} |
||||
} |
||||
|
||||
elev(y) { |
||||
this.main_drive.move([0, y, 0]); |
||||
} |
||||
|
||||
set_flymode(v) { |
||||
// When enabled, the camera can move toward any direction it faces.
|
||||
// When disabled, it should not move on the world y axis ("walk mode") unless explicitely sending move messages with non-null Y component.
|
||||
if (v == 1) { |
||||
this.flymode = true; |
||||
} else { |
||||
this.flymode = false; |
||||
} |
||||
this.apply_flymode(); |
||||
} |
||||
|
||||
apply_flymode() { |
||||
if (this.control_mode == "cam") { |
||||
if (this.flymode) { |
||||
// When enabling flymode, we move all rotations to main_node
|
||||
this.main_node.rotatexyz = [ |
||||
this.pitch_node.rotatexyz[0], |
||||
this.main_node.rotatexyz[1], |
||||
0, |
||||
]; |
||||
this.pitch_node.anim_reset(); |
||||
this.flymode_applyed = true; |
||||
} else if (this.flymode_applyed) { |
||||
// And we revert when disabling
|
||||
let rotatexyz = this.main_node.rotatexyz; |
||||
this.pitch_node.anim_reset(); |
||||
this.pitch_node.rotatexyz = [rotatexyz[0], 0, 0]; |
||||
this.main_node.rotatexyz = [0, rotatexyz[1], rotatexyz[2]]; |
||||
this.flymode_applyed = false; |
||||
} |
||||
} |
||||
} |
||||
|
||||
set_drawto(v) { |
||||
this.main_drive.drawto = v; |
||||
this.pitch_drive.drawto = v; |
||||
} |
||||
|
||||
set_ease(v) { |
||||
this.ease = v; |
||||
this.main_drive.ease = this.ease; |
||||
this.pitch_drive.ease = this.ease; |
||||
} |
||||
|
||||
reset() { |
||||
this.main_node.anim_reset(); |
||||
this.pitch_node.anim_reset(); |
||||
} |
||||
|
||||
resync() { |
||||
if (this.target.name != "") { |
||||
this.control(this.target.name, this.control_mode); |
||||
} |
||||
} |
||||
|
||||
destroy() { |
||||
if (this.target.name != "") { |
||||
this.target.send("anim"); |
||||
this.target.send("transform", this.pitch_node.worldtransform); |
||||
} |
||||
this.main_node.freepeer(); |
||||
this.main_drive.freepeer(); |
||||
this.pitch_node.freepeer(); |
||||
this.pitch_drive.freepeer(); |
||||
this.target.freepeer(); |
||||
} |
||||
} |
||||
|
||||
loadbang(); |
||||
|
||||
// Select the object to use as basis for view-space transforms
|
||||
function camera(name) { |
||||
ctlr.camera(name); |
||||
camera_node = ctlr.camera_node.name; |
||||
} |
||||
|
||||
// Select which object to control in view-space
|
||||
function control(obj_name) { |
||||
ctlr.control(obj_name, "obj"); |
||||
if (show_bounds) { |
||||
do_show_bounds(0); |
||||
} |
||||
controlled_gl_objects = []; |
||||
get_gl_obj_controlled_by_ctlr(); |
||||
if (show_bounds) { |
||||
do_show_bounds(show_bounds); |
||||
} |
||||
} |
||||
|
||||
// Select which camera to control
|
||||
function control_camera(obj_name) { |
||||
if (obj_name !== undefined) { |
||||
ctlr.control(obj_name, "cam"); |
||||
do_show_bounds(0); |
||||
} |
||||
} |
||||
|
||||
// Output from the second outlet a list of all controllable jit.gl and jit.anim.node objects in the entire patcher hierarchy
|
||||
function controllable() { |
||||
controllable_objects = []; |
||||
top_level_patcher.applydeepif( |
||||
add_to_controllable_list, |
||||
is_controllable_applydeepif |
||||
); |
||||
outlet(1, "controllable"); |
||||
for (let obj of controllable_objects) { |
||||
outlet(1, obj.maxclass, obj.getattr("name")); |
||||
} |
||||
outlet(1, "done"); |
||||
} |
||||
|
||||
function add_to_controllable_list(obj) { |
||||
controllable_objects.push(obj); |
||||
} |
||||
add_to_controllable_list.local = 1; |
||||
|
||||
function is_controllable_applydeepif(obj) { |
||||
return ( |
||||
obj.maxclass == "jit.anim.node" || |
||||
is_controllable(obj.maxclass.replaceAll(".", "_")) |
||||
); |
||||
} |
||||
is_controllable_applydeepif.local = 1; |
||||
|
||||
function resync() { |
||||
ctlr.resync(); |
||||
} |
||||
|
||||
function reset() { |
||||
ctlr.reset(); |
||||
} |
||||
|
||||
function move(x, y, z) { |
||||
ctlr.move(x, y, z); |
||||
} |
||||
|
||||
function turn(x, y, z) { |
||||
ctlr.turn(x, y, z); |
||||
} |
||||
|
||||
function elev(v) { |
||||
ctlr.elev(v); |
||||
} |
||||
|
||||
function set_ease(v) { |
||||
ctlr.set_ease(v); |
||||
ease = v; |
||||
} |
||||
|
||||
function set_flymode(v) { |
||||
flymode = v == undefined ? !flymode : v == 1; // If flymode with no argument is provided, act as a toggle
|
||||
ctlr.set_flymode(flymode); |
||||
} |
||||
|
||||
function set_show_bounds(v) { |
||||
show_bounds = v == true; |
||||
// ctlr.show_bounds = show_bounds;
|
||||
do_show_bounds(show_bounds); |
||||
} |
||||
|
||||
function do_show_bounds(v) { |
||||
const objects_without_bounds = [ |
||||
"jit.gl.sketch", |
||||
"jit.gl.skybox", |
||||
"jit.gl.camera", |
||||
]; |
||||
for (const obj of controlled_gl_objects) { |
||||
if (!objects_without_bounds.includes(obj.maxclass)) { |
||||
obj.setattr("drawbounds", v); |
||||
} |
||||
} |
||||
} |
||||
do_show_bounds.local = 1; |
||||
|
||||
function bang() { |
||||
outlet(0, ctlr.pitch_node.worldtransform); |
||||
} |
||||
|
||||
function loadbang() { |
||||
if (!top_level_patcher) { |
||||
top_level_patcher = get_top_level_patcher(); |
||||
ctlr = new Controller(drawto); |
||||
} |
||||
} |
||||
|
||||
function notifydeleted() { |
||||
ctlr.destroy(); |
||||
implicit_tracker.freepeer(); |
||||
} |
||||
|
||||
/////////////////////////////////////////////
|
||||
// HELPER METHODS
|
||||
/////////////////////////////////////////////
|
||||
|
||||
function get_anim_node(jit_obj_name, ctlr_node) { |
||||
const proxy = new JitterObject("jit.proxy"); |
||||
proxy.name = jit_obj_name; |
||||
const proxy_class = proxy.class; |
||||
proxy.freepeer(); |
||||
if (proxy_class == "jit_anim_node") { |
||||
return jit_obj_name; |
||||
} else if (proxy_class != "" && is_controllable(proxy_class)) { |
||||
const context_anim = get_context_anim(jit_obj_name); |
||||
return get_top_level_anim_node(jit_obj_name, context_anim, ctlr_node); |
||||
} |
||||
return; |
||||
} |
||||
get_anim_node.local = 1; |
||||
|
||||
function is_controllable(obj_class) { |
||||
// Using jit.proxy syntax (ie 'jit_gl_mesh' and not 'jit.gl.mesh')
|
||||
return ANIMABLE_GL_OBJECTS.includes(obj_class); |
||||
} |
||||
is_controllable.local = 1; |
||||
|
||||
function get_context_anim(jit_obj_name) { |
||||
// Get the first implicit jit.anim.node level for the object's rendering context
|
||||
const proxy = new JitterObject("jit.proxy"); |
||||
proxy.name = jit_obj_name; |
||||
proxy.name = proxy.send("getdrawto"); |
||||
const context_name = proxy.send("getanim"); |
||||
proxy.freepeer(); |
||||
return context_name; |
||||
} |
||||
get_context_anim.local = 1; |
||||
|
||||
function get_top_level_anim_node(jit_obj_name, context_anim, ctlr_node) { |
||||
// Recursively get the top level jit.anim.node before reaching context_anim, or no anim.node, or this.pitch_node (if trying to controller currently controlled object)
|
||||
const proxy = new JitterObject("jit.proxy"); |
||||
proxy.name = jit_obj_name; |
||||
const parent_name = proxy.send("getanim").toString(); |
||||
proxy.freepeer(); |
||||
if ( |
||||
parent_name == "" || |
||||
parent_name == context_anim || |
||||
parent_name == ctlr_node |
||||
) { |
||||
return jit_obj_name; |
||||
} else { |
||||
return get_top_level_anim_node(parent_name, context_anim, ctlr_node); |
||||
} |
||||
} |
||||
get_top_level_anim_node.local = 1; |
||||
|
||||
function get_top_level_patcher() { |
||||
let prev = 0; |
||||
let owner = this.patcher.box; |
||||
while (owner) { |
||||
prev = owner; |
||||
owner = owner.patcher.box; |
||||
} |
||||
return prev ? prev.patcher : this.patcher; |
||||
} |
||||
get_top_level_patcher.local = 1; |
||||
|
||||
// Get gl object(s) controlled by ctlr (for the sole purpose of drawing bounds of selected object)
|
||||
function get_gl_obj_controlled_by_ctlr() { |
||||
top_level_patcher.applydeepif( |
||||
add_to_controlled_obj_list, |
||||
is_controlled_by_ctlr |
||||
); |
||||
} |
||||
get_gl_obj_controlled_by_ctlr.local = 1; |
||||
|
||||
function is_controlled_by_ctlr(obj) { |
||||
if (is_controllable(obj.maxclass.replaceAll(".", "_"))) { |
||||
const obj_anim = obj.getattr("anim"); |
||||
if (obj_anim == ctlr.pitch_node.name) { |
||||
return true; |
||||
} else if (obj_anim != "") { |
||||
const proxy = new JitterObject("jit.proxy"); |
||||
proxy.name = obj_anim; |
||||
const parent_name = proxy.send("getanim").toString(); |
||||
proxy.freepeer(); |
||||
return parent_name == ctlr.pitch_node.name; |
||||
} |
||||
} |
||||
return false; |
||||
} |
||||
is_controlled_by_ctlr.local = 1; |
||||
|
||||
function add_to_controlled_obj_list(obj) { |
||||
controlled_gl_objects.push(obj); |
||||
} |
||||
add_to_controlled_obj_list.local = 1; |
||||
|
||||
/////////////////////////////////////////////
|
||||
// MATHS
|
||||
/////////////////////////////////////////////
|
||||
|
||||
function normalize(v) { |
||||
const len = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]); |
||||
if (len === 0) return v; |
||||
return [v[0] / len, v[1] / len, v[2] / len]; |
||||
} |
||||
normalize.local = 1; |
||||
|
||||
function cross(a, b) { |
||||
return [ |
||||
a[1] * b[2] - a[2] * b[1], |
||||
a[2] * b[0] - a[0] * b[2], |
||||
a[0] * b[1] - a[1] * b[0], |
||||
]; |
||||
} |
||||
cross.local = 1; |
||||
|
||||
function dot(a, b) { |
||||
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; |
||||
} |
||||
dot.local = 1; |
||||
|
||||
function multiplyMatrixVector(matrix, vector) { |
||||
return [ |
||||
dot(matrix[0], vector), |
||||
dot(matrix[1], vector), |
||||
dot(matrix[2], vector), |
||||
]; |
||||
} |
||||
multiplyMatrixVector.local = 1; |
||||
|
||||
/////////////////////////////////////////////
|
||||
// GL Context
|
||||
/////////////////////////////////////////////
|
||||
|
||||
let implicitdrawto = ""; |
||||
let explicitdrawto = false; |
||||
const implicit_tracker = new JitterObject("jit_gl_implicit"); |
||||
const implicit_lstnr = new JitterListener( |
||||
implicit_tracker.name, |
||||
implicit_callback |
||||
); |
||||
|
||||
function implicit_callback(event) { |
||||
if (!explicitdrawto && implicitdrawto != implicit_tracker.drawto[0]) { |
||||
// important! drawto is an array so get first element
|
||||
implicitdrawto = implicit_tracker.drawto[0]; |
||||
dosetdrawto(implicitdrawto); |
||||
} |
||||
} |
||||
implicit_callback.local = 1; |
||||
|
||||
function setdrawto(val) { |
||||
explicitdrawto = true; |
||||
dosetdrawto(val); |
||||
} |
||||
setdrawto.local = 1; |
||||
|
||||
function dosetdrawto(arg) { |
||||
if (arg === drawto || !arg) { |
||||
// bounce
|
||||
return; |
||||
} |
||||
|
||||
drawto = arg; |
||||
ctlr.set_drawto(drawto); |
||||
} |
||||
dosetdrawto.local = 1; |
Loading…
Reference in new issue