41

根据下图(我为圆形和平面使用了不同的颜色,以便可以看到它们,但最终颜色会相同),使用 Swift 和 Spritekit,我正在尝试创建圆形物体进入的效果一种粘稠的物质(不一定是粘性的)并与粘稠的物质分离。基本上,当圆形物体分离时,它会在形成圆形时从平面上拉开。我想使用图像动画帧,但由于对象是带有物理体的 SKSpriteNode,这将使对象与动画的碰撞计时变得非常困难。另一种方法是使用 CAAnimation,但我不知道如何将其与具有物理体的 SKSpriteNodes 结合使用。如何使用上述任何一种方法或不同的方法来创建这种分离效果?

在此处输入图像描述

更新

下图显示了当圆形物体进入厚物质直到被淹没时,厚物质表面的变化。

在此处输入图像描述

4

2 回答 2

2

您正在寻找流体模拟器

现代硬件确实可以做到这一点。让我们看看我们将在这里构建什么。

在此处输入图像描述

成分

为了实现这一点,我们需要

  • 创建几个具有物理体和模糊图像的分子
  • 使用 Shader 对 alpha > 0 的每个像素应用一种通用颜色

分子

这是分子类

import SpriteKit

class Molecule: SKSpriteNode {

    init() {
        let texture = SKTexture(imageNamed: "molecule")
        super.init(texture: texture, color: .clear, size: texture.size())

        let physicsBody = SKPhysicsBody(circleOfRadius: 8)
        physicsBody.restitution = 0.2
        physicsBody.affectedByGravity = true
        physicsBody.friction = 0
        physicsBody.linearDamping = 0.01
        physicsBody.angularDamping = 0.01
        physicsBody.density = 0.13
        self.physicsBody = physicsBody

    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

着色器

接下来我们需要一个片段着色器,让我们创建一个文件名Water.fsh

void main() {

    vec4 current_color = texture2D(u_texture, v_tex_coord);

    if (current_color.a > 0) {
        current_color.r = 0.0;
        current_color.g = 0.57;
        current_color.b = 0.95;
        current_color.a = 1.0;
    } else {
        current_color.a = 0.0;
    }

    gl_FragColor = current_color;
}

场景

最后我们可以定义场景

import SpriteKit

class GameScene: SKScene {

    lazy var label: SKLabelNode = {
        return childNode(withName: "label") as! SKLabelNode
    }()

    let effectNode = SKEffectNode()

    override func didMove(to view: SKView) {
        physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
        effectNode.shouldEnableEffects = true
        effectNode.shader = SKShader(fileNamed: "Water")
        addChild(effectNode)
    }

    var touchLocation: CGPoint?

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first else { return }
        let touchLocation = touch.location(in: self)
        if label.contains(touchLocation) {
            addRedCircle(location: touchLocation)
        } else {
            self.touchLocation = touchLocation
        }
    }

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        touchLocation = nil
    }


    override func update(_ currentTime: TimeInterval) {
        if let touchLocation = touchLocation {
            let randomizeX = CGFloat(arc4random_uniform(20)) - 10
            let randomizedLocation = CGPoint(x: touchLocation.x + randomizeX, y: touchLocation.y)
            addMolecule(location: randomizedLocation)
        }
    }

    private func addMolecule(location: CGPoint) {
        let molecule = Molecule()
        molecule.position = location
        effectNode.addChild(molecule)
    }

    private func addRedCircle(location: CGPoint) {
        let texture = SKTexture(imageNamed: "circle")
        let sprite = SKSpriteNode(texture: texture)
        let physicsBody = SKPhysicsBody(circleOfRadius: texture.size().width / 2)
        physicsBody.restitution = 0.2
        physicsBody.affectedByGravity = true
        physicsBody.friction = 0.1
        physicsBody.linearDamping = 0.1
        physicsBody.angularDamping = 0.1
        physicsBody.density = 1
        sprite.physicsBody = physicsBody
        sprite.position = location
        addChild(sprite)
    }

}

完整的项目可在我的 GitHub 帐户https://github.com/lucaangeletti/SpriteKitAqua上找到

于 2018-08-20T16:13:59.417 回答
1

从高层次的理解来看,有两种方法可以做到这一点。

  1. 不好的方式(但当流体有纹理时效果更好):提前创建精灵表,然后覆盖 SKSpriteNode 对象的另一个子对象。当它们之间的距离小于某个量时,动画精灵中的帧将是从球到表面的距离的函数。所需的距离范围(范围)必须映射到精灵帧号(frameIndex)。f(范围) = 帧索引。线性插值将在这里有所帮助。稍后将详细介绍插值。

  2. 正确的方法:使流体成为曲线对象,然后在初始、中间和最终状态之间使用线性插值对曲线上的点进行动画处理。这将需要三条曲线,每条曲线具有相同的点数。设初始流体状态为 F1。模型 F1 点为静态流体。当球被淹没一半时,流体状态为 F2。模型 F2 指向看起来像浸没在其最大宽度处的球。当球被淹没 75% 时,流体状态为 F3。请注意,当球完全浸没时,流体看起来没有变化。这就是为什么当球被淹没 75% 时,它具有最大的表面张力来抓球。就 SpriteKit 而言,您可以使用这些对象:

    CGMutablePathRef path = CGPathCreateMutable();
    CGPathMoveToPoint(path, NULL, 0, 0);
    CGPathAddQuadCurveToPoint(path, NULL, 50, 100, 100, 0);
    CGPathAddLineToPoint(path, NULL, 50, -100);
    CGPathCloseSubpath(path);
    
    SKShapeNode *shape = [[SKShapeNode alloc]init];
    shape.path = path;
    

然后使用带有 3D 矢量的矢量叉积来检测球何时位于流体外部,即使您的项目是 2D 也是如此。

 Ball Vector (Vb)
    ^
    |
(V) O--->  Closest Fluid Surface Vector (Vs)

V = Vb x Vs

然后查看 V 的 Z 分量,称为 Vz。如果 (Vz < 0),球在流体之外: 创建一个变量 t:

t = distOfBall/radiusOfBall

然后对流体形状中的每个有序点执行以下操作:

newFluidPointX = F1pointX*(t-1) + F2pointX*t
newFluidPointY = F1pointY*(t-1) + F2pointY*t

如果 Vz > 0),球在流体内部:

t = -(((distOfBall/radiusOfBall) + 0.5)^2)  *4 + 1
newFluidPointX = F2pointX*(t-1) + F3pointX*t
newFluidPointY = F2pointY*(t-1) + F3pointY*t

这是可行的,因为可以使用插值将任何两个形状混合在一起。参数“t”作为两个形状之间混合的百分比。

只要点数相同,您实际上可以在任何两个形状之间创建无缝混合。这就是好莱坞电影中一个人如何变身为狼的方式,或者一个人如何变身为液体水坑。这些效果的唯一原理是插值。插值是一个非常强大的工具。它被定义为:

    L = A*(t-1) + B*t
    where t is in between 0.0 and 1.0
    and A and B is what you are morphing from and to.

有关插值的更多信息,请参见: Wiki 文章

供进一步研究。如果您正在考虑为任何动态形状制作动画,我会考虑理解贝塞尔曲线。Pomax 有一篇关于这个主题的精彩文章。尽管许多框架中都有曲线,但大致了解它们的工作原理将使您能够广泛地操纵它们或在框架缺乏的地方滚动您自己的功能。她是 Pomax 的文章:

曲线入门

祝你进步顺利:)

于 2016-06-17T20:19:07.230 回答