LiquidFun Tutorial with Metal and Swift – Part 2

In this LiquidFun tutorial, you’ll learn how to simulate water on iOS using LiquidFun, and render it on screen with Metal and Swift. By Allen Tan.

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Create a Fragment Shader

While the vertex shader determines the position of each vertex, the fragment shader determines the color of each visible fragment on the screen. You don’t need any fancy colors yet for this tutorial, so you’ll use a very simple fragment shader.

Add the following code to the bottom of Shaders.metal:

fragment half4 basic_fragment() {
  return half4(1.0);
}

You create a fragment shader that simply returns the color white using RGBA values of (1, 1, 1, 1). Expect to see white particles soon—but it will behave like water, not snow!

Build a Render Pipeline

You’re almost there! The rest of the steps should be familiar to you from the Metal Tutorial for Beginners.

Open ViewController.swift and add the following properties and new method:

var pipelineState: MTLRenderPipelineState! = nil
var commandQueue: MTLCommandQueue! = nil

func buildRenderPipeline() {
  // 1
  let defaultLibrary = device.newDefaultLibrary()
  let fragmentProgram = defaultLibrary?.newFunctionWithName("basic_fragment")
  let vertexProgram = defaultLibrary?.newFunctionWithName("particle_vertex")
  
  // 2
  let pipelineDescriptor = MTLRenderPipelineDescriptor()
  pipelineDescriptor.vertexFunction = vertexProgram
  pipelineDescriptor.fragmentFunction = fragmentProgram
  pipelineDescriptor.colorAttachments[0].pixelFormat = .BGRA8Unorm
  
  var pipelineError : NSError?
  pipelineState = device.newRenderPipelineStateWithDescriptor(pipelineDescriptor, error: &pipelineError)
  
  if (pipelineState == nil) {
    println("Error occurred when creating render pipeline state: \(pipelineError)");
  }
  
  // 3
  commandQueue = device.newCommandQueue()
}

And just like you did with the other setup methods, add a call to this new method at the end of viewDidLoad:

buildRenderPipeline()

Inside buildRenderPipeline, you do the following:

  1. You use the MTLDevice object you created earlier to access your shader programs. Notice you access them using their names as strings.
  2. You initialize a MTLRenderPipelineDescriptor with your shaders and a pixel format. Then you use that descriptor to initialize pipelineState.
  3. Finally, you create an MTLCommandQueue for use later. The command queue is the channel you’ll use to submit work to the GPU.

Render the Particles

The final step is to draw your particles onscreen.

Still in ViewController.swift, add the following method:

func render() {
  var drawable = metalLayer.nextDrawable()
  
  let renderPassDescriptor = MTLRenderPassDescriptor()
  renderPassDescriptor.colorAttachments[0].texture = drawable.texture
  renderPassDescriptor.colorAttachments[0].loadAction = .Clear
  renderPassDescriptor.colorAttachments[0].storeAction = .Store
  renderPassDescriptor.colorAttachments[0].clearColor =
    MTLClearColor(red: 0.0, green: 104.0/255.0, blue: 5.0/255.0, alpha: 1.0)
  
  let commandBuffer = commandQueue.commandBuffer()

  if let renderEncoder = commandBuffer.renderCommandEncoderWithDescriptor(renderPassDescriptor) {
    renderEncoder.setRenderPipelineState(pipelineState)
    renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, atIndex: 0)
    renderEncoder.setVertexBuffer(uniformBuffer, offset: 0, atIndex: 1)
  
    renderEncoder.drawPrimitives(.Point, vertexStart: 0, vertexCount: particleCount, instanceCount: 1)
    renderEncoder.endEncoding()
  }

  commandBuffer.presentDrawable(drawable)
  commandBuffer.commit()
}

Here, you create a render pass descriptor to clear the screen and give it a fresh tint of green. Next, you create a render command encoder that tells the GPU to draw a set of points, set up using the pipeline state and vertex and uniform buffers you created previously. Finally, you use a command buffer to commit the transaction to send the task to the GPU.

Now call render at the end of viewDidLoad:

render()

Build and run the app on your device to see your particles onscreen for the first time:

particles_first

Moving Water

Now is when most of your work from this tutorial and the last will pay off—getting to see the liquid simulation in action. Currently, you have nine water particles onscreen, but they’re not moving. To get them to move, you need to trigger the following events repeatedly:

  1. LiquidFun needs to update the physics simulation.
  2. Metal needs to update the screen.

Open LiquidFun.h and add this method declaration:

+ (void)worldStep:(CFTimeInterval)timeStep velocityIterations:(int)velocityIterations 
  positionIterations:(int)positionIterations;

Switch to LiquidFun.mm and add this method definition:

+ (void)worldStep:(CFTimeInterval)timeStep velocityIterations:(int)velocityIterations
  positionIterations:(int)positionIterations {
  world->Step(timeStep, velocityIterations, positionIterations);
}

