How to use alpha component in gradient colors with GKNoise and SKTexture? - swift

I'm using GKNoise with a gradient map to generate color noise, getting a CGImage via SKTexture, on Mac OS. In particular I'm using a GKPerlinNoiseSource and setting two gradient colors, at values -1.0 and 1.0.
It works as expected if the two colors are opaque. If one or both colors has an alpha component less than 1.0, I expect the output to have transparency. However, it looks to me like the alpha is completely ignored in GKNoise's gradient color input (treated as if it's always 1.0) - or, if not ignored there, ignored in the SKTexture rendering of the image output.
I have found a couple of SO questions which reference limitations with SKTexture in other uses, including a possible workaround with SKView rendering, however that doesn't apply here because I'm only using an SKTexture instance to get a CGImage (not using SKView or otherwise using anything from SpriteKit) - and FWIW those questions focus on iOS. For reference:
Export SKTexture to a UIImage with alpha channel
SKSpriteNode created from SKTexture(data:size:) issues with Alpha (Opacity)
I'm looking for ideas on how to make alpha components in the gradient colors work using GKNoise/SKTexture.
Below is the test output image, and the code that reproduces it. In the view, both CGImages draw identically; I expect the one drawn with the red alpha=0.5 to be darker in the red parts when the background is black, lighter when it's white, etc.
import Foundation
import GameplayKit
class GKNoiseGradientIssue {
var redColor_opaque = NSColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)
var redColor_halfAlpha = NSColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 0.5)
var blueColor_opaque = NSColor(red: 0.0, green: 0.0, blue: 1.0, alpha: 1.0)
var opaqueImage: CGImage
var halfAlphaImage: CGImage
init() {
let noiseSource = GKPerlinNoiseSource(frequency: 0.15,
octaveCount: 7,
persistence: 1.25,
lacunarity: 0.5,
seed: 12345)
let opaqueGradient: [NSNumber: NSColor] = [-1.0: redColor_opaque, 1.0: blueColor_opaque]
let opaqueNoise = GKNoise(noiseSource, gradientColors: opaqueGradient)
let opaqueNoiseMap = GKNoiseMap(opaqueNoise,
size: [200.0, 200.0],
origin: [0.0, 0.0],
sampleCount: [200, 200],
seamless: true)
let opaqueTexture = SKTexture(noiseMap: opaqueNoiseMap)
self.opaqueImage = opaqueTexture.cgImage()
let halfAlphaGradient: [NSNumber: NSColor] = [-1.0: redColor_halfAlpha, 1.0: blueColor_opaque]
let halfAlphaNoise = GKNoise(noiseSource, gradientColors: halfAlphaGradient)
let halfAlphaNoiseMap = GKNoiseMap(halfAlphaNoise,
size: [200.0, 200.0],
origin: [0.0, 0.0],
sampleCount: [200, 200],
seamless: true)
let halfAlphaTexture = SKTexture(noiseMap: halfAlphaNoiseMap)
self.halfAlphaImage = halfAlphaTexture.cgImage()
}
}
class GradientIssueView: NSView {
var issue: GKNoiseGradientIssue?
override func awakeFromNib() {
self.issue = GKNoiseGradientIssue()
}
override func draw(_ dirtyRect: NSRect) {
NSColor.black.setFill()
self.bounds.fill()
if let cgc = NSGraphicsContext.current?.cgContext {
cgc.draw(self.issue!.opaqueImage,
in: CGRect(origin: CGPoint(x: 10.0, y: 10.0),
size: CGSize(width: 200.0, height: 200.0)))
cgc.draw(self.issue!.halfAlphaImage,
in: CGRect(origin: CGPoint(x: 10.0, y: 220.0),
size: CGSize(width: 200.0, height: 200.0)))
}
}
}

