OnChangeCallback for Vectors?

Hi guys,

I have a need to track changes in vectors in my code to keep certain things in sync between threads. I’m currently using my own Vector and Matrix implementations to emit “change” events whenever something changes, but I recently discovered that Quaternion already has functionality for this.

Is there already something on the roadmap to also have this functionality in Vector and Matrix classes, or is there any specific reason why it hasn’t been implemented (yet)?

If there are no objections to also implement the same functionality in Vector and maybe matrices (vectors are more urgent for my own use-case) I could write up a PR if there are no objections.

1 Like

_onChange in Quaternion is an internal method to keep Euler .rotation and .quaternion in sync, wouldn’t use it externally (see here, similarly for Euler._onChange.)

This issue may be of some help (kinda also an answer to the question from the title / PR.)

2 Likes

Hmm yeah, I am using a similar approach right now. However, the overhead of an optional callback, especially when defaulting it to void(0) is pretty much zero. Using a Proxy on the other hand, although being “native”, does add a bit more overhead :thinking:

Since the use of setter-accessors is pretty standard right now, I would love to see something like this in the default Vector* classes in 3js:

class Vector2
{
    onChangeCallback = void(0);

    #x = 0;
    #y = 0;

    set x(value)
    {
        this.#x = value;
        this.onChangeCallback();
    }

    set y(value)
    {
        this.#y = value;
        this.onChangeCallback();
    }
}

It shouldn’t really matter if the value is the same. A value is being set here, so regardless if the old value is the same, it would prevent having an extra if-statement being executed every time.

I may be wrong, but weren’t setters / getters a bit waaaay slower than direct access? :eyes:

(Setters / getters get ~50-75% perf result here and 25-51% here, compared to direct access.)

2 Likes

Oh, big oooff :exploding_head:

Yeah maybe that isn’t worth the tradeoff…

The test is not realistic, it runs for millions ops/sec, that’s not your typical three scene, generally you’ll have far less than a thousand Object3Ds, and when you’ll get to those numbers, vectors would be the least of your concerns, you’d better switch to instanced meshes or use some BufferAtrribute or any other optimization means.

Object3D is already using accessors with callbacks for rotation and quaternion. A good argument for setting vectors with callbacks everywhere! is the matrixAutoUpdate. Right now the default behavior, is to traverse the scene graph and update every matrix on its way (wish is convenient, and OK for most scenarios), setting the matrixAutoUpdate to true only when one of its vector is updated can boost performances and maybe a better option?

1 Like

JSPerf is a measurement tool, not a simulator :sweat_smile: For statistical reasons, you’ll get a more accurate measurement on 5m samples than on 40-50.

The message isn’t “you can only run 2.500.000 getters vs 5.000.000 direct accessors within a single frame” - rather “on average, getter / setter is 25-50% slower than direct access” :smiling_face_with_tear:

I’m not saying setters / getters bad - I’d just measure the influence on the FPS on projects like FreeCIV, gpu-pathtracer, and on VR projects - these things will likely be more prone to losing FPS than a regular portfolio website with a single Blendor model.

4 Likes

@mjurczyk you got a point … Now that I think about it, adding a callback for each property x, y, z, w, may trigger the callback multiple times depending on how you’re updating the vector (2 to 4 times from Vector2 to Quaternion), wish may not be ideal, and yeah it will impact performance. Plus the typescript #, will compile to a weakmap adding another overhead (can be mitigated by using simple props _x).

As you said, it’s not bad but use with caution.

Another way around, is to override the updateMatrix method, and add the callback. the drawback is you’ll need set the matrixAutoUpdate = false and manually manage it your self. Again it’s not ideal and you can’t know wish vector has triggered the event unless you do some comparisons.

I’m guessing if there was a straightforward solution, it would’ve been already implemented. But hey, where is the fun if you don’t hack your way around :stuck_out_tongue:

We hacked three once with that functionality. By overwriting prototypes, adding callbacks to most O3D attributes, but it turned into a scalability nightmare. Race conditions mostly. It got so bad the product we were working on had to be rebooted from scratch.

Keeping things in sync is usually inferior to having a state model which everything else reflects. The #1 anti pattern in app infra is that the view is used as a state model, and others views syncing to it.

Maybe there are better ways I guess is what I’m trying to say.

2 Likes

The real nightmare is the worldMatrix. I thought traversing the graph each frame just to update a single Object3D was wasteful, I realized how naive I was, when I experienced a real physical brain infinite loop… Introduce a lookAt to the mix and you got yourself a recipe for disaster.

I agree that using the view as “source of truth” to keep other systems in sync with is wasteful.

To give a little bit of insight in my own use-case: I have an ECS system in place that runs over multiple threads (workers). Components are automatically kept in sync between workers and a scheduler runs between them that ensures that “jobs” are run in parallel for a single tick (not frame). The tick-rate is different than framerate. Every “changed” component is marked as “dirty” when modified and is sent to the worker threads to keep everything in sync.

The renderer thread simply consists of a bunch of “systems” that acts on the current state of entities to reflect what is drawn on the screen. For example, entities can have a traditional “Transform” component, as well as a mesh-renderer or voxel component, etc. and acts on it. The main thread acts as a “master” thread with write-access that can influence the state of components on entities that is being synced to the workers that solely have read-access to these components to keep things simple (or until I have a need to do this bi-directionally, but i haven’t had the use-case for that yet).

The problem is this: I currently have two “Math” classes that are being used. THREE’s Vector/Matrix classes, as well as my own for the ECS. To keep things consistent, I would really like to have one consistent method of working with this. My transform component contains my own Vector classes, which I can’t directly feed into THREE, meaning I have to copy this data over. This is kind of wasteful.

1 Like

I love event-driven systems. They have their own drawbacks as pointed out here.

Personally, I have my own Vector3 implementation, same with Quaternion and the rest.

For all operations that mutate Vector3 I require them to use .set method. If the user modifies fields directly - that’s on them. You get mostly best of both worlds this way, good read performance and fairly low write overhead.

Here’s the general shape of it:

class Vector3{
	onChanged = new Signal()

	x = 0
	y = 0
	z = 0

	set(x, y, z){
		const _x = this.x;
		const _y = this.y;
		const _z = this.z;

		this.x = x;
		this.y = y;
		this.z = z;
		
		this.onChanged.dispatch(
			x, y, z,
			_x, _y, _z
		);
	}
	
	//...
	
	add(other){
		this.set(
			this.x + other.x,
			this.y + other.y,
			this.z + other.z
		);
	}
}

The Signal is just an event bus abstraction that you can subscribe to.

This is a compromise solution. Setters and getters are not free, function calls can be expensive etc. So I’m requiring the user to adhere to the contract of not modifying fields direclty and always using the .set method. Not fool-proof, but I’m pretty happy with it.

Oh yeah, I added TypeScript declarations (Vector3.d.ts) as well that make fields readonly like so:

class Vector3{
    readonly x: number
    readonly y: number
    readonly z: number

    //...
}

This helps warn users somewhat.

4 Likes