I'm trying to use the applyForce method of SceneKit on a SCNNode. The application is detecting when a specific node is touched via the touch screen, and it is supposed to apply a force on the node; however, the SCNNode does not move. I've properly set the physics to dynamic on the SCNNode, and it responds to gravity.
override func viewDidLoad() {
...
// retrieve the floor node
let floorNode = SCNScene(named: "art.scnassets/floor.dae")
floorNode?.rootNode.position = SCNVector3(x: 0, y: 0, z:-10)
floorNode?.rootNode.physicsBody = SCNPhysicsBody(type: .static, shape: nil)
scene.rootNode.addChildNode(floorNode!.rootNode)
let treeNode = SCNScene(named: "art.scnassets/tree.dae")
treeNode?.rootNode.position = SCNVector3(x: 0, y: 10, z:-10)
treeNode?.rootNode.physicsBody = SCNPhysicsBody(type: .dynamic, shape: nil)
scene.rootNode.addChildNode(treeNode!.rootNode)
...
}
func handleTap(_ gestureRecognize: UIGestureRecognizer) {
...
// check what nodes are tapped
let p = gestureRecognize.location(in: scnView)
let hitResults = scnView.hitTest(p, options: [:])
// check that we clicked on at least one object
if hitResults.count > 0 {
print("A hit!")
// retrieved the first clicked object
let result: AnyObject = hitResults[0]
result.node.physicsBody?.applyForce(SCNVector3(x:10000,y:1,z:1), asImpulse: true)
result.node.physicsBody?.applyTorque(SCNVector4(x:0,y:1,z:0, w:100.0), asImpulse: true)
}
}
I was trying to apply the force onto a child node (which the hit test returns), when I should have applied the force to the child node's parent.
I am importing SCNScenes via .DAE (Collada) files, and the rootNode of the SCNScene is not the model itself, but the parent of the model.
Related
3D object place perfectly in ARSCNView but problem is that when object placed in AR and move camera right, left, top and bottom too fast then 3D object started hovering and dancing anywhere with the planeNode
how I can fix this issue, trying lots of way to find the solution still not get any result
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
// Cast ARAnchor as ARPlaneAnchor
guard let planeAnchor = anchor as? ARPlaneAnchor else { return }
let planeGeometry = ARSCNPlaneGeometry(device: MTLCreateSystemDefaultDevice()!)!
planeGeometry.update(from: planeAnchor.geometry)
// Add material to geometry
let material = SCNMaterial()
material.diffuse.contents = UIColor.blue.withAlphaComponent(0.8)
planeGeometry.materials = [material]
// Create a SCNNode from geometry
let planeNode = SCNNode(geometry: planeGeometry)
self.privateNode = planeNode
self.anchors.append(planeNode)
DispatchQueue.main.async {
self.lbl_middle_heading.text = Constants.kSharedAppDelegate?.languageBundle.localizedString(forKey: "Please tap anywhere on screen.", value: "", table: nil)
self.showFeaturePoints(isShowDeugOptions: false)
}
// Add the newly created plane node as a child of the node created for the ARAnchor
node.addChildNode(planeNode)
}
func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
// Cast ARAnchor as ARPlaneAnchor, get the child node of the anchor, and cast that node's geometry as an ARSCNPlaneGeometry
guard
let planeAnchor = anchor as? ARPlaneAnchor,
let planeNode = node.childNodes.first,
let planeGeometry = planeNode.geometry as? ARSCNPlaneGeometry
else { return }
planeGeometry.update(from: planeAnchor.geometry)
}
The model is "dancing" or drifting away along some axis in AR app for three main reasons:
if you imported an animated model
if a model isn't tethered by ARAnchor
if a tracking of your scene is poor
Track a surrounding environment thoroughly and make sure it's accordingly lit and has quite detailed texture for getting a sufficient quantity of feature points to track.
From the sample I created
I am unable to set the circle's alpha / visiblity when mousedown draw line. But I am able to console log it when its detect hitTest.. Is there advise in this?
Below is the block of codes:
var canvas, stage;
var drawingCanvas;
var oldPt;
var oldMidPt;
var title;
var color;
var stroke;
var colors;
var index;
var rect, circle1;
var currMidPt;
$(document).ready(function() {
init();
});
function init() {
canvas = document.getElementById("canvas");
index = 0;
colors = ["#828b20", "#b0ac31", "#cbc53d", "#fad779", "#f9e4ad", "#faf2db", "#563512", "#9b4a0b", "#d36600", "#fe8a00", "#f9a71f"];
//check to see if we are running in a browser with touch support
stage = new createjs.Stage(canvas);
stage.autoClear = false;
stage.enableDOMEvents(true);
createjs.Touch.enable(stage);
createjs.Ticker.setFPS(24);
createjs.Ticker.on("tick", tick);
drawingCanvas = new createjs.Shape();
stage.addEventListener("stagemousedown", handleMouseDown);
stage.addEventListener("stagemouseup", handleMouseUp);
title = new createjs.Text("Click and Drag to draw", "36px Arial", "#777777");
title.x = 300;
title.y = 200;
rect = new createjs.Shape();
rect.graphics.beginFill("#000").drawRect(0, 0, stage.canvas.width, stage.canvas.height);
var container = new createjs.Container();
container.x = 0;
container.y = 0;
stage.addChild(container, title);
stage.addChild(drawingCanvas);
circle1 = new createjs.Shape();
circle1.graphics.beginFill("#990000").drawCircle(120,120,40);
container.addChild(circle1);
stage.update();
}
function handleMouseDown(event) {
if (!event.primary) { return; }
if (stage.contains(title)) {
stage.clear();
stage.removeChild(title);
}
color = colors[(index++) % colors.length];
stroke = Math.random() * 30 + 10 | 0;
oldPt = new createjs.Point(stage.mouseX, stage.mouseY);
oldMidPt = oldPt.clone();
stage.addEventListener("stagemousemove", handleMouseMove);
}
function handleMouseMove(event) {
if (!event.primary) { return; }
var midPt = new createjs.Point(oldPt.x + stage.mouseX >> 1, oldPt.y + stage.mouseY >> 1);
drawingCanvas.graphics.clear().setStrokeStyle(stroke, 'round', 'round').beginStroke(color).moveTo(midPt.x, midPt.y).curveTo(oldPt.x, oldPt.y, oldMidPt.x, oldMidPt.y);
oldPt.x = stage.mouseX;
oldPt.y = stage.mouseY;
oldMidPt.x = midPt.x;
oldMidPt.y = midPt.y;
currMidPt = midPt;
if(circle1.hitTest(currMidPt.x, currMidPt.y)) {
console.log('test');
circle1.alpha = 0.6;
circle1.visible = false;
}
stage.update();
}
function tick(event) {
// console.log(ndgmr.checkPixelCollision(drawingCanvas,circle1,0,false));
stage.update(event);
}
function handleMouseUp(event) {
if (!event.primary) { return; }
stage.removeEventListener("stagemousemove", handleMouseMove);
}
The main reason this doesn't work is because you are using a stage that never clears itself. This gives you the benefit of a "paint brush", since it just adds new curves as you draw, but the circle can never be removed, since it is painted on to the canvas. If you change the alpha, it just draws on top of the current circle. This is also why your circle gets all aliased, as it constantly draws on top of itself, multiplying the alpha.
Here is a quick edit that shows the circle moving its x position instead of adjusting the alpha. Any time you roll over the original position it will move 10 pixels to the right.
http://codepen.io/lannymcnie/pen/redrgo?editors=0010
The hitTest code you have used is not correct though, since it always checks the x/y position of the pen against the local coordinates of the circle -- this means the position of the circle doesn't matter. To fix this, you need to find the local coordinates instead:
currMidPt = circle1.globalToLocal(midPt.x, midPt.y);
Here is an updated fiddle showing that behaviour: http://codepen.io/lannymcnie/pen/grejVg?editors=0010
However, to get the effect you are probably looking for, you have to take a different approach. Instead of using the stage auto-clear, use a cached Shape, and update the cache when you draw. This will provide the paint effect, but let the rest of the stage get updated as usual.
// Remove this!
stage.autoClear = false;
// Cache the drawingCanvas once after creating it
drawingCanvas = new createjs.Shape();
drawingCanvas.cache(0,0,canvas.width,canvas.height);
// After drawing, update the cache. This is at the end of the mousemove function.
// Use "source-over" to apply the new contents on top, instead of clearing the cache.
drawingCanvas.updateCache("source-over");
stage.update();
I also made a few other changes:
Removed the Ticker listener that updates the stage. Your mousemove already updates the stage when the contents change. If you want to reintroduce the ticker update for other content, then remove the stage.update() calls everywhere else, as they are redundant.
Moved the drawingCanvas below the circle/container. This just makes sure the circle is always visible for the demo
Here is a final demo with these changes:
http://codepen.io/lannymcnie/pen/NNYLPX?editors=0010
Hope that helps!
I'm having some trouble while creating a camera Tween in THREE.js, specifically at the end and beginning of the animation, there always seems to be a camera 'jump', meaning that the camera flickers once the animation starts and once the animation ends. For reference, what I'm doing is :
Camera is overlooking a scenario from above.
When user clicks on an element of the scenario, the camera zooms on it (by a TWEEN) and when it's close enough, the OrbitControls target change to the centroid of the selected element, and autorotate starts, so the user sees the element rotating in the center of the screen.
When user clicks again, the camera zooms out to its initial position (by a TWEEN) and goes back to the original controls.
I'm experiencing 'jumps/flickers' at the beginning and end of each TWEEN.
This is my tween function :
var origpos = new THREE.Vector3().copy(camera.position); // original position
var origrot = new THREE.Euler().copy(camera.rotation); // original rotation
camera.position.set(x, y+500, z+variation);
camera.lookAt(new THREE.Vector3(x,y,z));
var dstrot = new THREE.Euler().copy(camera.rotation)
// reset original position and rotation
camera.position.set(origpos.x, origpos.y, origpos.z);
camera.rotation.set(origrot.x, origrot.y, origrot.z);
options = {duration: 3000};
//
// Tweening
//
// position
new TWEEN.Tween(camera.position).to({
x: x,
y: y+500,
z: z
}, options.duration).easing(TWEEN.Easing.Cubic.Out).onUpdate(function () {
camera.lookAt(new THREE.Vector3(x,y,z));
}).onComplete(function () {
controls.autoRotate = true;
controls.autoRotateSpeed = 5.0;
controls.target = new THREE.Vector3(x, y, z);
}).start();
// rotation (using slerp)
(function () {
var qa = camera.quaternion; // src quaternion
var qb = new THREE.Quaternion().setFromEuler(dstrot); // dst quaternion
var qm = new THREE.Quaternion();
var o = {t: 0};
new TWEEN.Tween(o).to({t: 1}, options.duration).onUpdate(function () {
THREE.Quaternion.slerp(qa, qb, qm, o.t);
camera.quaternion.set(qm.x, qm.y, qm.z, qm.w);
}).start();
}).call(this);
OK, it seems that the issue was in itself with the method I was using to rotate the camera, using quaternions and SLERP.
I found that the best way (easier and without flicker/jump) would be to interpolate the parameters of camera.rotation instead of interpolating the quaternions.
So, a Tween of camera.rotation.the_axis_where_you_want_to_rotate works perfectly, and done concurrently with a Tween on the position, achieves the effect I was looking for.
i have just started to study three.js and i am having some trouble to write a function that takes as arguments an object position (Vector3) and a time in milliseconds, and gradually rotate the camera to face it in that time. Substantially a lerp version of the builtin lookAt method.
First i've tried using tweenjs to get smooth rotate transition. For the start and end parameters i've created a dummy object and set its position, rotation and quaternion the same as the camera, then i have use the lookAt function on it to face towards the object and i've stored its quaternion in a new variable "targetQuaternion". Then i have used this variable as the target parameter in the TWEEN.Tween method to update camera.quaternion. I've tried before with quaternions to avoid gymbal lock and then with rotation, but none works fine.
function rotateCameraToObject(object3D, time) {
var cameraPosition = camera.position.clone(); // camera original position
var cameraRotation = camera.rotation.clone(); // camera original rotation
var cameraQuaternion = camera.quaternion.clone(); // camera original quaternion
var dummyObject = new THREE.Object3D(); // dummy object
// set dummyObject's position, rotation and quaternion the same as the camera
dummyObject.position.set(cameraPosition.x, cameraPosition.y, cameraPosition.z);
dummyObject.rotation.set(cameraRotation.x, cameraRotation.y, cameraRotation.z);
dummyObject.quaternion.set(cameraQuaternion.x, cameraQuaternion.y, cameraQuaternion.z);
// lookAt object3D
dummyObject.lookAt(object3D);
// store its quaternion in a variable
var targetQuaternion = dummyObject.quaternion.clone();
// tween start object
var tweenStart = {
x: cameraQuaternion.x,
y: cameraQuaternion.y,
z: cameraQuaternion.z,
w: cameraQuaternion.w
};
//tween target object
var tweenTarget = {
x: targetQuaternion.x,
y: targetQuaternion.y,
z: targetQuaternion.z,
w: targetQuaternion.w
};
// tween stuff
var tween = new TWEEN.Tween(tweenStart).to(tweenTarget, time);
tween.onUpdate(function() {
camera.quaternion.x = tweenStart.x;
camera.quaternion.y = tweenStart.y;
camera.quaternion.z = tweenStart.z;
camera.quaternion.w = tweenStart.w;
});
tween.start();
}
So this does not work.
I've also tried another approach, computing the angle between camera vector and object vector and use that angle as target rotation:
function rotateCameraToObject(object3D, time) {
// camera original position
var cameraPosition = camera.position.clone();
// object3D position
var objectPosition = object3D.position.clone();
// direction vector from camera towards object3D
var direction = objectPosition.sub(cameraPosition);
// compute Euler angle
var angle = new THREE.Euler();
angle.setFromVector3(direction);
/*
* tween stuff
*/
var start = {
x: camera.rotation.clone().x,
y: camera.rotation.clone().y,
z: camera.rotation.clone().z,
}
var end = {
x: angle._x,
y: angle._y,
z: angle._z,
}
var tween = new TWEEN.Tween(start).to(end, time);
tween.onUpdate(function() {
camera.rotation.y = start.x;
camera.rotation.y = start.y;
camera.rotation.y = start.z;
});
tween.start();
}
This doesn't work neither, eventually camera rotate towards the object but the rotation is not right.
Any help? What is the correct way to have a lerp rotate function for the camera?
Thanks in advance!
Have you updated the tween in your animation loop?
function animate() {
requestAnimationFrame( animate );
TWEEN.update();
render();
}
However it's probably better to use the quaternion and not the rotation to turn objects - prevents gimbal lock. THREE.js provides a handy function to use spherical linear interpolation (slerp) as opposed to using tween (lerp) which can give you undesired results, most notably at a 180 deg turn. Put this in your animation loop.
camera.quaternion.slerp(targetQuaternion,t); //t = normalized value 0 to 1
You could then tween t to give you your desired easing.
I have done a View in CollectionView with CustomLayout. In iOS6 it worked great but iOS7 it throws an exception like this.
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason:
'layout attributes for supplementary item at index path ( {length = 2, path = 0 - 0}) changed from CustomSupplementaryAttributes: 0xd1123a0 index path: (NSIndexPath: 0xd112580 {length = 2, path = 0 - 0}); element kind: (identifier); frame = (0 0; 1135.66 45); zIndex = -1; to CustomSupplementaryAttributes: 0xd583c80 index path: (NSIndexPath: 0xd583c70 {length = 2, path = 0 - 0}); element kind: (identifier); frame = (0 0; 1135.66 45); zIndex = -1; without invalidating the layout'
iOS 10
At iOS 10, a new feature is introduced, it is Cell Prefetching. It will let dynamic position of SupplementaryView crash. In order to run in the old behavior, it needs to disable prefetchingEnabled. It's true by default at iOS 10.
// Obj-C
// This function is available in iOS 10. Disable it for dynamic position of `SupplementaryView `.
if ([self.collectionView respondsToSelector:#selector(setPrefetchingEnabled:)]) {
self.collectionView.prefetchingEnabled = false;
}
// Swift
if #available(iOS 10, *) {
// Thanks #maksa
collectionView.prefetchingEnabled = false
// Swift 3 style
colView.isPrefetchingEnabled = false
}
I hate this problem. I spend 2 days for this problem.
A reference about Cell Pre-fetch #iOS 10.
iOS 9 and before ...
#Away Lin is right.. I solve the same problem by implementing that delegate method.
My Custom UICollectionViewLayout will modify the attributes in layoutAttributesForElementsInRect. The section position is dynamic, not static. So, I obtain warnings about the layout attributes for supplementary item at index path ... changed from ... to .... Before the changes, invalideLayout related methods should be called.
And, after implementing this delegate method to return true, the method invalidateLayoutWithContext: will be called when scrolling the UICollectionViewLayout. By default, it returns false.
- (BOOL) shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
return YES;
}
From Apple Docs
Return Value
true if the collection view requires a layout update or false if the
layout does not need to change.
Discussion The default implementation of this method returns false.
Subclasses can override it and return an appropriate value based on
whether changes in the bounds of the collection view require changes
to the layout of cells and supplementary views.
If the bounds of the collection view change and this method returns
true, the collection view invalidates the layout by calling the
invalidateLayoutWithContext: method.
Availability Available in iOS 6.0 and later.
And more ...
A nice example project on GitHub, for custom UICollectionViewLayout.
You need to invalidate the existing layout before updating, see the end of the error message:
without invalidating the layout'
[collectionViewLayout invalidateLayout];
Apple Documentation for UICollectionViewLayout
I had the same exception: in iOS 7, you need now to override the inherited isEqual: in your UICollectionViewLayoutAttributes subclass as stated in Apple documentation here.
I solved my problem by override the method at the subclase of UICollectionViewFlowLayout:
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBound
return YES
I'm not entirely certain how or why, but this appears to be fixed in iOS 12, supporting both supplementary view resizing and prefetching. The trick for me was to make sure things are happening in the correct order.
Here is a working implementation of a stretchable header view. Notice the implementation of the header resizing happening in layoutAttributesForElements(in rect: CGRect):
class StretchyHeaderLayout: UICollectionViewFlowLayout {
var cache = [UICollectionViewLayoutAttributes]()
override func prepare() {
super.prepare()
cache.removeAll()
guard let collectionView = collectionView else { return }
let sections = [Int](0..<collectionView.numberOfSections)
for section in sections {
let items = [Int](0..<collectionView.numberOfItems(inSection: section))
for item in items {
let indexPath = IndexPath(item: item, section: section)
if let attribute = layoutAttributesForItem(at: indexPath) {
cache.append(attribute)
}
}
}
if let header = layoutAttributesForSupplementaryView(ofKind: StretchyCollectionHeaderKind, at: IndexPath(item: 0, section: 0)) {
cache.append(header)
}
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let visibleAttributes = cache.filter { rect.contains($0.frame) || rect.intersects($0.frame) }
guard let collectionView = collectionView else { return visibleAttributes }
// Find the header and stretch it while scrolling.
guard let header = visibleAttributes.filter({ $0.representedElementKind == StretchyCollectionHeaderKind }).first else { return visibleAttributes }
header.frame.origin.y = collectionView.contentOffset.y
header.frame.size.height = headerHeight.home - collectionView.contentOffset.y
header.frame.size.width = collectionView.frame.size.width
return visibleAttributes
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let attributes = super.layoutAttributesForItem(at: indexPath as IndexPath)?.copy() as! UICollectionViewLayoutAttributes
guard collectionView != nil else { return attributes }
attributes.frame.origin.y = headerHeight.home + attributes.frame.origin.y
return attributes
}
override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: StretchyCollectionHeaderKind, with: indexPath)
}
override var collectionViewContentSize: CGSize {
get {
guard let collectionView = collectionView else { return .zero }
let numberOfSections = collectionView.numberOfSections
let lastSection = numberOfSections - 1
let numberOfItems = collectionView.numberOfItems(inSection: lastSection)
let lastItem = numberOfItems - 1
guard let lastCell = layoutAttributesForItem(at: IndexPath(item: lastItem, section: lastSection)) else { return .zero }
return CGSize(width: collectionView.frame.width, height: lastCell.frame.maxY + sectionInset.bottom)
}
}
}
P.S.: I'm aware the cache doesn't actually serve any purpose at this point :)
I had this problem too, because I had code that depended on the content size of the collection view. My code was accessing the content size via the collectionView!.contentSize instead of collectionViewContentSize.
The former uses the collectionView property of UICollectionViewLayout, while the latter uses the custom-implemented layout property. In my code, the first time the layout was asked for attributes, contentSize had not been set yet.
Select the CollectionView and Goto Attribute Inspector. Uncheck The Prefetching Enabled CheckBox. This is Fixed my Issue. Screenshot