voxelamming_gem 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,524 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "voxelamming_gem/version"
4
+ require 'json'
5
+ require 'faye/websocket'
6
+ require 'eventmachine'
7
+ require 'date'
8
+ require_relative 'matrix_util'
9
+ require 'csv'
10
+
11
+ module VoxelammingGem
12
+ class Error < StandardError; end
13
+
14
+ # test
15
+ def self.greet(name)
16
+ puts "Hello, #{name}! I'm Ruby!"
17
+ end
18
+
19
+ # Main process
20
+ class BuildBox
21
+ @@texture_names = ["grass", "stone", "dirt", "planks", "bricks"]
22
+
23
+ def initialize(room_name)
24
+ @room_name = room_name
25
+ @is_allowed_matrix = 0
26
+ @saved_matrices = []
27
+ @translation = [0, 0, 0, 0, 0, 0]
28
+ @matrix_translation = [0, 0, 0, 0, 0, 0]
29
+ @frame_translations = []
30
+ @global_animation = [0, 0, 0, 0, 0, 0, 1, 0]
31
+ @animation = [0, 0, 0, 0, 0, 0, 1, 0]
32
+ @boxes = []
33
+ @frames = []
34
+ @sentence = []
35
+ @lights = []
36
+ @commands = []
37
+ @size = 1
38
+ @shape = 'box'
39
+ @is_metallic = 0
40
+ @roughness = 0.5
41
+ @is_allowed_float = 0
42
+ @build_interval = 0.01
43
+ @is_framing = false
44
+ @frame_id = 0
45
+ end
46
+
47
+ def clear_data
48
+ @translation = [0, 0, 0, 0, 0, 0]
49
+ @is_allowed_matrix = 0
50
+ @saved_matrices = []
51
+ @translation = [0, 0, 0, 0, 0, 0]
52
+ @matrix_translation = [0, 0, 0, 0, 0, 0]
53
+ @frame_translations = []
54
+ @global_animation = [0, 0, 0, 0, 0, 0, 1, 0]
55
+ @animation = [0, 0, 0, 0, 0, 0, 1, 0]
56
+ @boxes = []
57
+ @frames = []
58
+ @sentence = []
59
+ @lights = []
60
+ @commands = []
61
+ @size = 1
62
+ @shape = 'box'
63
+ @is_metallic = 0
64
+ @roughness = 0.5
65
+ @is_allowed_float = 0
66
+ @build_interval = 0.01
67
+ @is_framing = false
68
+ @frame_id = 0
69
+ end
70
+
71
+ def set_frame_fps(fps = 2)
72
+ @commands << "fps #{fps}"
73
+ end
74
+
75
+ def set_frame_repeats(repeats = 10)
76
+ @commands << "repeats #{repeats}"
77
+ end
78
+
79
+ def frame_in
80
+ @is_framing = true
81
+ end
82
+
83
+ def frame_out
84
+ @is_framing = false
85
+ @frame_id += 1
86
+ end
87
+
88
+ def push_matrix
89
+ @is_allowed_matrix += 1
90
+ @saved_matrices.push(@matrix_translation)
91
+ end
92
+
93
+ def pop_matrix
94
+ @is_allowed_matrix -= 1
95
+ @matrix_translation = @saved_matrices.pop
96
+ end
97
+
98
+ def translate(x, y, z, pitch: 0, yaw: 0, roll: 0)
99
+ if @is_allowed_matrix > 0
100
+ # 移動用のマトリックスを計算する
101
+ matrix = @saved_matrices.last
102
+ puts "before matrix: #{matrix}"
103
+ base_position = matrix[0..2]
104
+
105
+ if matrix.length == 6
106
+ base_rotation_matrix = get_rotation_matrix(matrix[3], matrix[4], matrix[5])
107
+ else
108
+ base_rotation_matrix = [matrix[3..5], matrix[6..8], matrix[9..11]]
109
+ end
110
+
111
+ # Compute the new position after translation
112
+ add_x, add_y, add_z = transform_point_by_rotation_matrix([x, y, z], transpose_3x3(base_rotation_matrix))
113
+ x, y, z = add_vectors(base_position, [add_x, add_y, add_z])
114
+ x, y, z = round_numbers([x, y, z])
115
+
116
+ # Compute the rotation after translation
117
+ translate_rotation_matrix = get_rotation_matrix(-pitch, -yaw, -roll)
118
+ rotate_matrix = matrix_multiply(translate_rotation_matrix, base_rotation_matrix)
119
+ @matrix_translation = [x, y, z, *rotate_matrix[0], *rotate_matrix[1], *rotate_matrix[2]]
120
+ else
121
+ x, y, z = round_numbers([x, y, z])
122
+
123
+ if @is_framing
124
+ @frame_translations.append([x, y, z, pitch, yaw, roll, @frame_id])
125
+ else
126
+ @translation = [x, y, z, pitch, yaw, roll]
127
+ end
128
+ end
129
+ end
130
+
131
+ def create_box(x, y, z, r: 1, g: 1, b: 1, alpha: 1, texture: '')
132
+ if @is_allowed_matrix > 0
133
+ # 移動用のマトリックスにより位置を計算する
134
+ matrix = @matrix_translation
135
+ base_position = matrix[0..2]
136
+
137
+ if matrix.length == 6
138
+ base_rotation_matrix = get_rotation_matrix(matrix[3], matrix[4], matrix[5])
139
+ else
140
+ base_rotation_matrix = [matrix[3..5], matrix[6..8], matrix[9..11]]
141
+ end
142
+
143
+ # Compute the new position after translation
144
+ add_x, add_y, add_z = transform_point_by_rotation_matrix([x, y, z], transpose_3x3(base_rotation_matrix))
145
+ x, y, z = add_vectors(base_position, [add_x, add_y, add_z])
146
+ end
147
+
148
+ x, y, z = round_numbers([x, y, z])
149
+ r, g, b, alpha = round_colors([r, g, b, alpha])
150
+
151
+ # 重ねておくことを防止
152
+ remove_box(x, y, z)
153
+ if !@@texture_names.include?(texture)
154
+ texture_id = -1
155
+ else
156
+ texture_id = @@texture_names.index(texture)
157
+ end
158
+
159
+ if @is_framing
160
+ @frames.append([x, y, z, r, g, b, alpha, texture_id, @frame_id])
161
+ else
162
+ @boxes.append([x, y, z, r, g, b, alpha, texture_id])
163
+ end
164
+ end
165
+
166
+ def remove_box(x, y, z)
167
+ x, y, z = round_numbers([x, y, z])
168
+
169
+ if @is_framing
170
+ @frames.reject! { |frame| frame[0] == x && frame[1] == y && frame[2] == z && frame[8] == @frame_id }
171
+ else
172
+ @boxes.reject! { |box| box[0] == x && box[1] == y && box[2] == z }
173
+ end
174
+ end
175
+
176
+ def animate_global(x, y, z, pitch: 0, yaw: 0, roll: 0, scale: 1, interval: 10)
177
+ x, y, z = round_numbers([x, y, z])
178
+ @global_animation = [x, y, z, pitch, yaw, roll, scale, interval]
179
+ end
180
+
181
+ def animate(x, y, z, pitch: 0, yaw: 0, roll: 0, scale: 1, interval: 10)
182
+ x, y, z = round_numbers([x, y, z])
183
+ @animation = [x, y, z, pitch, yaw, roll, scale, interval]
184
+ end
185
+
186
+ def set_box_size(box_size)
187
+ @size = box_size
188
+ end
189
+
190
+ def set_build_interval(interval)
191
+ @build_interval = interval
192
+ end
193
+
194
+ def write_sentence(sentence, x, y, z, r: 1, g: 1, b: 1, alpha: 1)
195
+ x, y, z = round_numbers([x, y, z]).map(&:to_s)
196
+ r, g, b, alpha = round_colors([r, g, b, alpha])
197
+ r, g, b, alpha = [r, g, b, alpha].map(&:floor).map(&:to_s)
198
+ @sentence = [sentence, x, y, z, r, g, b, alpha]
199
+ end
200
+
201
+ def set_light(x, y, z, r: 1, g: 1, b: 1, alpha: 1, intensity: 1000, interval: 1, light_type: 'point')
202
+ x, y, z = round_numbers([x, y, z])
203
+ r, g, b, alpha = round_colors([r, g, b, alpha])
204
+
205
+ if light_type == 'point'
206
+ light_type = 1
207
+ elsif light_type == 'spot'
208
+ light_type = 2
209
+ elsif light_type == 'directional'
210
+ light_type = 3
211
+ else
212
+ light_type = 1
213
+ end
214
+ @lights << [x, y, z, r, g, b, alpha, intensity, interval, light_type]
215
+ end
216
+
217
+ def set_command(command)
218
+ @commands << command
219
+
220
+ if command == 'float'
221
+ @is_allowed_float = 1
222
+ end
223
+ end
224
+
225
+ def draw_line(x1, y1, z1, x2, y2, z2, r: 1, g: 1, b: 1, alpha: 1)
226
+ x1, y1, z1, x2, y2, z2 = [x1, y1, z1, x2, y2, z2].map(&:floor)
227
+ diff_x = x2 - x1
228
+ diff_y = y2 - y1
229
+ diff_z = z2 - z1
230
+ max_length = [diff_x.abs, diff_y.abs, diff_z.abs].max
231
+
232
+ return false if diff_x == 0 && diff_y == 0 && diff_z == 0
233
+
234
+ if diff_x.abs == max_length
235
+ if x2 > x1
236
+ (x1..x2).each do |x|
237
+ y = y1 + (x - x1) * diff_y.to_f / diff_x
238
+ z = z1 + (x - x1) * diff_z.to_f / diff_x
239
+ create_box(x, y, z, r: r, g: g, b: b, alpha: alpha)
240
+ end
241
+ else
242
+ x1.downto(x2 + 1) do |x|
243
+ y = y1 + (x - x1) * diff_y.to_f / diff_x
244
+ z = z1 + (x - x1) * diff_z.to_f / diff_x
245
+ create_box(x, y, z, r: r, g: g, b: b, alpha: alpha)
246
+ end
247
+ end
248
+ elsif diff_y.abs == max_length
249
+ if y2 > y1
250
+ (y1..y2).each do |y|
251
+ x = x1 + (y - y1) * diff_x.to_f / diff_y
252
+ z = z1 + (y - y1) * diff_z.to_f / diff_y
253
+ create_box(x, y, z, r: r, g: g, b: b, alpha: alpha)
254
+ end
255
+ else
256
+ y1.downto(y2 + 1) do |y|
257
+ x = x1 + (y - y1) * diff_x.to_f / diff_y
258
+ z = z1 + (y - y1) * diff_z.to_f / diff_y
259
+ create_box(x, y, z, r: r, g: g, b: b, alpha: alpha)
260
+ end
261
+ end
262
+ elsif diff_z.abs == max_length
263
+ if z2 > z1
264
+ (z1..z2).each do |z|
265
+ x = x1 + (z - z1) * diff_x.to_f / diff_z
266
+ y = y1 + (z - z1) * diff_y.to_f / diff_z
267
+ create_box(x, y, z, r: r, g: g, b: b, alpha: alpha)
268
+ end
269
+ else
270
+ z1.downto(z2 + 1) do |z|
271
+ x = x1 + (z - z1) * diff_x.to_f / diff_z
272
+ y = y1 + (z - z1) * diff_y.to_f / diff_z
273
+ create_box(x, y, z, r: r, g: g, b: b, alpha: alpha)
274
+ end
275
+ end
276
+ end
277
+ end
278
+
279
+ def change_shape(shape)
280
+ @shape = shape
281
+ end
282
+
283
+ def change_material(is_metallic: false, roughness: 0.5)
284
+ @is_metallic = is_metallic ? 1 : 0
285
+ @roughness = roughness
286
+ end
287
+
288
+ def send_data
289
+ puts 'send_data'
290
+ now = DateTime.now
291
+ data_to_send = {
292
+ "translation": @translation,
293
+ "frameTranslations": @frame_translations,
294
+ "globalAnimation": @global_animation,
295
+ "animation": @animation,
296
+ "boxes": @boxes,
297
+ "frames": @frames,
298
+ "sentence": @sentence,
299
+ "lights": @lights,
300
+ "commands": @commands,
301
+ "size": @size,
302
+ "shape": @shape,
303
+ "interval": @build_interval,
304
+ "isMetallic": @is_metallic,
305
+ "roughness": @roughness,
306
+ "isAllowedFloat": @is_allowed_float,
307
+ "date": now.to_s
308
+ }.to_json
309
+
310
+ EM.run do
311
+ ws = Faye::WebSocket::Client.new('wss://websocket.voxelamming.com')
312
+
313
+ ws.on :open do |_event|
314
+ p [:open]
315
+ puts 'WebSocket connection open'
316
+ ws.send(@room_name)
317
+ puts "Joined room: #{@room_name}"
318
+ ws.send(data_to_send)
319
+ puts data_to_send
320
+ puts 'Sent data to server'
321
+
322
+ EM.add_timer(1) do
323
+ ws.close
324
+ EM.stop
325
+ end
326
+ end
327
+
328
+ ws.on :error do |event|
329
+ puts "WebSocket error: #{event.message}"
330
+ EM.stop
331
+ end
332
+
333
+ ws.on :close do |_event|
334
+ puts 'WebSocket connection closed'
335
+ EM.stop
336
+ end
337
+ end
338
+ end
339
+
340
+ def round_numbers(num_list)
341
+ if @is_allowed_float == 1
342
+ num_list.map { |val| val.round(2) }
343
+ else
344
+ num_list.map { |val| val.round(1).floor }
345
+ end
346
+ end
347
+
348
+ def round_colors(num_list)
349
+ num_list.map { |val| val.round(2) }
350
+ end
351
+ end
352
+
353
+ # Turtle graphics
354
+ class Turtle
355
+ include Math
356
+
357
+ def initialize(build_box)
358
+ @build_box = build_box
359
+ @x = 0
360
+ @y = 0
361
+ @z = 0
362
+ @polar_theta = 90
363
+ @polar_phi = 0
364
+ @drawable = true
365
+ @color = [0, 0, 0, 1]
366
+ @size = 1
367
+ end
368
+
369
+ def forward(length)
370
+ z = @z + length * sin(radians(@polar_theta)) * cos(radians(@polar_phi))
371
+ x = @x + length * sin(radians(@polar_theta)) * sin(radians(@polar_phi))
372
+ y = @y + length * cos(radians(@polar_theta))
373
+ x, y, z = x.round(3), y.round(3), z.round(3)
374
+
375
+ if @drawable
376
+ @build_box.draw_line(@x, @y, @z, x, y, z, r: @color[0], g: @color[1], b: @color[2])
377
+ end
378
+
379
+ @x = x
380
+ @y = y
381
+ @z = z
382
+ end
383
+
384
+ def backward(length)
385
+ forward(-length)
386
+ end
387
+
388
+ def up(degree)
389
+ @polar_theta -= degree
390
+ end
391
+
392
+ def down(degree)
393
+ @polar_theta += degree
394
+ end
395
+
396
+ def right(degree)
397
+ @polar_phi -= degree
398
+ end
399
+
400
+ def left(degree)
401
+ @polar_phi += degree
402
+ end
403
+
404
+ def set_color(r, g, b, alpha = 1)
405
+ @color = [r, g, b, alpha]
406
+ end
407
+
408
+ def pen_down
409
+ @drawable = true
410
+ end
411
+
412
+ def pen_up
413
+ @drawable = false
414
+ end
415
+
416
+ def set_pos(x, y, z)
417
+ @x = x
418
+ @y = y
419
+ @z = z
420
+ end
421
+
422
+ def reset
423
+ @x = 0
424
+ @y = 0
425
+ @z = 0
426
+ @polar_theta = 90
427
+ @polar_phi = 0
428
+ @drawable = true
429
+ @color = [0, 0, 0, 1]
430
+ @size = 1
431
+ end
432
+
433
+ private
434
+
435
+ def radians(degrees)
436
+ degrees * Math::PI / 180
437
+ end
438
+ end
439
+
440
+ # Make model
441
+ def self.get_boxes_from_ply(ply_file)
442
+ box_positions = Set.new
443
+ File.open("./ply_file/#{ply_file}", 'r') do |f|
444
+ lines = f.read
445
+ lines = lines.gsub("\r\n", "\n")
446
+ lines = lines.strip
447
+ positions = lines.split("\n").select { |ln| self.is_included_six_numbers(ln) }.map { |ln| ln.split.map(&:to_f) }
448
+
449
+ number_of_faces = positions.length / 4
450
+ (0...number_of_faces).each do |i|
451
+ vertex1 = positions[i * 4]
452
+ vertex2 = positions[i * 4 + 1]
453
+ vertex3 = positions[i * 4 + 2]
454
+ vertex4 = positions[i * 4 + 3] # no need
455
+ x = [vertex1[0], vertex2[0], vertex3[0]].min
456
+ y = [vertex1[1], vertex2[1], vertex3[1]].min
457
+ z = [vertex1[2], vertex2[2], vertex3[2]].min
458
+ r = vertex1[3] / 255.0
459
+ g = vertex1[4] / 255.0
460
+ b = vertex1[5] / 255.0
461
+ alpha = 1
462
+
463
+ # ボックスを置く方向を解析
464
+ if vertex1[0] == vertex2[0] && vertex2[0] == vertex3[0] # y-z plane
465
+ step = [vertex1[1], vertex2[1], vertex3[1]].max - y
466
+ x -= step if vertex1[1] != vertex2[1]
467
+ elsif vertex1[1] == vertex2[1] && vertex2[1] == vertex3[1] # z-x plane
468
+ step = [vertex1[2], vertex2[2], vertex3[2]].max - z
469
+ y -= step if vertex1[2] != vertex2[2]
470
+ else # x-y plane
471
+ step = [vertex1[0], vertex2[0], vertex3[0]].max - x
472
+ z -= step if vertex1[0] != vertex2[0]
473
+ end
474
+
475
+ # minimum unit: 0.1
476
+ position_x = (x * 10.0 / step).round / 10.0
477
+ position_y = (y * 10.0 / step).round / 10.0
478
+ position_z = (z * 10.0 / step).round / 10.0
479
+ box_positions.add([position_x, position_z, -position_y, r, g, b, alpha])
480
+ end
481
+ end
482
+
483
+ box_positions
484
+ end
485
+
486
+ def self.is_included_six_numbers(line)
487
+ line_list = line.split
488
+ return false if line_list.length != 6
489
+
490
+ line_list.all? { |num| Float(num) rescue false }
491
+ end
492
+
493
+ # Map
494
+ $column_num, $row_num = 257, 257
495
+
496
+ def self.get_map_data_from_csv(csv_file, height_scale)
497
+ # csvファイルから地図データを読み込み
498
+ heights = []
499
+
500
+ CSV.foreach("./map_file/#{csv_file}") do |row|
501
+ for h in row
502
+ h = h.to_f
503
+ h = h != 0 ? (h * height_scale).floor : -1
504
+ heights << h
505
+ end
506
+ end
507
+ # puts "heights: #{heights}"
508
+
509
+ max_height = heights.max.floor
510
+ box_positions = (0...$row_num).map { |i| heights[i * $column_num, $column_num] }
511
+ puts "max_height: #{max_height}"
512
+ # puts "box_positions: #{box_positions}"
513
+ { 'boxes' => box_positions, 'max_height' => max_height }
514
+ end
515
+
516
+ def self.get_box_color(height, max_height, high_color, low_color)
517
+ # 高さによって色を変える
518
+ r = (high_color[0] - low_color[0]) * height * 1.0 / max_height + low_color[0]
519
+ g = (high_color[1] - low_color[1]) * height * 1.0 / max_height + low_color[1]
520
+ b = (high_color[2] - low_color[2]) * height * 1.0 / max_height + low_color[2]
521
+
522
+ [r, g, b]
523
+ end
524
+ end
data/main.rb ADDED
@@ -0,0 +1,21 @@
1
+ require 'voxelamming_gem'
2
+
3
+ room_name = '1000'
4
+ build_box = VoxelammingGem::BuildBox.new(room_name)
5
+
6
+ build_box.set_box_size(0.5)
7
+ build_box.set_build_interval(0.01)
8
+
9
+ for i in 0...100
10
+ build_box.create_box(-1, i, 0, r: 0, g: 1, b: 1)
11
+ build_box.create_box(0, i, 0, r: 1, g: 0, b: 0)
12
+ build_box.create_box(1, i, 0, r: 1, g: 1, b: 0)
13
+ build_box.create_box(2, i, 0, r: 0, g: 1, b: 1)
14
+ end
15
+
16
+ for i in 0...50
17
+ build_box.remove_box(0, i * 2, 0)
18
+ build_box.remove_box(1, i * 2 + 1, 0)
19
+ end
20
+
21
+ build_box.send_data
@@ -0,0 +1,18 @@
1
+ require 'voxelamming_gem'
2
+
3
+ room_name = '1000'
4
+ build_box = VoxelammingGem::BuildBox.new(room_name)
5
+
6
+ build_box.set_box_size(0.5)
7
+ build_box.set_build_interval(0.01)
8
+
9
+ ply_file_name = 'piyo.ply'
10
+
11
+ boxes = VoxelammingGem.get_boxes_from_ply(ply_file_name)
12
+ # puts boxes
13
+
14
+ boxes.each do |b|
15
+ build_box.create_box(b[0], b[1], b[2], r: b[3], g: b[4], b: b[5])
16
+ end
17
+
18
+ build_box.send_data
data/main_map.rb ADDED
@@ -0,0 +1,37 @@
1
+ require 'voxelamming_gem'
2
+
3
+ room_name = "1000"
4
+ build_box = VoxelammingGem::BuildBox.new(room_name)
5
+
6
+ build_box.set_box_size(0.1)
7
+ build_box.set_build_interval(0.001)
8
+ build_box.set_command('liteRender')
9
+
10
+ column_num, row_num = 257, 257
11
+ csv_file = 'map_38_138_100km.csv'
12
+ height_scale = 100
13
+ high_color = [0.5, 0, 0]
14
+ low_color = [0, 1, 0]
15
+ map_data = VoxelammingGem.get_map_data_from_csv(csv_file, height_scale)
16
+ boxes = map_data['boxes']
17
+ max_height = map_data['max_height']
18
+ # skip = 1 # high power device
19
+ skip = 2 # normal device
20
+ # skip = 4 # low power device
21
+
22
+ (row_num / skip).times do |j|
23
+ (column_num / skip).times do |i|
24
+ # puts "#{i} #{j}"
25
+ x = i - column_num / (skip * 2).floor
26
+ z = j - row_num / (skip * 2).floor
27
+ y = boxes[j * skip][i * skip]
28
+
29
+ if y >= 0
30
+ r, g, b = VoxelammingGem.get_box_color(y, max_height, high_color, low_color)
31
+ # print("r: #{r}, g: #{g}, b: #{b}\n")
32
+ build_box.create_box(x, y, z, r: r, g: g, b: b)
33
+ end
34
+ end
35
+ end
36
+
37
+ build_box.send_data()