Skip to content

Commit 76082c2

Browse files
garfenglafriks
andauthored
Support for object group rendering (#70)
Co-authored-by: Lauris BH <lauris@nix.lv>
1 parent 2d8b73d commit 76082c2

File tree

5 files changed

+409
-3
lines changed

5 files changed

+409
-3
lines changed

assets/test_render_objects.tmx

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<map version="1.9" tiledversion="1.9.2" orientation="orthogonal" renderorder="right-down" width="20" height="20" tilewidth="32" tileheight="32" infinite="0" nextlayerid="3" nextobjectid="11">
3+
<tileset firstgid="1" source="tilesets/test_wangset_tileset.tsx"/>
4+
<layer id="1" name="Layer 1" width="20" height="20">
5+
<data encoding="csv">
6+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
7+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
8+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
9+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
10+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
11+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
12+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
13+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
14+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
15+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
16+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
17+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
18+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
19+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
20+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
21+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
22+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
23+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
24+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
25+
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
26+
</data>
27+
</layer>
28+
<objectgroup id="2" name="Object Group 1">
29+
<object id="1" gid="1" x="0" y="32" width="32" height="32"/>
30+
<object id="2" gid="1" x="80" y="36" width="32" height="32" rotation="105"/>
31+
<object id="3" gid="21" x="49" y="28" width="32" height="32" rotation="105"/>
32+
<object id="4" gid="21" x="0" y="64" width="32" height="32"/>
33+
<object id="5" gid="3" x="64" y="96" width="32" height="32" rotation="-90"/>
34+
<object id="6" gid="1" x="2" y="169.311" width="50.4916" height="50.4916"/>
35+
<object id="7" gid="1" x="128.229" y="175.623" width="50.4916" height="50.4916" rotation="105"/>
36+
<object id="8" gid="21" x="79.3152" y="163" width="50.4916" height="50.4916" rotation="105"/>
37+
<object id="9" gid="21" x="2" y="219.803" width="50.4916" height="50.4916"/>
38+
<object id="10" gid="3" x="102.983" y="270.295" width="50.4916" height="50.4916" rotation="-90"/>
39+
</objectgroup>
40+
</map>

internal/utils/utils.go

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
Copyright (c) 2017 Lauris Bukšis-Haberkorns <lauris@nix.lv>
3+
4+
Permission is hereby granted, free of charge, to any person obtaining a copy
5+
of this software and associated documentation files (the "Software"), to deal
6+
in the Software without restriction, including without limitation the rights
7+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
copies of the Software, and to permit persons to whom the Software is
9+
furnished to do so, subject to the following conditions:
10+
11+
The above copyright notice and this permission notice shall be included in all
12+
copies or substantial portions of the Software.
13+
14+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20+
SOFTWARE.
21+
*/
22+
23+
// Package utils contains some generic internal utilities
24+
package utils
25+
26+
import (
27+
"sort"
28+
)
29+
30+
// SortAnySlice sorts a slice with given less method
31+
func SortAnySlice[T any](data []T, lessMethod func(a, b T) bool) []T {
32+
s := &sortable[T]{
33+
data: data,
34+
lessMethod: lessMethod,
35+
}
36+
sort.Sort(s)
37+
return s.data
38+
}
39+
40+
type sortable[T any] struct {
41+
data []T
42+
lessMethod func(a, b T) bool
43+
}
44+
45+
// Swap implements from sort.Interface
46+
func (s *sortable[T]) Swap(i, j int) {
47+
tmp := (s.data)[i]
48+
(s.data)[i] = (s.data)[j]
49+
(s.data)[j] = tmp
50+
}
51+
52+
// Less implements from sort.Interface
53+
func (s *sortable[T]) Less(i, j int) bool {
54+
return s.lessMethod(s.data[i], s.data[j])
55+
}
56+
57+
// Len implements from sort.Interface
58+
func (s *sortable[T]) Len() int {
59+
return len(s.data)
60+
}

render/render_objects.go

+234
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
/*
2+
Copyright (c) 2017 Lauris Bukšis-Haberkorns <lauris@nix.lv>
3+
4+
Permission is hereby granted, free of charge, to any person obtaining a copy
5+
of this software and associated documentation files (the "Software"), to deal
6+
in the Software without restriction, including without limitation the rights
7+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
copies of the Software, and to permit persons to whom the Software is
9+
furnished to do so, subject to the following conditions:
10+
11+
The above copyright notice and this permission notice shall be included in all
12+
copies or substantial portions of the Software.
13+
14+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20+
SOFTWARE.
21+
*/
22+
23+
package render
24+
25+
import (
26+
"image"
27+
"image/color"
28+
"image/draw"
29+
"math"
30+
31+
"github.com/lafriks/go-tiled"
32+
"github.com/lafriks/go-tiled/internal/utils"
33+
34+
"github.com/disintegration/imaging"
35+
)
36+
37+
// RenderVisibleGroups renders all visible groups
38+
func (r *Renderer) RenderVisibleGroups() error {
39+
for _, group := range r.m.Groups {
40+
if !group.Visible {
41+
continue
42+
}
43+
if err := r._renderGroup(group); err != nil {
44+
return err
45+
}
46+
}
47+
return nil
48+
}
49+
50+
// RenderGroup renders single group.
51+
func (r *Renderer) RenderGroup(groupID int) error {
52+
if groupID >= len(r.m.Groups) {
53+
return ErrOutOfBounds
54+
}
55+
56+
group := r.m.Groups[groupID]
57+
return r._renderGroup(group)
58+
}
59+
60+
func (r *Renderer) _renderGroup(group *tiled.Group) error {
61+
for _, layer := range group.Layers {
62+
if !layer.Visible {
63+
continue
64+
}
65+
if err := r._renderLayer(layer); err != nil {
66+
return err
67+
}
68+
}
69+
70+
for _, objectGroup := range group.ObjectGroups {
71+
if !objectGroup.Visible {
72+
continue
73+
}
74+
if err := r._renderObjectGroup(objectGroup); err != nil {
75+
return err
76+
}
77+
}
78+
79+
return nil
80+
}
81+
82+
// RenderVisibleLayersAndObjectGroups render all layers and object groups, layer first, objectGroup second
83+
// so the order may be incorrect,
84+
// you may put them into different groups, then call RenderVisibleGroups
85+
func (r *Renderer) RenderVisibleLayersAndObjectGroups() error {
86+
// TODO: The order maybe incorrect
87+
88+
if err := r.RenderVisibleLayers(); err != nil {
89+
return err
90+
}
91+
return r.RenderVisibleObjectGroups()
92+
}
93+
94+
// RenderVisibleObjectGroups renders all visible object groups
95+
func (r *Renderer) RenderVisibleObjectGroups() error {
96+
for i, layer := range r.m.ObjectGroups {
97+
if !layer.Visible {
98+
continue
99+
}
100+
if err := r.RenderObjectGroup(i); err != nil {
101+
return err
102+
}
103+
}
104+
return nil
105+
}
106+
107+
// RenderObjectGroup renders a single object group
108+
func (r *Renderer) RenderObjectGroup(i int) error {
109+
if i >= len(r.m.ObjectGroups) {
110+
return ErrOutOfBounds
111+
}
112+
113+
layer := r.m.ObjectGroups[i]
114+
return r._renderObjectGroup(layer)
115+
}
116+
117+
func (r *Renderer) _renderObjectGroup(objectGroup *tiled.ObjectGroup) error {
118+
objs := objectGroup.Objects
119+
120+
// sort objects from left top to right down
121+
objs = utils.SortAnySlice(objs, func(a, b *tiled.Object) bool {
122+
if a.Y != b.Y {
123+
return a.Y < b.Y
124+
}
125+
126+
return a.X < b.X
127+
})
128+
129+
for _, obj := range objs {
130+
if err := r.renderOneObject(objectGroup, obj); err != nil {
131+
return err
132+
}
133+
}
134+
return nil
135+
}
136+
137+
// RenderGroupObjectGroup renders single object group in a certain group.
138+
func (r *Renderer) RenderGroupObjectGroup(groupID, objectGroupID int) error {
139+
if groupID >= len(r.m.Groups) {
140+
return ErrOutOfBounds
141+
}
142+
143+
group := r.m.Groups[groupID]
144+
145+
if objectGroupID >= len(group.ObjectGroups) {
146+
return ErrOutOfBounds
147+
}
148+
149+
layer := group.ObjectGroups[objectGroupID]
150+
return r._renderObjectGroup(layer)
151+
}
152+
153+
func (r *Renderer) renderOneObject(layer *tiled.ObjectGroup, o *tiled.Object) error {
154+
if !o.Visible {
155+
return nil
156+
}
157+
158+
if o.GID == 0 {
159+
// TODO: o.GID == 0
160+
return nil
161+
}
162+
163+
tile, err := r.m.TileGIDToTile(o.GID)
164+
if err != nil {
165+
return err
166+
}
167+
168+
img, err := r.getTileImage(tile)
169+
if err != nil {
170+
return err
171+
}
172+
173+
bounds := img.Bounds()
174+
srcSize := bounds.Size()
175+
dstSize := image.Pt(int(o.Width), int(o.Height))
176+
177+
if !srcSize.Eq(dstSize) {
178+
img = imaging.Resize(img, dstSize.X, dstSize.Y, imaging.NearestNeighbor)
179+
}
180+
181+
var originPoint image.Point
182+
183+
img, originPoint = r._rotateObjectImage(img, o.Rotation)
184+
185+
bounds = img.Bounds()
186+
pos := bounds.Add(image.Pt(int(o.X), int(o.Y)).Sub(originPoint))
187+
188+
if layer.Opacity < 1 {
189+
mask := image.NewUniform(color.Alpha{uint8(layer.Opacity * 255)})
190+
191+
draw.DrawMask(r.Result, pos, img, img.Bounds().Min, mask, mask.Bounds().Min, draw.Over)
192+
} else {
193+
draw.Draw(r.Result, pos, img, img.Bounds().Min, draw.Over)
194+
}
195+
196+
return nil
197+
}
198+
199+
func (r *Renderer) _rotateObjectImage(img image.Image, rotation float64) (newImage image.Image, originPoint image.Point) {
200+
bounds := img.Bounds()
201+
w := bounds.Dx()
202+
h := bounds.Dy()
203+
points := []image.Point{
204+
image.Pt(0, 0),
205+
image.Pt(w-1, 0),
206+
image.Pt(w-1, h-1),
207+
image.Pt(0, h-1),
208+
}
209+
210+
sin, cos := math.Sincos(math.Pi * rotation / 180)
211+
212+
rotatedPointsX := []float64{}
213+
rotatedPointsY := []float64{}
214+
215+
for _, p := range points {
216+
x := float64(p.X)
217+
y := float64(p.Y)
218+
219+
rotatedPointsX = append(rotatedPointsX, x*cos-y*sin)
220+
rotatedPointsY = append(rotatedPointsY, x*sin+y*cos)
221+
}
222+
223+
rotatedMinX := rotatedPointsX[0]
224+
rotatedMinY := rotatedPointsY[0]
225+
226+
for i := 1; i < 4; i++ {
227+
rotatedMinX = math.Min(rotatedMinX, rotatedPointsX[i])
228+
rotatedMinY = math.Min(rotatedMinY, rotatedPointsY[i])
229+
}
230+
231+
originPoint = image.Pt(int(rotatedPointsX[3]-rotatedMinX), int(rotatedPointsY[3]-rotatedMinY))
232+
233+
return imaging.Rotate(img, -rotation, color.RGBA{}), originPoint
234+
}

render/render_objects_test.go

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
Copyright (c) 2023 Lauris Bukšis-Haberkorns <lauris@nix.lv>
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is
8+
furnished to do so, subject to the following conditions:
9+
The above copyright notice and this permission notice shall be included in all
10+
copies or substantial portions of the Software.
11+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
12+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
13+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
14+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
15+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
16+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
17+
SOFTWARE.
18+
*/
19+
20+
package render
21+
22+
import (
23+
"os"
24+
"testing"
25+
26+
"github.com/lafriks/go-tiled"
27+
)
28+
29+
func TestRenderer_RenderObjectGroup(t *testing.T) {
30+
tiledMap, err := tiled.LoadFile("../assets/test_render_objects.tmx")
31+
if err != nil {
32+
t.Error(err)
33+
return
34+
}
35+
36+
renderer, err := NewRenderer(tiledMap)
37+
if err != nil {
38+
t.Error(err)
39+
return
40+
}
41+
42+
renderer.RenderObjectGroup(0)
43+
44+
w, _ := os.Create("../assets/test_render_objects.png")
45+
defer w.Close()
46+
47+
if err = renderer.SaveAsPng(w); err != nil {
48+
t.Error(err)
49+
}
50+
}

0 commit comments

Comments
 (0)