Just following up here, for anyone else who might encounter this... While I never got a definitive reply from Apple Developer forums I did come to the conclusion that SKTexture + GKNoise only supports opaque colors, so the question as asked has no answer: one can't do this with SKTexture. Instead, I updated my approach to just use GKNoiseMap directly and calculate my own gradients.
An advantage of this approach is that I can also now create gradients from a noise source using more than two colors. I also have more options for creatively skewing or clamping the noise values before they're converted to colors.
To use GKNoiseMap directly instead of with the two-color gradients and SKTexture approach, create the noise with no reference to gradient colors, and create the GKNoiseMap the same way:
let myNoise = GKNoise(noiseSource)
let noiseMap = GKNoiseMap(myNoise,
size: [myModelWidth, myModelHeight],
origin: [myX, myY],
sampleCount: [myPointWidth, myPointHeight],
seamless: true)
Then when rendering, just call the noiseMap value() method to get the noise value for each point in your view or image output, and then calculate your own color gradient for that point including the alpha (if desired). Note that voronoi noise will be between 0.0 and 1.0 while other perlin-based noise will be between -1.0 and 1.0; to simplify here I'll assume one just wants a gradient between two colors at 0.0 and 1.0.
Given two colors in .deviceRGB color space (rgbColor1 and rgbColor2), and the noise value unitNoiseValue as a CGFloat,
let r_range = rgbColor2.redComponent - rgbColor1.redComponent
let g_range = rgbColor2.greenComponent - rgbColor1.greenComponent
let b_range = rgbColor2.blueComponent - rgbColor1.blueComponent
let a_range = rgbColor2.alphaComponent - rgbColor1.alphaComponent
let r = noiseUnitValue * r_range + rgbColor1.redComponent
let g = noiseUnitValue * g_range + rgbColor1.greenComponent
let b = noiseUnitValue * b_range + rgbColor1.blueComponent
let a = noiseUnitValue * a_range + rgbColor2.alphaComponent
return NSColor(red: r, green: g, blue: b, alpha: a)
In order to make this performant it may be helpful to limit the discrete levels of each gradient (e.g. 256 levels), and to cache calculated colors for large images and store in a hash that can be looked up quickly, and similar techniques. Also, because the conversion here just maps a unit value, you can have other skewing of your noise value if desired before calculation of the gradient color, such as additional fade or normalization or other 'shaping' of the input value.

Related

How to draw gradient in SKShapeNode?

I am trying to draw gradient in SKShapeNode object.
I draw a triangle with a color using alpha component and it works fine. And I would like to add gradient so one edge of the triangle disappears slowly to the background. What I want to achieve is to simulate sight range of a character.
I have found one answer regarding this challenge: How to apply a gradient to SKShapeNode created from a path, but the answer doesn't work in XCode 13.1.
The reason is that proposed solution of adding SKTexture can't be compiled:
var testTexture = SKTexture(size: CGSize(width: 200, height: 1), color1: CIColor(red: 1.0, green: 0.0, blue: 1.0, alpha: 1.0), color2: CIColor(red: 1.0, green: 0.0, blue: 1.0, alpha: 0.0), direction: GradientDirection.Left)
It results in the error "Cannot find 'GradientDirection' in scope...". And I cannot find any other method to add gradient.
I have found another answer which says that it isn't possible, but it is from 2013, so maybe something changed (How to create a Gradient in Spritekit?).
I also tried this solution: https://augmentedcode.io/2017/11/12/drawing-gradients-in-spritekit/, but it doesn't produce any effect and decreases performance of the application.
Some sources from the internet directed my to OpenGL Shading Language: https://www.khronos.org/opengl/wiki/Core_Language_(GLSL).
Here is partly solution:
let sight = SKShapeNode()
let gradientShader = SKShader(source: "void main() {" +
"float distanceFromCenter = distance(v_tex_coord, vec2(0.5,0.5));" +
"gl_FragColor = vec4(0.6, 0.3, 0.0, distanceFromCenter*6.0);" +
"}")
sight.fillColor = .clear
sight.fillShader = gradientShader
Here are helpful links: Drawing a circle with a shader in SpriteKit, https://thebookofshaders.com/02/, Add shader for SKShapeNode, https://developer.apple.com/documentation/spritekit/skshapenode/controlling_shape_drawing_with_shaders.
The above solution was half way, but after another couple of hours I managed to achieve what I wanted to.
let pos = n.sprite.position
let frame = n.sight.frame
let x = (pos.x - frame.minX) / frame.width
let y = (pos.y - frame.minY) / frame.height
let gradientShader = SKShader(source: "void main() {" +
"float distanceFromCenter = distance(v_tex_coord, npcLocation);" +
"float alpha = 0.8 - distanceFromCenter;" +
"if (alpha < 0) { alpha = 0.0;}" +
"gl_FragColor = vec4(0.0, 0.0, 0.0, alpha);" +
"}")
gradientShader.uniforms = [SKUniform(name: "npcLocation", vectorFloat2: vector_float2(Float(x), Float(y)))]
n.sight.fillColor = .clear
n.sight.fillShader = gradientShader

