Compare commits
3 Commits
4e0598e321
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c713d14491 | |||
| 21a7a4a48d | |||
| 33722a6250 |
19
README.md
Normal file
19
README.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# tc.fpcontrol
|
||||||
|
|
||||||
|
Easy first person control for camera and objects in "view space" with gamepad or keyboard in Max.
|
||||||
|
|
||||||
|
This is a small Max package consisting of two objects:
|
||||||
|
|
||||||
|
- [gamepad.drive] a simple abstraction that takes the output of a [gamepad] and converts it to move and turn messages in a typical jit.anim fashion. Left stick to move around, right stick to rotate, triggers to move up/down, featuring scaled radial deadzones for your quirky always-shifting Xbox sticks.
|
||||||
|
|
||||||
|
- [tc.fpcontrol]: a v8 object that:
|
||||||
|
- takes as input move and turn messages, like the ones from [gamepad.drive], but also [jit.anim.drive] (with a small trick), or any other custom source
|
||||||
|
- for controlling objects in camera space: if you move the left stick to the right, your object will go to the right of the screen, regardless of its orientation or the position of the camera. Like what you're generally used to in video games. You can always switch to a more classic object space mode where movements are applied relatively to the objects orientation.
|
||||||
|
- for controlling cameras, either at the same elevation (use triggers to move up/down), either in "flymode" (goes forward in the direction you're pointing to, without elevation lock)
|
||||||
|
- for controlling [jit.anim.node], either single ones, or within a hierarchy, allowing to control multiple objects at once.
|
||||||
|
- featuring a customizable bounds box indicating the target object/jit.anim.node
|
||||||
|
- gives you a list of all controllable objects in your patch. You can populate a menu from that and easily cycle through every object/anim.node to control them one by one.
|
||||||
|
|
||||||
|
## How to install
|
||||||
|
|
||||||
|
[Download](https://github.com/Teufeuleu/tc.fpcontrol/archive/refs/heads/main.zip) this repository, unzip the archive in your Max packages folder, and restart Max.
|
||||||
@@ -9,49 +9,18 @@
|
|||||||
"modernui": 1
|
"modernui": 1
|
||||||
},
|
},
|
||||||
"classnamespace": "box",
|
"classnamespace": "box",
|
||||||
"rect": [ 400.0, 100.0, 967.0, 848.0 ],
|
"rect": [ 34.0, 100.0, 1444.0, 848.0 ],
|
||||||
|
"openrect": [ 0.0, 0.0, 1181.43359375, 950.1171875 ],
|
||||||
|
"openrectmode": 0,
|
||||||
"integercoordinates": 1,
|
"integercoordinates": 1,
|
||||||
"boxes": [
|
"boxes": [
|
||||||
{
|
|
||||||
"box": {
|
|
||||||
"id": "obj-46",
|
|
||||||
"maxclass": "message",
|
|
||||||
"numinlets": 2,
|
|
||||||
"numoutlets": 1,
|
|
||||||
"outlettype": [ "" ],
|
|
||||||
"patching_rect": [ 160.0, 70.0, 48.0, 22.0 ],
|
|
||||||
"text": "fps 120"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"box": {
|
|
||||||
"id": "obj-25",
|
|
||||||
"maxclass": "message",
|
|
||||||
"numinlets": 2,
|
|
||||||
"numoutlets": 1,
|
|
||||||
"outlettype": [ "" ],
|
|
||||||
"patching_rect": [ 287.0, 86.0, 107.0, 22.0 ],
|
|
||||||
"text": "sendnode getanim"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"box": {
|
|
||||||
"id": "obj-22",
|
|
||||||
"maxclass": "message",
|
|
||||||
"numinlets": 2,
|
|
||||||
"numoutlets": 1,
|
|
||||||
"outlettype": [ "" ],
|
|
||||||
"patching_rect": [ 203.0, 78.0, 41.0, 22.0 ],
|
|
||||||
"text": "fps 30"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"box": {
|
"box": {
|
||||||
"id": "obj-21",
|
"id": "obj-21",
|
||||||
"maxclass": "newobj",
|
"maxclass": "newobj",
|
||||||
"numinlets": 1,
|
"numinlets": 1,
|
||||||
"numoutlets": 0,
|
"numoutlets": 0,
|
||||||
"patching_rect": [ 1408.0, 188.0, 88.0, 22.0 ],
|
"patching_rect": [ 935.0, 222.0, 88.0, 22.0 ],
|
||||||
"text": "s object_select"
|
"text": "s object_select"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -83,7 +52,7 @@
|
|||||||
"maxclass": "newobj",
|
"maxclass": "newobj",
|
||||||
"numinlets": 1,
|
"numinlets": 1,
|
||||||
"numoutlets": 0,
|
"numoutlets": 0,
|
||||||
"patching_rect": [ 1312.0, 188.0, 93.0, 22.0 ],
|
"patching_rect": [ 839.0, 222.0, 93.0, 22.0 ],
|
||||||
"text": "s to_tc.fpcontrol"
|
"text": "s to_tc.fpcontrol"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -893,7 +862,7 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"patching_rect": [ 1312.0, 155.0, 115.0, 22.0 ],
|
"patching_rect": [ 839.0, 189.0, 115.0, 22.0 ],
|
||||||
"text": "p gamepad_buttons"
|
"text": "p gamepad_buttons"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1192,7 +1161,7 @@
|
|||||||
"modernui": 1
|
"modernui": 1
|
||||||
},
|
},
|
||||||
"classnamespace": "box",
|
"classnamespace": "box",
|
||||||
"rect": [ 573.0, 140.0, 866.0, 755.0 ],
|
"rect": [ 746.0, 143.0, 866.0, 755.0 ],
|
||||||
"integercoordinates": 1,
|
"integercoordinates": 1,
|
||||||
"boxes": [
|
"boxes": [
|
||||||
{
|
{
|
||||||
@@ -1974,7 +1943,7 @@
|
|||||||
"maxclass": "comment",
|
"maxclass": "comment",
|
||||||
"numinlets": 1,
|
"numinlets": 1,
|
||||||
"numoutlets": 0,
|
"numoutlets": 0,
|
||||||
"patching_rect": [ 36.0, 654.0, 257.0, 66.0 ],
|
"patching_rect": [ 61.0, 658.0, 257.0, 66.0 ],
|
||||||
"text": "'control' outputs the given object name to be controlled (jit.gl or jit.anim.node), followed by the actual one being controlled (jit.anim.node)"
|
"text": "'control' outputs the given object name to be controlled (jit.gl or jit.anim.node), followed by the actual one being controlled (jit.anim.node)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2511,11 +2480,12 @@
|
|||||||
"box": {
|
"box": {
|
||||||
"color": [ 1.0, 0.612, 0.0, 1.0 ],
|
"color": [ 1.0, 0.612, 0.0, 1.0 ],
|
||||||
"id": "obj-189",
|
"id": "obj-189",
|
||||||
|
"linecount": 6,
|
||||||
"maxclass": "newobj",
|
"maxclass": "newobj",
|
||||||
"numinlets": 1,
|
"numinlets": 1,
|
||||||
"numoutlets": 2,
|
"numoutlets": 2,
|
||||||
"outlettype": [ "", "" ],
|
"outlettype": [ "", "" ],
|
||||||
"patching_rect": [ 720.0, 121.0, 611.0, 22.0 ],
|
"patching_rect": [ 720.0, 77.0, 138.0, 89.0 ],
|
||||||
"text": "gamepad.drive @l_deadzone 0.15 @r_deadzone 0.15 @l_sensitivity 0.4 @r_sensitivity 3 @trigger_sensitivity 0.4"
|
"text": "gamepad.drive @l_deadzone 0.15 @r_deadzone 0.15 @l_sensitivity 0.4 @r_sensitivity 3 @trigger_sensitivity 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2579,12 +2549,14 @@
|
|||||||
{
|
{
|
||||||
"box": {
|
"box": {
|
||||||
"dontreplace": 1,
|
"dontreplace": 1,
|
||||||
|
"fontface": 1,
|
||||||
|
"fontsize": 16.0,
|
||||||
"id": "obj-60",
|
"id": "obj-60",
|
||||||
"maxclass": "message",
|
"maxclass": "message",
|
||||||
"numinlets": 2,
|
"numinlets": 2,
|
||||||
"numoutlets": 1,
|
"numoutlets": 1,
|
||||||
"outlettype": [ "" ],
|
"outlettype": [ "" ],
|
||||||
"patching_rect": [ 53.0, 630.0, 222.0, 22.0 ]
|
"patching_rect": [ 53.0, 630.0, 415.0, 26.0 ]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -2618,7 +2590,7 @@
|
|||||||
"numinlets": 1,
|
"numinlets": 1,
|
||||||
"numoutlets": 3,
|
"numoutlets": 3,
|
||||||
"outlettype": [ "", "", "" ],
|
"outlettype": [ "", "", "" ],
|
||||||
"patching_rect": [ 720.0, 81.0, 59.0, 22.0 ],
|
"patching_rect": [ 720.0, 36.0, 59.0, 22.0 ],
|
||||||
"text": "gamepad"
|
"text": "gamepad"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2781,18 +2753,6 @@
|
|||||||
"source": [ "obj-202", 0 ]
|
"source": [ "obj-202", 0 ]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"patchline": {
|
|
||||||
"destination": [ "obj-3", 0 ],
|
|
||||||
"source": [ "obj-22", 0 ]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"patchline": {
|
|
||||||
"destination": [ "obj-3", 0 ],
|
|
||||||
"source": [ "obj-25", 0 ]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"patchline": {
|
"patchline": {
|
||||||
"destination": [ "obj-56", 0 ],
|
"destination": [ "obj-56", 0 ],
|
||||||
@@ -2836,12 +2796,6 @@
|
|||||||
"source": [ "obj-4", 0 ]
|
"source": [ "obj-4", 0 ]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"patchline": {
|
|
||||||
"destination": [ "obj-3", 0 ],
|
|
||||||
"source": [ "obj-46", 0 ]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"patchline": {
|
"patchline": {
|
||||||
"destination": [ "obj-109", 0 ],
|
"destination": [ "obj-109", 0 ],
|
||||||
@@ -19,13 +19,24 @@ GNU General Public License for more details:
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
autowatch = 1;
|
autowatch = 1;
|
||||||
|
|
||||||
outlets = 2;
|
outlets = 2;
|
||||||
|
|
||||||
|
const g = new Global("tc.fpcontrol");
|
||||||
|
const instance = (() => {
|
||||||
|
if (g.instance_count) {
|
||||||
|
g.instance_count += 1
|
||||||
|
} else {
|
||||||
|
g.instance_count = 1;
|
||||||
|
}
|
||||||
|
return g.instance_count
|
||||||
|
})();
|
||||||
|
|
||||||
let world_bangs = false; // is the world banging?
|
let world_bangs = false; // is the world banging?
|
||||||
const ctx_finder = require("implicit.context.js");
|
const ctx_finder = require("implicit.context.js");
|
||||||
ctx_finder.register_drawto(this, dosetdrawto, set_root);
|
ctx_finder.register_drawto(this, dosetdrawto, set_root);
|
||||||
|
|
||||||
|
include("FF_Vector.js");
|
||||||
|
|
||||||
var ease = 0.1;
|
var ease = 0.1;
|
||||||
declareattribute({
|
declareattribute({
|
||||||
name: "ease",
|
name: "ease",
|
||||||
@@ -105,14 +116,14 @@ class Controller {
|
|||||||
|
|
||||||
this.root_anim_node = null; // The implicit drawto context anim node
|
this.root_anim_node = null; // The implicit drawto context anim node
|
||||||
|
|
||||||
this.world_up = [0, 1, 0];
|
this.world_up = new FF_Vector(0, 1, 0);
|
||||||
this.camera_target = {
|
this.camera_target = {
|
||||||
name: '',
|
name: '',
|
||||||
class: '',
|
class: '',
|
||||||
anim: '',
|
anim: '',
|
||||||
}
|
}
|
||||||
this.camera_node_implicit = new JitterObject("jit.anim.node"); // Used if camera isn't attached to an anim.node (using the pre-made anim node of a jit.gl.camera doesn't work for some reason, so we use our own "implicit" one)
|
this.camera_node_implicit = new JitterObject("jit.anim.node"); // Used if camera isn't attached to an anim.node (using the pre-made anim node of a jit.gl.camera doesn't work for some reason, so we use our own "implicit" one)
|
||||||
this.camera_node_implicit.name = "camera_node_implicit";
|
this.camera_node_implicit.name = "camera_node_implicit_" + instance;
|
||||||
this.camera_node = new JitterObject("jit.proxy"); // Either proxy of a jit.anim.node defined by user, or of this.camera_node_implicit
|
this.camera_node = new JitterObject("jit.proxy"); // Either proxy of a jit.anim.node defined by user, or of this.camera_node_implicit
|
||||||
this.cam_direction, this.cam_up, this.cam_right;
|
this.cam_direction, this.cam_up, this.cam_right;
|
||||||
|
|
||||||
@@ -133,7 +144,7 @@ class Controller {
|
|||||||
|
|
||||||
// pitch_node used with pitch_drive for pitch rotation (over the objects local x axis)
|
// 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 = new JitterObject("jit.anim.node");
|
||||||
this.pitch_node.name = "fp_pitch_node";
|
this.pitch_node.name = "fp_pitch_node_" + instance;
|
||||||
this.pitch_node.anim = this.main_node.name;
|
this.pitch_node.anim = this.main_node.name;
|
||||||
this.pitch_node.turnmode = "parent";
|
this.pitch_node.turnmode = "parent";
|
||||||
this.pitch_node.movemode = "parent";
|
this.pitch_node.movemode = "parent";
|
||||||
@@ -168,7 +179,7 @@ class Controller {
|
|||||||
|
|
||||||
// Taking control over a new target
|
// Taking control over a new target
|
||||||
control(obj_name, obj_type) {
|
control(obj_name, obj_type) {
|
||||||
if (!this.camera_target.name) {
|
if (obj_name && !this.camera_target.name) {
|
||||||
error("No camera defined. Cannot control object.\n");
|
error("No camera defined. Cannot control object.\n");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -348,11 +359,15 @@ class Controller {
|
|||||||
|
|
||||||
get_cam_base_matrix() {
|
get_cam_base_matrix() {
|
||||||
if (this.camera_node.name != "") {
|
if (this.camera_node.name != "") {
|
||||||
this.cam_direction = this.camera_node.send("getdirection");
|
this.cam_direction = new FF_Vector(this.camera_node.send("getdirection"));
|
||||||
this.cam_direction[1] = 0;
|
this.cam_direction.y = 0;
|
||||||
this.cam_direction = normalize(this.cam_direction);
|
this.cam_direction.normalize();
|
||||||
this.cam_right = normalize(cross(this.cam_direction, this.world_up));
|
this.cam_right = this.cam_direction.crossNew(this.world_up);
|
||||||
this.cam_up = normalize(cross(this.cam_right, this.cam_direction));
|
this.cam_right.normalize();
|
||||||
|
// this.cam_right = normalize(cross(this.cam_direction, this.world_up));
|
||||||
|
this.cam_up = this.cam_right.crossNew(this.cam_direction);
|
||||||
|
this.cam_up.normalize();
|
||||||
|
// this.cam_up = normalize(cross(this.cam_right, this.cam_direction));
|
||||||
this.camera_base_matrix = [
|
this.camera_base_matrix = [
|
||||||
this.cam_right,
|
this.cam_right,
|
||||||
this.cam_up,
|
this.cam_up,
|
||||||
@@ -371,7 +386,7 @@ class Controller {
|
|||||||
x *= -1;
|
x *= -1;
|
||||||
z *= -1;
|
z *= -1;
|
||||||
}
|
}
|
||||||
translat_vec = multiplyMatrixVector(this.camera_base_matrix, [x, 0, -z]);
|
translat_vec = multiplyMatrixVector(this.camera_base_matrix, new FF_Vector(x, 0, -z));
|
||||||
}
|
}
|
||||||
this.main_drive.move(translat_vec[0], y, translat_vec[2]);
|
this.main_drive.move(translat_vec[0], y, translat_vec[2]);
|
||||||
}
|
}
|
||||||
@@ -380,7 +395,7 @@ class Controller {
|
|||||||
let rot_vec = [x, y, z];
|
let rot_vec = [x, y, z];
|
||||||
if (this.control_obj_type == "obj") {
|
if (this.control_obj_type == "obj") {
|
||||||
this.get_cam_base_matrix();
|
this.get_cam_base_matrix();
|
||||||
rot_vec = multiplyMatrixVector(this.camera_base_matrix, [-x, 0, 0]);
|
rot_vec = multiplyMatrixVector(this.camera_base_matrix, new FF_Vector(-x, 0, 0));
|
||||||
rot_vec[1] = -y;
|
rot_vec[1] = -y;
|
||||||
}
|
}
|
||||||
if (this.flymode && this.control_obj_type == "cam") {
|
if (this.flymode && this.control_obj_type == "cam") {
|
||||||
@@ -619,6 +634,8 @@ function loadbang() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function notifydeleted() {
|
function notifydeleted() {
|
||||||
|
control();
|
||||||
|
set_camera();
|
||||||
ctlr.destroy();
|
ctlr.destroy();
|
||||||
ctx_finder.dispose();
|
ctx_finder.dispose();
|
||||||
if (ctx_lstnr) ctx_lstnr.freepeer();
|
if (ctx_lstnr) ctx_lstnr.freepeer();
|
||||||
@@ -650,32 +667,11 @@ get_top_level_patcher.local = 1;
|
|||||||
// MATHS
|
// 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) {
|
function multiplyMatrixVector(matrix, vector) {
|
||||||
return [
|
return [
|
||||||
dot(matrix[0], vector),
|
matrix[0].dot(vector),
|
||||||
dot(matrix[1], vector),
|
matrix[1].dot(vector),
|
||||||
dot(matrix[2], vector),
|
matrix[2].dot(vector),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
multiplyMatrixVector.local = 1;
|
multiplyMatrixVector.local = 1;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name" : "tc.jit.anim.control",
|
"name" : "tc.fpcontrol",
|
||||||
"displayname" : "",
|
"displayname" : "",
|
||||||
"version" : "0.0.1",
|
"version" : "0.0.1",
|
||||||
"author" : "Théophile Clet",
|
"author" : "Théophile Clet",
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
"website" : "",
|
"website" : "",
|
||||||
"extends" : "",
|
"extends" : "",
|
||||||
"extensible" : 0,
|
"extensible" : 0,
|
||||||
"max_version_min" : "9.1.0",
|
"max_version_min" : "9.1.4",
|
||||||
"max_version_max" : "none",
|
"max_version_max" : "none",
|
||||||
"os" : {
|
"os" : {
|
||||||
"macintosh" : {
|
"macintosh" : {
|
||||||
|
|||||||
Reference in New Issue
Block a user