planeRotationGizmo.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. import { Observable } from "../Misc/observable.js";
  2. import { Quaternion, Matrix, Vector3, TmpVectors } from "../Maths/math.vector.js";
  3. import { Color3 } from "../Maths/math.color.js";
  4. import "../Meshes/Builders/linesBuilder.js";
  5. import { Mesh } from "../Meshes/mesh.js";
  6. import { PointerDragBehavior } from "../Behaviors/Meshes/pointerDragBehavior.js";
  7. import { Gizmo } from "./gizmo.js";
  8. import { UtilityLayerRenderer } from "../Rendering/utilityLayerRenderer.js";
  9. import { StandardMaterial } from "../Materials/standardMaterial.js";
  10. import { ShaderMaterial } from "../Materials/shaderMaterial.js";
  11. import { Effect } from "../Materials/effect.js";
  12. import { CreatePlane } from "../Meshes/Builders/planeBuilder.js";
  13. import { CreateTorus } from "../Meshes/Builders/torusBuilder.js";
  14. import { Epsilon } from "../Maths/math.constants.js";
  15. import { Logger } from "../Misc/logger.js";
  16. /**
  17. * Single plane rotation gizmo
  18. */
  19. export class PlaneRotationGizmo extends Gizmo {
  20. /** Default material used to render when gizmo is not disabled or hovered */
  21. get coloredMaterial() {
  22. return this._coloredMaterial;
  23. }
  24. /** Material used to render when gizmo is hovered with mouse */
  25. get hoverMaterial() {
  26. return this._hoverMaterial;
  27. }
  28. /** Color used to render the drag angle sector when gizmo is rotated with mouse */
  29. set rotationColor(color) {
  30. this._rotationShaderMaterial.setColor3("rotationColor", color);
  31. }
  32. /** Material used to render when gizmo is disabled. typically grey. */
  33. get disableMaterial() {
  34. return this._disableMaterial;
  35. }
  36. /**
  37. * Creates a PlaneRotationGizmo
  38. * @param planeNormal The normal of the plane which the gizmo will be able to rotate on
  39. * @param color The color of the gizmo
  40. * @param gizmoLayer The utility layer the gizmo will be added to
  41. * @param tessellation Amount of tessellation to be used when creating rotation circles
  42. * @param parent
  43. * @param useEulerRotation Use and update Euler angle instead of quaternion
  44. * @param thickness display gizmo axis thickness
  45. * @param hoverColor The color of the gizmo when hovering over and dragging
  46. * @param disableColor The Color of the gizmo when its disabled
  47. */
  48. constructor(planeNormal, color = Color3.Gray(), gizmoLayer = UtilityLayerRenderer.DefaultUtilityLayer, tessellation = 32, parent = null,
  49. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  50. useEulerRotation = false, thickness = 1, hoverColor = Color3.Yellow(), disableColor = Color3.Gray()) {
  51. super(gizmoLayer);
  52. this._pointerObserver = null;
  53. /**
  54. * Rotation distance in radians that the gizmo will snap to (Default: 0)
  55. */
  56. this.snapDistance = 0;
  57. /**
  58. * Event that fires each time the gizmo snaps to a new location.
  59. * * snapDistance is the change in distance
  60. */
  61. this.onSnapObservable = new Observable();
  62. /**
  63. * Accumulated relative angle value for rotation on the axis. Reset to 0 when a dragStart occurs
  64. */
  65. this.angle = 0;
  66. /**
  67. * Custom sensitivity value for the drag strength
  68. */
  69. this.sensitivity = 1;
  70. this._isEnabled = true;
  71. this._parent = null;
  72. this._dragging = false;
  73. this._angles = new Vector3();
  74. this._parent = parent;
  75. // Create Material
  76. this._coloredMaterial = new StandardMaterial("", gizmoLayer.utilityLayerScene);
  77. this._coloredMaterial.diffuseColor = color;
  78. this._coloredMaterial.specularColor = color.subtract(new Color3(0.1, 0.1, 0.1));
  79. this._hoverMaterial = new StandardMaterial("", gizmoLayer.utilityLayerScene);
  80. this._hoverMaterial.diffuseColor = hoverColor;
  81. this._hoverMaterial.specularColor = hoverColor;
  82. this._disableMaterial = new StandardMaterial("", gizmoLayer.utilityLayerScene);
  83. this._disableMaterial.diffuseColor = disableColor;
  84. this._disableMaterial.alpha = 0.4;
  85. // Build mesh on root node
  86. this._gizmoMesh = new Mesh("", gizmoLayer.utilityLayerScene);
  87. const { rotationMesh, collider } = this._createGizmoMesh(this._gizmoMesh, thickness, tessellation);
  88. // Setup Rotation Circle
  89. this._rotationDisplayPlane = CreatePlane("rotationDisplay", {
  90. size: 0.6,
  91. updatable: false,
  92. }, this.gizmoLayer.utilityLayerScene);
  93. this._rotationDisplayPlane.rotation.z = Math.PI * 0.5;
  94. this._rotationDisplayPlane.parent = this._gizmoMesh;
  95. this._rotationDisplayPlane.setEnabled(false);
  96. Effect.ShadersStore["rotationGizmoVertexShader"] = PlaneRotationGizmo._RotationGizmoVertexShader;
  97. Effect.ShadersStore["rotationGizmoFragmentShader"] = PlaneRotationGizmo._RotationGizmoFragmentShader;
  98. this._rotationShaderMaterial = new ShaderMaterial("shader", this.gizmoLayer.utilityLayerScene, {
  99. vertex: "rotationGizmo",
  100. fragment: "rotationGizmo",
  101. }, {
  102. attributes: ["position", "uv"],
  103. uniforms: ["worldViewProjection", "angles", "rotationColor"],
  104. });
  105. this._rotationShaderMaterial.backFaceCulling = false;
  106. this.rotationColor = hoverColor;
  107. this._rotationDisplayPlane.material = this._rotationShaderMaterial;
  108. this._rotationDisplayPlane.visibility = 0.999;
  109. this._gizmoMesh.lookAt(this._rootMesh.position.add(planeNormal));
  110. this._rootMesh.addChild(this._gizmoMesh, Gizmo.PreserveScaling);
  111. this._gizmoMesh.scaling.scaleInPlace(1 / 3);
  112. // Add drag behavior to handle events when the gizmo is dragged
  113. this.dragBehavior = new PointerDragBehavior({ dragPlaneNormal: planeNormal });
  114. this.dragBehavior.moveAttached = false;
  115. this.dragBehavior.maxDragAngle = PlaneRotationGizmo.MaxDragAngle;
  116. this.dragBehavior._useAlternatePickedPointAboveMaxDragAngle = true;
  117. this._rootMesh.addBehavior(this.dragBehavior);
  118. // Closures for drag logic
  119. const lastDragPosition = new Vector3();
  120. const rotationMatrix = new Matrix();
  121. const planeNormalTowardsCamera = new Vector3();
  122. let localPlaneNormalTowardsCamera = new Vector3();
  123. this.dragBehavior.onDragStartObservable.add((e) => {
  124. if (this.attachedNode) {
  125. lastDragPosition.copyFrom(e.dragPlanePoint);
  126. this._rotationDisplayPlane.setEnabled(true);
  127. this._rotationDisplayPlane.getWorldMatrix().invertToRef(rotationMatrix);
  128. Vector3.TransformCoordinatesToRef(e.dragPlanePoint, rotationMatrix, lastDragPosition);
  129. this._angles.x = Math.atan2(lastDragPosition.y, lastDragPosition.x) + Math.PI;
  130. this._angles.y = 0;
  131. this._angles.z = this.updateGizmoRotationToMatchAttachedMesh ? 1 : 0;
  132. this._dragging = true;
  133. lastDragPosition.copyFrom(e.dragPlanePoint);
  134. this._rotationShaderMaterial.setVector3("angles", this._angles);
  135. this.angle = 0;
  136. }
  137. });
  138. this.dragBehavior.onDragEndObservable.add(() => {
  139. this._dragging = false;
  140. this._rotationDisplayPlane.setEnabled(false);
  141. });
  142. const tmpSnapEvent = { snapDistance: 0 };
  143. let currentSnapDragDistance = 0;
  144. const tmpMatrix = new Matrix();
  145. const amountToRotate = new Quaternion();
  146. this.dragBehavior.onDragObservable.add((event) => {
  147. if (this.attachedNode) {
  148. // Calc angle over full 360 degree (https://stackoverflow.com/questions/43493711/the-angle-between-two-3d-vectors-with-a-result-range-0-360)
  149. const nodeScale = new Vector3(1, 1, 1);
  150. const nodeQuaternion = new Quaternion(0, 0, 0, 1);
  151. const nodeTranslation = new Vector3(0, 0, 0);
  152. this.attachedNode.getWorldMatrix().decompose(nodeScale, nodeQuaternion, nodeTranslation);
  153. // uniform scaling of absolute value of components
  154. // (-1,1,1) is uniform but (1,1.001,1) is not
  155. const uniformScaling = Math.abs(Math.abs(nodeScale.x) - Math.abs(nodeScale.y)) <= Epsilon && Math.abs(Math.abs(nodeScale.x) - Math.abs(nodeScale.z)) <= Epsilon;
  156. if (!uniformScaling && this.updateGizmoRotationToMatchAttachedMesh) {
  157. Logger.Warn("Unable to use a rotation gizmo matching mesh rotation with non uniform scaling. Use uniform scaling or set updateGizmoRotationToMatchAttachedMesh to false.");
  158. return;
  159. }
  160. nodeQuaternion.normalize();
  161. const nodeTranslationForOperation = this.updateGizmoPositionToMatchAttachedMesh ? nodeTranslation : this._rootMesh.absolutePosition;
  162. const newVector = event.dragPlanePoint.subtract(nodeTranslationForOperation).normalize();
  163. const originalVector = lastDragPosition.subtract(nodeTranslationForOperation).normalize();
  164. const cross = Vector3.Cross(newVector, originalVector);
  165. const dot = Vector3.Dot(newVector, originalVector);
  166. let angle = Math.atan2(cross.length(), dot) * this.sensitivity;
  167. planeNormalTowardsCamera.copyFrom(planeNormal);
  168. localPlaneNormalTowardsCamera.copyFrom(planeNormal);
  169. if (this.updateGizmoRotationToMatchAttachedMesh) {
  170. nodeQuaternion.toRotationMatrix(rotationMatrix);
  171. localPlaneNormalTowardsCamera = Vector3.TransformCoordinates(planeNormalTowardsCamera, rotationMatrix);
  172. }
  173. // Flip up vector depending on which side the camera is on
  174. let cameraFlipped = false;
  175. if (gizmoLayer.utilityLayerScene.activeCamera) {
  176. const camVec = gizmoLayer.utilityLayerScene.activeCamera.position.subtract(nodeTranslationForOperation).normalize();
  177. if (Vector3.Dot(camVec, localPlaneNormalTowardsCamera) > 0) {
  178. planeNormalTowardsCamera.scaleInPlace(-1);
  179. localPlaneNormalTowardsCamera.scaleInPlace(-1);
  180. cameraFlipped = true;
  181. }
  182. }
  183. const halfCircleSide = Vector3.Dot(localPlaneNormalTowardsCamera, cross) > 0.0;
  184. if (halfCircleSide) {
  185. angle = -angle;
  186. }
  187. TmpVectors.Vector3[0].set(angle, 0, 0);
  188. if (!this.dragBehavior.validateDrag(TmpVectors.Vector3[0])) {
  189. angle = 0;
  190. }
  191. // Snapping logic
  192. let snapped = false;
  193. if (this.snapDistance != 0) {
  194. currentSnapDragDistance += angle;
  195. if (Math.abs(currentSnapDragDistance) > this.snapDistance) {
  196. let dragSteps = Math.floor(Math.abs(currentSnapDragDistance) / this.snapDistance);
  197. if (currentSnapDragDistance < 0) {
  198. dragSteps *= -1;
  199. }
  200. currentSnapDragDistance = currentSnapDragDistance % this.snapDistance;
  201. angle = this.snapDistance * dragSteps;
  202. snapped = true;
  203. }
  204. else {
  205. angle = 0;
  206. }
  207. }
  208. // Convert angle and axis to quaternion (http://www.euclideanspace.com/maths/geometry/rotations/conversions/angleToQuaternion/index.htm)
  209. const quaternionCoefficient = Math.sin(angle / 2);
  210. amountToRotate.set(planeNormalTowardsCamera.x * quaternionCoefficient, planeNormalTowardsCamera.y * quaternionCoefficient, planeNormalTowardsCamera.z * quaternionCoefficient, Math.cos(angle / 2));
  211. // If the meshes local scale is inverted (eg. loaded gltf file parent with z scale of -1) the rotation needs to be inverted on the y axis
  212. if (tmpMatrix.determinant() > 0) {
  213. const tmpVector = new Vector3();
  214. amountToRotate.toEulerAnglesToRef(tmpVector);
  215. Quaternion.RotationYawPitchRollToRef(tmpVector.y, -tmpVector.x, -tmpVector.z, amountToRotate);
  216. }
  217. if (this.updateGizmoRotationToMatchAttachedMesh) {
  218. // Rotate selected mesh quaternion over fixed axis
  219. nodeQuaternion.multiplyToRef(amountToRotate, nodeQuaternion);
  220. nodeQuaternion.normalize();
  221. // recompose matrix
  222. Matrix.ComposeToRef(nodeScale, nodeQuaternion, nodeTranslation, this.attachedNode.getWorldMatrix());
  223. }
  224. else {
  225. // Rotate selected mesh quaternion over rotated axis
  226. amountToRotate.toRotationMatrix(TmpVectors.Matrix[0]);
  227. const translation = this.attachedNode.getWorldMatrix().getTranslation();
  228. this.attachedNode.getWorldMatrix().multiplyToRef(TmpVectors.Matrix[0], this.attachedNode.getWorldMatrix());
  229. this.attachedNode.getWorldMatrix().setTranslation(translation);
  230. }
  231. lastDragPosition.copyFrom(event.dragPlanePoint);
  232. if (snapped) {
  233. tmpSnapEvent.snapDistance = angle;
  234. this.onSnapObservable.notifyObservers(tmpSnapEvent);
  235. }
  236. this._angles.y += angle;
  237. this.angle += cameraFlipped ? -angle : angle;
  238. this._rotationShaderMaterial.setVector3("angles", this._angles);
  239. this._matrixChanged();
  240. }
  241. });
  242. const light = gizmoLayer._getSharedGizmoLight();
  243. light.includedOnlyMeshes = light.includedOnlyMeshes.concat(this._rootMesh.getChildMeshes(false));
  244. const cache = {
  245. colliderMeshes: [collider],
  246. gizmoMeshes: [rotationMesh],
  247. material: this._coloredMaterial,
  248. hoverMaterial: this._hoverMaterial,
  249. disableMaterial: this._disableMaterial,
  250. active: false,
  251. dragBehavior: this.dragBehavior,
  252. };
  253. this._parent?.addToAxisCache(this._gizmoMesh, cache);
  254. this._pointerObserver = gizmoLayer.utilityLayerScene.onPointerObservable.add((pointerInfo) => {
  255. if (this._customMeshSet) {
  256. return;
  257. }
  258. // updating here the maxangle because ondragstart is too late (value already used) and the updated value is not taken into account
  259. this.dragBehavior.maxDragAngle = PlaneRotationGizmo.MaxDragAngle;
  260. this._isHovered = !!(cache.colliderMeshes.indexOf(pointerInfo?.pickInfo?.pickedMesh) != -1);
  261. if (!this._parent) {
  262. const material = cache.dragBehavior.enabled ? (this._isHovered || this._dragging ? this._hoverMaterial : this._coloredMaterial) : this._disableMaterial;
  263. this._setGizmoMeshMaterial(cache.gizmoMeshes, material);
  264. }
  265. });
  266. this.dragBehavior.onEnabledObservable.add((newState) => {
  267. this._setGizmoMeshMaterial(cache.gizmoMeshes, newState ? this._coloredMaterial : this._disableMaterial);
  268. });
  269. }
  270. /**
  271. * @internal
  272. * Create Geometry for Gizmo
  273. * @param parentMesh
  274. * @param thickness
  275. * @param tessellation
  276. * @returns
  277. */
  278. _createGizmoMesh(parentMesh, thickness, tessellation) {
  279. const collider = CreateTorus("ignore", {
  280. diameter: 0.6,
  281. thickness: 0.03 * thickness,
  282. tessellation,
  283. }, this.gizmoLayer.utilityLayerScene);
  284. collider.visibility = 0;
  285. const rotationMesh = CreateTorus("", {
  286. diameter: 0.6,
  287. thickness: 0.005 * thickness,
  288. tessellation,
  289. }, this.gizmoLayer.utilityLayerScene);
  290. rotationMesh.material = this._coloredMaterial;
  291. // Position arrow pointing in its drag axis
  292. rotationMesh.rotation.x = Math.PI / 2;
  293. collider.rotation.x = Math.PI / 2;
  294. parentMesh.addChild(rotationMesh, Gizmo.PreserveScaling);
  295. parentMesh.addChild(collider, Gizmo.PreserveScaling);
  296. return { rotationMesh, collider };
  297. }
  298. _attachedNodeChanged(value) {
  299. if (this.dragBehavior) {
  300. this.dragBehavior.enabled = value ? true : false;
  301. }
  302. }
  303. /**
  304. * If the gizmo is enabled
  305. */
  306. set isEnabled(value) {
  307. this._isEnabled = value;
  308. if (!value) {
  309. this.attachedMesh = null;
  310. }
  311. else {
  312. if (this._parent) {
  313. this.attachedMesh = this._parent.attachedMesh;
  314. }
  315. }
  316. }
  317. get isEnabled() {
  318. return this._isEnabled;
  319. }
  320. /**
  321. * Disposes of the gizmo
  322. */
  323. dispose() {
  324. this.onSnapObservable.clear();
  325. this.gizmoLayer.utilityLayerScene.onPointerObservable.remove(this._pointerObserver);
  326. this.dragBehavior.detach();
  327. if (this._gizmoMesh) {
  328. this._gizmoMesh.dispose();
  329. }
  330. if (this._rotationDisplayPlane) {
  331. this._rotationDisplayPlane.dispose();
  332. }
  333. if (this._rotationShaderMaterial) {
  334. this._rotationShaderMaterial.dispose();
  335. }
  336. [this._coloredMaterial, this._hoverMaterial, this._disableMaterial].forEach((matl) => {
  337. if (matl) {
  338. matl.dispose();
  339. }
  340. });
  341. super.dispose();
  342. }
  343. }
  344. /**
  345. * The maximum angle between the camera and the rotation allowed for interaction
  346. * If a rotation plane appears 'flat', a lower value allows interaction.
  347. */
  348. PlaneRotationGizmo.MaxDragAngle = (Math.PI * 9) / 20;
  349. PlaneRotationGizmo._RotationGizmoVertexShader = `
  350. precision highp float;
  351. attribute vec3 position;
  352. attribute vec2 uv;
  353. uniform mat4 worldViewProjection;
  354. varying vec3 vPosition;
  355. varying vec2 vUV;
  356. void main(void) {
  357. gl_Position = worldViewProjection * vec4(position, 1.0);
  358. vUV = uv;
  359. }`;
  360. PlaneRotationGizmo._RotationGizmoFragmentShader = `
  361. precision highp float;
  362. varying vec2 vUV;
  363. varying vec3 vPosition;
  364. uniform vec3 angles;
  365. uniform vec3 rotationColor;
  366. #define twopi 6.283185307
  367. void main(void) {
  368. vec2 uv = vUV - vec2(0.5);
  369. float angle = atan(uv.y, uv.x) + 3.141592;
  370. float delta = gl_FrontFacing ? angles.y : -angles.y;
  371. float begin = angles.x - delta * angles.z;
  372. float start = (begin < (begin + delta)) ? begin : (begin + delta);
  373. float end = (begin > (begin + delta)) ? begin : (begin + delta);
  374. float len = sqrt(dot(uv,uv));
  375. float opacity = 1. - step(0.5, len);
  376. float base = abs(floor(start / twopi)) * twopi;
  377. start += base;
  378. end += base;
  379. float intensity = 0.;
  380. for (int i = 0; i < 5; i++)
  381. {
  382. intensity += max(step(start, angle) - step(end, angle), 0.);
  383. angle += twopi;
  384. }
  385. gl_FragColor = vec4(rotationColor, min(intensity * 0.25, 0.8)) * opacity;
  386. }
  387. `;
  388. //# sourceMappingURL=planeRotationGizmo.js.map