how to display MTKView with rgba16Float MTLPixelFormat

I have an MTKView set to use MTLPixelFormat.rgba16Float. I'm having display issues which can be best described with the following graphic:
So the intended UIColor becomes washed out, but only while it is being displayed in MTKView. When I convert the drawable texture back to an image for display in a UIView via CIIMage, I get back the original color. Here is how I create that output:
let colorSpace = CGColorSpaceCreateDeviceRGB()
let kciOptions = [kCIImageColorSpace: colorSpace,
kCIContextOutputPremultiplied: true,
kCIContextUseSoftwareRenderer: false] as [String : Any]
let strokeCIImage = CIImage(mtlTexture: metalTextureComposite, options: kciOptions)!.oriented(CGImagePropertyOrientation.downMirrored)
let imageCropCG = cicontext.createCGImage(strokeCIImage, from: bbox, format: kCIFormatABGR8, colorSpace: colorSpace)
Other pertinent settings:
uiColorBrushDefault: UIColor = UIColor(red: 0.92, green: 0.79, blue: 0.18, alpha: 1.0)
self.colorPixelFormat = MTLPixelFormat.rgba16Float
renderPipelineDescriptor.colorAttachments[0].pixelFormat = self.colorPixelFormat
// below is the colorspace for the texture which is tinted with UIColor
let colorSpace = CGColorSpaceCreateDeviceRGB()
let texDescriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: MTLPixelFormat.rgba8Unorm, width: Int(width), height: Int(height), mipmapped: isMipmaped)
target = texDescriptor.textureType
texture = device.makeTexture(descriptor: texDescriptor)
Some posts have hinted at sRGB being assumed somewhere, but no specifics as to how I can disable it.
I'd like the color that I display on MTKView to match the input (as close to it as possible anyway) and still be able to convert that texture into something I can display in an ImageView. I've tested this on an iPad Air and a new iPad Pro. Same behavior. Any help would be appreciated.
So, it looks like you are very close to the complete solution. But what you have is not quite correct. Here is a Metal function that will convert from sRGB to a linear value which you can then write in your Metal shader (I still suggest that you write to a sRGB texture but you can also write to a 16 bit texture). Note that sRGB is not a simple 2.2 gamma curve.
// Convert a non-linear log value to a linear value.
// Note that normV must be normalized in the range [0.0 1.0].
static inline
float sRGB_nonLinearNormToLinear(float normV)
{
if (normV <= 0.04045f) {
normV *= (1.0f / 12.92f);
} else {
const float a = 0.055f;
const float gamma = 2.4f;
//const float gamma = 1.0f / (1.0f / 2.4f);
normV = (normV + a) * (1.0f / (1.0f + a));
normV = pow(normV, gamma);
}
return normV;
}
The key turned out to be in undoing the gamma correction that is inherently embedded in a UIColor
let colorSRGB = UIColor(red: 0.92, green: 0.79, blue: 0.18, alpha: 1.0)
let rgbaSRGB = colorSRGB.getRGBAComponents()!
let gammapower : CGFloat = 2.2
let r = pow(rgbaSRGB.red, gammapower)
let g = pow(rgbaSRGB.green, gammapower)
let b = pow(rgbaSRGB.blue, gammapower)
let a = pow(rgbaSRGB.alpha, gammapower)
let colorNoGamma: UIColor = UIColor(red: CGFloat(r), green: CGFloat(g), blue: CGFloat(b), alpha: CGFloat(a))
Once I pass colorNoGamma to be applied in an MKTView with MTLPixelFormat.rgba16Float, the results will match a UIView displaying colorSRGB. It makes sense once you think through it...Thanks to #MoDJ for leading me in the right path.
Note that, on the MTKView side, I can keep the texture settings as originally defined, namely:
let texDescriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: MTLPixelFormat.rgba8Unorm, width: Int(width), height: Int(height), mipmapped: isMipmaped)
target = texDescriptor.textureType
And, if I want to convert the currentDrawable into a texture to be displayed in a UIImageView, then I want to make sure I don't apply ANY colorspace settings. I.e:
let strokeCIImage = CIImage(mtlTexture: metalTextureComposite, options: [:])!.oriented(CGImagePropertyOrientation.downMirrored)
let imageCropCG = cicontext.createCGImage(strokeCIImage, from: box, format: kCIFormatABGR8, colorSpace: colorSpace) // kCIFormatRGBA8 gives same result. Not sure what's proper
let layerStroke = CALayer()
layerStroke.frame = bbox
layerStroke.contents = imageCropCG
Note the argument options: [:] means I pass no colorspace/premultiplication/renderer settings as I did in the original post where I defined kciOptions

