Reputation: 471
I am making a sprite kit game and it is obviously more efficient to have one big SKSpriteNode with a tiled texture, than having multiple SKSpriteNodes with the tile texture. My Problem is that when I try to make a 5x5 tile using
SKTexture* tex = [SKTexture textureWithRect:CGRectMake(0,0,5,5) inTexture:[SKTexture textureWithImageNamed:@"tile.png"]];
SKSpriteNode* node = [SKSpriteNode spriteNodeWithTexture:tex size:CGSizeMake(100,100)];
The image is re sized appropriately, but it is clamped and not tiled. In openGL terms I am getting a GL_CLAMP_TO_EDGE where I want a GL_REPEAT. Is there anyway I could achieve the tiling effect of a texture in a single SKSpriteNode, without creating a very large image.
Here is an image of my problem:
Upvotes: 7
Views: 7523
Reputation: 483
The better and simpler way is certainly via SKShader
. A shader as simple as this does:
// SpriteTiled.fsh
//uniform vec2 u_offset;
//uniform vec2 u_tiling;
void main() {
vec2 uv = v_tex_coord.xy + u_offset;
vec2 phase = fract(uv / u_tiling);
vec4 current_color = texture2D(u_texture, phase);
gl_FragColor = current_color;
}
Then set the shader
property of your SKSpriteNode
with:
shader = SKShader(
named: "SpriteTiled",
uniforms: [
// e.g., 0 has no effect and 1 will offset the texture by its whole extent
SKUniform(name: "u_offset", vectorFloat2: vector_float2(0, 0)),
// e.g., 0.5 will repeat the texture twice, 1 produces no effect, and 1 < "zooms in" on the texture
SKUniform(name: "u_tiling", vectorFloat2: vector_float2(0.5, 0.5))
]
)
if your source texture isn't square, then simply multiply the first component of the value passed to u_tiling
by the aspect ratio of your texture.
Upvotes: 1
Reputation: 688
I used 薛永伟’s answer, and translated it to Swift in an extension:
extension SKTexture {
static func x_repeatableTexture(named name: String, toSize size: CGSize) -> SKTexture {
let image = UIImage(named: name)
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
image?.draw(in: CGRect(x: 0, y: 0, width: size.width, height: size.height))
guard let resultImage = UIGraphicsGetImageFromCurrentImageContext() else {
fatalError("Image could not be drawn.")
}
UIGraphicsEndImageContext()
return SKTexture(image: resultImage)
}
}
You can use it by doing:
let texture = SKTexture.x_repeatableTexture(named: textureName, toSize: size)
This solution allows to use slices done in the assets editor, which is really nice.
Upvotes: 0
Reputation: 11
I found the question not perfectly answered. I solved it this way.
You know you can do this in UIButton or UIImageView by resizableImageWithCapInsets ,so ,Here is my solution :
-(SKTexture *)textureWithImage:(UIImage *)image tiledForSize:(CGSize)size withCapInsets:(UIEdgeInsets)capInsets{
//get resizable image
image = [image resizableImageWithCapInsets:capInsets];
//redraw image to target size
UIGraphicsBeginImageContextWithOptions(size, NO, 0.0f);
[image drawInRect:CGRectMake(0, 0, size.width-1, size.height-1)];
[[UIColor clearColor] setFill];
//get target image
UIImage *ResultImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// get texture
SKTexture * texture = [SKTexture textureWithImage:ResultImage];
return texture;
}
Upvotes: 1
Reputation: 8357
SKTexture* tex = [SKTexture textureWithRect:CGRectMake(0,0,5,5) inTexture:[SKTexture textureWithImageNamed:@"tile.png"]];
The textureWithRect:inTexture:
creates a subtexture, and the rectangle argument is in unit coordinate space, which means all values are 0-1
Behavior of CGRectMake(0,0,5,5)
is non defined, but seems to be stretching the texture by a factor of 5.
Edit: Unit Coordinate Space
You can think of the unit coordinates as specifying a percentage of the total possible value. Every coordinate in the unit coordinate space has a range of 0.0 to 1.0. For example, along the x-axis, the left edge is at the coordinate 0.0 and the right edge is at the coordinate 1.0. Along the y-axis, the orientation of unit coordinate values changes depending on the platform
Core Animation Programming Guide, Layers Use Two Types of Coordinate Systems
Upvotes: 1
Reputation: 1184
In order to help me convert coordinate systems, I use the following function - that way I can use the absolute coordinates like you did above with (5,5):
//Pass the absolute sizes of your sub-texture (subRect) and main-texture (mainRect)
CGRect unitCoordinateGivenSubRectInRect (CGRect subRect, CGRect mainRect)
{
if (mainRect.size.height == 0 || mainRect.size.width == 0) {
return CGRectZero;
}
float x = subRect.origin.x / mainRect.size.width;
float y = subRect.origin.y / mainRect.size.height;
float sizeX = subRect.size.width / mainRect.size.width;
float sizeY = subRect.size.height / mainRect.size.height;
return CGRectMake(x, y, sizeX, sizeY);
}
Upvotes: 0
Reputation: 3763
Afaik, there is no way to create sprite with tiled texture. But what you can do, is render lot's of sprites in a single drawing pass.
From Apple's documentation (Sprite Kit Best Practices -> Drawing your content):
If all of the children of a node use the same blend mode and texture atlas, then Sprite Kit can usually draw these sprites in a single drawing pass. On the other hand, if the children are organized so that the drawing mode changes for each new sprite, then Sprite Kit might perform as one drawing pass per sprite, which is quite inefficient.
Upvotes: 10