Explorar el Código

Examples: Add 3D cursor and clean up Sculpt addon.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Mr.doob hace 1 mes
padre
commit
66b1280e52
Se han modificado 3 ficheros con 133 adiciones y 42 borrados
  1. 45 26
      examples/jsm/sculpt/Sculpt.js
  2. BIN
      examples/screenshots/webgl_sculpt.jpg
  3. 88 16
      examples/webgl_sculpt.html

+ 45 - 26
examples/jsm/sculpt/Sculpt.js

@@ -77,6 +77,7 @@ class Sculpt {
 		this._dragDir = [ 0, 0, 0 ];
 		this._scalePrevX = 0;
 		this._scaleDelta = 0;
+		this._cachedRect = null;
 
 		// Bind event handlers
 		this._onPointerDown = this._onPointerDown.bind( this );
@@ -99,9 +100,21 @@ class Sculpt {
 
 	// ---- Picking ----
 
+	_getRect() {
+
+		if ( this._cachedRect === null ) {
+
+			this._cachedRect = this._domElement.getBoundingClientRect();
+
+		}
+
+		return this._cachedRect;
+
+	}
+
 	_unproject( mouseX, mouseY, z ) {
 
-		const rect = this._domElement.getBoundingClientRect();
+		const rect = this._getRect();
 		const x = ( ( mouseX - rect.left ) / rect.width ) * 2 - 1;
 		const y = - ( ( mouseY - rect.top ) / rect.height ) * 2 + 1;
 		_v3Temp.set( x, y, z ).unproject( this._camera );
@@ -112,7 +125,7 @@ class Sculpt {
 	_project( point ) {
 
 		_v3Temp.set( point[ 0 ], point[ 1 ], point[ 2 ] ).project( this._camera );
-		const rect = this._domElement.getBoundingClientRect();
+		const rect = this._getRect();
 		return [
 			( _v3Temp.x * 0.5 + 0.5 ) * rect.width + rect.left,
 			( - _v3Temp.y * 0.5 + 0.5 ) * rect.height + rect.top,
@@ -540,41 +553,29 @@ class Sculpt {
 
 		if ( event.button !== 0 ) return;
 
+		this._cachedRect = null;
+
 		const mouseX = event.clientX;
 		const mouseY = event.clientY;
 
-		if ( this.tool === 'drag' ) {
-
-			if ( ! this._intersectionRayMesh( mouseX, mouseY ) ) return;
-			this._sculpting = true;
-			this._lastMouseX = mouseX;
-			this._lastMouseY = mouseY;
-			try { this._domElement.setPointerCapture( event.pointerId ); } catch ( e ) { /* synthetic events */ }
-			return;
+		if ( ! this._intersectionRayMesh( mouseX, mouseY ) ) return;
 
-		}
+		this._sculpting = true;
+		this._lastMouseX = mouseX;
+		this._lastMouseY = mouseY;
+		try { this._domElement.setPointerCapture( event.pointerId ); } catch ( e ) { /* synthetic events */ }
 
 		if ( this.tool === 'scale' ) {
 
-			if ( ! this._intersectionRayMesh( mouseX, mouseY ) ) return;
-			this._sculpting = true;
 			this._scalePrevX = mouseX;
-			this._lastMouseX = mouseX;
-			this._lastMouseY = mouseY;
-			try { this._domElement.setPointerCapture( event.pointerId ); } catch ( e ) { /* synthetic events */ }
-			return;
 
-		}
+		} else if ( this.tool !== 'drag' ) {
 
-		if ( ! this._intersectionRayMesh( mouseX, mouseY ) ) return;
-		this._sculpting = true;
-		this._lastMouseX = mouseX;
-		this._lastMouseY = mouseY;
-		try { this._domElement.setPointerCapture( event.pointerId ); } catch ( e ) { /* synthetic events */ }
+			// Do first stroke immediately
+			this._makeStroke( mouseX, mouseY );
+			this._syncGeometry();
 
-		// Do first stroke immediately
-		this._makeStroke( mouseX, mouseY );
-		this._syncGeometry();
+		}
 
 	}
 
@@ -582,6 +583,8 @@ class Sculpt {
 
 		if ( ! this._sculpting ) return;
 
+		this._cachedRect = null;
+
 		const mouseX = event.clientX;
 		const mouseY = event.clientY;
 
@@ -624,6 +627,22 @@ class Sculpt {
 
 	}
 
+	get hitNormal() {
+
+		return this._pickedNormal;
+
+	}
+
+	pickFromMouse( mouseX, mouseY ) {
+
+		this._cachedRect = null;
+
+		if ( ! this._intersectionRayMesh( mouseX, mouseY ) ) return false;
+		this._computePickedNormal();
+		return true;
+
+	}
+
 }
 
 export { Sculpt };

BIN
examples/screenshots/webgl_sculpt.jpg


+ 88 - 16
examples/webgl_sculpt.html

@@ -6,13 +6,7 @@
 		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
 		<link type="text/css" rel="stylesheet" href="main.css">
 		<style>
-			.cursor-circle {
-				position: absolute;
-				border: 1.5px solid rgba(255,255,255,0.6);
-				border-radius: 50%;
-				pointer-events: none;
-				transform: translate(-50%, -50%);
-			}
+			canvas { cursor: none; }
 		</style>
 	</head>
 	<body>
@@ -21,8 +15,6 @@
 			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> webgl - sculpt
 		</div>
 
-		<div class="cursor-circle" id="cursorCircle"></div>
-
 		<script type="importmap">
 			{
 				"imports": {
@@ -40,6 +32,11 @@
 			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
 
 			let renderer, scene, camera, controls, sculpt, mesh;
+			let cursorGroup, cursorRing, cursorDot;
+
+			const _normal = new THREE.Vector3();
+			const _forward = new THREE.Vector3( 0, 0, 1 );
+			const _mouse = new THREE.Vector3();
 
 			init();
 
@@ -170,16 +167,91 @@
 
 				} );
 
-				// Cursor circle
+				// 3D Cursor
+
+				const cursorMaterial = new THREE.MeshBasicMaterial( {
+					color: 0xffffff,
+					side: THREE.DoubleSide,
+					transparent: true,
+					opacity: 0.5,
+					depthTest: false
+				} );
+
+				cursorRing = new THREE.Mesh( new THREE.RingGeometry( 0.95, 1, 48 ), cursorMaterial );
+				cursorDot = new THREE.Mesh( new THREE.CircleGeometry( 0.04, 16 ), cursorMaterial );
+
+				cursorGroup = new THREE.Group();
+				cursorGroup.add( cursorRing );
+				cursorGroup.add( cursorDot );
+				cursorGroup.visible = false;
+				cursorGroup.renderOrder = 1;
+				cursorRing.renderOrder = 1;
+				cursorDot.renderOrder = 1;
+				scene.add( cursorGroup );
 
-				const cursorCircle = document.getElementById( 'cursorCircle' );
 				renderer.domElement.addEventListener( 'pointermove', function ( event ) {
 
-					const size = sculpt.radius * 2;
-					cursorCircle.style.width = size + 'px';
-					cursorCircle.style.height = size + 'px';
-					cursorCircle.style.left = event.clientX + 'px';
-					cursorCircle.style.top = event.clientY + 'px';
+					// Hide cursor while orbiting
+					if ( event.buttons > 0 && ! sculpt.isSculpting ) {
+
+						cursorGroup.visible = false;
+						return;
+
+					}
+
+					const picking = ! sculpt.isSculpting;
+					const hit = picking && sculpt.pickFromMouse( event.clientX, event.clientY );
+
+					if ( hit || sculpt.isSculpting ) {
+
+						const ip = sculpt.hitPoint;
+						const n = sculpt.hitNormal;
+
+						// Position in world space
+						cursorGroup.position.set( ip[ 0 ], ip[ 1 ], ip[ 2 ] ).applyMatrix4( mesh.matrixWorld );
+
+						// Orient to surface normal (transform normal to world space)
+						if ( picking ) {
+
+							_normal.set( n[ 0 ], n[ 1 ], n[ 2 ] ).transformDirection( mesh.matrixWorld );
+							cursorGroup.quaternion.setFromUnitVectors( _forward, _normal );
+
+						}
+
+						// Scale to match brush radius in world space
+						const dist = cursorGroup.position.distanceTo( camera.position );
+						const fov = camera.fov * Math.PI / 180;
+						const screenHeight = 2 * dist * Math.tan( fov / 2 );
+						const worldRadius = ( sculpt.radius / window.innerHeight ) * screenHeight;
+						cursorGroup.scale.setScalar( worldRadius );
+
+						cursorRing.visible = picking;
+						cursorDot.visible = true;
+						cursorGroup.visible = true;
+
+					} else {
+
+						// No intersection: show ring facing camera at fixed depth
+						const rect = renderer.domElement.getBoundingClientRect();
+						_mouse.x = ( ( event.clientX - rect.left ) / rect.width ) * 2 - 1;
+						_mouse.y = - ( ( event.clientY - rect.top ) / rect.height ) * 2 + 1;
+						_mouse.z = 0.5;
+						_mouse.unproject( camera );
+
+						cursorGroup.position.copy( _mouse );
+						cursorGroup.quaternion.copy( camera.quaternion );
+
+						const dist = cursorGroup.position.distanceTo( camera.position );
+						const fov = camera.fov * Math.PI / 180;
+						const screenHeight = 2 * dist * Math.tan( fov / 2 );
+						const worldRadius = ( sculpt.radius / window.innerHeight ) * screenHeight;
+						cursorGroup.scale.setScalar( worldRadius );
+
+						cursorRing.visible = true;
+						cursorDot.visible = true;
+						cursorGroup.visible = true;
+
+					}
 
 				} );
 

粤ICP备19079148号