Issue with invisible Shadow Plane in SceneKit / ARKit

Since a while I am looking around to find answers to my Issue, but cannot find anything that really helps, or explains what is happening. I also over and over checked everything I found on SO, but couldn't find the answer.
The Issue is happening continuously when displaying Objects in the AR World. iEx I place an object to a Plane on the floor, which is my invisible Shadow Plane. Then it depends all on the viewing angle from the device. To clarify, I added to images, which has just a slightly different viewing angle. Have a look what is happening to the shadows:
I would like to have a good shadow all the time and not such artefacts as you can see.
Note: I already played around using the shadowSampleCount, the Bias, and all the other options, that should help to get a proper, low rendering cost shadow.
Here are is the extract of the relevant code for Lighting and Plane, Material, etc
For the SCNLight:
class func directionalLight() -> SCNLight {
let light = SCNLight()
light.type = .directional
light.castsShadow = true
light.color = UIColor.white
light.shadowMode = .deferred
light.shadowSampleCount = 8
light.shadowRadius = 1
// light.automaticallyAdjustsShadowProjection = false
light.shadowColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.75)
light.categoryBitMask = -1
return light
}
and how I add it:
func setupLights() {
lightDirectionNode.light = Lighting.directionalLight()
// lightDirectionNode.eulerAngles = SCNVector3(-66.degreesToRadians, 0, 0)
lightDirectionNode.eulerAngles = SCNVector3(0, 90.degreesToRadians, 45.degreesToRadians)
sceneView.scene.rootNode.addChildNode(lightDirectionNode)
}
For the SCNPlane:
class func shadowPlane() -> SCNNode {
let objectShape = SCNPlane(width: 200, height: 200)
objectShape.heightSegmentCount = 2
objectShape.widthSegmentCount = 2
objectShape.cornerRadius = 100
objectShape.cornerSegmentCount = 16
let objectNode = SCNNode(geometry: objectShape)
objectNode.geometry?.firstMaterial?.diffuse.contents = UIColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0)
objectNode.geometry?.firstMaterial?.colorBufferWriteMask = SCNColorMask(rawValue: 0)
objectNode.physicsBody = Physics.floorPhysicsBody(shape: objectShape)
objectNode.name = "floor"
objectNode.renderingOrder = -10 // renderingOrder // 0
return objectNode
}
and how I add it:
func setupShadowPlane() {
let shadowPlane = NodeFactory.shadowPlane()
// Set the Node's properties
shadowPlane.position = SCNVector3(x: (focusSquare.lastPosition?.x)!, y: (focusSquare.lastPosition?.y)!, z: (focusSquare.lastPosition?.z)!)
shadowPlane.eulerAngles = SCNVector3(-90.degreesToRadians, 0.0, 0.0)
sceneView.scene.rootNode.addChildNode(shadowPlane)
}
What am I doing wrong? Can anyone help?
There are 3 more instance properties to take into consideration:
var shadowRadius: CGFloat { get set }
var shadowCascadeCount: Int { get set }
var shadowCascadeSplittingFactor: CGFloat { get set }
If you don't setup these ones they definitely cause rendering artifacts.
let lightNode = SCNNode()
lightNode.light = SCNLight()
// POSITION OF DIRECTIONAL LIGHT ISN'T IMPORTANT.
// ONLY DIRECTION IS CRUCIAL FOR DIRECTIONAL LIGHTS.
lightNode.rotation = SCNVector4(x: 0, y: 0, z: 0, w: 1)
lightNode.light!.type = .directional
lightNode.light!.castsShadow = true
lightNode.light?.shadowMode = .deferred
/* THREE INSTANCE PROPERTIES TO SETUP */
lightNode.light?.shadowRadius = 3.25
lightNode.light?.shadowCascadeCount = 3
lightNode.light?.shadowCascadeSplittingFactor = 0.09
lightNode.light?.shadowColor = UIColor(white: 0, alpha: 0.75)
scene.rootNode.addChildNode(lightNode)
And one more thing – when Auto Adjust is off:
Light, like a camera, has near and far clipping planes for setup.
lightNode.light?.zNear = 0
lightNode.light?.zFar = 1000000 // Far Clipping Plane is important
Hope this helps.

