For the life of me, I cannot understand why this shader animation is not running. Any help is appreciated. Please see the code below... the shader displays correctly, but does not animate. Thank you.
NOTE: Xcode 9.4.1, Swift 4.1, iOS 11.4
import SpriteKit
class GameScene: SKScene {
static let marquee: SKShader = {
let shader = SKShader(
source: "void main() {" +
"float np = mod(a_path_phase + v_path_distance / u_path_length, 1.0);" +
"vec3 hsb = vec3(np, 1.0, 1.0);" +
"vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);" +
"vec3 p = abs(fract(hsb.xxx + K.xyz) * 6.0 - K.www);" +
"vec3 rgb = hsb.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), hsb.y);" +
"vec4 df = SKDefaultShading();" +
"gl_FragColor = vec4(rgb, df.z);" +
"}"
)
shader.attributes = [SKAttribute(name: "a_path_phase", type: .float)]
return shader
} ()
private let beat: TimeInterval = 0.05
private var phase: Float = 0
override func didMove(to view: SKView) {
let node = SKShapeNode(rectOf: CGSize(width: 256, height: 256), cornerRadius: 16)
node.fillColor = .clear
node.strokeColor = .white
node.strokeShader = GameScene.marquee
node.lineWidth = 8
node.glowWidth = 4
node.run(
SKAction.repeatForever(
SKAction.sequence(
[
SKAction.wait(forDuration: beat),
SKAction.run() { [weak self] in
if let weak = self {
weak.phase = fmodf(weak.phase + Float(weak.beat), 1)
node.setValue(SKAttributeValue(float: weak.phase), forAttribute: "a_path_phase")
}
}
]
)
)
)
addChild(node)
}
}
I was also having trouble with .setValue(forAttribute:). It did not seem to work. Upon investigation, my conclusion is that...
I believe SKShapeNode.setValue(forAttribute:) is not working.
The fact that SKSpriteNode.setValue(forAttribute:) did work correctly under the same set of conditions led me to conclude that it was almost surely some bug in iOS in SKShapeNode.
The fact was that any SKAttributes I might set in
SKShapeNode.fillShader or SKShapeNode.strokeShader kept their
values set to 0.0 no matter what I did.
However, the same did not happen with SKSpriteNode.shader.
So I had to find a way to circumvent this bug.
Some sort of hack.
Fortunately, in my case, I did find a way.
And fortunately, it seems to be enough to solve your problem too.
What you can do, in this case, is to pass the value of your phase as one of the components of the strokeColor, for example, the red component.
To do this you will have to normalize the phase to values between 0.0 to 1.0.
You can pass a phase multiplier using a shader.uniform to help denormalize it.
Then read the phase value inside the shader from SKDefaultShading().x, (the red component).
Make sure to set the alpha component of the
strokeColor to 1.0, for SKDefaultShading() will have its color
component values already premultiplied by its alpha.
Now, because your strokeColor is already being used to pass the phase, you can pass the actual color of the stroked line as a vec4 shader.uniform.
In my case, I also made use of u_time to help in performing my
animation. Stuff like fract(u_time) can be very useful.
Here is how the corrected code might look like:
> NOTE: There is a much simpler solution. Look by the end of this post.
import SpriteKit
class GameScene: SKScene {
private typealias Me = GameScene
static let phaseMultiplier: Float = 1000
static let marquee: SKShader = {
let shader = SKShader(
source: "void main() {" +
"float red = SKDefaultShading().x;" +
"float phase = red * u_phase_multiplier;" +
"float np = mod(phase + v_path_distance / u_path_length, 1.0);" +
"vec3 hsb = vec3(np, 1.0, 1.0);" +
"vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);" +
"vec3 p = abs(fract(hsb.xxx + K.xyz) * 6.0 - K.www);" +
"vec3 rgb = hsb.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), hsb.y);" +
"vec4 df = u_stroke_color;" +
"gl_FragColor = vec4(rgb, df.z);" +
"}"
)
let white = vector_float4.init(1, 1, 1, 1)
shader.uniforms = [
SKUniform(name: "u_phase_multiplier", float: Me.phaseMultiplier),
SKUniform(name: "u_stroke_color", vectorFloat4: white)]
return shader
} ()
private let beat: TimeInterval = 0.05
private var phase: Float = 0
override func didMove(to view: SKView) {
let node = SKShapeNode(rectOf: CGSize(width: 256, height: 256), cornerRadius: 16)
node.fillColor = .clear
node.strokeColor = .white
node.strokeShader = Me.marquee
node.lineWidth = 8
node.glowWidth = 4
node.run(
SKAction.repeatForever(
SKAction.sequence(
[
SKAction.wait(forDuration: beat),
SKAction.run() { [weak self] in
if let weak = self {
weak.phase = fmodf(weak.phase + Float(weak.beat), 1)
let phase = weak.phase / Me.phaseMultiplier
node.strokeColor = SKColor.init(red: phase, green: 0, blue: 0, alpha: 1)
}
}
]
)
)
)
addChild(node)
}
}
NOTE: I did not compile the code above posted. I should serve merely as a sketch.
The day after this post, I found out that attribute parameters are not supposed to be used in fragment shaders, only in vertex shaders.
Take a look at StackOverflow question:
In WebGL what are the differences between an attribute, a uniform, and a varying variable?
The SDK documentation of SKAttribute and SKAttributeValue, however, seem to suggest that there may be no direct relation between "SKAttributes" and actual shader attribute parameters. They just seem to be named the same.
Simpler Solution:
In the end, however, there is never any need to make use of any hack like the one I show here.
All that needs to be done is to set the value of a uniform through SKShader.uniformNamed("u_phase")?.floatValue = phase, instead of using .setValue(forAttribute:) just as jmdecombe, correctly, later pointed out.
I will keep my answer here for reference, as it presents an unconventional solution to a problem.
Related
The task is to add 10 buttons (0...9) with labels using for in loop.
I created buttons based on class ButtonPrototype. I assigned label to each button via counter inside for in loop.
It works, but there is incorrect labels order:
I need another order:
How can I implement correct order?
Code:
func createButtons() {
for y in 0...1 {
for x in 0...4 {
counterForLoop += 1
self.button = ButtonPrototype(pos: .init( CGFloat(x)/7, CGFloat(y)/7, 0 ), imageName: "\(counterForLoop)")
parentNode.addChildNode(button)
parentNode.position = SCNVector3(x: 100,
y: 100,
z: 100)
}
}
}
The following approach perfectly makes the trick:
for y in 0...1 {
for x in 0...4 {
let textNode = SCNNode()
let ascendingOrder: String = "\(((x+1)+(y*5)) % 10)"
let geo = SCNText(string: ascendingOrder, extrusionDepth: 0.5)
geo.flatness = 0.04
geo.firstMaterial?.diffuse.contents = UIImage(named: ascendingOrder)
textNode.geometry = geo
textNode.position = SCNVector3(x*10, -y*10, 0)
sceneView.scene?.rootNode.addChildNode(textNode)
print(ascendingOrder)
}
}
You have at least two problems with your code. Your smallest button label is in the lower left and you want it to be in the lower right, and your labels go 0-9, and you want them to go from 1 to 10 (but display 10 as “0”).
To reverse the x ordering, change X to 10-x in your creation of a position, and change your imageName to “((counterForLoop+1)%10)”:
self.button = ButtonPrototype(
pos: .init(
CGFloat(10-x)/7,
CGFloat(y)/7,
0),
imageName: "\((counterForLoop+1)%10)")
By the way, you should add a SceneKit tag to your question. That seems more important than either the label tag or the loops tag.
I am trying to create some AR experience.
I load the Model with animations as an Entity. Lets call it a Toy.
I create an AnchorEntity.
I attach the Toy to the AnchorEntity. Up to this point everything works great.
I want the Toy to walk in random directions. And it does for the first time. Then it gets interesting, allow me to share my code:
First method uses a newly created Transform for the Toy with the modified translation x, y, to make the Toy move and that is it.
func walk(completion: #escaping () -> Void) {
guard let robot = robot else {
return
}
let currentTransform = robot.transform
guard let path = randomPath(from: currentTransform) else {
return
}
let (newTranslation , travelTime) = path
let newTransform = Transform(scale: currentTransform.scale,
rotation: currentTransform.rotation,
translation: newTranslation)
robot.move(to: newTransform, relativeTo: nil, duration: travelTime)
DispatchQueue.main.asyncAfter(deadline: .now() + travelTime + 1) {
completion()
}
}
We get that new Transform from the method below.
func randomPath(from currentTransform: Transform) -> (SIMD3<Float>, TimeInterval)? {
// Get the robot's current transform and translation
let robotTranslation = currentTransform.translation
// Generate random distances for a model to cross, relative to origin
let randomXTranslation = Float.random(in: 0.1...0.4) * [-1.0,1.0].randomElement()!
let randomZTranslation = Float.random(in: 0.1...0.4) * [-1.0,1.0].randomElement()!
// Create a translation relative to the current transform
let relativeXTranslation = robotTranslation.x + randomXTranslation
let relativeZTranslation = robotTranslation.z + randomZTranslation
// Find a path
var path = (randomXTranslation * randomXTranslation + randomZTranslation * randomZTranslation).squareRoot()
// Path only positive
if path < 0 { path = -path }
// Calculate the time of walking based on the distance and default speed
let timeOfWalking: Float = path / settings.robotSpeed
// Based on old trasnlation calculate the new one
let newTranslation: SIMD3<Float> = [relativeXTranslation,
Float(0),
relativeZTranslation]
return (newTranslation, TimeInterval(timeOfWalking))
}
The problem is that the value of Entity.transform.translation.y grows from 0 to some random value < 1. Always after the second time the walk() method is being called.
As you can see, every time the method is called, newTranslation sets the Y value to be 0. And yet the Toy's translation:
I am out of ideas any help is appreciated. I can share the whole code if needed.
I have managed to fix the issue by specifying parameter relativeTo as Toy's AnchorEntity:
toy.move(to: newTransform, relativeTo: anchorEntity, duration: travelTime)
I found this pretty useful code online but I'm having trouble getting it to run. The variable names are all correct and I've used print statements to make sure it does make it to this function. It just doesn't seem to run the sequence on the Label nodes. Thanks
func fadeOutInfoText(){
infoLabel1.removeAllActions()
infoLabel2.removeAllActions()
speechIcon.removeAllActions()
let wait:SKAction = SKAction.wait(forDuration: 0.5)
let fade:SKAction = SKAction.fadeAlpha(to: 0, duration: 0.5)
let run:SKAction = SKAction.run {
self.infoLabel1.text = ""
self.infoLabel2.text = ""
self.infoLabel1.alpha = 1
self.infoLabel2.alpha = 1
self.speechIcon.alpha = 1
self.speechIcon.isHidden = true
}
let seq:SKAction = SKAction.sequence([wait,fade,run])
let seq2:SKAction = SKAction.sequence([wait,fade])
infoLabel1.run(seq)
infoLabel2.run(seq2)
speechIcon.run(seq2)
}
NOTE: This would be a comment (not enough reputation to do so yet :)
Executing the above code line-for-line (and adding in the nodes in an empty scene) gives what appears to be the desired result. Presumably you are not calling this function from the scene's update(_:) method, for this keeps the labels and speech icon from doing anything since the actions are removed before the scene performs the actions (see here). Make sure you aren't removing all actions and changing the alpha of the labels before this set of actions can complete elsewhere.
This is a sample code for Sequence.
let sprite = SKSpriteNode(imageNamed:"Spaceship")
let scale = SKAction.scale(to: 0.1, duration: 0.5)
let fade = SKAction.fadeOut(withDuration: 0.5)
let sequence = SKAction.sequence([scale, fade])
sprite.run(sequence)
Let me know it is useful or not.
For my studies, I have to create a game on iOS (like a space invaders). Actually, I'm stucked for the game area : I need that the bullet can be pulled from the maximum value of the x position no matter the form factor.
Indeed, my code works perfectly on the old form factor (as iPhone 8). However, on the new form factor (iPhone X, XS..), the planet can disappear of all the screen.
(I'm a beginner about Swift, so I know there is a lot of mistake in my code). But I think that the problem is from the fact that I divide by an int.
First picture : what I want (which works on iPhone 8)
Second one : My problem.
Thanks for you help (and sorry for my bad english, french people ahaha).
translate :
zone_de_jeu = game_area
largeur_jouable = width_playable
marge_largeur = width_margin
hauteur_jouable = height_playable
marge_hauteur = height_margin
enter code here
var zone_de_jeu: CGRect
override init(size: CGSize) {
let max_ratio_aspect: CGFloat = 16.0 / 9.0
let largeur_jouable = size.height / max_ratio_aspect
let marge_largeur = (size.width - largeur_jouable) / 2
let hauteur_jouable = size.width / max_ratio_aspect
let marge_hauteur = (size.height - hauteur_jouable) / 2
zone_de_jeu = CGRect(x: marge_largeur, y: marge_hauteur, width: largeur_jouable, height: hauteur_jouable)
super.init(size: size)
let initialisation_mouvement_planete = touch.location(in: self)
let mouvement_planete = touch.previousLocation(in: self)
let arrive_final_planete_x = initialisation_mouvement_planete.x - mouvement_planete.x
planete.position.x += arrive_final_planete_x
if planete.position.x > zone_de_jeu.maxX - planete.size.width / 6.5 {
planete.position.x = zone_de_jeu.maxX - planete.size.width / 6.5
}
if planete.position.x < zone_de_jeu.minX + planete.size.width / 6.5 {
planete.position.x = zone_de_jeu.minX + planete.size.width / 6.5
}
Should I modify, my divider by something more general ?
I'm looking for some help understanding why my speedToPoint variable is always 1 when it gets to the wait action.
This is part of an SKSpriteNode extension i wrote for making a bird fly to a random point in a predefined rectangle. The speedToPoint is also randomized between 1 and 4 and used as the duration for the moveTo action. However, i also need to use that TimeInterval for my wait block in the action sequence.
speedToPoint is indeed being randomized in the run block (i've confirmed). How can i use that same randomized number in the wait block in the next part of the sequence?
var speedToPoint:TimeInterval = 1
self.run(SKAction.repeatForever(
SKAction.sequence([
SKAction.run{
speedToPoint = TimeInterval(Globals.sharedInstance.randomF(min: 1, max: 4))
var pointX = Globals.sharedInstance.randomF(min: left,max: right)
let pointY = Globals.sharedInstance.randomF(min: top,max:bottom)
while abs(self.position.x.distance(to: pointX)) < 200 {
pointX = Globals.sharedInstance.randomF(min: left,max: right)
}
self.xScale = pointX < self.position.x ? -1 : 1
self.run(SKAction.move(to: CGPoint(x:pointX,y:pointY),duration: speedToPoint))
},
SKAction.wait(forDuration: speedToPoint)])
),withKey: "inFrame")
To clarify, what i'm really tring to do is have the bird fly to a point, once it's arrived at that point, fly to another point. I'm still wrapping my heard around action sequences and whether or not they actually wait for completion to move on. Which from what i've read, they do, but not for any move actions. That's why the wait is in there. Perhaps there is another way?
An SKAction, once created, can't be modified, and it is meant to be reused eg. you can't modify the duration parameter, or change other passed parameters. This means that you have to re-create it if you need it changed. Of course you can change the speed property of an existing action, or you can pause the action but that's pretty much it when it comes to modifying the existing action.
To solve your issue, you could do next:
1) Create an action which moves a sprite to a specific location
2) Once the action is completed, you create a new one which does the same
you can do this using recursion, like this (just copy & paste the code to see how it works):
import SpriteKit
class GameScene: SKScene, SKPhysicsContactDelegate {
private var bird = SKSpriteNode(color: .purple, size: CGSize(width: 100, height: 100))
override func didMove(to view: SKView) {
addChild(bird)
recursive()
}
func recursive(){
let sequence = SKAction.sequence([
SKAction.move(to: self.randomPoint(inRect: self.frame), duration: TimeInterval(self.random(between: 1, and: 3))),
SKAction.run({[unowned self] in NSLog("Block executed"); self.recursive()})
])
self.bird.run(sequence, withKey: "aKey")
}
func random(between minimum: CGFloat, and maximum: CGFloat) -> CGFloat{
return CGFloat(arc4random()) / CGFloat(UINT32_MAX) * abs(minimum - maximum) + min(minimum, maximum)
}
func randomPoint(inRect rect:CGRect)->CGPoint{
let x = random( between: -rect.size.width / 2.0 , and: rect.origin.x + rect.size.width/2.0)
let y = random(between: -rect.size.height / 2.0 , and: rect.origin.y + rect.size.height/2.0)
return CGPoint(x: x, y: y)
}
}
To stop this action, remove the key associated with it.