You’re adding another Objective-C pass-through method for your wrapper class, this time for the world object’s Step method. This method advances the physics simulation forward by a measure of time called the timeStep.

velocityIterations and positionIterations affect the accuracy and performance of the simulation. Higher values mean greater accuracy, but at a greater performance cost.

Open ViewController.swift and add the following new method:

func update(displayLink:CADisplayLink) {
  autoreleasepool {
    LiquidFun.worldStep(displayLink.duration, velocityIterations: 8, positionIterations: 3)
    self.refreshVertexBuffer()
    self.render()
  }
}

Next, add the following code at the end of viewDidLoad:

let displayLink = CADisplayLink(target: self, selector: Selector("update:"))
displayLink.frameInterval = 1
displayLink.addToRunLoop(NSRunLoop.currentRunLoop(), forMode: NSDefaultRunLoopMode)

You’re creating a CADisplayLink that calls your new update method every time the screen refreshes. Then in update, you do the following:

  1. You ask LiquidFun to step through the physics simulation using the time interval between the last execution of update and the current execution, as represented by displayLink.duration.
  2. You tell the physics simulation to do eight iterations of velocity and three iterations of position. You are free to change these values to how accurate you want the simulation of your particles to be at every time step.
  3. After LiquidFun steps through the physics simulation, you expect all your particles to have a different position than before. You call refreshVertexBuffer() to repopulate the vertex buffer with the new positions.
  4. You send this updated buffer to the render command encoder to show the new positions onscreen.

Build and run, and watch your particles fall off the bottom of the screen:

particles_fall

That’s not quite the effect you’re looking for. You can prevent the particles from falling off by adding walls to your physics world, and to keep things interesting, you’ll also move the particles using the device accelerometer.

Open LiquidFun.h and add these method declarations:

+ (void *)createEdgeBoxWithOrigin:(Vector2D)origin size:(Size2D)size;
+ (void)setGravity:(Vector2D)gravity;

Switch to LiquidFun.mm and add these methods:

+ (void *)createEdgeBoxWithOrigin:(Vector2D)origin size:(Size2D)size {
  // create the body
  b2BodyDef bodyDef;
  bodyDef.position.Set(origin.x, origin.y);
  b2Body *body = world->CreateBody(&bodyDef);
  
  // create the edges of the box
  b2EdgeShape shape;
  
  // bottom
  shape.Set(b2Vec2(0, 0), b2Vec2(size.width, 0));
  body->CreateFixture(&shape, 0);
  
  // top
  shape.Set(b2Vec2(0, size.height), b2Vec2(size.width, size.height));
  body->CreateFixture(&shape, 0);
  
  // left
  shape.Set(b2Vec2(0, size.height), b2Vec2(0, 0));
  body->CreateFixture(&shape, 0);
  
  // right
  shape.Set(b2Vec2(size.width, size.height), b2Vec2(size.width, 0));
  body->CreateFixture(&shape, 0);
  
  return body;
}

+ (void)setGravity:(Vector2D)gravity {
  world->SetGravity(b2Vec2(gravity.x, gravity.y));
}

createEdgeBoxWithOrigin creates a bounding box shape, given an origin (located at the lower-left corner) and size. It creates a b2EdgeShape, defines the four corners of the shape’s rectangle and attaches it to a new b2Body.

setGravity is another pass-through method for the world object’s SetGravity method. You use it to change the current world’s horizontal and vertical gravities.

Switch to ViewController.swift and add the following import:

import CoreMotion

You’re importing the CoreMotion framework because you need it to work with the accelerometer. Now add the following property:

let motionManager: CMMotionManager = CMMotionManager()

Here you create a CMMotionManager to report on the accelerometer’s state.

Now, create the world boundary by adding the following line inside viewDidLoad, before the call to createMetalLayer:

LiquidFun.createEdgeBoxWithOrigin(Vector2D(x: 0, y: 0), 
  size: Size2D(width: screenWidth / ptmRatio, height: screenHeight / ptmRatio))

This should prevent particles from falling off the screen.

Finally, add the following code at the end of viewDidLoad:

motionManager.startAccelerometerUpdatesToQueue(NSOperationQueue(), 
  withHandler: { (accelerometerData, error) -> Void in
  let acceleration = accelerometerData.acceleration
  let gravityX = self.gravity * Float(acceleration.x)
  let gravityY = self.gravity * Float(acceleration.y)
  LiquidFun.setGravity(Vector2D(x: gravityX, y: gravityY))
})

Here, you create a closure that receives updates from CMMotionManager whenever there are changes to the accelerometer. The accelerometer contains 3D data on the current device’s orientation. Since you’re only concerned with 2D space, you set the world’s gravity to the x- and y-values of the accelerometer.

Build and run, and tilt your device to move the particles:

moving_particles

The particles will slide around, and with a bit of imagination you can see them as water droplets!

Allen Tan

Contributors

Allen Tan

Author

Over 300 content creators. Join our team.