Swift: Make translucent overlapping lines of the same color not change color when intersecting

Currently I have drawn 2 lines on the screen which are the same color but both have an alpha value less than 1. When these lines intersect, the intersection is a different color than the rest of the lines. There was a previous post addressing the same issue: swift drawing translucent lines, how to make overlapping parts not getting darker? However, this post wasn't sufficiently answered. I draw the lines currently like this:
var points = [CGPoint]()
points = [CGPoint(x: -100, y: 100), CGPoint(x: 100, y: -100)]
let FirstLine = SKShapeNode(points: &points, count: points.count)
FirstLine.strokeColor = UIColor.init(red: 0.25, green: 0.62, blue: 0.0, alpha: 0.5)
FirstLine.lineWidth = 30
addChild(FirstLine)
points = [CGPoint(x: 100, y: 100), CGPoint(x: -100, y: -100)]
let SecondLine = SKShapeNode(points: &points, count: points.count)
SecondLine.strokeColor = UIColor.init(red: 0.25, green: 0.62, blue: 0.0, alpha: 0.5)
SecondLine.lineWidth = 30
addChild(SecondLine)
Here is what it looks like:
I understand why this occurs, but is there any way to make the intersection look the same so it looks much better?
Edit: I have decided to implement #Confused's answer. However, the problem now is that the texture always centres to the middle of the screen. This is an example:
The red cross is in its correct position as it is connecting the specified points together. However, once I make the red cross a texture, it always centres to the middle of the screen (the green cross is the red cross as a texture). Can I use any code that could re-position the texture to its correct position. Note: this code can't just work for this example, I need one that works all the time regardless of the red cross' position.
FINAL EDIT FOR PEOPLE WITH THE SAME PROBLEM
First, setup everything like this:
var points = [CGPoint]()
let crossParent = SKNode()
addChild(crossParent)
Please note THAT YOU MUST create a parent SKNode for the texture otherwise everything on the screen will become the texture and not just the node that you want. Then, add that parent node to the scene.
After, create the lines that you want (in this case the green cross):
//The first line of the green cross
points = [CGPoint(x: -300, y: 300), CGPoint(x: -100, y: 100)]
let FirstLine = SKShapeNode(points: &points, count: points.count)
FirstLine.strokeColor = UIColor.init(red: 0.25, green: 0.62, blue: 0.0, alpha: 1.0)
FirstLine.lineWidth = 30
crossParent.addChild(FirstLine)
Remember to NOT add the first line that you create to the scene, but rather to the parent SKNode that you made at the beginning. Also, set the alpha value to 1.0 for every line that you draw. Then add your other lines:
//The second line of the green cross
points = [CGPoint(x: -100, y: 300), CGPoint(x: -300, y: 100)]
let SecondLine = SKShapeNode(points: &points, count: points.count)
SecondLine.strokeColor = UIColor.init(red: 0.25, green: 0.62, blue: 0.0, alpha: 1.0)
SecondLine.lineWidth = 30
FirstLine.addChild(SecondLine)
Note that you MUST add that line to the first line like this and not to the scene. If you are adding more than one line after adding the first line, then add it to the first line like I have done here too for each consecutive
line that you add.
Now, create the texture like this:
let tex = view?.texture(from: FirstLine)
let cross = SKSpriteNode(texture: tex, color: .clear, size: (tex?.size())!)
cross.alpha = 0.5
addChild(cross)
After doing this, whatever you call cross will be your texture, and you can change the alpha value like I have done here to anything you like and the image will not have varying colors. Remember to then add that texture to the scene.
Finally, you may notice that the texture is not in the same position as where you originally put the points. You can put it back into the same position like this:
cross.position = CGPoint(x: (FirstLine.frame.midX), y: (FirstLine.frame.midY))
Hope this helps :) Thank you to #Confused for the texture part of the program :D
This is a technique. It does not solve your problem, nor directly answer your question. Instead it offers a way to have the desired result, but without the flexibility and inherent nature of lines you actually want.
You can create textures from anything you draw with any number of nodes, with any number of techniques.
You do this (easiest way) by attaching all the drawing elements to a single node, within an SKView space that you have access to, and then render the "parent" node of your drawn objects to a texture.
How does this help?
I'm glad you asked:
You can draw everything at an opacity level of 100%, and render it to a texture, then take that texture of your drawings, put it where you like, and reduce its opacity to any percentage you like, and get an even result. No bright spots where things overlay each other.
Here's code that does all the above:
var points = [CGPoint]()
points = [CGPoint(x: -100, y: 100), CGPoint(x: 100, y: -100)]
let FirstLine = SKShapeNode(points: &points, count: points.count)
FirstLine.strokeColor = UIColor.init(red: 0.25, green: 0.62, blue: 0.0, alpha: 1)
FirstLine.lineWidth = 30
// ^^ Note the FirstLine is not added to the Scene
points = [CGPoint(x: 100, y: 100), CGPoint(x: -100, y: -100)]
let SecondLine = SKShapeNode(points: &points, count: points.count)
SecondLine.strokeColor = UIColor.init(red: 0.25, green: 0.62, blue: 0.0, alpha: 1)
SecondLine.lineWidth = 30
FirstLine.addChild(SecondLine)
// ^^ Note SecondLine being added to FirstLine, and that they both have alpha of 1
// Now the magic: use the view of the SKScene to render FirstLine and its child (SecondLine)
// They are rendered into a texture, named, imaginatively, "tex"
let tex = view.texture(from: FirstLine)
let cross = SKSpriteNode(texture: tex, color: .clear, size: (tex?.size())!)
cross.alpha = 0.5
// ^^ The alpha of the above sprite is set to your original desire of 0.5
// And then added to the scene, with the desired result.
addChild(cross)
and here's the result:
NO! I think is, unfortunately, the answer.
There are the following blend modes, none of which provide the result you're looking for:
case alpha // Blends the source and destination colors by multiplying the source alpha value.
case add // Blends the source and destination colors by adding them up.
case subtract // Blends the source and destination colors by subtracting the source from the destination.
case multiply // Blends the source and destination colors by multiplying them.
case multiplyX2 // Blends the source and destination colors by multiplying them and doubling the result.
case screen // Blends the source and destination colors by multiplying one minus the source with the destination and adding the source.
case replace // Replaces the destination with the source (ignores alpha).
The only option that might work is a custom shader that figures out if it's overlapping itself, somehow. But I have NO CLUE where to even start making something like that, or if it's even possible.
Make the stroke color fully opaque.
Add your path nodes to an SKEffectNode.
Set the alpha of the effect node to the desired value.
Put the effect node into your main scene.
While doing this you can also put all lines in one shape node:
let path = CGMutablePath()
path.move( to: CGPoint(x: -100, y: 100))
path.addLine(to: CGPoint(x: 100, y: -100))
path.move( to: CGPoint(x: 100, y: 100))
path.addLine(to: CGPoint(x: -100, y: -100))
let shape = SKShapeNode(path: path)
shape.strokeColor = UIColor(red: 0.25, green: 0.62, blue: 0.0, alpha: 1)
shape.lineWidth = 30
let effect = SKEffectNode()
effect.addChild(shape)
effect.alpha = 0.5
addChild(effect)
The problem is you are doing the alpha blend at the color level, do the alpha blending at the node level
var points = [CGPoint]()
var cross = SKEffectNode()
points = [CGPoint(x: -100, y: 100), CGPoint(x: 100, y: -100)]
let FirstLine = SKShapeNode(points: &points, count: points.count)
FirstLine.strokeColor = UIColor.init(red: 0.25, green: 0.62, blue: 0.0, alpha: 1)
FirstLine.lineWidth = 30
cross.addChild(FirstLine)
points = [CGPoint(x: 100, y: 100), CGPoint(x: -100, y: -100)]
let SecondLine = SKShapeNode(points: &points, count: points.count)
SecondLine.strokeColor = UIColor.init(red: 0.25, green: 0.62, blue: 0.0, alpha: 1)
SecondLine.lineWidth = 30
cross.addChild(SecondLine)
cross.alpha = 0.5
addChild(cross)

Creating transparent gradient and use it as an alpha-mask in SpriteKit

I am trying to make a gradient and use it as an alpha mask. Right now, I am able to make an image similar to this (from black to transparent):
This is the code which I use to make all this:
private func createImage(width: CGFloat, height: CGFloat) -> CGImageRef?{
if let ciFilter = CIFilter(name: "CILinearGradient"){
let ciContext = CIContext()
ciFilter.setDefaults()
let startColor = CIColor(red: 0, green: 0, blue: 0, alpha: 0)
let endColor = CIColor(red: 0, green: 0, blue: 0, alpha: 1)
let startVector = CIVector(x: 0, y: height-10)
let endVector = CIVector(x: 0, y: height-22)
ciFilter.setValue(startColor, forKey: "inputColor0")
ciFilter.setValue(endColor, forKey: "inputColor1")
ciFilter.setValue(startVector, forKey: "inputPoint0")
ciFilter.setValue(endVector, forKey: "inputPoint1")
if let outputImage = ciFilter.outputImage {
let cgImage:CGImageRef = ciContext.createCGImage(outputImage, fromRect: CGRect(x: 0, y: 0, width: width, height: height))
return cgImage
}
}
return nil
}
For me, this way works pretty much perfect because I create a mask only once in my code (when GUI element is created) so performance is not affected at all.
The thing is that I actually need a resulting image (gradient texture) to look like this (gradients should have fixed height, but size of black area may vary):
Because CILinearGradient filter doesn't have an inputImage parameter, I can't chain two filters one after another. But rather I have to create an image, and apply a filter, then to create another image, and apply a new filter.
I solved this just like described above. Also I had to extend a process in three steps (create upper gradient, create middle, black area and create lower gradient) and then to combine all that into one image.
I wonder is there anything about CILinearGradient filter I am not aware of ? Maybe there is an easier way to attack this problem and make a complex gradient?
Note that I am using SpriteKit and solving this with CAGradientLayer is not a way I want to go.
I'm not sure Core Image is the right tool for the job here — you'd probably be better off using CGGradient functions to draw your image.
Aside: What you're asking would be possible but cumbersome in CoreImage. Think of it by analogy to what you'd do to create these images as a user of Photoshop or other common graphics software... the CoreImage way would be something like this:
Create a layer for the upper gradient, fill the entire canvas with your gradient, and transform the layer to the proper position and size
Create a layer for the middle black portion, fill the entire canvas with black, and transform the layer to the proper position and size
Ditto #1, but for the bottom gradient.
You could do this with a combination of generator filters, transform filters, and compositing filters...
gradient -> transform -\
}-> composite -\
solid color -> transform -/ \
}-> composite
gradient -> transform ————————————————--/
But setting that up would be ugly.
Likewise, in Photoshop you could use selection tools and fill/gradient tools to create your gradient-fill-gradient design completely within a single layer... that's what drawing a single image using CoreGraphics is analogous to.
So, how to do that single-pass CG drawing? Something like this...
func createImage(width: CGFloat, _ height: CGFloat) -> CGImage {
let gradientOffset: CGFloat = 10 // white at bottom and top before/after gradient
let gradientLength: CGFloat = 12 // height of gradient region
// create colors and gradient for drawing with
let space = CGColorSpaceCreateDeviceRGB()
let startColor = UIColor.whiteColor().CGColor
let endColor = UIColor.blackColor().CGColor
let colors: CFArray = [startColor, endColor]
let gradient = CGGradientCreateWithColors(space, colors, [0,1])
// start an image context
UIGraphicsBeginImageContext(CGSize(width: width, height: height))
defer { UIGraphicsEndImageContext() }
let context = UIGraphicsGetCurrentContext()
// fill the whole thing with white (that'll show through at the ends when done)
CGContextSetFillColorWithColor(context, startColor)
CGContextFillRect(context, CGRect(x: 0, y: 0, width: width, height: height))
// draw top gradient
CGContextDrawLinearGradient(context, gradient, CGPoint(x: 0, y: gradientOffset), CGPoint(x: 0, y: gradientOffset + gradientLength), [])
// fill solid black middle
CGContextSetFillColorWithColor(context, endColor)
CGContextFillRect(context, CGRect(x: 0, y: gradientOffset + gradientLength, width: width, height: height - 2 * gradientOffset - 2 * gradientLength))
// draw bottom gradient
CGContextDrawLinearGradient(context, gradient, CGPoint(x: 0, y: height - gradientOffset), CGPoint(x: 0, y: height - gradientOffset - gradientLength), [])
return CGBitmapContextCreateImage(context)!
}