├── .gitignore
├── .npmignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── engine
├── animation.coffee
├── armature.coffee
├── behaviour.coffee
├── camera.coffee
├── color.coffee
├── cubemap.coffee
├── curve.coffee
├── custom_fetch_polyfills.coffee
├── debug_camera.coffee
├── debug_draw.coffee
├── effects
│ ├── FXAA.coffee
│ ├── SSAO.coffee
│ ├── SSAO.glsl
│ ├── base.coffee
│ ├── bloom.coffee
│ ├── graph.coffee
│ └── index.coffee
├── fetch_assets.coffee
├── filters.coffee
├── framebuffer.coffee
├── gameobject.coffee
├── glray.coffee
├── init.coffee
├── input.coffee
├── lamp.coffee
├── libs
│ ├── ammo.asm.js
│ ├── ammo.wasm.js
│ ├── ammo.wasm.wasm
│ └── crunch.js
├── loader.coffee
├── main_loop.coffee
├── material.coffee
├── material_shaders
│ ├── blender_cycles_pbr.coffee
│ ├── blender_internal.coffee
│ └── plain.coffee
├── math_utils
│ ├── g2.coffee
│ ├── g3.coffee
│ ├── math_extra.coffee
│ └── vmath_extra.coffee
├── mesh.coffee
├── mesh_factory.coffee
├── myou.coffee
├── node_fetch_file.coffee
├── physics
│ └── bullet.coffee
├── probe.coffee
├── render.coffee
├── scene.coffee
├── screen.coffee
├── texture.coffee
├── vertex_modifiers.coffee
├── viewport.coffee
└── webvr.coffee
├── main.js
├── pack.coffee
├── package-lock.json
├── package.json
└── webpack.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | /**/node_modules/*
2 | /node_modules/
3 | /_cache/*
4 |
5 | #webpack builds
6 | **/build/*
7 | /example/build/*
8 |
9 | #temporal files
10 | *.blend1
11 | *.blend2
12 | /assets/*
13 | *~
14 | .directory
15 | *.orig
16 | *.rej
17 | __pycache__
18 |
19 | #log files
20 | npm-debug.log
21 |
22 | #local files
23 | local/
24 | *.local.*
25 | Thumbs.db
26 | *.local
27 | /dist/*
28 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | /**/node_modules/*
2 | /node_modules/
3 | /_cache/*
4 |
5 | #webpack builds
6 | **/build/*
7 | /example/build/*
8 |
9 | #temporal files
10 | *.blend1
11 | *.blend2
12 | /assets/*
13 | *~
14 | .directory
15 | *.orig
16 | *.rej
17 | __pycache__
18 |
19 | #log files
20 | npm-debug.log
21 |
22 | #local files
23 | local/
24 | *.local.*
25 | Thumbs.db
26 | *.local
27 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ## [0.4.2](https://github.com/myou-engine/myou-engine) (2017-05-15)
4 |
5 |
6 | ### Features
7 |
8 | * **Documentation:** Added CHANGELOG.md.
9 | * **Documentation:** Added some code documentation to be parsed by codo from the repo [myou-engine-doc](https://github.com/myou-engine/myou-engine-doc).
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016 by Alberto Torres Ruiz
2 | Copyright (c) 2016 by Julio Manuel López Tercero
3 |
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Myou
2 |
3 | Myou Engine is a game engine for web, for mobile and for VR (soon).
4 |
5 | It's designed for easy of use with **Blender**, packing as many features as possible in a small package of **120 kb** (gzipped) and an approachable source code for both beginners and experts.
6 |
7 | It supports both **Cycles/Eevee materials** (WYSIWYG with [Blender PBR branch] and the future Blender 2.8), and **Blender internal/Blender game** materials.
8 |
9 | It's [FOSS](https://en.wikipedia.org/wiki/Free_and_open-source_software) published under the industry-friendly MIT license.
10 |
11 | ## Features
12 | * Blender based exporter and editor.
13 | * Efficient data formats.
14 | * Pluggable API for mesh modifiers and materials.
15 | * Support for Blender internal/Blender game GLSL materials and nodes.
16 | * Support for Blender Cycles nodes (WYSIWYG with [Blender PBR branch] and the future Blender 2.8).
17 | * Environment maps, soft shadows, reflection, refraction, etc.
18 | * Armatures with constraints (including IK).
19 | * Shape keys.
20 | * Support for animations including mixing with Blender NLA.
21 | * Animation of any attribute, including any material parameter.
22 | * Support for animation drivers from Blender.
23 | * Automatic LoD based on multi-resolution, subsurf and decimation.
24 | * Physics: Currently Bullet (ammo.js when running in JS) is supported.
25 | * Deferred loading of the physics engine and physic objects, for fast startup times.
26 | * Multiple self-contained engine instances are allowed on the same webpage.
27 | * Simple game-oriented event system for mouse, touch, keyboard and game input devices.
28 | * WebVR support.
29 | * Native Vulkan support with VR (soon).
30 |
31 | For a more visual introduction see http://myou.cat/#engine/features
32 |
33 | ## Supported platforms
34 | * Web browsers with __WebGL__ support, including mobile devices. It can use WebGL 2 where available.
35 | * Any platform supported by Rust-lang and with Vulkan (soon).
36 |
37 | -----
38 | ## Usage
39 | Go to http://myou.cat/#engine/tutorials
40 |
41 | ## Documentation
42 | We are working on the documentation. It will be added soon.
43 |
44 | ## Feedback
45 |
46 | You can send any feedback or question to:
47 | * Julio Manuel López
48 | * Alberto Torres Ruiz
49 |
--------------------------------------------------------------------------------
/engine/armature.coffee:
--------------------------------------------------------------------------------
1 | {GameObject} = require './gameobject'
2 | {mat4, vec3, quat} = require 'vmath'
3 |
4 | # FUTURE OPTIMIZATION STRATEGIES
5 | # Make a single flat array for positions and rotations,
6 | # Sending them as textures where available
7 | # Or as uniformMatrix4fv where not
8 | # Baking animation loops into spare framebuffer texture
9 | # sending bone locations in parent space instead of local
10 |
11 | # Uniforms (in armature space) = parent uniform * base pose * local
12 |
13 | #UNIT_MAT4 = mat4.create()
14 | VECTOR_Y = vec3.new 0, 1, 0
15 |
16 | class Bone
17 | constructor: (@context)->
18 | # Base pose position and rotation in PARENT space
19 | @base_position = vec3.create()
20 | @base_rotation = quat.create()
21 | # Position and rotation in LOCAL (base) space
22 | @position = vec3.create()
23 | @rotation = quat.create()
24 | #@rotation_order = 'Q'
25 | @scale = vec3.new 1, 1, 1
26 | # parents and constraints will set those
27 | @final_position = vec3.create()
28 | @final_rotation = quat.create()
29 | @final_scale = vec3.new 1, 1, 1
30 | # which will be used to compute
31 | @matrix = mat4.create()
32 | # Object local matrix (relative to rest pose)
33 | @ol_matrix = mat4.create()
34 | @parent = null
35 | @index = 0
36 | # TODO: probably it was faster to use this for constraintless
37 | # armatures (test)
38 | #@parent_matrix = UNIT_MAT4 # pointer to parent's
39 |
40 | # Set at the beginning, using recalculate_bone_matrices
41 | # with the rest pose and without constraints
42 | @inv_rest_matrix = mat4.create()
43 | @deform_id = -1
44 | @blength = 1.0
45 | @constraints = []
46 | @object_children = []
47 | # This is the inverse of bone parenting and since Blender doesn't have
48 | # an equivalent, we use bone parent and invert it with
49 | # ob.convert_bone_child_to_bone_parent()
50 | @parent_object = null
51 |
52 | clone_to: (new_armature)->
53 | n = new Bone @context
54 | vec3.copy n.base_position, @base_position
55 | quat.copy n.base_rotation, @base_rotation
56 | vec3.copy n.position, @position
57 | quat.copy n.rotation, @rotation
58 | vec3.copy n.scale, @scale
59 | vec3.copy n.final_position, @final_position
60 | quat.copy n.final_rotation, @final_rotation
61 | vec3.copy n.final_scale, @final_scale
62 | mat4.copy n.matrix, @matrix
63 | mat4.copy n.ol_matrix, @ol_matrix
64 | mat4.copy n.inv_rest_matrix, @inv_rest_matrix
65 | if @parent?
66 | n.parent = new_armature._bone_list[@parent.index]
67 | n.index = @index
68 | n.deform_id = @deform_id
69 | n.blength = @blength
70 | n.constraints = @constraints # TODO: clone array?
71 | n.parent_object = @parent_object?.clone()
72 | # object_children will be assigned by armature
73 | return n
74 |
75 | class Armature extends GameObject
76 |
77 | type : 'ARMATURE'
78 |
79 | constructor: (context)->
80 | super context
81 | @bones = {} # all bones by name
82 | @_bone_list = [] # all bones, ordered, parents first
83 | @deform_bones = []
84 | @unfc = 0 # uniform count
85 | @_m = mat4.create()
86 | @pose = {}
87 |
88 | add_bones: (bones)->
89 | for b,i in bones
90 | bone = new Bone @context
91 | vec3.copyArray bone.base_position, b['position']
92 | quat.copyArray bone.base_rotation, b['rotation']
93 | deform_id = b['deform_id']
94 | if deform_id != -1
95 | bone.deform_id = deform_id
96 | @deform_bones[deform_id] = bone
97 | parent = b['parent']
98 | if parent != ""
99 | bone.parent = @bones[parent]
100 | #bone.parent_matrix = bone.parent.matrix
101 | # TODO: only for debug
102 | bone.blength = b.blength
103 | bone.index = i
104 | #bone.name = b.name
105 | @_bone_list.push bone
106 | @bones[b.name] = bone
107 | # Note: this relies on constraints not being evaluated
108 | # because they are not added yet
109 | @recalculate_bone_matrices()
110 | i = 0
111 | for bone in @_bone_list
112 | # Get inverse matrix from rest pose, which
113 | # is used in recalculate_bone_matrices
114 | mat4.invert bone.inv_rest_matrix, bone.matrix
115 | # Not needed for poses stored in actions
116 | i += 1
117 | for b in bones
118 | for c in b['constraints']
119 | c[0] = BoneConstraints.prototype[c[0]]
120 | c[1] = @bones[c[1]]
121 | c[2] = @bones[c[2]]
122 | @bones[b.name].constraints = b['constraints']
123 | return
124 |
125 | recalculate_bone_matrices: (use_constraints=true) ->
126 | inv = mat4.clone @world_matrix
127 | mat4.invert inv, inv
128 | for bone in @_bone_list when not bone.parent_object?
129 | pos = bone.final_position
130 | rot = quat.copy bone.final_rotation, bone.rotation
131 | scl = vec3.copy bone.final_scale, bone.scale
132 | vec3.transformQuat pos, bone.position, bone.base_rotation
133 | vec3.add pos, bone.base_position, pos
134 | quat.mul rot, bone.base_rotation, bone.rotation
135 |
136 | parent = bone.parent
137 | if parent
138 | vec3.mul scl, parent.final_scale, scl
139 | quat.mul rot, parent.final_rotation, rot
140 | vec3.mul pos, pos, parent.final_scale
141 | vec3.transformQuat pos, pos, parent.final_rotation
142 | vec3.add pos, pos, parent.final_position
143 |
144 | if use_constraints
145 | for con in bone.constraints
146 | con[0](con[1], con[2], con[3], con[4])
147 |
148 | for bone in @_bone_list
149 | m = bone.matrix
150 | ob = bone.parent_object
151 | if not ob?
152 | pos = bone.final_position
153 | rot = bone.final_rotation
154 | scl = bone.final_scale
155 | # TODO: scale is not calculated correctly
156 | # when parent's scale X!=Y!=Z
157 | mat4.fromRTS m, rot, pos, scl
158 | else
159 | mat4.mul m, ob.world_matrix, ob.properties.inv_bone_matrix
160 | mat4.mul m, inv, m
161 | # TODO: scale?
162 | mat4.toRT bone.final_rotation, bone.final_position, m
163 | mat4.mul bone.ol_matrix, m, bone.inv_rest_matrix, m
164 |
165 | return this
166 |
167 | apply_pose_arrays: (pose=@pose)->
168 | @pose = pose
169 | for bname of pose
170 | p = pose[bname]
171 | b = @bones[bname]
172 | vec3.copyArray b.position, p.position
173 | quat.copyArray b.rotation, p.rotation
174 | vec3.copyArray b.scale, p.scale
175 | return this
176 |
177 | apply_rigid_body_constraints: ->
178 | # set the pose of all bones to the middle of their limits,
179 | # to use them as base rotation of the cone constraint
180 | # (since the cone must be symmetrical and limits can be asymmetrical)
181 | for bname,bone of @bones
182 | pose = @pose[bname]
183 | {rotation} = bone
184 | quat.identity rotation
185 | {use_ik_limit_x, ik_min_x, ik_max_x,
186 | use_ik_limit_y, ik_min_y, ik_max_y,
187 | use_ik_limit_z, ik_min_z, ik_max_z} = pose
188 | if use_ik_limit_x
189 | quat.rotateX rotation, rotation, (ik_min_x+ik_max_x) * .5
190 | if use_ik_limit_y
191 | quat.rotateY rotation, rotation, (ik_min_y+ik_max_y) * .5
192 | if use_ik_limit_z
193 | quat.rotateZ rotation, rotation, (ik_min_z+ik_max_z) * .5
194 | @recalculate_bone_matrices(false)
195 | # invert parental relationship,
196 | # i.e. make the bone move with the object
197 | for bone in @_bone_list
198 | for c in bone.object_children by -1
199 | if c.body.type != 'NO_COLLISION'
200 | c.convert_bone_child_to_bone_parent()
201 | c.body.instance()
202 | rotation = @get_world_rotation()
203 | {world_matrix} = this
204 | rotZ90 = quat.create()
205 | quat.rotateZ rotZ90, rotZ90, Math.PI*.5
206 | for bname,bone of @bones
207 | # put bone in world space
208 | {final_position, final_rotation} = bone
209 | vec3.transformMat4 final_position, final_position, world_matrix
210 | quat.mul final_rotation, rotation, final_rotation
211 | # rotate bone 90 degrees in local Z axis, because cone twist
212 | # goes around Y axis on the bone and X axis on bullet
213 | quat.mul final_rotation, final_rotation, rotZ90
214 | for ob in bone.object_children when ob.body.type != 'NO_COLLISION'
215 | # if bone parent has no physics, it will be attached to
216 | # armature's parent body (if it has one)
217 | parent = @parent
218 | if bone.parent?
219 | for c in bone.parent.object_children
220 | if c.body.type != 'NO_COLLISION'
221 | parent = c
222 | break
223 | # if it has physical parent,
224 | # attach ob with parent with a rigid body constraint
225 | if parent? and parent.body.type != 'NO_COLLISION'
226 | # determine cone twist angles
227 | pose = @pose[bname]
228 | {ik_stiffness_x, use_ik_limit_x, ik_min_x, ik_max_x,
229 | ik_stiffness_y, use_ik_limit_y, ik_min_y, ik_max_y,
230 | ik_stiffness_z, use_ik_limit_z, ik_min_z, ik_max_z} = pose
231 | angle_x = angle_y = angle_z = Math.PI
232 | if use_ik_limit_x
233 | angle_x = (ik_max_x-ik_min_x) * .5
234 | if use_ik_limit_y
235 | angle_y = (ik_max_y-ik_min_y) * .5
236 | if use_ik_limit_z
237 | angle_z = (ik_max_z-ik_min_z) * .5
238 | ob.body.add_conetwist_constraint parent.body, final_position,
239 | final_rotation, angle_x, angle_y, angle_z
240 | break
241 | return this
242 |
243 | clone: (options, options2) ->
244 | n = super options, options2
245 | n.bones = bones = {}
246 | n._bone_list = list = []
247 | n.deform_bones = deform_bones = []
248 | # NOTE: Assuming JS objects are always ordered!
249 | for bname,bone of @bones
250 | b = bone.clone_to n
251 | b.object_children = []
252 | if b.deform_id != -1
253 | deform_bones[b.deform_id] = b
254 | list.push bones[bname] = b
255 | for c in n.children
256 | if c.parent_bone_index != -1
257 | list[c.parent_bone_index].object_children.push c
258 | if c.armature == this
259 | c.armature = n
260 | for {object} in c.lod_objects
261 | object.armature = n
262 | n.pose = @pose
263 | return n
264 |
265 | rotation_to = (out, p1, p2, maxang)->
266 | angle =
267 | Math.atan2 vec3.len(vec3.cross(vec3.create(),p1,p2)), vec3.dot(p1,p2)
268 | angle = Math.max -maxang, Math.min(maxang, angle)
269 | axis = vec3.cross vec3.create(), p1, p2
270 | vec3.normalize axis, axis
271 | quat.setAxisAngle out, axis, angle
272 | quat.normalize out, out
273 | return out
274 |
275 | class BoneConstraints
276 | # Assuming world coordinates
277 | copy_location: (owner, target)->
278 | quat.copy owner.final_position, target.final_position
279 |
280 | copy_rotation: (owner, target, influence=1)->
281 | rot = owner.final_rotation
282 | quat.slerp rot, rot, target.final_rotation, influence
283 |
284 | copy_scale: (owner, target)->
285 | quat.copy owner.final_scale, target.final_scale
286 |
287 | track_to_y: (owner, target)->
288 | pass
289 |
290 | copy_rotation_one_axis: (owner, target, axis)->
291 | # Assuming local coordinates
292 | rot = target.final_rotation
293 | q = quat.create()
294 | if target.parent
295 | quat.invert q, target.parent.final_rotation
296 | rot = quat.mul quat.create(), q, rot
297 | t = vec3.transformQuat vec3.create(), axis, rot
298 | q = rotation_to q, t, axis, 9999
299 | quat.mul q, q, rot
300 | quat.mul owner.final_rotation, owner.final_rotation, q
301 |
302 | stretch_to: (owner, target, rest_length, bulge)->
303 | # Assuming scale of parents is 1 for now
304 | dist = vec3.dist owner.final_position, target.final_position
305 | scl = owner.final_scale
306 | scl.y *= dist / rest_length
307 | XZ = 1 - Math.sqrt(bulge) + Math.sqrt(bulge * (rest_length / dist))
308 | scl.x *= XZ
309 | scl.z *= XZ
310 | v = vec3.sub vec3.create(), target.final_position, owner.final_position
311 | v2 = vec3.transformQuat vec3.create(), VECTOR_Y, owner.final_rotation
312 | q = rotation_to quat.create(), v2, v, 9999
313 | quat.mul owner.final_rotation, q, owner.final_rotation
314 |
315 | ik: (owner, target, chain_length, num_iterations)->
316 | bones=[]
317 | tip_bone = b = owner
318 | while chain_length and b
319 | bones.push b
320 | b = b.parent
321 | chain_length -= 1
322 | first = bones[bones.length-1].final_position
323 | target = vec3.clone target.final_position
324 | vec3.sub target, target, first
325 | points = []
326 | for b in bones[...-1]
327 | points.push vec3.sub(vec3.create(), b.final_position, first)
328 | tip = vec3.new 0, tip_bone.blength, 0
329 | vec3.transformQuat tip, tip, tip_bone.final_rotation
330 | vec3.add tip, tip, tip_bone.final_position
331 | vec3.sub tip, tip, first
332 | points.unshift tip
333 | original_points = []
334 | for p in points
335 | original_points.push vec3.clone(p)
336 |
337 | # now we have a list of points (tips) relative to the base bone
338 | # from last (tip) to first (base)
339 |
340 | # for each iteration
341 | # - make all relative (including target)
342 | # - for each point
343 | # - add the current point to all previous and target
344 | # - get rotation from tip to target
345 | # - rotate current and all previous points
346 | # with the quat of the previous step
347 |
348 | q = []
349 |
350 | for [0...num_iterations] by 1
351 | vec3.sub target, target, points[0]
352 | for i in [0...points.length-1]
353 | vec3.sub points[i], points[i], points[i+1]
354 | for i in [0...points.length]
355 | vec3.add target, target, points[i]
356 | for j in [0...i]
357 | vec3.add points[j], points[j], points[i]
358 |
359 | rotation_to q, points[0], target, 0.4
360 | # IK limits should be applied here to q
361 | for j in [0...i+1]
362 | vec3.transformQuat points[j], points[j], q
363 |
364 | for i in [0...points.length]
365 | vec3.add points[i], points[i], first
366 | vec3.add original_points[i], original_points[i], first
367 |
368 | points.push first
369 | original_points.push first
370 | points.push {x: 0, y:0, z:0}
371 | original_points.push {x: 0, y:0, z:0}
372 | for i in [0...points.length-2]
373 | # Set bone to final position
374 | vec3.copy bones[i].final_position, points[i+1]
375 | # Make relative and exctract rotation
376 | vec3.sub points[i], points[i], points[i+1]
377 | original_point = original_points[i]
378 | vec3.sub original_point, original_point, original_points[i+1]
379 | rotation_to q, original_points[i], points[i], 100
380 | r = bones[i].final_rotation
381 | quat.mul r, q, r
382 | return
383 |
384 | module.exports = {Armature}
385 |
--------------------------------------------------------------------------------
/engine/camera.coffee:
--------------------------------------------------------------------------------
1 | {GameObject} = require './gameobject'
2 | {mat3, mat4, vec3, vec4, quat} = require 'vmath'
3 | {plane_from_norm_point} = require './math_utils/g3'
4 |
5 | VECTOR_X = vec3.new 1,0,0
6 | VECTOR_Y = vec3.new 0,1,0
7 |
8 | class Camera extends GameObject
9 |
10 |
11 | constructor: (context, options={}) ->
12 | super context
13 | @type = 'CAMERA'
14 | {
15 | @near_plane = 0.1,
16 | @far_plane = 10000,
17 | @field_of_view = 30,
18 | @ortho_scale = 8,
19 | @aspect_ratio = 1,
20 | @cam_type = 'PERSP',
21 | @sensor_fit = 'AUTO',
22 | } = options
23 | # if non-zero, will use as up, right, down and left FoV
24 | @fov_4 = [0,0,0,0]
25 | @target_aspect_ratio = @aspect_ratio
26 | @projection_matrix = mat4.create()
27 | @projection_matrix_inv = mat4.create()
28 | @world_to_screen_matrix = mat4.create()
29 | @cull_planes = (vec4.create() for [0...6])
30 | @update_projection()
31 |
32 | # Returns a clone of the object
33 | # @option options scene [Scene] Destination scene
34 | # @option options recursive [bool] Whether to clone its children
35 | # @option options behaviours [bool] Whether to clone its behaviours
36 | clone: (options) ->
37 | clone = super options
38 | clone.near_plane = @near_plane
39 | clone.far_plane = @far_plane
40 | clone.field_of_view = @field_of_view
41 | clone.fov_4 = @fov_4[...]
42 | clone.projection_matrix = mat4.clone @projection_matrix
43 | clone.projection_matrix_inv = mat4.clone @projection_matrix_inv
44 | clone.world_to_screen_matrix = mat4.clone @world_to_screen_matrix
45 | clone.aspect_ratio = @aspect_ratio
46 | clone.target_aspect_ratio = @target_aspect_ratio
47 | clone.cam_type = @cam_type
48 | clone.sensor_fit = @sensor_fit
49 | clone.cull_planes = (vec4.clone v for v in @cull_planes)
50 | return clone
51 |
52 | # @nodoc
53 | # Avoid physical lamps and cameras
54 | instance_physics: ->
55 |
56 | # Returns a world vector from screen coordinates,
57 | # 0 to 1, where (0,0) is the upper left corner.
58 | get_ray_direction: (x, y)-> @get_ray_direction_into vec3.create(), x, y
59 |
60 | # Returns a world vector from screen coordinates,
61 | # 0 to 1, where (0,0) is the upper left corner.
62 | get_ray_direction_into: (out, x, y)->
63 | vec3.set out, x*2-1, 1-y*2, 1
64 | vec3.transformMat4 out, out, @projection_matrix_inv
65 | vec3.transformQuat out, out, @get_world_rotation()
66 | return out
67 |
68 | get_ray_direction_local: (x, y)->
69 | @get_ray_direction_local_into vec3.create(), x, y
70 |
71 | get_ray_direction_local_into: (out, x, y)->
72 | vec3.set out, x*2-1, 1-y*2, 1
73 | vec3.transformMat4 out, out, @projection_matrix_inv
74 | vec3.transformQuat out, out, @rotation
75 | return out
76 |
77 | look_at: (target, options={}) ->
78 | options.front ?= '-Z'
79 | options.up ?= '+Y'
80 | super target, options
81 |
82 | is_vertical_fit: ->
83 | switch @sensor_fit
84 | when 'AUTO'
85 | return @aspect_ratio <= 1
86 | when 'HORIZONTAL'
87 | return false
88 | when 'VERTICAL'
89 | return true
90 | when 'COVER'
91 | return @aspect_ratio <= @target_aspect_ratio
92 | when 'CONTAIN'
93 | return @aspect_ratio > @target_aspect_ratio
94 | else
95 | throw Error "Camera.sensor_fit must be
96 | AUTO, HORIZONTAL, VERTICAL, COVER or CONTAIN."
97 |
98 | set_projection_matrix: (matrix, adjust_aspect_ratio=false) ->
99 | pm = mat4.copy @projection_matrix, matrix
100 | if adjust_aspect_ratio
101 | m_ratio = matrix.m00 / matrix.m05
102 | # TODO: Use sensor_fit, we'll assume 'cover' for now
103 | # TODO: it's expecting an inverse aspect ratio, probably
104 | ratio_factor = m_ratio/@aspect_ratio
105 | if ratio_factor > 1
106 | mat4.scale pm, pm, vec3.new 1, ratio_factor, 1
107 | else
108 | mat4.scale pm, pm, vec3.new 1/ratio_factor, 1, 1
109 | mat4.invert @projection_matrix_inv, @projection_matrix
110 | @_calculate_culling_planes()
111 |
112 | update_projection: ->
113 | @_calculate_projection()
114 | @_calculate_culling_planes()
115 |
116 | # @nodoc
117 | _calculate_projection: ->
118 | near_plane = @near_plane
119 | far_plane = @far_plane
120 | if @fov_4[0] == 0
121 | # Regular symmetrical FoV
122 | if @cam_type == 'PERSP'
123 | half_size = near_plane * Math.tan(@field_of_view/2)
124 | else if @cam_type == 'ORTHO'
125 | half_size = @ortho_scale/2
126 | else
127 | throw Error "Camera.cam_type must be PERSP or ORTHO."
128 |
129 | if @is_vertical_fit()
130 | top = half_size
131 | if /CONTAIN|COVER/.test @sensor_fit
132 | top /= @target_aspect_ratio
133 | right = top * @aspect_ratio
134 | else
135 | right = half_size
136 | top = right / @aspect_ratio
137 |
138 | bottom = -top
139 | left = -right
140 | else
141 | # Custom FoV in each direction, for VR
142 | [top, right, bottom, left] = @fov_4
143 | top = near_plane * Math.tan(top * Math.PI / 180.0)
144 | right = near_plane * Math.tan(right * Math.PI / 180.0)
145 | bottom = near_plane * Math.tan(bottom * Math.PI / -180.0)
146 | left = near_plane * Math.tan(left * Math.PI / -180.0)
147 |
148 | pm = @projection_matrix
149 | a = (right + left) / (right - left)
150 | b = (top + bottom) / (top - bottom)
151 | c = -(far_plane + near_plane) / (far_plane - near_plane)
152 | if @cam_type == 'PERSP'
153 | d = -(2 * far_plane * near_plane) / (far_plane - near_plane)
154 | x = (2 * near_plane) / (right - left)
155 | y = (2 * near_plane) / (top - bottom)
156 | # x, 0, 0, 0,
157 | # 0, y, 0, 0,
158 | # a, b, c, -1,
159 | # 0, 0, d, 0
160 | pm.m00 = x
161 | pm.m01 = 0
162 | pm.m02 = 0
163 | pm.m03 = 0
164 | pm.m04 = 0
165 | pm.m05 = y
166 | pm.m06 = 0
167 | pm.m07 = 0
168 | pm.m08 = a
169 | pm.m09 = b
170 | pm.m10 = c
171 | pm.m11 = -1
172 | pm.m12 = 0
173 | pm.m13 = 0
174 | pm.m14 = d
175 | pm.m15 = 0
176 | mat4.invert @projection_matrix_inv, @projection_matrix
177 | else
178 | d = -2 / (far_plane - near_plane)
179 | x = 2 / (right - left)
180 | y = 2 / (top - bottom)
181 | # x, 0, 0, 0,
182 | # 0, y, 0, 0,
183 | # 0, 0, d, 0,
184 | # -a,-b, c, 1
185 | pm.m00 = x
186 | pm.m01 = 0
187 | pm.m02 = 0
188 | pm.m03 = 0
189 | pm.m04 = 0
190 | pm.m05 = y
191 | pm.m06 = 0
192 | pm.m07 = 0
193 | pm.m08 = 0
194 | pm.m09 = 0
195 | pm.m10 = d
196 | pm.m11 = 0
197 | pm.m12 = -a
198 | pm.m13 = -b
199 | pm.m14 = c
200 | pm.m15 = 1
201 | mat4.invert @projection_matrix_inv, @projection_matrix
202 |
203 | # @nodoc
204 | # Calculate frustum culling planes from projection_matrix_inv
205 | _calculate_culling_planes: ->
206 | a = vec3.create()
207 | b = vec3.create()
208 | c = vec3.create()
209 | normal = vec3.create()
210 | q = quat.create()
211 | i = 0
212 | for axis in [0...3]
213 | for side in [-1,1]
214 | # We calculate 3 points in the untransformed plane,
215 | # in screen space (-1 to 1 box)
216 | vec3.set a, side, 0, 0
217 | vec3.set b, side, .5, 0
218 | vec3.set c, side, 0, .5*side
219 | for [0...axis] by 1
220 | _shift a
221 | _shift b
222 | _shift c
223 | # Then we transform them to world space
224 | vec3.transformMat4 a, a, @projection_matrix_inv
225 | vec3.transformMat4 b, b, @projection_matrix_inv
226 | vec3.transformMat4 c, c, @projection_matrix_inv
227 | # make b and c relative to a, to calculate the normal
228 | vec3.sub b, b, a
229 | vec3.sub c, c, a
230 | vec3.cross normal, b, c
231 | vec3.normalize normal, normal
232 | plane_from_norm_point @cull_planes[i++], normal, a
233 | return
234 |
235 | _shift = (v) ->
236 | {x,y,z} = v
237 | v.x = z
238 | v.y = x
239 | v.z = y
240 |
241 | module.exports = {Camera}
242 |
--------------------------------------------------------------------------------
/engine/color.coffee:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | srgb_to_linearrgb = (out, color) ->
6 | for c,i in color
7 | if c < 0.04045
8 | c = Math.max(c,0) * (1.0 / 12.92)
9 | else
10 | c = Math.pow((c + 0.055)*(1.0/1.055), 2.4)
11 | out[i] = c
12 | return out
13 |
14 | linearrgb_to_srgb = (out, color) ->
15 | for c,i in color
16 | if c < 0.0031308
17 | c = Math.max(c,0) * 12.92
18 | else
19 | c = 1.055 * Math.pow(c, 1.0/2.4) - 0.055
20 | out[i] = c
21 | return out
22 |
23 | module.exports = {srgb_to_linearrgb, linearrgb_to_srgb}
24 |
--------------------------------------------------------------------------------
/engine/cubemap.coffee:
--------------------------------------------------------------------------------
1 |
2 | sh = require 'cubemap-sh'
3 |
4 | _temp_framebuffers = {}
5 |
6 | # Cubemap texture, currently only for rendering environment maps and probes,
7 | # not loaded from a file yet.
8 | #
9 | # See {Texture} for more information.
10 | class Cubemap
11 | # @nodoc
12 | type: 'TEXTURE'
13 | # @nodoc
14 | texture_type: 'cubemap'
15 | # Size of each face of the cubemap
16 | size: 128
17 | # GL texture type (default: gl.UNSIGNED_BYTE)
18 | gl_type: 0
19 |
20 | constructor: (@context, options={}) ->
21 | {gl} = @context.render_manager
22 | {
23 | @size=128
24 | @gl_type=gl.UNSIGNED_BYTE
25 | @gl_internal_format=gl.RGBA
26 | @gl_format=gl.RGBA
27 | @use_filter=true
28 | @use_mipmap=true
29 | @color
30 | } = options
31 | @gl_target = 34067 # gl.TEXTURE_CUBE_MAP
32 | @gl_tex = null
33 | @coefficients = (new Float32Array(3) for [0...9])
34 | @loaded = false
35 | @bound_unit = -1
36 | @last_used_material = null
37 | @is_framebuffer_active = false
38 | @context.all_cubemaps.push this
39 | @instance()
40 |
41 |
42 | instance: (data=null) ->
43 | {gl} = @context.render_manager
44 | @gl_tex = gl.createTexture()
45 | @loaded = true # bind_texture requires this
46 | @context.render_manager.bind_texture @
47 | if @color?
48 | @fill_color(@color)
49 | else
50 | @set_data(data or undefined)
51 | if @use_filter
52 | min_filter = mag_filter = gl.LINEAR
53 | if @use_mipmap
54 | min_filter = gl.LINEAR_MIPMAP_NEAREST
55 | else
56 | min_filter = mag_filter = gl.NEAREST
57 | if @use_mipmap
58 | min_filter = gl.NEAREST_MIPMAP_NEAREST
59 | gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MIN_FILTER, min_filter)
60 | gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MAG_FILTER, mag_filter)
61 | gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
62 | gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
63 | return @
64 |
65 | set_data: (data=[null,null,null,null,null,null]) ->
66 | {gl} = @context.render_manager
67 | i = gl.TEXTURE_CUBE_MAP_POSITIVE_X
68 | {gl_internal_format: ifmt, size, gl_format, gl_type} = this
69 | gl.texImage2D(i++, 0, ifmt, size, size, 0, gl_format, gl_type, data[0])
70 | gl.texImage2D(i++, 0, ifmt, size, size, 0, gl_format, gl_type, data[1])
71 | gl.texImage2D(i++, 0, ifmt, size, size, 0, gl_format, gl_type, data[2])
72 | gl.texImage2D(i++, 0, ifmt, size, size, 0, gl_format, gl_type, data[3])
73 | gl.texImage2D(i++, 0, ifmt, size, size, 0, gl_format, gl_type, data[4])
74 | gl.texImage2D(i++, 0, ifmt, size, size, 0, gl_format, gl_type, data[5])
75 | if @use_mipmap?
76 | gl.generateMipmap gl.TEXTURE_CUBE_MAP
77 | return @
78 |
79 | fill_color: (color) ->
80 | # NOTE: This was made for test purposes.
81 | # It's much better to use render_to_cubemap and clear color
82 | {r, g, b, a=1} = color
83 | r = Math.min(Math.max(0,r*255),255)|0
84 | g = Math.min(Math.max(0,g*255),255)|0
85 | b = Math.min(Math.max(0,b*255),255)|0
86 | a = Math.min(Math.max(0,a*255),255)|0
87 | pixels = new Uint8Array(@size*@size*4)
88 | for i in [0...pixels.length] by 4
89 | pixels[i] = r
90 | pixels[i+1] = g
91 | pixels[i+2] = b
92 | pixels[i+3] = a
93 | @set_data [pixels, pixels, pixels, pixels, pixels, pixels]
94 |
95 | bind: ->
96 | @context.render_manager.bind_texture @
97 |
98 | generate_mipmap: ->
99 | {gl} = @context.render_manager
100 | @context.render_manager.bind_texture @
101 | gl.generateMipmap gl.TEXTURE_CUBE_MAP
102 | return @
103 |
104 | # Render all cubemap faces to a framebuffer with a size of at least
105 | # 3*size by 2*size.
106 | #
107 | # The format is six faces in a 3*2 mosaic like this:
108 | #
109 | # | -X -Y -Z
110 | # | +X +Y +Z
111 | # 0,0 --------
112 | #
113 | # You can see the OpenGL cube texture convention here:
114 | # http://stackoverflow.com/questions/11685608/convention-of-faces-in-opengl-cubemapping
115 | # @param fb [framebuffer] Destination framebuffer.
116 | # @param size [number] Size of each face.
117 | # @return [Cubemap] self
118 | render_to_framebuffer: (fb, size=@size) ->
119 | {gl, quad} = @context.render_manager
120 | # TODO: Simplify all this by converting to filter
121 | # and remove render_manager.quad
122 | material = get_resize_material(@context, @)
123 | shader = material.shaders.shader
124 | fb.enable [0, 0, size*3, size*2]
125 | material.inputs.cube.value = @
126 | material.inputs.size.value = size
127 | shader.use()
128 | @context.render_manager.bind_texture @
129 | shader.uniform_assign_func(gl, shader, null, null, null)
130 | gl.bindBuffer gl.ARRAY_BUFFER, quad
131 | @context.render_manager.change_enabled_attributes shader.attrib_bitmask
132 | gl.vertexAttribPointer shader.attrib_pointers[0][0], 3.0, gl.FLOAT, false, 0, 0
133 | gl.drawArrays gl.TRIANGLE_STRIP, 0, 4
134 | # Test this with:
135 | # $myou.scenes.Scene.post_draw_callbacks.push(function({
136 | # $myou.objects.Cube.probe_cube.cubemap.render_to_framebuffer(
137 | # $myou.render_manager.main_fb)
138 | # })
139 | return @
140 |
141 | # Gets the pixels of the six cube faces.
142 | # @param faces [Array] An array of six Uint8Array (enough to hold amount of pixels*4) to write into
143 | read_faces: (faces, size=@size) ->
144 | {gl} = @context.render_manager
145 | fb = _temp_framebuffers[size]
146 | if not fb?
147 | fb = new @context.ByteFramebuffer size: [size*4, size*2]
148 | _temp_framebuffers[size] = fb
149 | @render_to_framebuffer fb, size
150 | for pixels,i in faces
151 | x = size * (i%3)
152 | y = size * ((i>=3)|0)
153 | gl.readPixels(x, y, size, size, gl.RGBA, gl.UNSIGNED_BYTE, pixels)
154 | return
155 |
156 | # Generate spherical harmonics for diffuse shading
157 | generate_spherical_harmonics: (size=@size)->
158 | faces = (new Uint8Array(size*size*4) for [0...6])
159 | @read_faces(faces, size)
160 | [posx, posy, posz, negx, negy, negz] = faces
161 | @coefficients = sh [posx, negx, posy, negy, posz, negz], size, 4
162 | # See spherical_harmonics_L2() in original shader and
163 | # in shader_lib_extractor.py
164 | # result is very similar to Blender but not quite the same
165 | m=[0.282095,0.488603,0.488603,0.488603,
166 | 1.092548,1.092548,0.315392,1.092548,0.546274]
167 | for c,i in @coefficients[1...]
168 | inv = 1/m[i]
169 | c[0] *= inv
170 | c[1] *= inv
171 | c[2] *= inv
172 | return @
173 |
174 | destroy: ->
175 | if @gl_tex
176 | @context.render_manager.gl.deleteTexture @gl_tex
177 | @gl_tex = null
178 | @loaded = false
179 | @context.all_cubemaps.splice @context.all_cubemaps.indexOf(this), 1
180 |
181 |
182 |
183 |
184 | resize_material = null
185 | # @nodoc
186 | get_resize_material = (context, any_cubemap) ->
187 | if resize_material?
188 | return resize_material
189 | resize_material = new context.Material '_cubemap_resize', {
190 | material_type: 'PLAIN_SHADER',
191 | vertex: 'attribute vec3 vertex;
192 | void main(){ gl_Position = vec4(vertex.xy*2.0-1.0, -1.0, 1.0); }',
193 | fragment: '''
194 | precision highp float;
195 | uniform samplerCube cube;
196 | uniform float size;
197 | void main() {
198 | float hsize = size * 0.5;
199 | float size2 = size * 2.0;
200 | vec3 co = vec3(gl_FragCoord.xy-vec2(hsize), -hsize);
201 | if(gl_FragCoord.y < size){
202 | if(gl_FragCoord.x < size){
203 | gl_FragColor = textureCube(cube, vec3(co.z, -co.y, -co.x));
204 | }else if(gl_FragCoord.x < size2){
205 | co.x -= size;
206 | gl_FragColor = textureCube(cube, vec3(co.x, co.z, co.y));
207 | }else{
208 | co.x -= size2;
209 | gl_FragColor = textureCube(cube, vec3(co.x, -co.y, co.z));
210 | }
211 | }else{
212 | co.y -= size;
213 | if(gl_FragCoord.x < size){
214 | gl_FragColor = textureCube(cube, vec3(-co.z, -co.y, co.x));
215 | }else if(gl_FragCoord.x < size2){
216 | co.x -= size;
217 | gl_FragColor = textureCube(cube, vec3(co.x, -co.z, -co.y));
218 | }else{
219 | co.x -= size2;
220 | gl_FragColor = textureCube(cube, vec3(-co.x, -co.y, -co.z));
221 | }
222 | }
223 | }
224 | ''',
225 | uniforms: [
226 | {varname: 'size', value: 128},
227 | {varname: 'cube', value: any_cubemap},
228 | ],
229 | # double_sided: true,
230 | }
231 | fake_mesh = {
232 | _signature:'shader'
233 | layout: [{"name":"vertex","type":"f","count":3,"offset":0,"location":0}]
234 | vertex_modifiers: []
235 | material_defines: {}
236 | }
237 | resize_material.get_shader fake_mesh
238 | return resize_material
239 |
240 |
241 | module.exports = {Cubemap}
242 |
--------------------------------------------------------------------------------
/engine/curve.coffee:
--------------------------------------------------------------------------------
1 | {vec3} = require 'vmath'
2 | {cubic_bezier} = require './math_utils/math_extra'
3 | {GameObject} = require './gameobject'
4 |
5 | class Curve extends GameObject
6 |
7 | constructor: (context)->
8 | super context
9 | @type = 'CURVE'
10 |
11 | instance_physics: ->
12 | # For now, use debug mesh for drawing the curve
13 | # and disable physics
14 | # pass
15 |
16 | set_curves: (curves, resolution, nodes=false)->
17 | # TODO: This hasn't been tested since the port
18 | #The nodes could be precalculed while exporting
19 | @curves = curves
20 | @calculated_curves = []
21 | indices = []
22 | vertices = []
23 | n = 0
24 | @origins = origins = []
25 | for c in curves
26 | cn = 0
27 | c_indices = []
28 | c_vertices = []
29 |
30 | for i in [0...Math.floor((c.length/9) - 1)]
31 | i9 = i*9
32 | p0x = c[i9+3]
33 | p0y = c[i9+4]
34 | p0z = c[i9+5]
35 | p1x = c[i9+6]
36 | p1y = c[i9+7]
37 | p1z = c[i9+8]
38 | p2x = c[i9+9]
39 | p2y = c[i9+10]
40 | p2z = c[i9+11]
41 | p3x = c[i9+12]
42 | p3y = c[i9+13]
43 | p3z = c[i9+14]
44 |
45 | for j in [0...resolution]
46 | x = cubic_bezier j/resolution, p0x, p1x, p2x, p3x
47 | y = cubic_bezier j/resolution, p0y, p1y, p2y, p3y
48 | z = cubic_bezier j/resolution, p0z, p1z, p2z, p3z
49 |
50 | vertices.push x, y, z
51 | indices.push n
52 | indices.push n+1
53 |
54 | #sub_curve vertices and indices
55 | c_vertices.push x, y, z
56 | c_indices.push cn
57 | c_indices.push cn+1
58 |
59 | n += 1
60 | cn += 1
61 |
62 |
63 | c_vertices.push p3x, p3y, p3z
64 | cva = new Float32Array c_vertices
65 | cia = new Uint16Array c_indices
66 | @calculated_curves.push {'ia':cia, 'va':cva}
67 | vertices.push p3x, p3y, p3z
68 | n += 1
69 | @va = new Float32Array vertices
70 | @ia = new Uint16Array indices
71 |
72 | curve_index = 0
73 | for c in @calculated_curves
74 | if nodes
75 | c.nodes = nodes[curve_index]
76 | else
77 | c.nodes = @get_nodes curve_index
78 | c.la = @get_curve_edges_length curve_index
79 | c.da = @get_curve_direction_vectors curve_index
80 | c.curve = @
81 | c.length = 0
82 | for e in c.la
83 | c.length+=e
84 | curve_index += 1
85 | return
86 |
87 | closest_point: (q, scale={x: 1, y:1, z:1}) ->
88 | # TODO: This is completely broken since vmath
89 | # winning point
90 | wp = vec3.create()
91 | wn = vec3.create() # normal
92 | ds = Infinity # distance squared
93 |
94 | # temp vars
95 | p1 = vec3.create()
96 | p2 = vec3.create()
97 | np1 = vec3.create() # normal of plane of p1
98 | np2 = vec3.create()
99 | d1 = vec3.create() # q - p1
100 | d2 = vec3.create()
101 | p = vec3.create()
102 |
103 | va = @va
104 | ia = @ia
105 | for i in [0...Math.floor(ia.length * 0.5)]
106 | i2 = i*2
107 | vec3.mul p1, va.subarray(ia[i2]*3, ia[i2]*3+3), scale
108 | vec3.mul p2, va.subarray(ia[i2+1]*3, ia[i2+1]*3+3), scale
109 | # calculate planes (todo: use tangents for that)
110 | vec3.sub np1, p2, p1
111 | vec3.sub np2, p1, p2
112 | # dot products = squared distance to planes
113 | vec3.sub d1, q, p1
114 | vec3.sub d2, q, p2
115 | dp1 = vec3.dot np1, d1
116 | dp2 = vec3.dot np2, d2
117 | sum = dp1 + dp2
118 | # clamp (0,1) and get point
119 | f = max(0, min(1, dp1/sum))
120 | vec3.lerp p, p1, p2, f
121 | ds_ = vec3.sqrDist p, q
122 | if ds_ < ds
123 | ds = ds_
124 | vec3.copy wp, p
125 | vec3.sub wn, p2, p1
126 |
127 | vec3.normalize wn, wn
128 | return [wp, wn]
129 |
130 | get_curve_edges_length: (curve_index) ->
131 | scale = @scale
132 | curve = @calculated_curves[curve_index]
133 | ia = curve.ia
134 | va = curve.va
135 | l = []
136 | for i in [0...Math.floor(ia.length * 0.5)]
137 | p1 = vec3.create()
138 | p2 = vec3.create()
139 | i2 = i*2
140 | vec3.mul p1, va.subarray(ia[i2]*3, ia[i2]*3+3), scale
141 | vec3.mul p2, va.subarray(ia[i2+1]*3, ia[i2+1]*3+3), scale
142 | l.push vec3.dist(p1, p2)
143 | return new Float32Array l
144 |
145 | get_curve_direction_vectors: (curve_index)->
146 | scale = @scale
147 | curve = @calculated_curves[curve_index]
148 | ia = curve.ia
149 | va = curve.va
150 | l = []
151 | for i in [0...Math.floor(ia.length * 0.5)]
152 | p1 = vec3.create()
153 | p2 = vec3.create()
154 | i2 = i*2
155 | vec3.mul p1, va.subarray(ia[i2]*3, ia[i2]*3+3), scale
156 | vec3.mul p2, va.subarray(ia[i2+1]*3, ia[i2+1]*3+3), scale
157 | v = vec3.normalize(vec3.create(),vec3.sub(vec3.create(),p2, p1))
158 | l.push v.x, v.y, v.z
159 | return new Float32Array l
160 |
161 | get_nodes: (main_curve_index=0, precision=0.0001)->
162 | # TODO: This hasn't been tested since the port
163 | main_curve = @calculated_curves[main_curve_index]
164 |
165 | nodes = {}
166 | for i in [0...Math.floor(main_curve.ia.length * 0.5)]
167 | i2 = i*2
168 | main_p = main_curve.va.subarray main_curve.ia[i2]*3, main_curve.ia[i2]*3+3
169 | ci = 0
170 | for curve in @calculated_curves
171 | if ci != main_curve_index
172 | for ii in [0...Math.floor(curve.ia.length * 0.5)]
173 | ii2 = ii*2
174 | p = curve.va.subarray curve.ia[ii2]*3, curve.ia[ii2]*3+3
175 | d = vec3.dist main_p,p
176 | if d < precision
177 | if not (i in nodes)
178 | nodes[i]=[[ci,ii]]
179 | else
180 | nodes[i].push [ci,ii]
181 | ci += 1
182 | return nodes
183 |
184 | module.exports = {Curve}
185 |
--------------------------------------------------------------------------------
/engine/custom_fetch_polyfills.coffee:
--------------------------------------------------------------------------------
1 |
2 | # These polyfills work in place of
3 | # fetch(base+path).then((data)->data.json()) and
4 | # fetch(base+path).then((data)->data.arrayBuffer())
5 | # They work properly but they're currently not used anywhere.
6 | # TODO: We added this to check if this is more efficient or compatible than
7 | # the regular fetch polyfill. They were created as an attempt to fix
8 | # problems with Safari.
9 |
10 | window.fetch_json = (uri) ->
11 | new Promise (resolve, reject) ->
12 | console.log 'fetching json', uri
13 | xhr = new XMLHttpRequest
14 | xhr.open('GET', uri, true)
15 | xhr.timeout = 60000
16 | xhr.onload = ->
17 | if xhr.status == 200 or xhr.status == 0
18 | console.log uri,'resolves'
19 | resolve(JSON.parse(xhr.response))
20 | else
21 | console.log uri,'errors'
22 | reject('Error '+xhr.status+': '+xhr.response)
23 | xhr.onerror = xhr.ontimeout = ->
24 | console.log uri,'onerror'
25 | reject('Error '+xhr.status+': '+xhr.response)
26 | xhr.send()
27 |
28 | window.fetch_buffer = (uri) ->
29 | new Promise (resolve, reject) ->
30 | console.log 'fetching bafer', uri
31 | xhr = new XMLHttpRequest
32 | xhr.open('GET', uri, true)
33 | xhr.timeout = 60000
34 | xhr.responseType = 'arraybuffer'
35 | xhr.onload = ->
36 | if xhr.status == 200 or xhr.status == 0
37 | console.log uri,'resolves'
38 | resolve(xhr.response)
39 | else
40 | console.log uri,'errors'
41 | reject('Error '+xhr.status+': '+xhr.response)
42 | xhr.onerror = xhr.ontimeout = ->
43 | console.log uri,'onerror'
44 | reject('Error '+xhr.status+': '+xhr.response)
45 | xhr.send()
46 |
--------------------------------------------------------------------------------
/engine/debug_camera.coffee:
--------------------------------------------------------------------------------
1 | {vec3, vec4, clamp} = require 'vmath'
2 | {Behaviour} = require './behaviour'
3 |
4 | class DebugCamera extends Behaviour
5 | constructor: (args...) ->
6 | super args...
7 | @debug_camera = @viewports[0].camera.clone()
8 | @scene.clear_parent @debug_camera
9 | @debug_camera.set_rotation_order 'XYZ'
10 | @debug_camera.far_plane *= 10
11 | @debug_camera.cam_type = 'PERSP'
12 | @debug_camera.update_projection()
13 | @pivot = new @context.GameObject
14 | @scene.add_object @pivot
15 | @pivot.set_rotation_order 'XYZ'
16 | # we use @active instead of enabling/disabling the behaviour,
17 | # to be able to re-enable with a key
18 | @active = false
19 | @rotating = false
20 | @panning = false
21 | @distance = @pan_distance = vec3.len @debug_camera.position
22 | @debug = @scene.get_debug_draw()
23 | @pivot_vis = new @debug.Point
24 | @pivot_vis.position = @pivot.position
25 | @disable_context_menu()
26 | this.enable_object_picking()
27 | @activate()
28 |
29 | on_tick: ->
30 | return if not @active
31 | if not @rotating
32 | # Change pivot and distance
33 | {width, height} = @viewports[0]
34 | {point} = @pick_object width*.5, height*.5, @viewports[0]
35 | if point?
36 | @distance = vec3.dist @debug_camera.position, point
37 | vec3.copy @pivot.position, point
38 | else
39 | vec3.set @pivot.position, 0, 0, -@distance
40 | wm = @debug_camera.get_world_matrix()
41 | vec3.transformMat4 @pivot.position, @pivot.position, wm
42 |
43 | on_pointer_down: (event) ->
44 | return if not @active
45 | if event.button == 0 and not @rotating
46 | @rotating = true
47 | vec4.copy @pivot.rotation, @debug_camera.rotation
48 | @pivot.rotate_x_deg -90, @pivot
49 | @debug_camera.parent_to @pivot
50 | if event.button == 2
51 | @pan_distance = @distance
52 | @panning = true
53 |
54 | on_pointer_move: (event) ->
55 | return if not @active
56 | if @rotating
57 | {rotation} = @pivot
58 | HPI = Math.PI * .5
59 | rotation.z -= event.delta_x * 0.01
60 | rotation.x -= event.delta_y * 0.01
61 | rotation.x = clamp rotation.x, -HPI, HPI
62 | else if @panning
63 | ratio = event.viewport.pixels_to_units * @pan_distance * 2
64 | x = -event.delta_x * ratio
65 | y = event.delta_y * ratio
66 | @debug_camera.translate vec3.new(x, y, 0), @debug_camera
67 |
68 | on_pointer_up: (event) ->
69 | # for some reason you can't trust that the button will be detected
70 | # (e.g. left down, right down, left up, right up: left not detected)
71 | # so we reset all buttons here
72 | if @rotating
73 | @rotating = false
74 | @debug_camera.clear_parent()
75 | @panning = false
76 |
77 | on_wheel: (event) ->
78 | return if not @active
79 | # zoom with wheel, but avoid going through objects
80 | # 54 is the approximate amount of pixels of one scroll step
81 | delta = @distance * (4/5) ** (-event.delta_y/54) - @distance
82 | delta = Math.max delta, -(@distance - @debug_camera.near_plane*1.2)
83 | @debug_camera.translate_z delta, @debug_camera
84 | @distance = vec3.dist @debug_camera.position, @pivot.position
85 |
86 | activate: ->
87 | if not @active
88 | [viewport] = @viewports
89 | viewport.debug_camera = @debug_camera
90 | viewport.recalc_aspect()
91 | for behaviour in @context.behaviours when behaviour != this
92 | if viewport in behaviour._real_viewports
93 | behaviour._real_viewports = behaviour._real_viewports[...]
94 | behaviour._real_viewports.splice(
95 | behaviour._real_viewports.indexOf(viewport), 1)
96 | @active = true
97 |
98 | deactivate: ->
99 | if @active
100 | [viewport] = @viewports
101 | viewport.debug_camera = null
102 | for behaviour in @context.behaviours when behaviour != this
103 | if viewport in behaviour.viewports
104 | behaviour._real_viewports = rv = []
105 | for v in behaviour.viewports when not v.debug_camera?
106 | rv.push v
107 | @active = false
108 |
109 | on_key_down: (event) ->
110 | switch event.key.toLowerCase()
111 | when 'q'
112 | if @active
113 | @deactivate()
114 | else
115 | @activate()
116 |
117 | # on_object_pointer_down: (event) -> console.log 'down', event.object.name
118 | # on_object_pointer_up: (event) -> console.log 'up', event.object.name
119 | # on_object_pointer_move: (event) -> console.log 'move', event.object.name
120 | # on_object_pointer_over: (event) -> console.log 'over', event.object.name
121 | # on_object_pointer_out: (event) -> console.log 'out', event.object.name
122 |
123 | module.exports = {DebugCamera}
124 |
--------------------------------------------------------------------------------
/engine/effects/FXAA.coffee:
--------------------------------------------------------------------------------
1 | # /* This allows us to set the editor to "GLSL"
2 |
3 | # NVIDIA FXAA by Timothy Lottes
4 | # http://timothylottes.blogspot.com/2011/06/fxaa3-source-released.html
5 | # - WebGL port by @supereggbert
6 | # http://www.glge.org/demos/fxaa/
7 |
8 | # TODO: separate GLSL code into files to avoid linter errors, etc
9 |
10 | library = ''' #line 8 /**/
11 | #define FXAA_REDUCE_MIN (1.0/128.0)
12 | #define FXAA_REDUCE_MUL (1.0/8.0)
13 | #define FXAA_SPAN_MAX 8.0
14 | vec4 FXAA(sampler2D sampler, vec2 orig_px_size){
15 | vec3 rgbNW = texture2D(sampler, (gl_FragCoord.xy + vec2( -1.0, -1.0 ))*orig_px_size).xyz;
16 | vec3 rgbNE = texture2D(sampler, (gl_FragCoord.xy + vec2( 1.0, -1.0 ))*orig_px_size).xyz;
17 | vec3 rgbSW = texture2D(sampler, (gl_FragCoord.xy + vec2( -1.0, 1.0 ))*orig_px_size).xyz;
18 | vec3 rgbSE = texture2D(sampler, (gl_FragCoord.xy + vec2( 1.0, 1.0 ))*orig_px_size).xyz;
19 | vec4 rgbaM = texture2D(sampler, gl_FragCoord.xy*orig_px_size);
20 | vec3 rgbM = rgbaM.xyz;
21 | vec3 luma = vec3( 0.299, 0.587, 0.114 );
22 | float lumaNW = dot( rgbNW, luma );
23 | float lumaNE = dot( rgbNE, luma );
24 | float lumaSW = dot( rgbSW, luma );
25 | float lumaSE = dot( rgbSE, luma );
26 | float lumaM = dot( rgbM, luma );
27 | float lumaMin = min( lumaM, min( min( lumaNW, lumaNE ), min( lumaSW, lumaSE ) ) );
28 | float lumaMax = max( lumaM, max( max( lumaNW, lumaNE) , max( lumaSW, lumaSE ) ) );
29 | vec2 dir;
30 | dir.x = -((lumaNW + lumaNE) - (lumaSW + lumaSE));
31 | dir.y = ((lumaNW + lumaSW) - (lumaNE + lumaSE));
32 | float dirReduce = max( ( lumaNW + lumaNE + lumaSW + lumaSE ) * ( 0.25 * FXAA_REDUCE_MUL ), FXAA_REDUCE_MIN );
33 | float rcpDirMin = 1.0 / ( min( abs( dir.x ), abs( dir.y ) ) + dirReduce );
34 | dir = min( vec2( FXAA_SPAN_MAX, FXAA_SPAN_MAX),
35 | max( vec2(-FXAA_SPAN_MAX, -FXAA_SPAN_MAX),
36 | dir * rcpDirMin));
37 | vec4 rgbA = (1.0/2.0) * (
38 | texture2D(sampler, (gl_FragCoord.xy + dir * (1.0/3.0 - 0.5))*orig_px_size) +
39 | texture2D(sampler, (gl_FragCoord.xy + dir * (2.0/3.0 - 0.5))*orig_px_size));
40 | vec4 rgbB = rgbA * (1.0/2.0) + (1.0/4.0) * (
41 | texture2D(sampler, (gl_FragCoord.xy + dir * (0.0/3.0 - 0.5))*orig_px_size) +
42 | texture2D(sampler, (gl_FragCoord.xy + dir * (3.0/3.0 - 0.5))*orig_px_size));
43 | float lumaB = dot(rgbB, vec4(luma, 0.0));
44 | if ( ( lumaB < lumaMin ) || ( lumaB > lumaMax ) ) {
45 | return rgbA;
46 | } else {
47 | return rgbB;
48 | }
49 | }
50 | '''
51 |
52 | {BaseFilter} = require '../filters'
53 | {FilterEffect} = require './base'
54 |
55 | class FXAAFilter extends BaseFilter
56 | constructor: (context) ->
57 | super context, 'fxaa'
58 | @fragment = """
59 | precision highp float;
60 | #{library}
61 | uniform sampler2D source;
62 | uniform vec2 source_size_inverse;
63 | varying vec2 source_coord;
64 | void main() {
65 | gl_FragColor = FXAA(source, source_size_inverse);
66 | }
67 | """
68 |
69 | class FXAAEffect extends FilterEffect
70 | constructor: (context) ->
71 | super context, new FXAAFilter context
72 |
73 | module.exports = {FXAAEffect}
74 |
--------------------------------------------------------------------------------
/engine/effects/SSAO.coffee:
--------------------------------------------------------------------------------
1 |
2 | {BaseFilter} = require '../filters'
3 | {FilterEffect} = require './base'
4 | {vec2} = require 'vmath'
5 | MersenneTwister = require 'mersennetwister'
6 | {next_POT} = require '../math_utils/math_extra'
7 |
8 | class SSAOFilter extends BaseFilter
9 | constructor: (context, effect) ->
10 | super context, 'ssao'
11 | {
12 | @radius
13 | @zrange
14 | @strength
15 | @samples
16 | @clumping
17 | @noise_size
18 | @fade_start
19 | @fade_end
20 | } = effect
21 | @disk_texture_uniform = {varname: 'disk_texture', value: null}
22 | @noise_texture_uniform = {varname: 'noise_texture', value: null}
23 | @make_textures()
24 | @uniforms = [
25 | {varname: 'radius', value: @radius}
26 | {varname: 'iradius4', value: 4/@radius}
27 | {varname: 'strength', value: @strength}
28 | {varname: 'zrange', value: 1/@zrange}
29 | {varname: 'fade', value: vec2.new(@fade_start, @fade_end)}
30 | @disk_texture_uniform
31 | @noise_texture_uniform
32 | ]
33 | @defines = {
34 | SAMPLES: @samples
35 | ISAMPLES: 1/@samples
36 | NOISE_ISIZE: 1/@noise_size
37 | }
38 | # TODO!! Optimize depth reads!
39 | # original was:
40 | # (2.0 * near) / (far + near - texture2D(...).x * (far-near));
41 | @add_depth()
42 | @fragment = require('raw-loader!./SSAO.glsl').replace \
43 | '/*library goes here*/', @library
44 |
45 | make_textures: (seed_=59066, seed_r=886577) ->
46 | # seed_r=1679174 is good for 16x16
47 | # disk kernel texture
48 | seed = seed_ ? ((Math.random()*10000000)|0)
49 | mt = new MersenneTwister seed
50 | v = vec2.create()
51 | pixels = new Uint8Array @samples*4
52 | # vsum = vec2.create()
53 | for i in [0...@samples] by 1
54 | r = Math.pow((i+1)/@samples, @clumping)
55 | th = mt.random() * Math.PI * 2
56 | v.x = Math.cos(th) * r
57 | v.y = Math.sin(th) * r
58 | # vec2.add vsum, vsum, v
59 | pixels[i*4] = (v.x+1)*127.5
60 | pixels[i*4+1] = (v.y+1)*127.5
61 | pixels[i*4+2] = r*255
62 | # TODO: Sort and benchmark
63 |
64 | # find the sum of the smallest distance between any two points
65 | # (this is only for finding a good seed, do not use)
66 | # if vec2.len(vsum) < 2.15
67 | # sum = 0
68 | # for a in [0...@samples-1] by 1
69 | # pa = {x: pixels[a*4], y: pixels[a*4+1], }
70 | # dist_to_a = Infinity
71 | # for b in [a+1...@samples] by 1
72 | # pb = {x: pixels[b*4], y: pixels[b*4+1], }
73 | # dist_to_a = Math.min(dist_to_a, vec2.sqrDist(pa, pb))
74 | # sum += dist_to_a
75 | # (@attempts = @attempts ? []).push {seed, sum}
76 |
77 | # random rotation texture
78 | seed = seed_r ? ((Math.random()*10000000)|0)
79 | # console.log seed
80 | mt = new MersenneTwister seed
81 | noise_size = @noise_size
82 | rot_pixels = new Uint8Array noise_size * noise_size * 4
83 | for i in [0...noise_size*noise_size] by 1
84 | th = mt.random() * Math.PI * 2
85 | x = Math.cos(th)
86 | y = Math.sin(th)
87 | rot_pixels[i*4] = (x+1)*127.5
88 | rot_pixels[i*4+1] = (-y+1)*127.5
89 | rot_pixels[i*4+2] = (y+1)*127.5
90 | rot_pixels[i*4+3] = (x+1)*127.5
91 |
92 | # find the sum between consecutive pixels (must be as high as possible)
93 | # (this is only for finding a good seed, do not use)
94 | # min_dist = 0
95 | # sum_dist = 0
96 | # arrlen = rot_pixels.length
97 | # for x in [0...noise_size] by 1
98 | # for y in [0...noise_size] by 1
99 | # i1 = (x+(y*noise_size))*4
100 | # x = rot_pixels[((x+(y*noise_size))*4)%arrlen]
101 | # x2 = rot_pixels[((x+1+(y*noise_size))*4)%arrlen]
102 | # y = rot_pixels[((x+((y+1)*noise_size))*4 + 1)%arrlen]
103 | # y2 = rot_pixels[((x+((y+1)*noise_size))*4 + 1)%arrlen]
104 | # dist = Math.sqrt(Math.pow(x2-x,2)+Math.pow(y2-y,2))
105 | # sum_dist += dist
106 | # min_dist = Math.max(min_dist, dist)
107 | # if min_dist > 350
108 | # (@attempts = @attempts ? []).push {seed, min_dist, sum_dist}
109 |
110 | if seed_r? or not @material?
111 | tex = @disk_texture_uniform.value = new @context.Texture {@context},
112 | formats: raw_pixels: {
113 | width: @samples, height: 1, pixels: pixels,
114 | }
115 | tex.load()
116 | if @material?
117 | @set_input 'disk_texture', tex
118 | tex = @noise_texture_uniform.value = new @context.Texture {@context},
119 | formats: raw_pixels: {
120 | width: noise_size, height: noise_size, pixels: rot_pixels,
121 | }
122 | tex.load()
123 | if @material?
124 | @set_input 'noise_texture', tex
125 |
126 | class MinMaxBlurFilter extends BaseFilter
127 | constructor: (context) ->
128 | super context, 'minmaxblur'
129 | code = []
130 | for v in ['a.', 'b.', 'c.']
131 | for v2 in [v+'xy', v+'zw']
132 | d = v+v2[3] # a.xy -> a.y
133 | code.push \
134 | "infl = (#{d} - min_d) * inv_dist;",
135 | "o.xy += #{v2} * infl;",
136 | "count.x += infl;",
137 | "infl = (max_d - #{d}) * inv_dist;",
138 | "o.zw += #{v2} * infl;",
139 | "count.y += infl;"
140 | @fragment = """
141 | precision highp float;
142 | uniform sampler2D source;
143 | varying vec2 source_coord;
144 | uniform vec2 vector;
145 | void main() {
146 | vec2 count = vec2(0.0);
147 | vec4 a = texture2D(source, source_coord-vector);
148 | vec4 b = texture2D(source, source_coord);
149 | vec4 c = texture2D(source, source_coord+vector);
150 | vec4 o = vec4(0.0);
151 | float max_d = max(max(max(max(max(a.y,a.w),b.y),b.w),c.y),c.w)+0.001;
152 | float min_d = min(min(min(min(min(a.y,a.w),b.y),b.w),c.y),c.w)-0.001;
153 | float inv_dist = 1./(max_d-min_d);
154 | float infl;
155 | #{code.join '\n '}
156 | gl_FragColor = o / count.xxyy;
157 | }
158 | """
159 |
160 | @vector = vec2.new(0,0)
161 | @uniforms.push {varname: 'vector', value: @vector}
162 |
163 |
164 | class SSAOCompose extends BaseFilter
165 | constructor: (context) ->
166 | super context, 'ssao_compose'
167 | @uniforms = [
168 | {varname: 'ssao', value: {type: 'TEXTURE'}}
169 | {varname: 'ssao_iscale', value: vec2.create()}
170 | ]
171 | @add_depth()
172 | @fragment = """
173 | precision highp float;
174 | uniform sampler2D source, ssao;
175 | uniform vec2 ssao_iscale;
176 | varying vec2 source_coord, coord;
177 | #{@library.join '\n'}
178 | void main() {
179 | vec4 color = texture2D(source, source_coord);
180 | float luminance = color.r*0.2126+color.g*0.7152+color.b*0.0722;
181 | luminance = max(0., luminance-.8) * 5.;
182 | vec2 uv = gl_FragCoord.xy * ssao_iscale;
183 | vec4 p = texture2D(ssao, uv);
184 | float ao1 = p.x, depth1 = p.y, ao2 = p.z, depth2 = p.w;
185 | float dist = abs(depth1-depth2) + 0.001;
186 | float min_d = min(depth1, depth2);
187 | float infl = (get_depth(coord) - min_d) / dist;
188 | float ao = mix(ao2, ao1, infl);
189 | gl_FragColor = vec4(color.rgb * mix(ao,1., luminance), color.a);
190 | }
191 | """
192 |
193 | class SSAOEffect extends FilterEffect
194 | constructor: (context, options={}) ->
195 | super context
196 | {
197 | @radius=2.5
198 | @zrange=2
199 | @strength=1
200 | @samples=16
201 | @clumping=2
202 | @noise_size=32
203 | @buffer_scale=1
204 | @blur_steps=2
205 | @fade_start=4000
206 | @fade_end=5000
207 | } = options
208 | @filter = new SSAOFilter(@context, this)
209 | @blur = new MinMaxBlurFilter @context
210 | @compose = new SSAOCompose @context
211 | @buffer = @buffer2 = null
212 |
213 | set_radius: (@radius) ->
214 | @filter.set_input 'radius', @radius
215 | @filter.set_input 'iradius4', 4/@radius
216 |
217 | set_strength: (@strength) ->
218 | @filter.set_input 'strength', @strength
219 |
220 | set_zrange: (@zrange) ->
221 | @filter.set_input 'zrange', 1/@zrange
222 |
223 | set_fade: (@fade_start, @fade_end) ->
224 | @filter.set_input 'fade', vec2.new @fade_start, @fade_end
225 |
226 | on_viewport_update: (@viewport) ->
227 | {width, height} = @viewport
228 | @width = next_POT width
229 | @height = next_POT height
230 | @buffer?.destroy()
231 | @buffer2?.destroy()
232 | @buffer = new @context.FloatFramebuffer {size: [@width, @height]}
233 | if @blur_steps != 0
234 | @buffer2 = new @context.FloatFramebuffer size: [@width/2, @height/2]
235 | @compose.set_input 'ssao', @buffer.texture
236 | s = @blur_steps
237 | @compose.set_input 'ssao_iscale', vec2.new 1/(@width<
241 | r = [0, 0, rect[2], rect[3]]
242 | destination = temporary
243 | @filter.apply source, @buffer, r, {}, clear: true
244 | vector = @blur.get_material().inputs.vector.value
245 | bleed = 0
246 | {current_size_x, current_size_y} = @buffer
247 | drect = [0, 0, current_size_x+bleed, current_size_y+bleed]
248 | for [0...@blur_steps] by 1
249 | drect[2]=((drect[2]-bleed)>>1)+bleed
250 | drect[3]=((drect[3]-bleed)>>1)+bleed
251 | # this is downscaling by a factor of 2, so
252 | # the blur vector needs to be 2 pixels relative to the source
253 | vec2.set vector, 2/@buffer.size_x, 0
254 | @blur.apply @buffer, @buffer2, drect, {}, clear: true
255 | vec2.set vector, 0, 1/@buffer2.size_y
256 | @blur.apply @buffer2, @buffer, drect, {}, clear: true
257 | vec2.set vector, 1/@buffer.size_x, 0
258 | @blur.apply @buffer, @buffer2, drect#, {}, clear: true
259 | vec2.set vector, 0, 1/@buffer2.size_y
260 | @blur.apply @buffer2, @buffer, drect#, {}, clear: true
261 | @compose.apply source, destination, rect
262 | # test filter only
263 | # @filter.apply source, destination, r
264 | return {destination, temporary: source}
265 |
266 |
267 | module.exports = {SSAOEffect}
268 |
--------------------------------------------------------------------------------
/engine/effects/SSAO.glsl:
--------------------------------------------------------------------------------
1 | // TODO: Tilt the disk using derivatives, and clamp to 0 instead of -1
2 | //#extension GL_OES_standard_derivatives : enable
3 | precision highp float;
4 | uniform sampler2D source, disk_texture, noise_texture;
5 | uniform mat4 projection_matrix;
6 | uniform vec2 source_size_inverse, source_scale;
7 | varying vec2 source_coord;
8 | uniform float radius, iradius4, strength, zrange;
9 | uniform vec2 fade;
10 |
11 | #define PI 3.14159265
12 | /*library goes here*/
13 |
14 | void main(void)
15 | {
16 | // NOTE: Assuming source_scale and depth_scale are the same!
17 | float depth = get_depth_no_scale(source_coord);
18 | if(depth > fade.y){
19 | gl_FragColor = vec4(1, depth, 1, depth);
20 | return;
21 | }
22 |
23 | vec2 noise_co = gl_FragCoord.xy * NOISE_ISIZE;
24 | vec4 n = texture2D(noise_texture, noise_co) - .5;
25 | mat2 rot = mat2(n.xy, n.zw);
26 |
27 | vec4 r = projection_matrix * vec4(radius, radius, -1., 1.);
28 | // pow(depth, 1.36)*2.5. seems to preserve radius much better
29 | // but without pow is kind of interesting and of course faster
30 | vec2 ratio = r.xy*depth_scale/depth;
31 |
32 | float ao = 0., samples = 0.;
33 | float d, dif, infl;
34 | vec4 t;
35 | vec2 p;
36 | vec2 limit = depth_scale - source_size_inverse;
37 |
38 | for(float i=ISAMPLES*.5; i<1.; i += ISAMPLES){
39 | t = texture2D(disk_texture, vec2(i, .5));
40 | p = rot * (t.xy - .5);
41 | // t = normalize(t); // use this to test radius
42 | // TODO: define this
43 | #ifdef NPOT_TEXTURES
44 | d = get_depth_no_scale( (source_coord + p*ratio, vec2(0.), limit));
45 | #else
46 | d = get_depth_no_scale(clamp(source_coord + p*ratio, vec2(0.), limit));
47 | #endif
48 | dif = (depth-d) * iradius4;
49 | infl = clamp(2.-dif * zrange, 0., 1.);
50 | ao += clamp(dif, -1.,1.) * infl;
51 | samples += infl;
52 | }
53 |
54 | ao /= samples;
55 | float fade = min((fade.y - depth)/(fade.y - fade.x), 1.);
56 | ao = 1.0-max(0., ao * strength * fade);
57 |
58 | // NOTE: For some reason, RGB of float textures are multiplied by sqrt(a)
59 | // So we're packing both AO in one float...
60 | // ao =
61 | // gl_FragColor = vec4(ao, depth, depth, 1.0);
62 | gl_FragColor = vec4(ao, depth, ao, depth);
63 |
64 | // gl_FragColor = vec4(n.xyz+.5, 1.);
65 | // gl_FragColor = vec4(vec3(depth*0.001), 1.0);
66 | // vec4 tt;
67 | // float o = 0.;
68 | // for(float i=ISAMPLES*.5; i<1.; i += ISAMPLES){
69 | // tt = texture2D(disk_texture, vec2(i, .5));
70 | // if(distance(tt.xy*800., gl_FragCoord.xy)<1.){
71 | // o = 1.;
72 | // }
73 | // }
74 | // gl_FragColor = vec4(o,o,o,1.);
75 | // if(distance(gl_FragCoord.xy, vec2(400.))<1.){
76 | // gl_FragColor.r = 1.;
77 | // }
78 | }
79 |
--------------------------------------------------------------------------------
/engine/effects/base.coffee:
--------------------------------------------------------------------------------
1 |
2 | class BaseEffect
3 | constructor: (@context) ->
4 | @requires_float_source = false
5 | @requires_float_destination = false
6 |
7 | on_viewport_update: (@viewport) ->
8 | # Called after it's added and when viewport changes size.
9 | # All buffers should be created here
10 |
11 | apply: (source, temporary, rect) ->
12 | # Example of passthrough effect
13 | return {destination: source, temporary}
14 |
15 | on_viewport_remove: ->
16 | # Remove buffers here
17 |
18 | class FilterEffect extends BaseEffect
19 | constructor: (context, @filter) ->
20 | super context
21 |
22 | apply: (source, temporary, rect) ->
23 | destination = temporary
24 | @filter.apply source, destination, rect
25 | return {destination, temporary: source}
26 |
27 | class CopyEffect extends FilterEffect
28 | constructor: (context) ->
29 | super context, new context.CopyFilter
30 |
31 | module.exports = {BaseEffect, FilterEffect, CopyEffect}
32 |
--------------------------------------------------------------------------------
/engine/effects/bloom.coffee:
--------------------------------------------------------------------------------
1 |
2 | {vec2} = require 'vmath'
3 | {next_POT} = require '../math_utils/math_extra'
4 |
5 | class BloomEffect
6 | constructor: (@context, @steps=4, @intensity=1.2, @threshold=0.8) ->
7 | # functions = '''
8 | # vec3 vpow(vec3 v, float p){
9 | # return vec3(pow(v.r, p), pow(v.g, p), pow(v.b, p));
10 | # }'''
11 | @highlight = new @context.ExprFilter 1,
12 | "(a.r*0.2126+a.g*0.7152+a.b*0.0722 >
13 | #{@threshold.toFixed 7})?a:vec3(0.0)"
14 | @blur = new @context.DirectionalBlurFilter
15 | # expression is "screen" mix function * emission
16 | @screen_mix = new @context.ExprFilter 2,
17 | "vec4(1.0 - (1.0-a.rgb)*(1.0-b.rgb*#{@intensity.toFixed 7}), a.a)"
18 | use_vec4: true
19 | @buffer = null
20 |
21 | on_viewport_update: (@viewport) ->
22 | {width, height} = @viewport
23 | initial_scale = 1/2
24 | # TODO: Handle big differences of POT/non POT
25 | # that cause highlights to be squashed
26 | @width = next_POT width*initial_scale
27 | @height = next_POT height*initial_scale
28 | @buffer?.destroy()
29 | @buffer2?.destroy()
30 | @buffer = new @context.ByteFramebuffer {size: [@width, @height]}
31 | @buffer2 = new @context.ByteFramebuffer {size: [@width/2, @height/2]}
32 | @screen_mix.set_input 'b_texture', @buffer.texture
33 | scale = 1/(1<<(@steps))
34 | @screen_mix.set_input 'b_scale', vec2.new scale, scale
35 |
36 | apply: (source, temporary, rect) ->
37 | # Step 1: put shiny things in @buffer
38 | @highlight.apply source, @buffer, null
39 | # Step 2: downscale and box blur @steps times
40 | # by doing radial blur twice in each iteration
41 | # horizontal+downscale from @buffer to @buffer2
42 | # vertical from @buffer2 to @buffer
43 | vector = @blur.get_material().inputs.vector.value
44 | # extend a couple of pixels outwards to avoid bleeding
45 | # (TODO: benchmark against clearing whole buffer)
46 | bleed = 0
47 | drect = [0, 0, @width+bleed, @height+bleed]
48 | for [0...@steps] by 1
49 | drect[2]=((drect[2]-bleed)>>1)+bleed
50 | drect[3]=((drect[3]-bleed)>>1)+bleed
51 | # this is downscaling by a factor of 2, so
52 | # the blur vector needs to be 2 pixels relative to the source
53 | vec2.set vector, 2/@buffer.size_x, 0
54 | @blur.apply @buffer, @buffer2, drect, {}, clear: true
55 | vec2.set vector, 0, 1/@buffer2.size_y
56 | @blur.apply @buffer2, @buffer, drect, {}, clear: true
57 | vec2.set vector, 1/@buffer.size_x, 0
58 | @blur.apply @buffer, @buffer2, drect#, {}, clear: true
59 | vec2.set vector, 0, 1/@buffer2.size_y
60 | @blur.apply @buffer2, @buffer, drect#, {}, clear: true
61 | # Step 3: Mix source and @buffer with screen mix.
62 | # The @buffer inputs are already set in previous functions
63 | destination = temporary
64 | @screen_mix.apply source, destination, rect
65 | return {destination, temporary: source}
66 |
67 | on_viewport_remove: ->
68 | @buffer.destroy()
69 | @buffer2.destroy()
70 |
71 |
72 | module.exports = {BloomEffect}
73 |
--------------------------------------------------------------------------------
/engine/effects/graph.coffee:
--------------------------------------------------------------------------------
1 |
2 | {vec2} = require 'vmath'
3 | {next_POT} = require '../math_utils/math_extra'
4 |
5 | class GraphEffect
6 | constructor: (@context, mix=.1) ->
7 | @edge = new @context.ExprFilter 2,
8 | "mix(step(source_coord.y, a), b, #{mix.toFixed 7})"
9 | @buffer = null
10 | @requires_float_source = true
11 | @requires_float_destination = true
12 |
13 | on_viewport_update: (@viewport) ->
14 | {width, height} = @viewport
15 | @buffer?.destroy()
16 | @buffer = new @context.Framebuffer {size: [width, 1]}
17 |
18 | apply: (source, temporary, rect) ->
19 | [x,y,w,h] = rect
20 | source.blit_to @buffer, [x, y+(h>>1), w, 1], [x, y, w, 1]
21 | destination = temporary
22 | @edge.set_buffers source
23 | @edge.apply @buffer, destination, rect
24 | return {destination, temporary: source}
25 |
26 | on_viewport_remove: ->
27 | @buffer.destroy()
28 |
29 |
30 | module.exports = {GraphEffect}
31 |
--------------------------------------------------------------------------------
/engine/effects/index.coffee:
--------------------------------------------------------------------------------
1 |
2 | module.exports = Object.assign(
3 | require './base'
4 | require './bloom'
5 | require './FXAA'
6 | require './SSAO'
7 | require './graph'
8 | )
9 |
--------------------------------------------------------------------------------
/engine/fetch_assets.coffee:
--------------------------------------------------------------------------------
1 |
2 | fetch = window.myou_fetch ? window.fetch
3 |
4 | fetch_promises = {} # {file_name: promise}
5 |
6 | ## Uncomment this to find out why a politician lies
7 | # Promise._all = Promise._all or Promise.all
8 | # Promise.all = (list) ->
9 | # for l in list
10 | # if not l
11 | # debugger
12 | # politician = Promise._all list
13 | # politician.list = list
14 | # politician
15 |
16 | # Load a mesh of a mesh type object, return a promise
17 | fetch_mesh = (mesh_object, options={}) ->
18 | {max_mesh_lod=1} = options
19 | promise = new Promise (resolve, reject) ->
20 | if mesh_object.type != 'MESH'
21 | reject 'object is not a mesh'
22 | {context} = mesh_object
23 | file_name = mesh_object.packed_file or mesh_object.hash
24 |
25 | # Load LoD
26 | lod_promises = []
27 | lod_objects = mesh_object.lod_objects
28 | smallest_lod = lod_objects[0]
29 | if smallest_lod
30 | max_mesh_lod = Math.max max_mesh_lod, smallest_lod.factor
31 | for lod_ob in lod_objects
32 | if lod_ob.factor <= max_mesh_lod
33 | lod_promises.push fetch_mesh(lod_ob.object)
34 |
35 | # Not load self if only lower LoDs were loaded, or it was already loaded
36 | if (max_mesh_lod < 1 and lod_promises.length != 0) or mesh_object.data
37 | return resolve Promise.all(lod_promises)
38 |
39 | fetch_promise = if file_name of fetch_promises
40 | fetch_promises[file_name]
41 | else
42 | embed_mesh = context.embed_meshes[mesh_object.hash]
43 | if embed_mesh?
44 | buffer = (new Uint32Array(embed_mesh.int_list)).buffer
45 | embed_mesh.int_list = null # No longer needed, free space
46 | console.log 'loaded as int list'
47 | Promise.resolve(buffer)
48 | else
49 | base = mesh_object.scene.data_dir + '/scenes/'
50 | uri = base + mesh_object.source_scene_name + "/#{file_name}.mesh"
51 | fetch(uri).then (response)->
52 | if not response.ok
53 | return Promise.reject "Mesh '#{mesh_object.name}'
54 | could not be loaded from URL '#{uri}' with error
55 | '#{response.status} #{response.statusText}'"
56 | response.arrayBuffer()
57 |
58 |
59 | fetch_promises[file_name] = fetch_promise
60 |
61 | fetch_promise.then (data) ->
62 | mesh_data = context.mesh_datas[mesh_object.hash]
63 |
64 | offset = 0
65 | if data.buffer? and data.byteOffset?
66 | # this is a node Buffer or a Uint8Array,
67 | # only in node or electron
68 | offset = data.byteOffset
69 | data = data.buffer
70 | mesh_object.load_from_arraybuffer data, offset
71 | if mesh_object.pending_bodies.length != 0
72 | context.main_loop.add_frame_callback ->
73 | for body in mesh_object.pending_bodies
74 | body.instance()
75 | mesh_object.pending_bodies.splice 0
76 | resolve(mesh_object)
77 | else
78 | resolve(mesh_object)
79 | return
80 | .catch (e) ->
81 | reject e
82 | return
83 | promise.mesh_object = mesh_object
84 | return promise
85 |
86 | # @nodoc
87 | # This returns a promise of all things necessary to display the object
88 | # (meshes, textures, materials)
89 | # See scene.load_objects etc
90 | fetch_objects = (object_list, options={}) ->
91 | if not object_list.length
92 | return Promise.resolve()
93 |
94 | promises = []
95 | for ob in object_list
96 | if ob.type == 'MESH'
97 | if not ob.data
98 | promises.push fetch_mesh(ob, options)
99 | for mat in ob.materials
100 | promises.push mat.load(options)
101 | # TODO: Options to not instance some, or to reuse cube maps etc.
102 | ob.instance_probes()
103 | Promise.all promises
104 |
105 | module.exports = {
106 | fetch_mesh, fetch_objects
107 | }
108 |
--------------------------------------------------------------------------------
/engine/filters.coffee:
--------------------------------------------------------------------------------
1 | {vec2, mat4} = require 'vmath'
2 | {glsl100to300} = require './material'
3 |
4 | class BaseFilter
5 | constructor: (@context, @name) ->
6 | @fragment = '#error Filter has no fragment shader'
7 | @uniforms = []
8 | @material = null
9 | @use_derivatives = false
10 | @use_depth = false
11 | @library = []
12 | @defines = {}
13 |
14 | get_material: ->
15 | if @material?
16 | return @material
17 | {blank_texture} = @context.render_manager
18 | if @use_depth
19 | # Make sure add_depth was called correctly
20 | has_sampler = false
21 | for {varname} in @uniforms when varname == 'depth_sampler'
22 | has_sampler = true
23 | break
24 | if not has_sampler
25 | throw Error "Cannot find depth sampler. Make sure you called
26 | add_depth() after assigning uniforms and before inserting
27 | this.library in the shader."
28 | header = []
29 | for k,v of @defines
30 | header.push "#define #{k} #{v}\n"
31 | {fragment} = this
32 | if header.length != 0
33 | fragment = header.join('') + fragment
34 | return @material = new @context.Material '_filter_'+@name, {
35 | material_type: 'PLAIN_SHADER',
36 | vertex: '''
37 | attribute vec3 vertex;
38 | uniform vec2 source_size, source_scale, source_size_inverse;
39 | varying vec2 source_coord;
40 | varying vec2 coord, pixel_corner;
41 | void main(){
42 | source_coord = vertex.xy*source_scale;
43 | coord = vertex.xy;
44 | gl_Position = vec4(vertex.xy*2.0-1.0, 0.0, 1.0); }''',
45 | fragment: if @use_derivatives and @context.is_webgl2
46 | glsl100to300 fragment
47 | else
48 | fragment
49 | uniforms: [
50 | {varname: 'source', value: blank_texture},
51 | # Add this input explicitely when needed
52 | {varname: 'source_size', value: vec2.new(128, 128)},
53 | {varname: 'source_size_inverse', value: vec2.new(1/128, 1/128)},
54 | {varname: 'source_scale', value: vec2.new(1, 1)},
55 | ].concat @uniforms,
56 | }
57 |
58 | add_depth: ->
59 | {blank_texture} = @context.render_manager
60 | @uniforms.push {varname: 'depth_sampler', value: blank_texture},
61 | {varname: 'depth_scale', value: vec2.new(1, 1)},
62 | {varname: 'projection_matrix_inverse', value: mat4.create()}
63 | @library.push '''
64 | uniform sampler2D depth_sampler;
65 | uniform vec2 depth_scale;
66 | uniform mat4 projection_matrix_inverse;
67 | float get_depth_no_scale(vec2 co){
68 | float z = texture2D(depth_sampler, co).r;
69 | vec4 v = projection_matrix_inverse * vec4(0., 0., z, 1.);
70 | return -v.z/v.w;
71 | }
72 | float get_depth(vec2 co){
73 | return get_depth_no_scale(co*depth_scale);
74 | }
75 | '''
76 | @use_depth = true
77 |
78 | set_input: (name, value) ->
79 | if not value?
80 | throw Error "Invalid value"
81 | input = @get_material().inputs[name]
82 | if not input?
83 | throw Error "Filter has no input '#{name}'. Inputs are:
84 | '#{Object.keys(@get_material().inputs).join "', '"}'"
85 | input.value = value
86 |
87 | apply: (source, destination, rect, inputs, options={}) ->
88 | {clear=false} = options
89 | if not source.texture?
90 | throw Error "Source must be a regular framebuffer"
91 | if clear
92 | destination.clear()
93 | destination.enable rect
94 | destination.last_viewport = source.last_viewport
95 | source.draw_with_filter this, inputs
96 |
97 | set_debugger: (dbg) ->
98 | {bg_mesh} = @context.render_manager
99 | @get_material().get_shader(bg_mesh).set_debugger(dbg)
100 |
101 | class CopyFilter extends BaseFilter
102 | constructor: (context) ->
103 | super context, 'copy'
104 | @fragment = '''
105 | precision highp float;
106 | uniform sampler2D source;
107 | varying vec2 source_coord;
108 | void main() {
109 | gl_FragColor = texture2D(source, source_coord);
110 | }
111 | '''
112 |
113 | class FlipFilter extends BaseFilter
114 | constructor: (context) ->
115 | super context, 'flip'
116 | @fragment = '''
117 | precision highp float;
118 | uniform sampler2D source;
119 | uniform vec2 source_scale;
120 | varying vec2 source_coord;
121 | void main() {
122 | vec2 co = source_coord;
123 | co.y = source_scale.y - co.y;
124 | gl_FragColor = texture2D(source, co);
125 | }
126 | '''
127 |
128 | class BoxBlurFilter extends BaseFilter
129 | constructor: (context) ->
130 | super context, 'boxblur'
131 | @fragment = """
132 | precision highp float;
133 | uniform sampler2D source;
134 | uniform vec2 source_size_inverse;
135 | varying vec2 source_coord;
136 | void main() {
137 | float x = source_coord.x, y = source_coord.y;
138 | float px = source_size_inverse.x, py = source_size_inverse.y;
139 | gl_FragColor = (
140 | texture2D(source, vec2(x-px,y-py))+
141 | texture2D(source, vec2(x-px,y))+
142 | texture2D(source, vec2(x-px,y+py))+
143 | texture2D(source, vec2(x ,y-py))+
144 | texture2D(source, vec2(x ,y))+
145 | texture2D(source, vec2(x ,y+py))+
146 | texture2D(source, vec2(x+px,y-py))+
147 | texture2D(source, vec2(x+px,y))+
148 | texture2D(source, vec2(x+px,y+py))
149 | )*#{1/9};
150 | }
151 | """
152 |
153 | class Block2x2Blur extends BaseFilter
154 | constructor: (context, options) ->
155 | {
156 | use_depth_as_alpha=false
157 | } = options ? {}
158 | super context, '2x2blur'
159 | out = 'texture2D(source, co)'
160 | if use_depth_as_alpha
161 | @add_depth()
162 | # TODO: Assuming depth is same size as source!!
163 | out = 'vec4(texture2D(source, co).rgb, get_depth_no_scale(co))'
164 | @fragment = """
165 | precision highp float;
166 | uniform sampler2D source;
167 | uniform vec2 source_size, source_size_inverse;
168 | varying vec2 source_coord;
169 | #{@library.join '\n'}
170 | vec2 round2(vec2 v){
171 | #ifdef round
172 | return vec2(round(v.x), round(v.y));
173 | #else
174 | return vec2(floor(v.x+.5), floor(v.y+.5));
175 | #endif
176 | }
177 | void main() {
178 | vec2 cosa = vec2(0.05224489795918367);
179 | vec2 co = (round2(source_coord * source_size / 2.)-.5)
180 | * 2. * source_size_inverse;
181 | gl_FragColor = #{out};
182 | }
183 | """
184 |
185 | class MipmapBiasFilter extends BaseFilter
186 | constructor: (context, @bias) ->
187 | super context, 'mipmapbias'
188 | @fragment = """
189 | //#extension GL_OES_standard_derivatives : enable
190 | //#extension GL_EXT_shader_texture_lod : enable
191 | precision highp float;
192 | uniform sampler2D source;
193 | uniform vec2 source_size_inverse;
194 | varying vec2 source_coord;
195 | uniform float bias;
196 | void main() {
197 | gl_FragColor =
198 | texture2D(source, source_coord, #{@bias.toFixed 7});
199 | }
200 | """
201 |
202 | class DirectionalBlurFilter extends BaseFilter
203 | constructor: (context, options) ->
204 | super context, 'directionalblur'
205 | {
206 | use_vec4=false
207 | use_depth_as_alpha=false
208 | } = options ? {}
209 | conversion = depth_code = inputs = ''
210 | if use_depth_as_alpha
211 | @add_depth()
212 | inputs = 'varying vec2 coord;'
213 | depth_code = '''
214 | float depth = (
215 | get_depth(coord-vector)+
216 | get_depth(coord)*2.0+
217 | get_depth(coord+vector)
218 | ) * 0.25;
219 | '''
220 | conversion = '.rgb, depth'
221 | else if use_vec4
222 | conversion = '.rgb, 1.0'
223 | @fragment = """
224 | precision highp float;
225 | uniform sampler2D source;
226 | varying vec2 source_coord;
227 | uniform vec2 vector;
228 | #{inputs}
229 | #{@library.join '\n'}
230 | void main() {
231 | #{depth_code}
232 | gl_FragColor = vec4(((
233 | texture2D(source, source_coord-vector)+
234 | texture2D(source, source_coord)*2.0+
235 | texture2D(source, source_coord+vector)
236 | ) * 0.25)#{conversion});
237 | }
238 | """
239 | @vector = vec2.new(0,0)
240 | @uniforms.push {varname: 'vector', value: @vector}
241 |
242 | class ExprFilter extends BaseFilter
243 | constructor: (context, @num_inputs=2, @expression="a+b", options={}) ->
244 | super context, 'expr'
245 | {
246 | use_vec4=false,
247 | use_depth=false,
248 | functions='',
249 | debug_vector=null,
250 | use_derivatives,
251 | } = options
252 | names = 'abcdefghijkl'
253 | @use_derivatives = use_derivatives ? /\bdFd[xy]\b/.test @expression
254 | if use_vec4
255 | type = 'vec4'
256 | swizzle = ''
257 | {expression} = this
258 | else
259 | type = 'vec3'
260 | swizzle = '.rgb'
261 | expression = "vec4(#{@expression}, 1.0);"
262 | {blank_texture} = @context.render_manager
263 | unf_lines = []
264 | sample = []
265 | for i in [0...@num_inputs] by 1
266 | letter = names[i]
267 | if i == 0
268 | tex = "texture2D(source, source_coord)"
269 | else
270 | unf_lines.push """
271 | uniform sampler2D #{letter}_texture;
272 | uniform vec2 #{letter}_scale;
273 | """
274 | @uniforms.push \
275 | {varname: letter+'_texture', value: blank_texture},
276 | {varname: letter+'_scale', value: vec2.new(1,1)}
277 | tex = "texture2D(#{letter}_texture, coord*#{letter}_scale)"
278 | sample.push " #{type} #{letter} = #{tex}#{swizzle};"
279 | if use_depth
280 | @add_depth()
281 | sample.push " float depth = get_depth(coord);"
282 | if debug_vector?
283 | unf_lines.push 'uniform vec4 v;'
284 | @uniforms.push {varname: 'v', value: debug_vector},
285 | code = [
286 | """
287 | precision highp float;
288 | uniform sampler2D source;
289 | """
290 | unf_lines...
291 | @library...
292 | """
293 | varying vec2 source_coord;
294 | varying vec2 coord;
295 | float sq(float n){return n*n;}
296 | #{functions}
297 | void main() {
298 | """
299 | sample...
300 | """
301 | gl_FragColor = #{expression};
302 | }
303 | """
304 | ]
305 | @fragment = code.join '\n'
306 |
307 |
308 | set_buffers: (buffers...) ->
309 | # TODO: move this logic to filter.apply
310 | # to accept an array of sources as input?
311 | letters = 'bcdefghijkl'
312 | {inputs} = @get_material()
313 | if @num_inputs - 1 != buffers.length
314 | throw Error "Expected #{@num_inputs-1} buffers"
315 | for buffer,i in buffers
316 | letter = letters[i]
317 | scale = inputs[letter+'_scale'].value
318 | scale.x = buffer.current_size_x/buffer.size_x
319 | scale.y = buffer.current_size_y/buffer.size_y
320 | inputs[letter+'_texture'].value = buffer.texture ? \
321 | do -> throw Error "Buffer #{letter} has no texture"
322 | return
323 |
324 | class FunctionFilter extends ExprFilter
325 | constructor: (context, function_="vec4 f(x,y){return x+y}", options={}) ->
326 | options.functions = function_
327 | [_, type, name, argstr] =
328 | function_.match /^(vec3|vec4)\s+(\w+)\((.+?)\)/
329 | options.use_vec4 = type == 'vec4'
330 | options.use_derivatives = /\bdFd[xy]\b/.test function_
331 | args = []
332 | num_inputs = 0
333 | letters = 'abcdefghijkl'
334 | for arg in argstr.split ','
335 | arg = (' '+arg).replace /\s+/g, ' '
336 | [_, t, n] = arg.split ' '
337 | if n == 'depth'
338 | options.use_depth = true
339 | else
340 | n = letters[num_inputs++]
341 | args.push n
342 | super context, num_inputs, name+"(#{args.join ','})", options
343 |
344 |
345 | module.exports = {
346 | BaseFilter, CopyFilter, FlipFilter, BoxBlurFilter, Block2x2Blur,
347 | MipmapBiasFilter, DirectionalBlurFilter, ExprFilter, FunctionFilter,
348 | }
349 |
--------------------------------------------------------------------------------
/engine/glray.coffee:
--------------------------------------------------------------------------------
1 | {mat4, vec3} = require 'vmath'
2 | {Framebuffer} = require './framebuffer'
3 | {Shader} = require './material'
4 |
5 | # TODO: assign different group_ids to mirrored and linked meshes
6 | # TODO: use depth buffer instead of short depth when available
7 | # TODO: make alternate version for when depth buffers AND draw_buffers are available
8 |
9 | gl_ray_vs = (max_distance)->
10 | shader = """
11 | precision highp float;
12 | uniform mat4 projection_matrix;
13 | uniform mat4 model_view_matrix;
14 | attribute vec3 vertex;
15 | attribute vec4 vnormal;
16 | varying float vardepth;
17 | varying float mesh_id;
18 | void main(){
19 | vec4 pos = model_view_matrix * vec4(vertex, 1.0);
20 | pos.z = min(pos.z, #{max_distance.toFixed(20)});
21 | mesh_id = vnormal.w;
22 | vardepth = -pos.z;
23 | gl_Position = projection_matrix * pos;
24 | }
25 | """
26 | return shader
27 |
28 | # This fragment shader encodes the depth in 2 bytes of the color output
29 | # and the object ID in the other 2 (group_id and mesh_id)
30 | gl_ray_fs = (max_distance) ->
31 | shader = """
32 | precision highp float;
33 | varying float vardepth;
34 | varying float mesh_id;
35 | uniform float group_id;
36 |
37 | void main(){
38 | float depth = vardepth * #{(255/max_distance).toFixed(20)};
39 | float f = floor(depth);
40 | gl_FragColor = vec4(vec3(mesh_id, group_id, f) * #{1/255}, depth-f);
41 | //gl_FragColor = vec4(vec3(mesh_id, group_id, 0) * #{1/255}, 1);
42 | }
43 | """
44 | return shader
45 |
46 | next_group_id = 0
47 | next_mesh_id = 0
48 |
49 | asign_group_and_mesh_id = (ob)->
50 | if next_group_id == 256
51 | console.log 'ERROR: Max number of meshes exceeded'
52 | return
53 | ob.group_id = next_group_id #Math.floor(Math.random() * 256)
54 | ob.mesh_id = next_mesh_id #Math.floor(Math.random() * 256)
55 |
56 | id = ob.ob_id = (ob.group_id<<8)|ob.mesh_id
57 | if next_mesh_id == 255
58 | next_group_id += 1
59 | next_mesh_id = 0
60 | else
61 | next_mesh_id += 1
62 | return id
63 |
64 | class GLRay
65 | constructor: (@context, options={}) ->
66 | {@debug_canvas, @width=512, @height=256,
67 | @max_distance=10, @render_steps=8, @wait_steps=3} = options
68 | @buffer = new Framebuffer(@context, {size: [@width, @height], color_type: 'UNSIGNED_BYTE', use_depth: true})
69 | @pixels = new Uint8Array(@width * @height * 4)
70 | @pixels16 = new Uint16Array(@pixels.buffer)
71 | @distance = 0
72 | @alpha_treshold = 0.5
73 | @step = 0
74 | @rounds = 0
75 | # Detect invalid max_distance (NaN, Infinity, 0)
76 | if (@max_distance/@max_distance) != 1
77 | console.warn "GLRay: max_distance of #{@max_distance} is invalid. Defaulting to 10."
78 | @max_distance = 10
79 | @mat = new Shader(@context, {
80 | name: 'gl_ray', vertex: gl_ray_vs(@max_distance), fragment: gl_ray_fs(@max_distance)},
81 | null,
82 | [{"name":"vertex","type":"f","count":3,"offset":0},
83 | {"name":"vnormal","type":"b","count":4,"offset":12}],[],{})
84 | @m4 = mat4.create()
85 | @world2cam = mat4.create()
86 | @cam2world = mat4.create()
87 | @last_cam2world = mat4.create()
88 | @meshes = []
89 | @sorted_meshes = null
90 | @mesh_by_id = [] #sparse array with all meshes by group_id<<8|mesh_id
91 | @debug_x = 0
92 | @debug_y = 0
93 | return
94 |
95 | resize: (@width, @height) ->
96 | @pixels = new Uint8Array(@width * @height * 4)
97 | @pixels16 = new Uint16Array(@pixels.buffer)
98 | @buffer.destroy()
99 | @buffer = new Framebuffer(@context, {size: [@width, @height], color_type: 'UNSIGNED_BYTE', use_depth: true})
100 | if @debug_canvas
101 | @debug_canvas.width = @width
102 | @debug_canvas.height = @height
103 | @ctx = null
104 | @step = 0
105 |
106 | init: (scene, camera, add_callback=true) ->
107 | @add_scene(scene)
108 | @scene = scene
109 | @camera = camera
110 | do_step_callback = (scene, frame_duration)=>
111 | @do_step()
112 | if add_callback
113 | scene.post_draw_callbacks.push do_step_callback
114 |
115 | add_scene: (scene) ->
116 | for ob in scene.children
117 | if ob.type == 'MESH'
118 | id = asign_group_and_mesh_id(ob)
119 | @mesh_by_id[id] = ob
120 |
121 | debug_xy: (x, y) ->
122 | x = (x*@width)|0
123 | y = ((1-y)*@height)|0
124 | @debug_x = x
125 | @debug_y = y
126 |
127 | get_byte_coords: (x, y) ->
128 | # same as in the function below for getting x/y in pixels
129 | # and then the array byte index
130 | x = (x*(@width-1))|0
131 | y = ((1-y)*(@height-1))|0
132 | index = (x + @width*y)<<2
133 | return {x,y,index}
134 |
135 | pick_object: (x, y) ->
136 | if @context._HMD
137 | return null
138 | # x/y in camera space
139 | xf = (x*2-1)*@inv_proj_x
140 | yf = (y*-2+1)*@inv_proj_y
141 | # x/y in pixels
142 | x = (x*(@width-1))|0
143 | y = ((1-y)*(@height-1))|0
144 | coord = (x + @width*y)<<2
145 | coord16 = coord>>1
146 | # mesh_id = @pixels[coord]
147 | # group_id = @pixels[coord+1]
148 | depth_h = @pixels[coord+2]
149 | depth_l = @pixels[coord+3]
150 | # id = (group_id<<8)|mesh_id
151 | id = @pixels16[coord16]
152 | depth = ((depth_h<<8)|depth_l) * @max_distance * 0.000015318627450980392 # 1/255/256
153 | # First round has wrong camera matrices
154 | if id == 65535 or depth == 0 or @rounds <= 1
155 | return null
156 | object = @mesh_by_id[id]
157 | if not object
158 | # TODO: This shouldn't happen!
159 | return null
160 | cam = object.scene.active_camera
161 | point = vec3.create()
162 | # Assuming perspective projection without shifting
163 | point.x = xf*depth
164 | point.y = yf*depth
165 | point.z = -depth
166 | vec3.transformMat4(point, point, @last_cam2world)
167 | # we do this instead of just passing depth to use the current camera position
168 | # TODO: move this out of this function to perform it only when it's used?
169 | wm = cam.world_matrix
170 | distance = vec3.distance(point, vec3.new(wm.m12,wm.m13,wm.m14))
171 | # I don't know why does this happen
172 | if isNaN distance
173 | return null
174 | return {object, point, distance}
175 |
176 | do_step: ->
177 | gl = @context.render_manager.gl
178 | {scene, camera, m4, mat, world2cam} = @
179 | mat.use()
180 | attr_loc_vertex = mat.attrib_pointers[0][0]
181 | attr_loc_normal = mat.attrib_pointers[1][0]
182 | @buffer.enable()
183 | restore_near = false
184 |
185 | # Clear buffer, save camera matrices, calculate meshes to render
186 | if @step == 0
187 | if @context._HMD
188 | return
189 | # # Change the far plane when it's too near
190 | # if @pick_object(0.5,0.5)?.distance < 0.01
191 | # old_near = camera.near_plane
192 | # camera.near_plane = 0.00001
193 | # camera.update_projection()
194 | # camera.near_plane = old_near
195 | # restore_near = true
196 | gl.clearColor(1, 1, 1, 1)
197 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
198 | mat4.copy(world2cam, @context.render_manager._world2cam)
199 | mat4.copy(@cam2world, camera.world_matrix)
200 | # Assuming perspective projection and no shifting
201 | @inv_proj_x = camera.projection_matrix_inv.m00
202 | @inv_proj_y = camera.projection_matrix_inv.m05
203 | @meshes = for m in scene.mesh_passes[0] when m.physics_type != 'NO_COLLISION'
204 | m
205 | for m in scene.mesh_passes[1] when m.alpha >= @alpha_treshold and m.physics_type != 'NO_COLLISION'
206 | @meshes.push (m)
207 |
208 | gl.uniformMatrix4fv(mat.u_projection_matrix, false, camera.projection_matrix.toJSON())
209 | if restore_near
210 | camera.update_projection()
211 |
212 | # Enable vertex+normal
213 | @context.render_manager.change_enabled_attributes(mat.attrib_bitmask)
214 |
215 | # Rendering a few meshes at a time
216 | # TODO: This is all broken now.
217 | part = (@meshes.length / @render_steps | 0) + 1
218 | if @step < @render_steps
219 | for mesh in @meshes[@step * part ... (@step + 1) * part]
220 | data = mesh.last_lod[camera.name]?.mesh?.data or mesh.data
221 | if data and (mesh.culled_in_last_frame ^ mesh.visible)
222 | # We're doing the same render commands as the engine,
223 | # except that we only set the attribute and uniforms we use
224 | if mat.u_group_id? and mat.group_id != mesh.group_id
225 | mat.group_id = mesh.group_id
226 | gl.uniform1f(mat.u_group_id, mat.group_id)
227 | if mat.u_mesh_id? and mat.mesh_id != mesh.mesh_id
228 | mat.mesh_id = mesh.mesh_id
229 | gl.uniform1f(mat.u_mesh_id, mat.mesh_id)
230 | mesh2world = mesh.world_matrix
231 | data = mesh.last_lod[camera.name]?.mesh?.data or mesh.data
232 | for submesh_idx in [0...data.vertex_buffers.length]
233 | gl.bindBuffer(gl.ARRAY_BUFFER, data.vertex_buffers[submesh_idx])
234 | # vertex attribute
235 | gl.vertexAttribPointer(attr_loc_vertex, 3, 5126, false, data.stride, 0)
236 | # vnormal attribute (necessary for mesh_id), length of attribute 4 instead of 3
237 | # and type UNSIGNED_BYTE instead of BYTE
238 | gl.vertexAttribPointer(attr_loc_normal, 4, 5121, false, data.stride, 12)
239 | # draw mesh
240 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, data.index_buffers[submesh_idx])
241 | mat4.multiply(m4, world2cam, mesh2world)
242 | gl.uniformMatrix4fv(mat.u_model_view_matrix, false, m4.toJSON())
243 | gl.drawElements(data.draw_method, data.num_indices[submesh_idx], 5123, 0) # gl.UNSIGNED_SHORT
244 | @step += 1
245 |
246 | # Extract pixels (some time after render is queued, to avoid stalls)
247 | if @step == @render_steps + @wait_steps
248 | # t = performance.now()
249 | gl.readPixels(0, 0, @width, @height, gl.RGBA, gl.UNSIGNED_BYTE, @pixels)
250 | # console.log((performance.now() - t).toFixed(2) + ' ms')
251 | @step = 0
252 | @rounds += 1
253 | mat4.copy(@last_cam2world, @cam2world)
254 | @draw_debug_canvas()
255 | return
256 |
257 | draw_debug_canvas: ->
258 | if @debug_canvas?
259 | if not @ctx
260 | @debug_canvas.width = @width
261 | @debug_canvas.height = @height
262 | @ctx = @debug_canvas.getContext('2d', {alpha: false})
263 | @imagedata = @ctx.createImageData(@width, @height)
264 | @imagedata.data.set(@pixels)
265 | d = @imagedata.data
266 | i = 3
267 | for y in [0...@height]
268 | for x in [0...@width]
269 | d[i] = if x == @debug_x or y == @debug_y
270 | 0
271 | else
272 | 255
273 | i += 4
274 | @ctx.putImageData(@imagedata, 0, 0)
275 | return
276 |
277 | single_step_pick_object: (x, y) ->
278 | {gl} = @context.render_manager
279 | @step = 0
280 | coords = @get_byte_coords(x, y)
281 | @buffer.enable()
282 | # TODO! Render/capture only one pixel?
283 | @do_step()
284 | while @step != 0
285 | @do_step()
286 | @rounds += 2
287 | @debug_xy x,y
288 | @pick_object(x, y)
289 |
290 | create_debug_canvas: ->
291 | if @debug_canvas?
292 | @destroy_debug_canvas()
293 | @debug_canvas = document.createElement 'canvas'
294 | @debug_canvas.width = @width
295 | @debug_canvas.height = @height
296 | @debug_canvas.style.position = 'fixed'
297 | @debug_canvas.style.top = '0'
298 | @debug_canvas.style.left = '0'
299 | @debug_canvas.style.transform = 'scaleY(-1)'
300 | document.body.appendChild @debug_canvas
301 | @ctx = null
302 |
303 | destroy_debug_canvas: ->
304 | document.body.removeChild @debug_canvas
305 | @ctx = null
306 |
307 | debug_random: ->
308 | for i in [0...1000]
309 | pick = null
310 | while pick == null
311 | pick = @pick_object(Math.random(), Math.random())
312 | return pick
313 | module.exports = {GLRay}
314 |
--------------------------------------------------------------------------------
/engine/init.coffee:
--------------------------------------------------------------------------------
1 | ```
2 | if (typeof Object.assign != 'function') {
3 | // Must be writable: true, enumerable: false, configurable: true
4 | Object.defineProperty(Object, "assign", {
5 | value: function assign(target, varArgs) { // .length of function is 2
6 | 'use strict';
7 | if (target == null) { // TypeError if undefined or null
8 | throw new TypeError('Cannot convert undefined or null to object');
9 | }
10 |
11 | var to = Object(target);
12 |
13 | for (var index = 1; index < arguments.length; index++) {
14 | var nextSource = arguments[index];
15 |
16 | if (nextSource != null) { // Skip over if undefined or null
17 | for (var nextKey in nextSource) {
18 | // Avoid bugs when hasOwnProperty is shadowed
19 | if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
20 | to[nextKey] = nextSource[nextKey];
21 | }
22 | }
23 | }
24 | }
25 | return to;
26 | },
27 | writable: true,
28 | configurable: true
29 | });
30 | }
31 | ```
32 |
33 | if not window.fetch?
34 | require 'whatwg-fetch'
35 |
36 | require './node_fetch_file'
37 |
38 | require './math_utils/math_extra'
39 |
40 | # Browser prefix management
41 | window.requestAnimationFrame =
42 | window.requestAnimationFrame or\
43 | window.mozRequestAnimationFrame or\
44 | window.webkitRequestAnimationFrame or\
45 | window.msRequestAnimationFrame or\
46 | window.setImmediate
47 |
48 | window.cancelAnimationFrame =
49 | window.cancelAnimationFrame or\
50 | window.mozCancelAnimationFrame or\
51 | window.webkitCancelAnimationFrame or\
52 | window.msCancelAnimationFrame
53 |
54 | eproto = HTMLElement?.prototype or {}
55 |
56 | eproto.requestPointerLock =
57 | eproto.requestPointerLock or\
58 | eproto.mozRequestPointerLock or\
59 | eproto.webkitRequestPointerLock
60 |
61 | eproto.requestFullscreen =
62 | eproto.requestFullscreen or\
63 | eproto.mozRequestFullScreen or\
64 | eproto.webkitRequestFullscreen or\
65 | eproto.msRequestFullScreen
66 |
67 | document.exitPointerLock =
68 | document.exitPointerLock or\
69 | document.mozExitPointerLock or\
70 | document.webkitExitPointerLock
71 |
72 | document.exitFullscreen =
73 | document.exitFullscreen or\
74 | document.mozCancelFullScreen or\
75 | document.webkitExitFullscreen or\
76 | document.msExitFullScreen
77 |
78 | if not window.performance
79 | window.performance = Date
80 |
81 | window.is_64_bit_os = (/x86_64|x86-64|Win64|x64;|amd64|AMD64|WOW64|x64_64/).test(navigator.userAgent)
82 |
--------------------------------------------------------------------------------
/engine/lamp.coffee:
--------------------------------------------------------------------------------
1 | {mat4, vec3, color4} = require 'vmath'
2 | {GameObject} = require './gameobject'
3 | {Framebuffer} = require './framebuffer'
4 | {Material, glsl100to300} = require './material'
5 | LIGHT_PROJ_TO_DEPTH = mat4.new(
6 | 0.5, 0.0, 0.0, 0.0,
7 | 0.0, 0.5, 0.0, 0.0,
8 | 0.0, 0.0, 0.5, 0.0,
9 | 0.5, 0.5, 0.5, 1.0)
10 |
11 | class Lamp extends GameObject
12 | # @nodoc
13 | type: 'LAMP'
14 | # shadow_options contains the configuration for buffer shadow rendering.
15 | # If available in the scene data, overwrites the default options and
16 | # triggers init_shadow()
17 | shadow_options:
18 | texture_size: 0 # pixels
19 | frustum_size: 0.0 # world units
20 | clip_start: 0.0
21 | clip_end: 0.0
22 | bias: 0.0
23 | bleed_bias: 0.0
24 |
25 | constructor: (context)->
26 | super context
27 | @lamp_type = 'POINT'
28 | @use_shadow = false
29 | @shadow_fb = null
30 | @shadow_texture = null
31 | # this option allows to stop rendering the shadow when
32 | # stuff didn't change
33 | @render_shadow = true
34 | @color = color4.new 1,1,1,1
35 | @energy = 1
36 | @spot_size = 1.3
37 | @spot_blend = 0.15
38 | @_view_pos = vec3.create()
39 | @_dir = vec3.create()
40 | @_depth_matrix = mat4.create()
41 | @_cam2depth = mat4.create()
42 | @_projection_matrix = mat4.create()
43 | @size_x = 0
44 | @size_y = 0
45 |
46 |
47 | #Avoid physical lamps and cameras
48 | instance_physics: ->
49 |
50 | recalculate_render_data: (world2cam, cam2world, world2light) ->
51 | wm = @world_matrix
52 | vec3.transformMat4 @_view_pos, vec3.new(wm.m12,wm.m13,wm.m14), world2cam
53 |
54 | # mat4.multiply m4, world2cam, @world_matrix
55 | # @_dir.x = -m4.m08
56 | # @_dir.y = -m4.m09
57 | # @_dir.z = -m4.m10
58 | ##We're doing the previous lines, but just for the terms we need
59 | a = world2cam
60 | b = @world_matrix
61 | b0 = b.m08; b1 = b.m09; b2 = b.m10; b3 = b.m11
62 | x = b0*a.m00 + b1*a.m04 + b2*a.m08 + b3*a.m12
63 | y = b0*a.m01 + b1*a.m05 + b2*a.m09 + b3*a.m13
64 | z = b0*a.m02 + b1*a.m06 + b2*a.m10 + b3*a.m14
65 | @_dir.x = -x
66 | @_dir.y = -y
67 | @_dir.z = -z
68 |
69 | if @shadow_fb?
70 | mat4.multiply @_cam2depth, world2light, cam2world
71 | mat4.multiply @_cam2depth, @_depth_matrix, @_cam2depth
72 | return
73 |
74 | init_shadow: ->
75 | {texture_size, frustum_size, clip_start, clip_end} = @shadow_options
76 | # This one has no depth because we're using common_shadow_fb,
77 | # then applying box blur and storing here
78 | texture_size = Math.min texture_size,
79 | @context.render_manager.max_texture_size / 4
80 | size = [texture_size, texture_size]
81 | @shadow_fb = new Framebuffer @context, {size, use_depth: false}
82 | @shadow_texture = @shadow_fb.texture
83 |
84 | # If using half float buffers, add a little bit of extra bias
85 | extra_bias = ''
86 | if @shadow_fb.tex_type == 0x8D61 # HALF_FLOAT_OES
87 | # TODO: make configurable? or calculate depending on scene size?
88 | extra_bias = '-0.0007'
89 | # else
90 | # # TODO: Why is this needed for android? is it messing with other things?
91 | # extra_bias = '+0.0007'
92 |
93 | varyings = [{type: 'PROJ_POSITION', varname: 'proj_position'}]
94 | fs = fs_tex = """#extension GL_OES_standard_derivatives : enable
95 | precision highp float;
96 | #ifdef USE_TEXTURE
97 | uniform sampler2D samp;
98 | varying vec2 uv;
99 | #endif
100 | varying vec4 proj_position;
101 | void main(){
102 | #ifdef USE_TEXTURE
103 | if(texture2D(samp, uv).a < 0.5) discard;
104 | #endif
105 | float depth = proj_position.z/proj_position.w;
106 | depth = depth * 0.5 + 0.5;
107 | float dx = dFdx(depth);
108 | float dy = dFdy(depth);
109 | gl_FragColor = vec4(depth #{extra_bias},
110 | pow(depth, 2.0) + 0.25*(dx*dx + dy*dy), 0.0, 1.0);
111 | }"""
112 | if @context.is_webgl2
113 | fs = glsl100to300 fs
114 | fs_tex = glsl100to300 fs_tex, USE_TEXTURE: 1
115 |
116 | # regular shadows
117 | mat = new Material @context, @name+'_shadow', {
118 | fragment: fs, varyings, material_type: 'PLAIN_SHADER',
119 | }
120 | mat.is_shadow_material = true
121 | @_shadow_material = mat
122 |
123 | # clip alpha shadows with texture
124 | {blank_texture} = @context.render_manager
125 | mat = new Material @context, @name+'_alpha_shadow', {
126 | fragment: fs_tex, material_type: 'PLAIN_SHADER',
127 | uniforms: [{varname: 'samp', value: blank_texture}],
128 | varyings: varyings.concat [{varname: 'uv', type: 'UV'}]
129 | }
130 | mat.is_shadow_material = true
131 | @_alpha_shadow_material = mat
132 |
133 | mat4.ortho(
134 | @_projection_matrix,
135 | -frustum_size,
136 | frustum_size,
137 | -frustum_size,
138 | frustum_size,
139 | clip_start,
140 | clip_end
141 | )
142 | mat4.multiply(
143 | @_depth_matrix,
144 | LIGHT_PROJ_TO_DEPTH,
145 | @_projection_matrix
146 | )
147 | return
148 |
149 | destroy_shadow: ->
150 | @shadow_fb?.destroy()
151 | @shadow_fb = null
152 | @material?.destroy()
153 | @material = null
154 | @shadow_texture?.gl_tex = @context.render_manager.white_texture
155 | return
156 |
157 | module.exports = {Lamp}
158 |
--------------------------------------------------------------------------------
/engine/libs/ammo.wasm.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/myou-engine/myou-engine-js/1e71552db1f58678652b7e0d582248da1ce5172e/engine/libs/ammo.wasm.wasm
--------------------------------------------------------------------------------
/engine/main_loop.coffee:
--------------------------------------------------------------------------------
1 | {evaluate_all_animations} = require './animation'
2 | # Logic assumes a frame won't be longer than this
3 | # Below that point, things go slow motion
4 | MAX_FRAME_DURATION = 167 # 6 fps
5 | MAX_TASK_DURATION = MAX_FRAME_DURATION * 0.5
6 |
7 | # setImmediate emulation
8 | set_immediate_pending = []
9 |
10 | class MainLoop
11 |
12 | constructor: (context)->
13 | # All in milliseconds
14 | @frame_duration = 16
15 | # Time from beginning of a tick to the next (including idle time)
16 | (@last_frame_durations = new Float32Array(30)).fill?(16)
17 | # Time it takes for running (logic) JS code
18 | (@logic_durations = new Float32Array(30)).fill?(16)
19 | # Time it takes for physics evaluations
20 | (@physics_durations = new Float32Array(30)).fill?(16)
21 | # Time it takes for evaluating animations and constraints
22 | (@animation_durations = new Float32Array(30)).fill?(16)
23 | # Time it takes for submitting GL commands
24 | (@render_durations = new Float32Array(30)).fill?(16)
25 | @_fdi = 0
26 | @timeout_time = context.MYOU_PARAMS.timeout
27 | @tasks_per_tick = context.MYOU_PARAMS.tasks_per_tick || 1
28 | @reset_timeout()
29 | @last_time = 0
30 | @enabled = false
31 | @stopped = true
32 | @use_raf = true
33 | @use_frame_callbacks = true
34 | @context = context
35 | @_bound_tick = @tick.bind @
36 | @_bound_run = @run.bind @
37 | @_frame_callbacks = []
38 | @frame_number = 0
39 | @update_fps = null # assign a function to be called every 30 frames
40 |
41 | run: ->
42 | @stopped = false
43 | @enabled = true
44 | if not @req_tick
45 | @req_tick = requestAnimationFrame @_bound_tick
46 | @last_time = performance.now()
47 |
48 |
49 | stop: ->
50 | if @req_tick?
51 | cancelAnimationFrame @req_tick
52 | @req_tick = null
53 | @enabled = false
54 | @stopped = true
55 |
56 | sleep: (time)->
57 | if @sleep_timeout_id?
58 | clearTimeout(@sleep_timeout_id)
59 | @sleep_timeout_id = null
60 | if @enabled
61 | @stop()
62 | @sleep_timeout_id = setTimeout(@_bound_run, time)
63 |
64 | add_frame_callback: (callback)->
65 | if not @use_frame_callbacks
66 | return callback()
67 | if callback.next?
68 | # it's a generator instance
69 | callback = callback.next.bind callback
70 | @_frame_callbacks.push callback
71 |
72 | timeout: (time)->
73 | if @stopped
74 | return
75 | if @timeout_id?
76 | clearTimeout(@timeout_id)
77 | @timeout_id = null
78 | @enabled = true
79 | @timeout_id = setTimeout((=>@enabled = false), time)
80 |
81 | reset_timeout: =>
82 | if @timeout_time
83 | @timeout(@timeout_time)
84 |
85 | tick_once: ->
86 | if @req_tick?
87 | cancelAnimationFrame @req_tick
88 | @tick()
89 | else
90 | @tick()
91 | cancelAnimationFrame @req_tick
92 | @req_tick = null
93 |
94 | tick: ->
95 | if @use_raf
96 | HMD = @context.vr_screen?.HMD
97 | if HMD?
98 | @req_tick = HMD.requestAnimationFrame @_bound_tick
99 | else
100 | @req_tick = requestAnimationFrame @_bound_tick
101 | if set_immediate_pending.length != 0
102 | for f in set_immediate_pending.splice 0
103 | f()
104 | time = performance.now()
105 | @frame_duration = frame_duration = time - @last_time
106 | @last_time = time
107 |
108 | task_time = time
109 | max_task_time = MAX_TASK_DURATION + time
110 | while (task_time < max_task_time) and (@_frame_callbacks.length != 0)
111 | f = @_frame_callbacks.shift()
112 | ret = f()
113 | if ret?.done? and ret.done == false
114 | @_frame_callbacks.push f
115 | task_time = performance.now()
116 |
117 | if not @enabled
118 | return
119 | @last_frame_durations[@_fdi] = frame_duration
120 |
121 | # Limit low speed of logic and physics
122 | frame_duration = Math.min(frame_duration, MAX_FRAME_DURATION)
123 |
124 | @context.input_manager.update_axes()
125 |
126 | for scene_name in @context.loaded_scenes
127 | scene = @context.scenes[scene_name]
128 | pdc = scene.pre_draw_callbacks
129 | i = pdc.length+1
130 | while --i != 0
131 | pdc[pdc.length-i] scene, frame_duration
132 |
133 | time2 = performance.now()
134 |
135 | for scene_name in @context.loaded_scenes
136 | @context.scenes[scene_name].world.step frame_duration
137 |
138 | time3 = performance.now()
139 |
140 | evaluate_all_animations @context, frame_duration
141 |
142 | time4 = performance.now()
143 |
144 | for name, video_texture of @context.video_textures
145 | video_texture.update_texture?()
146 |
147 | @context.render_manager.draw_all()
148 |
149 | time5 = performance.now()
150 |
151 | for scene_name in @context.loaded_scenes
152 | scene = @context.scenes[scene_name]
153 | pdc = scene.post_draw_callbacks
154 | i = pdc.length+1
155 | while --i != 0
156 | pdc[pdc.length-i] scene, frame_duration
157 |
158 | @context.input_manager.reset_buttons()
159 |
160 | @frame_number += 1
161 |
162 | time6 = performance.now()
163 |
164 | @logic_durations[@_fdi] = (time2 - time) + (time6 - time5)
165 | @physics_durations[@_fdi] = time3 - time2
166 | @animation_durations[@_fdi] = time4 - time3
167 | @render_durations[@_fdi] =
168 | Math.max @context.render_manager.last_time_ms, time5 - time4
169 | @_fdi = (@_fdi+1) % @last_frame_durations.length
170 | if @_fdi == 0 and @update_fps
171 | @update_fps {
172 | max_fps: 1000/Math.min.apply(null, @last_frame_durations),
173 | min_fps: 1000/Math.max.apply(null, @last_frame_durations),
174 | average_fps: 1000/average(@last_frame_durations),
175 | max_logic_duration: 1000/Math.max.apply(null, @logic_durations),
176 | average_logic_duration: average(@logic_durations),
177 | max_physics_durations: \
178 | 1000/Math.max.apply(null, @physics_durations),
179 | average_physics_durations: average(@physics_durations),
180 | max_animation_durations: \
181 | 1000/Math.max.apply(null, @animation_durations),
182 | average_animation_durations: average(@animation_durations),
183 | max_render_durations: \
184 | 1000/Math.max.apply(null, @render_durations),
185 | average_render_durations: average(@render_durations),
186 | }
187 | if set_immediate_pending.length != 0
188 | for f in set_immediate_pending.splice 0
189 | f()
190 | return
191 |
192 |
193 | average = (list) ->
194 | r = 0
195 | for v in list
196 | r += v
197 | return r/list.length
198 |
199 | set_immediate = (func) ->
200 | set_immediate_pending.push func
201 | return
202 |
203 | module.exports = {MainLoop, set_immediate}
204 |
--------------------------------------------------------------------------------
/engine/material_shaders/blender_internal.coffee:
--------------------------------------------------------------------------------
1 |
2 | GPU_DYNAMIC_GROUP_MISC = 0x10000
3 | GPU_DYNAMIC_GROUP_LAMP = 0x20000
4 | GPU_DYNAMIC_GROUP_OBJECT = 0x30000
5 | GPU_DYNAMIC_GROUP_SAMPLER = 0x40000
6 | GPU_DYNAMIC_GROUP_MIST = 0x50000
7 | GPU_DYNAMIC_GROUP_WORLD = 0x60000
8 | GPU_DYNAMIC_GROUP_MAT = 0x70000
9 |
10 | GPU_DYNAMIC_OBJECT_VIEWMAT = 1 | GPU_DYNAMIC_GROUP_OBJECT
11 | GPU_DYNAMIC_OBJECT_MAT = 2 | GPU_DYNAMIC_GROUP_OBJECT
12 | GPU_DYNAMIC_OBJECT_VIEWIMAT = 3 | GPU_DYNAMIC_GROUP_OBJECT
13 | GPU_DYNAMIC_OBJECT_IMAT = 4 | GPU_DYNAMIC_GROUP_OBJECT
14 | GPU_DYNAMIC_OBJECT_COLOR = 5 | GPU_DYNAMIC_GROUP_OBJECT
15 | GPU_DYNAMIC_OBJECT_AUTOBUMPSCALE = 6 | GPU_DYNAMIC_GROUP_OBJECT
16 | GPU_DYNAMIC_OBJECT_LOCTOVIEWMAT = 7 | GPU_DYNAMIC_GROUP_OBJECT
17 | GPU_DYNAMIC_OBJECT_LOCTOVIEWIMAT = 8 | GPU_DYNAMIC_GROUP_OBJECT
18 |
19 | # point sun spot hemi area
20 | GPU_DYNAMIC_LAMP_DYNVEC = 1 | 0x20000 # X X X X
21 | GPU_DYNAMIC_LAMP_DYNCO = 2 | 0x20000 # X X X
22 | GPU_DYNAMIC_LAMP_DYNIMAT = 3 | 0x20000 # X
23 | GPU_DYNAMIC_LAMP_DYNPERSMAT = 4 | 0x20000 # X X
24 | GPU_DYNAMIC_LAMP_DYNENERGY = 5 | 0x20000 # X X X X X
25 | GPU_DYNAMIC_LAMP_DYNCOL = 6 | 0x20000 # X X X X X
26 | GPU_DYNAMIC_LAMP_DISTANCE = 7 | 0x20000 # X X
27 | GPU_DYNAMIC_LAMP_ATT1 = 8 | 0x20000 # X X
28 | GPU_DYNAMIC_LAMP_ATT2 = 9 | 0x20000 # X X
29 | GPU_DYNAMIC_LAMP_SPOTSIZE = 10 | 0x20000 # X
30 | GPU_DYNAMIC_LAMP_SPOTBLEND = 11 | 0x20000 # missing?
31 | GPU_DYNAMIC_LAMP_SPOTSCALE = 12 | 0x20000 # X
32 | GPU_DYNAMIC_LAMP_COEFFCONST = 13 | 0x20000 # X X
33 | GPU_DYNAMIC_LAMP_COEFFLIN = 14 | 0x20000 # X X
34 | GPU_DYNAMIC_LAMP_COEFFQUAD = 15 | 0x20000 # X X
35 |
36 | GPU_DYNAMIC_SAMPLER_2DBUFFER = 1 | GPU_DYNAMIC_GROUP_SAMPLER
37 | GPU_DYNAMIC_SAMPLER_2DIMAGE = 2 | GPU_DYNAMIC_GROUP_SAMPLER
38 | GPU_DYNAMIC_SAMPLER_2DSHADOW = 3 | GPU_DYNAMIC_GROUP_SAMPLER
39 |
40 | GPU_DYNAMIC_MIST_ENABLE = 1 | GPU_DYNAMIC_GROUP_MIST
41 | GPU_DYNAMIC_MIST_START = 2 | GPU_DYNAMIC_GROUP_MIST
42 | GPU_DYNAMIC_MIST_DISTANCE = 3 | GPU_DYNAMIC_GROUP_MIST
43 | GPU_DYNAMIC_MIST_INTENSITY = 4 | GPU_DYNAMIC_GROUP_MIST
44 | GPU_DYNAMIC_MIST_TYPE = 5 | GPU_DYNAMIC_GROUP_MIST
45 | GPU_DYNAMIC_MIST_COLOR = 6 | GPU_DYNAMIC_GROUP_MIST
46 |
47 | GPU_DYNAMIC_HORIZON_COLOR = 1 | GPU_DYNAMIC_GROUP_WORLD
48 | GPU_DYNAMIC_AMBIENT_COLOR = 2 | GPU_DYNAMIC_GROUP_WORLD
49 | GPU_DYNAMIC_ZENITH_COLOR = 3 | GPU_DYNAMIC_GROUP_WORLD
50 |
51 | GPU_DYNAMIC_MAT_DIFFRGB = 1 | GPU_DYNAMIC_GROUP_MAT
52 | GPU_DYNAMIC_MAT_REF = 2 | GPU_DYNAMIC_GROUP_MAT
53 | GPU_DYNAMIC_MAT_SPECRGB = 3 | GPU_DYNAMIC_GROUP_MAT
54 | GPU_DYNAMIC_MAT_SPEC = 4 | GPU_DYNAMIC_GROUP_MAT
55 | GPU_DYNAMIC_MAT_HARD = 5 | GPU_DYNAMIC_GROUP_MAT
56 | GPU_DYNAMIC_MAT_EMIT = 6 | GPU_DYNAMIC_GROUP_MAT
57 | GPU_DYNAMIC_MAT_AMB = 7 | GPU_DYNAMIC_GROUP_MAT
58 | GPU_DYNAMIC_MAT_ALPHA = 8 | GPU_DYNAMIC_GROUP_MAT
59 | GPU_DYNAMIC_MAT_MIR = 9 | GPU_DYNAMIC_GROUP_MAT
60 |
61 | material_module = null
62 |
63 | # TODO: export constant names instead of numbers?
64 |
65 | class BlenderInternalMaterial
66 | constructor: (@material) ->
67 | {data, _input_list, inputs, _texture_list, @context} = @material
68 | {blank_texture} = @context.render_manager
69 | for u in data.uniforms
70 | switch u.type
71 | when -1
72 | path = u.path or u.index
73 | value = if u.value.length? then new Float32Array(u.value)
74 | else u.value
75 | _input_list.push inputs[path] = {value, type: u.count, path}
76 | when 13, GPU_DYNAMIC_SAMPLER_2DIMAGE, \
77 | GPU_DYNAMIC_SAMPLER_2DBUFFER, \
78 | 14, GPU_DYNAMIC_SAMPLER_2DSHADOW
79 | _texture_list.push {value: blank_texture}
80 | return
81 |
82 | assign_textures: ->
83 | {data, _input_list, _texture_list, scene, render_scene} = @material
84 | texture_count = 0
85 | for u in data.uniforms
86 | switch u.type
87 | when 14, GPU_DYNAMIC_SAMPLER_2DSHADOW
88 | tex = render_scene.objects[u.lamp].shadow_texture
89 | if tex
90 | @material._texture_list[texture_count++].value = tex
91 | when 13, GPU_DYNAMIC_SAMPLER_2DIMAGE, \
92 | GPU_DYNAMIC_SAMPLER_2DBUFFER, \
93 | 14, GPU_DYNAMIC_SAMPLER_2DSHADOW # 2D image
94 | tex = scene?.textures[u.image]
95 | if not tex?
96 | throw Error "Texture #{u.image} not found
97 | (in material #{@material.name})."
98 | @material._texture_list[texture_count++].value = tex
99 | return
100 |
101 | get_model_view_matrix_name: ->
102 | for u in @material.data.uniforms or []
103 | switch u.type
104 | when 1, GPU_DYNAMIC_OBJECT_VIEWMAT # model_view_matrix
105 | return u.varname
106 | return "model_view_matrix"
107 |
108 | get_projection_matrix_name: ->
109 | return "projection_matrix"
110 |
111 | get_code: (defines) ->
112 | glsl_version = 100
113 | fragment = @material.shader_library + @material.data.fragment
114 | if @context.is_webgl2
115 | if @material.data.use_egl_image_external
116 | fragment = '#extension GL_OES_EGL_image_external_essl3 : require\n' + fragment
117 | material_module ?= require '../material'
118 | fragment = material_module.glsl100to300 fragment, defines
119 | glsl_version = 300
120 | else
121 | if @material.data.use_egl_image_external
122 | fragment = '#extension GL_OES_EGL_image_external : require\n' + fragment
123 | return {fragment, glsl_version}
124 |
125 | get_uniform_assign: (gl, program) ->
126 | # TODO: reassign lamps when cloning etc
127 | {scene, scene:{objects}, render_scene} = @material
128 | code = [] # lines for the @uniform_assign_func function
129 | lamp_indices = {}
130 | lamps = []
131 | current_lamp = null
132 | curr_lamp_name = ''
133 | current_input = -1
134 | locations = []
135 | texture_count = -1
136 | for u in @material.data.uniforms or []
137 | # Advance counters independently of uniform presence
138 | switch u.type
139 | when -1 # custom uniforms are material.inputs
140 | current_input++
141 | when 13, GPU_DYNAMIC_SAMPLER_2DIMAGE, \
142 | GPU_DYNAMIC_SAMPLER_2DBUFFER, \
143 | 14, GPU_DYNAMIC_SAMPLER_2DSHADOW
144 | texture_count++
145 | uloc = gl.getUniformLocation(program, u.varname)
146 | if not uloc? or uloc == -1
147 | continue
148 | # We'll use this location in a JS function that we'll be generating
149 | # below. The result is @uniform_assign_func
150 | loc_idx = locations.length
151 | locations.push uloc
152 | # Magic numbers correspond to the old values of blender constants
153 | is_lamp = (u.type & 0xff0000) == GPU_DYNAMIC_GROUP_LAMP
154 | switch u.type
155 | when 6, 7, 9, 10, 11, 16
156 | is_lamp = true
157 | if is_lamp
158 | # In Blender 2.71, there's some lamp properties with missing
159 | # lamp name. For that reason we use "u.lamp or curr_lamp_name".
160 | # It assumes it's preceded by another attribute with lamp name.
161 | curr_lamp_name = u.lamp or curr_lamp_name
162 | current_lamp = lamp_indices[curr_lamp_name]
163 | if not current_lamp?
164 | current_lamp = lamp_indices[curr_lamp_name] = lamps.length
165 | lamp = objects[curr_lamp_name]
166 | lamps.push lamp
167 | if not lamp?
168 | console.error "Lamp '#{name}' not found,
169 | referenced in material '#{@material.name}"
170 | continue
171 | switch u.type
172 | when 1, GPU_DYNAMIC_OBJECT_VIEWMAT # model_view_matrix
173 | code # Ignored, used only for get_model_view_matrix_name()
174 | when 2, GPU_DYNAMIC_OBJECT_MAT # object_matrix
175 | code.push "gl.uniformMatrix4fv(locations[#{loc_idx}], false,
176 | ob.world_matrix.toJSON());"
177 | when 3, GPU_DYNAMIC_OBJECT_VIEWIMAT # inverse view_matrix
178 | # (not model_view_matrix!)
179 | code.push "gl.uniformMatrix4fv(locations[#{loc_idx}], false,
180 | render._cam2world.toJSON());"
181 | when 4, GPU_DYNAMIC_OBJECT_IMAT # inverse object_matrix
182 | # NOTE: Objects with zero scale are not drawn,
183 | # otherwise m4 could be null
184 | code.push "m4 = mat4.invert(render._m4, ob.world_matrix);"
185 | code.push "gl.uniformMatrix4fv(locations[#{loc_idx}], false,
186 | m4.toJSON());"
187 | when 5, GPU_DYNAMIC_OBJECT_COLOR # object color
188 | code.push "v=ob.color;gl.uniform4f(locations[#{loc_idx}],
189 | v.r, v.g, v.b, v.a);"
190 | when GPU_DYNAMIC_OBJECT_LOCTOVIEWMAT
191 | code.push "gl.uniformMatrix4fv(locations[#{loc_idx}], false,
192 | render._model_view_matrix.toJSON());"
193 | when 6, GPU_DYNAMIC_LAMP_DYNVEC # lamp direction in camera space
194 | code.push "v=lamps[#{current_lamp}]._dir;
195 | gl.uniform3f(locations[#{loc_idx}], v.x, v.y, v.z);"
196 | when 7, GPU_DYNAMIC_LAMP_DYNCO # lamp position in camera space
197 | code.push "v=lamps[#{current_lamp}]._view_pos;
198 | gl.uniform3f(locations[#{loc_idx}], v.x, v.y, v.z);"
199 | when 9, GPU_DYNAMIC_LAMP_DYNPERSMAT#camera to lamp shadow matrix
200 | code.push "gl.uniformMatrix4fv(locations[#{loc_idx}], false,
201 | lamps[#{current_lamp}]._cam2depth.toJSON());"
202 | when 10, GPU_DYNAMIC_LAMP_DYNENERGY # lamp energy
203 | code.push "gl.uniform1f(locations[#{loc_idx}],
204 | lamps[#{current_lamp}].energy);"
205 | when 11, GPU_DYNAMIC_LAMP_DYNCOL # lamp color
206 | if u.datatype == 4 # vec3
207 | code.push "v=lamps[#{current_lamp}].color;
208 | gl.uniform3f(locations[#{loc_idx}], v.r, v.g, v.b);"
209 | else # vec4
210 | code.push "v=lamps[#{current_lamp}].color;
211 | gl.uniform4f(locations[#{loc_idx}],
212 | v.r, v.g, v.b, v.a);"
213 | when 16, GPU_DYNAMIC_LAMP_DISTANCE # lamp falloff distance
214 | code.push "gl.uniform1f(locations[#{loc_idx}],
215 | lamps[#{current_lamp}].falloff_distance);"
216 | when 19, GPU_DYNAMIC_LAMP_SPOTSIZE
217 | # TODO: Wtf?
218 | code#.push "gl.uniform1f(locations[#{loc_idx}],
219 | # lamps[#{current_lamp}].spot_size);"
220 | when 20, GPU_DYNAMIC_LAMP_SPOTBLEND
221 | code#.push "gl.uniform1f(locations[#{loc_idx}],
222 | # lamps[#{current_lamp}].spot_blend);"
223 | when 14, GPU_DYNAMIC_SAMPLER_2DSHADOW
224 | code.push "gl.uniform1i(locations[#{loc_idx}],
225 | tex_list[#{texture_count}].value.bound_unit);"
226 | when 13, GPU_DYNAMIC_SAMPLER_2DIMAGE, \
227 | GPU_DYNAMIC_SAMPLER_2DBUFFER # 2D image
228 | code.push "gl.uniform1i(locations[#{loc_idx}],
229 | tex_list[#{texture_count}].value.bound_unit);"
230 | when GPU_DYNAMIC_AMBIENT_COLOR
231 | code.push "v=ob.scene.ambient_color;
232 | gl.uniform4f(locations[#{loc_idx}], v.r, v.g, v.b, v.a)"
233 | when GPU_DYNAMIC_LAMP_COEFFCONST
234 | console.warn u.lamp, 'TODO: lamp coefficient const'
235 | when GPU_DYNAMIC_LAMP_COEFFLIN
236 | console.warn u.lamp, 'TODO: lamp coefficient lin'
237 | when GPU_DYNAMIC_LAMP_COEFFQUAD
238 | console.warn u.lamp, 'TODO: lamp coefficient quad'
239 | when GPU_DYNAMIC_MIST_COLOR
240 | var_mistcol = u.varname # TODO: mist
241 | when GPU_DYNAMIC_MIST_DISTANCE
242 | var_mistdist = u.varname
243 | when GPU_DYNAMIC_MIST_ENABLE
244 | var_mistenable = u.varname
245 | when GPU_DYNAMIC_MIST_INTENSITY
246 | var_mistint = u.varname
247 | when GPU_DYNAMIC_MIST_START
248 | var_miststart = u.varname
249 | when GPU_DYNAMIC_MIST_TYPE
250 | var_misttype = u.varname
251 | when GPU_DYNAMIC_HORIZON_COLOR
252 | code.push "v=ob.scene.background_color;
253 | gl.uniform3f(locations[#{loc_idx}], v.r, v.g, v.b);"
254 | when -1 # custom
255 | {value, type} = @material._input_list[current_input]
256 | value_code = "inputs[#{current_input}].value"
257 | vlen = Object.keys(value).length
258 | # TODO: Optimize by having four value_codes
259 | # and putting them depending on type
260 | code.push if vlen
261 | if value.x?
262 | value_args = ['v.x','v.y','v.z','v.w'][...vlen].join(',')
263 | else
264 | value_args = ['v.r','v.g','v.b','v.a'][...vlen].join(',')
265 | "v=#{value_code};
266 | gl.uniform#{vlen}f(locations[#{loc_idx}],
267 | #{value_args});"
268 | else if type == 1
269 | "gl.uniform1f(locations[#{loc_idx}], #{value_code});"
270 | else
271 | filler = ([0,0,0,0][...type]+'')[1...]
272 | "gl.uniform#{type}fv(locations[#{loc_idx}],
273 | [#{value_code}#{filler}]);"
274 | else
275 | console.log "Warning: unknown uniform", u.varname, \
276 | u.type>>16, u.type&0xffff, "of data type", \
277 | ['0','1i','1f','2f','3f','4f',
278 | 'm3','m4','4ub'][u.datatype]
279 | preamble = 'var v, locations=shader.uniform_locations,
280 | lamps=shader.lamps, material=shader.material,
281 | inputs=material._input_list, tex_list=material._texture_list;\n'
282 | func = new Function 'gl', 'shader', 'ob', 'render', 'mat4',
283 | preamble+code.join '\n'
284 | {uniform_assign_func: func, uniform_locations: locations, lamps}
285 |
286 |
287 | module.exports = {BlenderInternalMaterial}
288 |
--------------------------------------------------------------------------------
/engine/material_shaders/plain.coffee:
--------------------------------------------------------------------------------
1 |
2 |
3 | class PlainShaderMaterial
4 | constructor: (@material) ->
5 | {data, _input_list, inputs, _texture_list} = @material
6 | for u in data.uniforms or [] when u?
7 | {varname, value} = u
8 | _input_list.push inputs[varname] = u
9 | if value.type == 'TEXTURE'
10 | _texture_list.push inputs[varname]
11 | @use_projection_matrix_inverse = false
12 |
13 | assign_textures: ->
14 |
15 | get_model_view_matrix_name: -> 'model_view_matrix'
16 |
17 | get_projection_matrix_name: -> 'projection_matrix'
18 |
19 | get_projection_matrix_inverse_name: ->
20 | @use_projection_matrix_inverse = true
21 | return 'projection_matrix_inverse'
22 |
23 | get_code: ->
24 | {fragment} = @material.data
25 | glsl_version = 100
26 | head = ''
27 | if fragment[...15] == '#version 300 es'
28 | glsl_version = 300
29 | head = '#version 300 es\n'
30 | fragment = fragment[16...]
31 | if @material.context.is_webgl2
32 | if @material.data.use_egl_image_external and glsl_version == 100
33 | fragment = '#extension GL_OES_EGL_image_external_essl3 : require\n'+fragment
34 | else
35 | if @material.data.use_egl_image_external and glsl_version == 100
36 | fragment = '#extension GL_OES_EGL_image_external : require\n'+fragment
37 | {fragment: head+fragment, glsl_version}
38 |
39 | get_uniform_assign: (gl, program) ->
40 | code = []
41 | locations = []
42 | for u,i in @material.data.uniforms or [] when u?
43 | {varname, value} = u
44 | uloc = gl.getUniformLocation(program, u.varname)
45 | if not uloc? or uloc == -1
46 | continue
47 | loc_idx = locations.length
48 | value_code = "inputs[#{i}].value"
49 | code.push if value.type == 'TEXTURE'
50 | "gl.uniform1i(locations[#{loc_idx}], #{value_code}.bound_unit);"
51 | else if value.length?
52 | "gl.uniform#{value.length}fv(locations[#{loc_idx}],
53 | #{value_code});"
54 | else if value.w?
55 | "v=#{value_code};gl.uniform4f(locations[#{loc_idx}],
56 | v.x, v.y, v.z, v.w);"
57 | else if value.z?
58 | "v=#{value_code};gl.uniform3f(locations[#{loc_idx}],
59 | v.x, v.y, v.z);"
60 | else if value.y?
61 | "v=#{value_code};gl.uniform2f(locations[#{loc_idx}], v.x, v.y);"
62 | else if value.a?
63 | "v=#{value_code};gl.uniform4f(locations[#{loc_idx}],
64 | v.r, v.g, v.b, v.a);"
65 | else if value.b?
66 | "v=#{value_code};gl.uniform3f(locations[#{loc_idx}],
67 | v.r, v.g, v.b);"
68 | else if value.m15?
69 | "gl.uniformMatrix4fv(locations[#{loc_idx}], false,
70 | #{value_code}.toJSON());"
71 | else if value.m08?
72 | "gl.uniformMatrix3fv(locations[#{loc_idx}], false,
73 | #{value_code}.toJSON());"
74 | else
75 | "gl.uniform1f(locations[#{loc_idx}], #{value_code});"
76 | locations.push uloc
77 | if @use_projection_matrix_inverse
78 | if (uloc = gl.getUniformLocation(program, 'projection_matrix_inverse'))?
79 | loc_idx = locations.length
80 | code.push "gl.uniformMatrix4fv(locations[#{loc_idx}], false,
81 | render.projection_matrix_inverse.toJSON());"
82 | locations.push uloc
83 | if code.length
84 | preamble = 'var locations=shader.uniform_locations,
85 | inputs=shader.material._input_list, v;\n'
86 | uniform_assign_func = new Function 'gl', 'shader', 'ob', 'render',
87 | 'mat4', preamble+code.join '\n'
88 | else
89 | uniform_assign_func = ->
90 | {
91 | uniform_assign_func,
92 | uniform_locations: locations,
93 | }
94 |
95 |
96 | module.exports = {PlainShaderMaterial}
97 |
--------------------------------------------------------------------------------
/engine/math_utils/g2.coffee:
--------------------------------------------------------------------------------
1 |
2 | {vec2, vec3} = require 'vmath'
3 | tv3 = vec3.create()
4 | tvv3 = vec3.create()
5 | tv2 = vec2.create()
6 |
7 | rect_from_points = (out, a, b)->
8 | if a.x == b.x and a.y == b.y
9 | return false
10 | else if a.x == b.x
11 | out.x = 1
12 | out.y = 0
13 | out.z = a.x
14 | else if a.y == b.y
15 | out.x = 0
16 | out.y = 1
17 | out.z = a.y
18 | else
19 | bax = (a.x - b.x)
20 | bay = (a.y - b.y)
21 | out.x = 1/bax
22 | out.y = - 1/bay
23 | out.z = a.y/bay - a.x/bax
24 | return out
25 |
26 | rects_intersection = (out, ra, rb)->
27 | t = rb.x/ra.x
28 | y = (t * ra.z - rb.z) / (rb.y - t * ra.y)
29 | x = (-ra.z - ra.y * y) / ra.x
30 | out.x = -x
31 | out.y = -y
32 | return out
33 |
34 | segments_intersection = (out, sa, sb)->
35 | # Segments (sa and sb) are defined by arrays of two points
36 | [sa0, sa1] = sa
37 | [sb0, sb1] = sb
38 | ra = rect_from_points tv3, sa0, sa1
39 | rb = rect_from_points tvv3, sb0, sb1
40 | if not ra? and not rb? # both are points
41 | if sa0.x == sb0.x and sa0.y == sb0.y #sa == sb
42 | out.x = sa0.x
43 | out.y = sa0.y
44 | else #sa != sb
45 | return false # no intersection
46 |
47 | else if not ra? # sa is a point
48 | if rb.x * sa0.x + rb.y * sa0.y + rb.z == 0 # sa in rb
49 | out.x = sa0.x
50 | out.y = sa0.y
51 | else
52 | return false
53 | else if not rb? # sb is a point
54 | if ra.x * sb0.x + ra.y * sb0.y + ra.z == 0 # sb in ra
55 | out.x = sb0.x
56 | out.y = sb0.y
57 | else
58 | return false
59 | else
60 | ma = (sa1.y - sa0.y) / (sa1.x - sa0.x)
61 | mb = (sb1.y - sb0.y) / (sb1.x - sb0.x)
62 | if ma == mb # parallel segments or cosegments
63 | return false
64 | else
65 | i = rects_intersection tv2, ra, rb
66 | if (sa1.x >= i.x >= sa0.x) or (sa1.x <= i.x <= sa0.x)
67 | out.x = i.x
68 | out.y = i.y
69 | return out
70 |
71 | module.exports = {rect_from_points, rects_intersection, segments_intersection}
72 |
--------------------------------------------------------------------------------
/engine/math_utils/g3.coffee:
--------------------------------------------------------------------------------
1 |
2 | {mat3, vec3, vec4, quat} = require 'vmath'
3 |
4 | #Constants
5 | Z_VECTOR = vec3.new 0,0,1
6 |
7 | #Temporary
8 | m3 = mat3.create()
9 | v1 = vec3.create()
10 | v2 = vec3.create()
11 | q = quat.create()
12 |
13 | # Calculates the instersection between three planes, in the same format as given
14 | # by plane_from_norm_point.
15 | #
16 | # @param out [vec3] Output vector
17 | # @param a [vec4] Plane
18 | # @param b [vec4] Plane
19 | # @param c [vec4] Plane
20 | # @return [vec3] Output vector
21 | planes_intersection = (out, a, b, c)->
22 | # a, b, c are plane equations as vec4
23 | mat3.fromColumns m3, a, b, c
24 | mat3.invert m3, m3
25 | out.x = - ((m3.m00 * a.w) + (m3.m01 * b.w) + (m3.m02 * c.w))
26 | out.y = - ((m3.m03 * a.w) + (m3.m04 * b.w) + (m3.m05 * c.w))
27 | out.z = - ((m3.m06 * a.w) + (m3.m07 * b.w) + (m3.m08 * c.w))
28 | return out
29 |
30 | # Calculates a plane from a normal and a point in the plane.
31 | # Gives the plane equation in form Ax + By + Cy + D = 0,
32 | # where A,B,C,D is given as vec4 {x,y,z,w} respectively.
33 | #
34 | # @param out [vec4] Output vector
35 | # @param n [vec3] Is the normal of the plane (NOTE: must be normalized)
36 | # @param p [vec3] Is a point of the plane
37 | # @return [vec4] Output vector
38 | plane_from_norm_point = (out, n, p)->
39 | return vec4.set out, n.x, n.y, n.z, -(n.x*p.x+n.y*p.y+n.z*p.z)
40 |
41 | rect_from_dir_point = (out1, out2, d, p)-> # UNTESTED
42 | # p is a point of the rect
43 | # d is the director vector of the rect, NORMALIZED
44 | # the result will be 2 planes which define the rect, in a
45 | # 4x2 row-major matrix (or first half of 4x4)
46 | vec3.set v1, 1,0,0
47 | vec3.set v2, 0,1,0
48 | quat.rotationTo q, Z_VECTOR, d
49 | vec3.transformQuat v1, v1, q
50 | vec3.transformQuat v2, v2, q
51 | plane_from_norm_point out1, v2, p
52 | plane_from_norm_point out2, v1, p
53 | return
54 |
55 | tmp4a = vec4.create()
56 | tmp4b = vec4.create()
57 | intersect_vector_plane = (out, origin, vector, plane) ->
58 | # out is vec3
59 | # origin is vec3
60 | # vector is vec3 NORMALIZED
61 | # plane is vec4
62 | rect_from_dir_point tmp4a, tmp4b, vector, origin
63 | planes_intersection out, tmp4a, tmp4b, plane
64 |
65 | v_dist_point_to_rect = (out, p, rp, dir)->
66 | # p is a point
67 | # rp is a point of the rect
68 | # dir is the director vector of the rect
69 | vec3.cross out, vec3.sub(v2,p,rp), vec3.normalize v1, dir
70 | return out
71 |
72 | project_vector_to_plane = (out, v, n)->
73 | #it requires normalized normal vector (n)
74 | l = - vec3.dot v, n
75 | v_proj_n = vec3.scale v1, n, l
76 | vec3.add out, v_proj_n, v
77 | return out
78 |
79 | reflect_vector = (out, v, n)->
80 | l = -2*vec3.dot v, n
81 | v_proj_n = vec3.scale v1, n, l
82 | vec3.add out, v_proj_n, v
83 | return out
84 |
85 | module.exports = {
86 | planes_intersection, plane_from_norm_point,
87 | rect_from_dir_point, intersect_vector_plane,
88 | v_dist_point_to_rect,
89 | project_vector_to_plane, reflect_vector
90 | }
91 |
--------------------------------------------------------------------------------
/engine/math_utils/math_extra.coffee:
--------------------------------------------------------------------------------
1 | vmath = require 'vmath'
2 | # TODO: Export or remove these globals
3 |
4 | cubic_bezier = (t, p0, p1, p2, p3)->
5 | t2 = t * t
6 | t3 = t2 * t
7 |
8 | c0 = p0
9 | c1 = -3.0 * p0 + 3.0 * p1
10 | c2 = 3.0 * p0 - 6.0 * p1 + 3.0 * p2
11 | c3 = -p0 + 3.0 * p1 - 3.0 * p2 + p3
12 |
13 | return c0 + t * c1 + t2 * c2 + t3 * c3
14 |
15 | wave = (a, b, d, t)->
16 | # https://www.desmos.com/calculator/gou6pxz4ie
17 | result = 0.5 * ( (b - a) * Math.sin(Math.PI*t/d - Math.PI*0.5) + b + a )
18 |
19 | ease_in_out = (a, b, d=1, t)->
20 | if t <= 0
21 | result = a
22 | else if 0 < t < d
23 | result = wave a, b, d, t
24 | else
25 | result = b
26 | return result
27 |
28 | wave = (a, b, d, t)->
29 | result = 0.5 * ( (b - a) * Math.sin(Math.PI*t/d - Math.PI*0.5) + b + a )
30 |
31 | # Gives the next power of two after X if X is not power of two already
32 | # @param x [number] input number
33 | next_POT = (x)->
34 | x = Math.max(0, x-1)
35 | return Math.pow(2, Math.floor(Math.log(x)/Math.log(2))+1)
36 |
37 | # Gives the previous power of two after X if X is not power of two already
38 | # @param x [number] input number
39 | previous_POT = (x)->
40 | x = Math.max(0, x)
41 | return Math.pow(2, Math.floor(Math.log(x)/Math.log(2)))
42 |
43 | # Gives the nearest power of two of X
44 | # @param x [number] input number
45 | nearest_POT = (x) ->
46 | x = Math.max(0, x)
47 | return Math.pow(2, Math.round(Math.log(x)/Math.log(2)))
48 |
49 | module.exports = {cubic_bezier, next_POT, previous_POT, nearest_POT, ease_in_out, wave}
50 |
--------------------------------------------------------------------------------
/engine/math_utils/vmath_extra.coffee:
--------------------------------------------------------------------------------
1 | vmath = require 'vmath'
2 | {ease_in_out} = require './math_extra'
3 |
4 | # http://stackoverflow.com/questions/1031005/is-there-an-algorithm-for-converting-quaternion-rotations-to-euler-angle-rotatio
5 |
6 | threeaxisrot = (out, r11, r12, r21, r31, r32) ->
7 | out.x = Math.atan2( r31, r32 )
8 | out.y = Math.asin ( r21 )
9 | out.z = Math.atan2( r11, r12 )
10 |
11 | # NOTE: It uses Blender's convention for euler rotations:
12 | # XYZ means that to convert back to quat you must rotate Z, then Y, then X
13 |
14 | vmath.quat.to_euler_XYZ = (out, q) ->
15 | {x, y, z, w} = q
16 | threeaxisrot(out, 2*(x*y + w*z),
17 | w*w + x*x - y*y - z*z,
18 | -2*(x*z - w*y),
19 | 2*(y*z + w*x),
20 | w*w - x*x - y*y + z*z)
21 | return out
22 |
23 | vmath.quat.to_euler_XZY = (out, q) ->
24 | {x, y, z, w} = q
25 | threeaxisrot(out, -2*(x*z - w*y),
26 | w*w + x*x - y*y - z*z,
27 | 2*(x*y + w*z),
28 | -2*(y*z - w*x),
29 | w*w - x*x + y*y - z*z)
30 | return out
31 |
32 | vmath.quat.to_euler_YXZ = (out, q) ->
33 | {x, y, z, w} = q
34 | threeaxisrot(out, -2*(x*y - w*z),
35 | w*w - x*x + y*y - z*z,
36 | 2*(y*z + w*x),
37 | -2*(x*z - w*y),
38 | w*w - x*x - y*y + z*z)
39 | return out
40 |
41 | vmath.quat.to_euler_YZX = (out, q) ->
42 | {x, y, z, w} = q
43 | threeaxisrot(out, 2*(y*z + w*x),
44 | w*w - x*x + y*y - z*z,
45 | -2*(x*y - w*z),
46 | 2*(x*z + w*y),
47 | w*w + x*x - y*y - z*z)
48 | return out
49 |
50 | vmath.quat.to_euler_ZXY = (out, q) ->
51 | {x, y, z, w} = q
52 | threeaxisrot(out, 2*(x*z + w*y),
53 | w*w - x*x - y*y + z*z,
54 | -2*(y*z - w*x),
55 | 2*(x*y + w*z),
56 | w*w - x*x + y*y - z*z)
57 | return out
58 |
59 | vmath.quat.to_euler_ZYX = (out, q) ->
60 | {x, y, z, w} = q
61 | threeaxisrot(out, -2*(y*z - w*x),
62 | w*w - x*x - y*y + z*z,
63 | 2*(x*z + w*y),
64 | -2*(x*y - w*z),
65 | w*w + x*x - y*y - z*z)
66 | return out
67 |
68 | # TODO: inline and add to vmath?
69 | vmath.vec3.signedAngle = (a, b, n) ->
70 | {dot, cross, angle, create} = vmath.vec3
71 | result = angle a, b
72 | c = cross create(), a, b
73 | if dot(n, c) < 0
74 | result *= -1
75 |
76 | return result
77 |
78 | vmath.vec3.copyArray = (out, arr) ->
79 | out.x = arr[0]
80 | out.y = arr[1]
81 | out.z = arr[2]
82 | return out
83 |
84 | vmath.quat.copyArray = (out, arr) ->
85 | out.x = arr[0]
86 | out.y = arr[1]
87 | out.z = arr[2]
88 | out.w = arr[3]
89 | return out
90 |
91 | vmath.color3.copyArray = (out, arr) ->
92 | out.r = arr[0]
93 | out.g = arr[1]
94 | out.b = arr[2]
95 | return out
96 |
97 | vmath.color4.copyArray = (out, arr) ->
98 | out.r = arr[0]
99 | out.g = arr[1]
100 | out.b = arr[2]
101 | out.a = arr[3]
102 | return out
103 |
104 | vmath.mat4.fromMat3 = (out, m) ->
105 | out.m00 = m.m00
106 | out.m01 = m.m01
107 | out.m02 = m.m02
108 | out.m03 = 0
109 | out.m04 = m.m03
110 | out.m05 = m.m04
111 | out.m06 = m.m05
112 | out.m07 = 0
113 | out.m08 = m.m06
114 | out.m09 = m.m07
115 | out.m10 = m.m08
116 | out.m11 = 0
117 | out.m12 = 0
118 | out.m13 = 0
119 | out.m14 = 0
120 | out.m15 = 1
121 | return out
122 |
123 | vmath.mat4.copyArray = (out, arr) ->
124 | out.m00 = arr[0]
125 | out.m01 = arr[1]
126 | out.m02 = arr[2]
127 | out.m03 = arr[3]
128 | out.m04 = arr[4]
129 | out.m05 = arr[5]
130 | out.m06 = arr[6]
131 | out.m07 = arr[7]
132 | out.m08 = arr[8]
133 | out.m09 = arr[9]
134 | out.m10 = arr[10]
135 | out.m11 = arr[11]
136 | out.m12 = arr[12]
137 | out.m13 = arr[13]
138 | out.m14 = arr[14]
139 | out.m15 = arr[15]
140 | return out
141 |
142 | vmath.mat4.setTranslation = (out, v) ->
143 | out.m12 = v.x
144 | out.m13 = v.y
145 | out.m14 = v.z
146 | return out
147 |
148 | vmath.mat4.fromVec4Columns = (out, a, b, c, d) ->
149 | out.m00 = a.x
150 | out.m01 = a.y
151 | out.m02 = a.z
152 | out.m03 = a.w
153 | out.m04 = b.x
154 | out.m05 = b.y
155 | out.m06 = b.z
156 | out.m07 = b.w
157 | out.m08 = c.x
158 | out.m09 = c.y
159 | out.m10 = c.z
160 | out.m11 = c.w
161 | out.m12 = d.x
162 | out.m13 = d.y
163 | out.m14 = d.z
164 | out.m15 = d.w
165 | return out
166 |
167 | vmath.mat3.fromColumns = (out, a, b, c) ->
168 | out.m00 = a.x
169 | out.m01 = a.y
170 | out.m02 = a.z
171 | out.m03 = b.x
172 | out.m04 = b.y
173 | out.m05 = b.z
174 | out.m06 = c.x
175 | out.m07 = c.y
176 | out.m08 = c.z
177 | return out
178 |
179 | {vec3} = vmath
180 | vmath.vec3.fromMat4Scale = (out, m) ->
181 | x = vec3.new m.m00, m.m01, m.m02
182 | y = vec3.new m.m04, m.m05, m.m06
183 | z = vec3.new m.m08, m.m09, m.m10
184 | return vec3.set out, vec3.len(x), vec3.len(y), vec3.len(z)
185 |
186 | vmath.vec3.ease_in_out = (out, a, b, d=1, t) ->
187 | out.x = ease_in_out a.x, b.x, d, t
188 | out.y = ease_in_out a.y, b.y, d, t
189 | out.z = ease_in_out a.z, b.z, d, t
190 | return out
191 |
192 | vmath.mat3.rotationFromMat4 = (out, m) ->
193 | x = vec3.new m.m00, m.m01, m.m02
194 | y = vec3.new m.m04, m.m05, m.m06
195 | z = vec3.new m.m08, m.m09, m.m10
196 | vec3.normalize x,x
197 | vec3.normalize y,y
198 | vec3.normalize z,z
199 | # This favours the Z axis to preserve
200 | # the direction of cameras and lights
201 | vec3.cross x,y,z
202 | vec3.cross y,z,x
203 | return vmath.mat3.fromColumns out, x,y,z
204 |
205 | vmath.quat.setAxisAngle = (out, axis, rad) ->
206 | rad = rad * 0.5
207 | s = Math.sin(rad)
208 | out.x = s * axis.x
209 | out.y = s * axis.y
210 | out.z = s * axis.z
211 | out.w = Math.cos(rad)
212 | return out
213 |
214 | {rotateX, rotateY, rotateZ} = vmath.quat
215 | vmath.quat.fromEulerOrder = (out, v, order) ->
216 | {x,y,z} = v
217 | out.x = out.y = out.z = 0
218 | out.w = 1
219 | switch order
220 | when 'XYZ'
221 | rotateZ out, out, z
222 | rotateY out, out, y
223 | rotateX out, out, x
224 | when 'XZY'
225 | rotateY out, out, y
226 | rotateZ out, out, z
227 | rotateX out, out, x
228 | when 'YXZ'
229 | rotateZ out, out, z
230 | rotateX out, out, x
231 | rotateY out, out, y
232 | when 'YZX'
233 | rotateX out, out, x
234 | rotateZ out, out, z
235 | rotateY out, out, y
236 | when 'ZXY'
237 | rotateY out, out, y
238 | rotateX out, out, x
239 | rotateZ out, out, z
240 | when 'ZYX'
241 | rotateX out, out, x
242 | rotateY out, out, y
243 | rotateZ out, out, z
244 | return out
245 |
246 | vmath.vec3.fixAxes = (x_axis, y_axis, z_axis, main_axis, secondary_axis)->
247 | third_axis = 3 - (main_axis + secondary_axis)
248 | axes = [x_axis, y_axis, z_axis]
249 | main = axes[main_axis]
250 | vec3.normalize main, main
251 | third = axes[third_axis]
252 | vec3.cross third, axes[(third_axis+1)%3], axes[(third_axis+2)%3]
253 | vec3.normalize third, third
254 | secondary = axes[secondary_axis]
255 | vec3.cross secondary, axes[(secondary_axis+1)%3], axes[(secondary_axis+2)%3]
256 | return
257 |
258 | vmath.quat.fromMat4 = (out, m) ->
259 | m00 = m.m00; m01 = m.m04; m02 = m.m08
260 | m10 = m.m01; m11 = m.m05; m12 = m.m09
261 | m20 = m.m02; m21 = m.m06; m22 = m.m10
262 |
263 | trace = m00 + m11 + m22
264 |
265 | if trace > 0
266 | s = 0.5 / Math.sqrt(trace + 1.0)
267 |
268 | out.w = 0.25 / s
269 | out.x = (m21 - m12) * s
270 | out.y = (m02 - m20) * s
271 | out.z = (m10 - m01) * s
272 |
273 | else if (m00 > m11) && (m00 > m22)
274 | s = 2.0 * Math.sqrt(1.0 + m00 - m11 - m22)
275 |
276 | out.w = (m21 - m12) / s
277 | out.x = 0.25 * s
278 | out.y = (m01 + m10) / s
279 | out.z = (m02 + m20) / s
280 |
281 | else if (m11 > m22)
282 | s = 2.0 * Math.sqrt(1.0 + m11 - m00 - m22)
283 |
284 | out.w = (m02 - m20) / s
285 | out.x = (m01 + m10) / s
286 | out.y = 0.25 * s
287 | out.z = (m12 + m21) / s
288 |
289 | else
290 | s = 2.0 * Math.sqrt(1.0 + m22 - m00 - m11)
291 |
292 | out.w = (m10 - m01) / s
293 | out.x = (m02 + m20) / s
294 | out.y = (m12 + m21) / s
295 | out.z = 0.25 * s
296 |
297 | return out
298 |
299 | vmath.mat4.toRT = (rotation, translation, m) ->
300 | vmath.quat.fromMat4 rotation, m
301 | translation.x = m.m12
302 | translation.y = m.m13
303 | translation.z = m.m14
304 |
305 | module.exports = vmath
306 |
--------------------------------------------------------------------------------
/engine/mesh_factory.coffee:
--------------------------------------------------------------------------------
1 |
2 | {Mesh} = require './mesh'
3 |
4 | class MeshFactory
5 | constructor: (@context) ->
6 |
7 | make_sphere: (options) ->
8 | {radius, segments=32, rings=16, flip_normals=false} = options
9 | segment_angle = Math.PI*2/segments
10 | ring_angle = Math.PI/rings
11 | segment_frac = 1/segments
12 | rings_frac = 1/rings
13 | vrings = rings + 1 # rings of vertices
14 | normal_scale = 127
15 | [a,b,c] = [0,1,2] # face winding order
16 | if flip_normals
17 | normal_scale = -127
18 | [a,b,c] = [2,1,0]
19 | # 6 floats per vertex (3 for position, 1 for normal, 2 for uv)
20 | # +1 line of segments because of UVs
21 | stride = 6
22 | verts = new Float32Array (segments+1)*vrings*stride
23 | verts_bytes = new Int8Array verts.buffer
24 | # Each cap is made of triangles instead of quads
25 | # so the count is the same as if it's only one cap made of quads
26 | indices = new Uint16Array segments*(vrings-1)*2*3
27 | i = 0
28 | ib = 3*4 # offset of the normal attribute in bytes
29 | for x in [0...(segments+1)]
30 | ssin = Math.sin x*segment_angle
31 | scos = Math.cos x*segment_angle
32 | for y in [0...vrings]
33 | rsin = Math.sin y*ring_angle
34 | cx = ssin * rsin
35 | cy = scos * rsin
36 | cz = Math.cos y*ring_angle
37 | verts[i] = cx * radius
38 | verts[i+1] = cy * radius
39 | verts[i+2] = cz * radius
40 | verts[i+4] = x * segment_frac
41 | verts[i+5] = 1 - y * rings_frac
42 | verts_bytes[ib] = cx * normal_scale
43 | verts_bytes[ib+1] = cy * normal_scale
44 | verts_bytes[ib+2] = cz * normal_scale
45 | i += stride
46 | ib += stride*4
47 | i = 0
48 | col_start = 0
49 | next_col = vrings
50 | for x in [0...segments]
51 | for y in [0...vrings-1]
52 | indices[i+a] = col_start+y
53 | indices[i+b] = next_col+y
54 | indices[i+c] = next_col+y+1
55 | i += 3
56 | indices[i+a] = next_col+y+1
57 | indices[i+b] = col_start+y+1
58 | indices[i+c] = col_start+y
59 | i += 3
60 | col_start = next_col
61 | next_col += vrings
62 | mesh = new Mesh @context
63 | mesh.offsets = [0, 0, verts.length, indices.length]
64 | mesh.stride = stride*4
65 | mesh.elements = [['normal'], ['uv', 'UVMap']]
66 | mesh.radius = radius
67 | mesh.load_from_va_ia(verts, indices)
68 | return mesh
69 |
70 | module.exports = {MeshFactory}
71 |
--------------------------------------------------------------------------------
/engine/myou.coffee:
--------------------------------------------------------------------------------
1 | {RenderManager} = require './render'
2 | {MainLoop} = require './main_loop'
3 | loader = require './loader'
4 | vr = require './webvr'
5 | {MeshFactory} = require './mesh_factory'
6 | {fetch_objects} = require './fetch_assets'
7 | {Action, Animation, LoopedAnimation, FiniteAnimation, PingPongAnimation} =
8 | require './animation'
9 | {Viewport} = require './viewport'
10 | {Texture} = require './texture'
11 | {Armature} = require './armature'
12 | {Camera} = require './camera'
13 | {Curve} = require './curve'
14 | effects = require './effects/index'
15 | filters = require './filters'
16 | {Framebuffer, ByteFramebuffer, ShortFramebuffer, FloatFramebuffer} =
17 | require './framebuffer'
18 | {Cubemap} = require './cubemap'
19 | {GameObject} = require './gameobject'
20 | {GLRay} = require './glray'
21 | {Lamp} = require './lamp'
22 | {Material} = require './material'
23 | {Mesh} = require './mesh'
24 | {Scene} = require './scene'
25 | {DebugDraw} = require './debug_draw'
26 | {Button, Axis, Axes2, InputSource, InputManager} = require './input'
27 |
28 | context_dependent_modules = {
29 | GameObject, Mesh, Armature, Curve, Camera, Lamp,
30 | Framebuffer, ByteFramebuffer, ShortFramebuffer, FloatFramebuffer,
31 | Cubemap, GLRay, Material, Scene, DebugDraw,
32 | Button, Axis, Axes2, InputSource,
33 | }
34 |
35 | # This is the main engine class.
36 | # You need to instance it to start using the engine.
37 | # The engine instance is frequently referred internally as `context`.
38 | #
39 | # It instances and contains several singletons like `render_manager` and
40 | # `main_loop`.
41 | class Myou
42 | # @nodoc
43 | fetch_objects: fetch_objects
44 | Action: Action
45 | Animation: Animation
46 | LoopedAnimation: LoopedAnimation
47 | FiniteAnimation: FiniteAnimation
48 | PingPongAnimation: PingPongAnimation
49 | Viewport: Viewport
50 | Texture: Texture
51 | # @property [Object]
52 | # Object with all game objects in memory. The key is the name.
53 | objects: null
54 | # @property [MainLoop] Main loop singleton.
55 | main_loop: null
56 | # @property [RenderManager] Render manager singleton.
57 | render_manager: null
58 | # @property [number]
59 | # Minimum length of the average poligon for LoD calculation, in pixels.
60 | mesh_lod_min_length_px: 13
61 | # @property [Array] viewports
62 | # List of viewports of the canvas screen. Convenience reference to
63 | # myou.canvas_screen.viewports (use myou.canvas_screen to operate on these).
64 | viewports: []
65 |
66 | constructor: (root, options)->
67 | if not root?
68 | throw Error "Missing root DOM element, got null or undefined"
69 | if not options?
70 | throw Error "Missing options"
71 | @screens = []
72 | @behaviours = @behaviors = []
73 | @canvas_screen = null
74 | @vr_screen = null
75 | @scenes = dict()
76 | @loaded_scenes = []
77 | @objects = dict()
78 | @actions = dict()
79 | @video_textures = dict()
80 | @debug_loader = null
81 | @canvas = null
82 | @all_materials = dict()
83 | @mesh_datas = dict()
84 | @embed_meshes = dict()
85 | @active_animations = dict()
86 | @all_cubemaps = []
87 | @all_framebuffers = []
88 | @enabled_behaviours = []
89 | @root = root
90 | @options = @MYOU_PARAMS = options
91 | @hash = Math.random()
92 | @initial_scene_loaded = false
93 | @is_webgl2 = false
94 | @webpack_flags = global_myou_engine_webpack_flags ? null
95 |
96 | # VR
97 | @_HMD = @_vrscene = null
98 | @use_VR_position = true
99 |
100 | # Adding context to context_dependent_modules
101 | for name,cls of context_dependent_modules
102 | @[name] = cls.bind cls, @
103 | for name,cls of filters
104 | @[name] = cls.bind cls, @
105 | for name,cls of effects
106 | @[name] = cls.bind cls, @
107 |
108 | # The root element needs to be positioned, so the mouse events
109 | # (layerX/Y) are registered correctly, and the canvas is scaled inside
110 | if getComputedStyle?(root).position == 'static'
111 | root.style.position = 'relative'
112 |
113 | # The canvas could be inside other element (root)
114 | # used to get the mouse events
115 | canvas = @canvas = if root.tagName == 'CANVAS'
116 | root
117 | else
118 | root.querySelector 'canvas'
119 |
120 | @update_root_rect = =>
121 | rect = @root.getClientRects()[0]
122 | if rect
123 | @root_rect = {
124 | top: rect.top + pageYOffset
125 | left: rect.left + pageXOffset
126 | }
127 | else
128 | @root_rect = {top: 0, left: 0}
129 |
130 | window.addEventListener 'resize', @update_root_rect
131 | @update_root_rect()
132 |
133 | @main_loop = new MainLoop @
134 | new RenderManager(
135 | @,
136 | canvas,
137 | options.gl_options or {antialias: true, alpha: false}
138 | )
139 |
140 | data_dir = options.data_dir or './data'
141 | data_dir = options.data_dir = data_dir.replace(/\/$/g, '')
142 |
143 | @mesh_factory = new MeshFactory @
144 | @input_manager = new InputManager @
145 | @has_created_debug_view = false
146 | @main_loop.run()
147 |
148 | update_layout: ->
149 | @canvas_screen.resize_to_canvas()
150 | @update_root_rect()
151 |
152 | load_scene: (name, options={}) ->
153 | return loader.load_scene(name, null, options, @)
154 |
155 | hasVR: vr.has_HMD
156 | initVR: vr.init
157 | exitVR: vr.exit
158 |
159 | # Makes a screenshot and returns a blob containing it
160 | # @param width [number] Width of the desired screenshot in pixels
161 | # @param height [number] Height of the desired screenshot in pixels
162 | # @option options supersampling [number]
163 | # Amount of samples per pixel for antialiasing
164 | # @option options format [string] Image format such as "png" or "jpeg"
165 | # @option options jpeg_quality [number]
166 | # Quality for compressed formats like jpeg and webp. Between 0 and 1.
167 | # @return [Promise] Promise resolving a [Blob]
168 | screenshot_as_blob: (width, height, options={}) ->
169 | @render_manager.screenshot_as_blob width, height, options
170 |
171 | # Change WebGL context flags, by replacing the canvas by a new one.
172 | # Note that it will remove DOM events (except events.coffee) and may take
173 | # a while to re-upload all GPU data.
174 | change_gl_flags: (gl_flags) ->
175 | @render_manager.instance_gl_context gl_flags, true
176 |
177 | enable_debug_camera: (viewport_number=0)->
178 | viewport = @canvas_screen.viewports[viewport_number]
179 | if viewport.enable_debug_camera()
180 | @has_created_debug_view = not viewport.camera.scene.has_debug_draw()
181 |
182 | disable_debug_camera: (viewport_number=0)->
183 | if @canvas_screen.viewports[viewport_number].disable_debug_camera()
184 | if @has_created_debug_view
185 | viewport.camera.scene.remove_debug_draw()
186 |
187 |
188 |
189 | # Convenience function for creating an HTML canvas element
190 | # and adding it to another element.
191 | #
192 | # @param root [HTMLElement] HTML element to insert the canvas into.
193 | # @param id [string] Element ID attribute to assign.
194 | # @param className [string] Element class attribute to assign.
195 | # @return [HTMLCanvasElement] Canvas element.
196 | create_canvas = (root, id, className='MyouEngineCanvas')->
197 | canvas = document.createElement 'canvas'
198 | if root?
199 | canvas.style.position = 'relative'
200 | canvas.style.width = '100vw'
201 | canvas.style.height = '100vh'
202 | root.insertBefore canvas, root.firstChild
203 | canvas.id = id
204 | canvas.className = className
205 | return canvas
206 |
207 | # Convenience function for creating an HTML canvas element
208 | # that fills the whole viewport.
209 | #
210 | # Ideal for a HTML file with no elements in the body.
211 | #
212 | # @return [HTMLCanvasElement] Canvas element.
213 | create_full_window_canvas = ->
214 | document.body.style.margin = '0 0 0 0'
215 | document.body.style.height = '100vh'
216 | canvas = create_canvas document.body, 'canvas'
217 | canvas.style.marginBottom = '-100px'
218 | return canvas
219 |
220 | # Using objects as dicts by disabling hidden object optimization
221 | # @nodoc
222 | dict = ->
223 | d = {}
224 | delete d.x
225 | d
226 |
227 | module.exports = {Myou, create_canvas, create_full_window_canvas}
228 |
--------------------------------------------------------------------------------
/engine/node_fetch_file.coffee:
--------------------------------------------------------------------------------
1 |
2 | # This module allows fetch to load local files from file://
3 | # in node.js, electron and NW.js
4 |
5 | if window.process?.execPath and process.execPath != '/'
6 |
7 | req = eval 'require'
8 | fs = req 'fs'
9 |
10 | class Body
11 | constructor: (err, @buffer) ->
12 | @ok = not err
13 | @status = ''
14 | @statusText = err?.message
15 | # NOTE: We're returning a _node_ buffer, not an arrayBuffer
16 | # so we can handle byteOffsets in electron...
17 | arrayBuffer: -> Promise.resolve @buffer
18 | text: -> Promise.resolve @buffer.toString()
19 | json: -> Promise.resolve JSON.parse @buffer.toString()
20 |
21 | _fetch = window._native_fetch = window._native_fetch or fetch
22 | window.fetch = (uri) ->
23 | if /^(https?:)?\/\//.test uri
24 | _fetch.apply window, arguments
25 | else
26 | if /^file:\/\/\/[a-z]:/.test uri
27 | uri = uri[8...]
28 | else if /^file:\/\//.test uri
29 | uri = uri[7...]
30 | new Promise (resolve, reject) ->
31 | fs.readFile uri, (err, data) ->
32 | resolve(new Body(err, data))
33 |
--------------------------------------------------------------------------------
/engine/probe.coffee:
--------------------------------------------------------------------------------
1 | {mat4, vec3, vec4, quat} = require 'vmath'
2 | {nearest_POT} = require './math_utils/math_extra'
3 | {plane_from_norm_point} = require './math_utils/g3'
4 |
5 | range_mat = mat4.new .5,0,0,0, 0,.5,0,0, 0,0,.5,0, .5,.5,.5,1
6 |
7 | class Probe
8 | constructor: (@object, options) ->
9 | {@context, @scene} = @object
10 | if @object.type == 'SCENE'
11 | @scene = @object
12 | @object = null
13 | {
14 | @type
15 | object
16 | @auto_refresh
17 | @compute_sh
18 | @double_refresh
19 | @same_layers
20 | @size
21 | @sh_quality
22 | @clip_start
23 | @clip_end
24 | @parallax_type
25 | @parallax_volume
26 | @reflection_plane
27 | @background_only = false
28 | } = options
29 | @size = nearest_POT @size
30 | @target_object =
31 | @scene.parents[object] ? @object ? new @context.GameObject
32 | @parallax_object = @scene.parents[@parallax_volume] ? @target_object
33 | @cubemap = @planar = @reflection_camera = null
34 | switch @type
35 | when 'CUBEMAP', 'CUBE'
36 | @cubemap = new @context.Cubemap {@size}
37 | @cubemap.loaded = false
38 | when 'PLANE'
39 | @planar = new @context.ByteFramebuffer size: [@size,@size], \
40 | use_depth: true
41 | # TODO: Detect actual viewport camera
42 | cam = @scene.active_camera
43 | # we're not using clone because
44 | # this copies its parameters but not the parent
45 | @reflection_camera = new @context.Camera cam
46 | @reflection_camera.scene = @scene
47 | @reflection_camera.rotation_order = 'Q'
48 | # TODO: Use a real viewport by having a BufferScreen
49 | @fake_vp =
50 | camera: @reflection_camera
51 | eye_shift: vec3.new 0,0,0
52 | clear_bits: 16384|256
53 | units_to_pixels: 1
54 | @position = vec3.create()
55 | @rotation = quat.create()
56 | @normal = vec3.create()
57 | @view_normal = vec3.create()
58 | @planarreflectmat = mat4.create()
59 | @set_lod_factor()
60 | @scene.probes.push @
61 | if @auto_refresh
62 | @context.render_manager.probes.push @
63 | else
64 | @render()
65 | @clip_start = Math.max @clip_start, 0.0001
66 |
67 | set_auto_refresh: (auto_refresh) ->
68 | if @auto_refresh
69 | if (index = @context.render_manager.probes.indexOf @) != -1
70 | @context.render_manager.probes.splice index,1
71 | if auto_refresh
72 | @context.render_manager.probes.push @
73 | @auto_refresh = auto_refresh
74 |
75 | set_lod_factor: ->
76 | {bsdf_samples} = @scene
77 | if not @context.is_webgl2
78 | bsdf_samples = 1
79 | @lodfactor = 0.5 * Math.log((@size*@size / bsdf_samples)) / Math.log(2)
80 | @lodfactor -= @scene.lod_bias
81 |
82 | render: ->
83 | if @cubemap?
84 | if @size != @cubemap.size
85 | @size = nearest_POT @size
86 | @cubemap.size = @size
87 | @cubemap.set_data()
88 | @set_lod_factor()
89 | @object?.get_world_position_into(@position)
90 | @context.render_manager.draw_cubemap(@scene, @cubemap,
91 | @position, @clip_start, @clip_end, @background_only)
92 | # TODO: Detect if any material uses this!
93 | if @compute_sh
94 | @cubemap.generate_spherical_harmonics(@sh_quality)
95 | @cubemap.loaded = true
96 | else if @planar?
97 | @object?.get_world_position_rotation_into(@position, @rotation)
98 | # plane normal
99 | vec3.set @normal, 0, 0, 1
100 | vec3.transformQuat @normal, @normal, @rotation
101 | # reflect camera
102 | # TODO: Detect actual viewport camera
103 | cam = @scene.active_camera
104 | rcam = @reflection_camera
105 | wm = mat4.copy rcam.world_matrix, cam.world_matrix
106 | # NOTE: Blender PBR calculates a reflection matrix instead of this
107 | # (it's probably simpler, by reflecting an identity matrix
108 | # with a plane equation, but this was done and it works)
109 | inv_obj = mat4.invert mat4.create(), @object.world_matrix
110 | mat4.mul wm, inv_obj, wm
111 | # invert column X and row Z
112 | wm.m00 = -wm.m00
113 | wm.m01 = -wm.m01
114 | # wm.m03 = -wm.m03
115 | wm.m06 = -wm.m06
116 | wm.m10 = -wm.m10
117 | wm.m14 = -wm.m14
118 | mat4.mul wm, @object.world_matrix, wm
119 | proj = mat4.copy rcam.projection_matrix, cam.projection_matrix
120 | # mat4.copy $myou.objects.cam_view.world_matrix, wm
121 |
122 | # set planarreflectmat (range_mat * projection * view of reflection)
123 | mat4.invert @planarreflectmat, rcam.world_matrix
124 | mat4.mul @planarreflectmat, proj, @planarreflectmat
125 | mat4.mul @planarreflectmat, range_mat, @planarreflectmat
126 |
127 | # get view position and normal to calculate clipping plane
128 | # TODO: optimize?
129 | wmi = mat4.invert(mat4.create(), rcam.world_matrix)
130 | view_pos = vec3.create()
131 | vec3.transformMat4 view_pos, @position, wmi
132 | view_nor = vec4.new @normal.x, @normal.y, @normal.z, 0
133 | vec4.transformMat4 view_nor, view_nor, wmi
134 |
135 | # render camera
136 | {visible} = @object
137 | @object.visible = false
138 | rm = @context.render_manager
139 | # rm.flip_normals = true
140 | plane_from_norm_point rm.clipping_plane, view_nor, view_pos
141 | rm.draw_viewport @fake_vp, [0,0,@size,@size], @planar, [0,1]
142 | vec4.set rm.clipping_plane, 0,0,-1,999990
143 | # rm.flip_normals = false
144 | @object.visible = visible
145 |
146 | destroy: ->
147 | @scene.probes.splice _,1 if (_ = @scene.probes.indexOf @) != -1
148 | if @auto_refresh
149 | if (index = @context.render_manager.probes.indexOf @) != -1
150 | @context.render_manager.probes.splice index,1
151 | @cubemap?.destroy()
152 | @planar?.destroy()
153 | @cubemap = @planar = null
154 |
155 |
156 | module.exports = {Probe}
157 |
--------------------------------------------------------------------------------
/engine/screen.coffee:
--------------------------------------------------------------------------------
1 |
2 | {Viewport} = require './viewport'
3 | {MainFramebuffer} = require './framebuffer'
4 |
5 | class Screen
6 | constructor: (@context, args...) ->
7 | @context.screens.push this
8 | @viewports = []
9 | @framebuffer = null
10 | @width = @height = @diagonal = 0
11 | @pixel_ratio_x = @pixel_ratio_y = 1
12 | @enabled = true
13 | @init @context, args...
14 |
15 | add_viewport: (camera) ->
16 | v = new Viewport @context, this, camera
17 | @viewports.push v
18 | return v
19 |
20 | resize: ->
21 |
22 | # Change the aspect ratio of viewports. Useful for very quick changes
23 | # of the size of the canvas or framebuffer, such as with a CSS animation.
24 | # Much cheaper than a regular resize, because it doesn't change the
25 | # resolution.
26 | resize_soft: (width, height)->
27 | for v in @viewports
28 | v.recalc_aspect(true)
29 | return
30 |
31 | pre_draw: ->
32 |
33 | post_draw: ->
34 |
35 | # From a screen (x,y) pixel position, return the viewport and the
36 | # (x,y) relative to that viewport. Upper left corner is (0,0)
37 | get_viewport_coordinates: (x, y) ->
38 | y = @height - y
39 | for viewport in @viewports by -1
40 | [left, bottom, width, height] = viewport.rect
41 | left *= @width
42 | width *= @width
43 | bottom *= @height
44 | height *= @height
45 | right = left+width
46 | top = bottom+height
47 | if left < x < right and bottom < y < top
48 | x -= left
49 | y = @height - (y - bottom)
50 | return {x, y, viewport}
51 | return {x, y, viewport: null}
52 |
53 |
54 |
55 | class CanvasScreen extends Screen
56 |
57 | init: (@context) ->
58 | if @context.canvas_screen?
59 | throw Error "There's a canvas screen already"
60 | @context.canvas_screen = this
61 | @viewports = @context.viewports = []
62 | @canvas = @context.canvas
63 | @framebuffer = new MainFramebuffer @context
64 | @resize(@canvas.clientWidth, @canvas.clientHeight)
65 | {@auto_resize_to_canvas=true} = @context.options
66 | window.addEventListener 'resize', =>
67 | if not @context.vr_screen? and @auto_resize_to_canvas
68 | @resize_to_canvas()
69 |
70 | resize_to_canvas: (ratio_x=@pixel_ratio_x, ratio_y=@pixel_ratio_y) ->
71 | return if not @canvas?
72 | {clientWidth, clientHeight} = @canvas
73 | if clientWidth == @width and clientHeight == @height and
74 | ratio_x == @pixel_ratio_x and ratio_y == @pixel_ratio_y
75 | return
76 | @resize(clientWidth, clientHeight, ratio_x, ratio_y)
77 |
78 | # Changes the resolution of the canvas and aspect ratio of viewports.
79 | # It doesn't handle the final size (that's done through HTML styles).
80 | # usually called when the window is resized.
81 | resize: (width, height, @pixel_ratio_x=1, @pixel_ratio_y=1)->
82 | @width = width
83 | @height = height
84 | @canvas.width = @framebuffer.size_x = width * @pixel_ratio_x
85 | @canvas.height = @framebuffer.size_y = height * @pixel_ratio_y
86 | @diagonal = Math.sqrt(width*width + height*height)
87 | for v in @viewports
88 | v.recalc_aspect(false)
89 | return
90 |
91 |
92 |
93 |
94 | module.exports = {Screen, CanvasScreen}
95 |
--------------------------------------------------------------------------------
/engine/vertex_modifiers.coffee:
--------------------------------------------------------------------------------
1 |
2 | # Vertex modifiers are pluggable vertex shader functions.
3 | #
4 | # They're stored in the list mesh.vertex_modifiers, with
5 | # possible per-mesh custom information, type, signature,
6 | # and the following methods:
7 | #
8 | # get_code: returns code lines for the vertex shader:
9 | # A list of uniform declaration and a list for main body.
10 | # The body is expected to modify local variables: co and normal.
11 | #
12 | # get_data_store: returns a data structure to store
13 | # uniform locations and optional values in the Shader object,
14 | # It's provided with the GL context and the GL program,
15 | # Usually to call gl.getUniformLocation.
16 | # The data store is only used by update_uniforms below, and
17 | # it's usually just a list with uniform locations.
18 | #
19 | # update_uniforms: is called for every rendered mesh.
20 | # Receives the GL context and the store,
21 | # usually to use gl.uniform* and gl.uniformMatrix* functions.
22 | # Don't forget to return, otherwise the loop wastes resources.
23 | #
24 | # required_attributes: a list of additional used vertex attributes,
25 | # required by the body. Note that it's not currently enforced but
26 | # it's necessary for optimizing shadows.
27 | # TODO: Use it to optimize shadows.
28 |
29 |
30 | class ShapeKeyModifier
31 | constructor: (options) ->
32 | @type = 'SHAPE_KEYS'
33 | # Format of keys:
34 | # { Key1: { value: 1.0, index: 0 }, ... }
35 | {@count, @data_type, @keys} = options
36 | @ordered_keys = for k,v of @keys then v
37 | @ordered_keys.sort (a,b) -> a.index - b.index
38 |
39 | @required_attributes = ("shape#{i}" for i in [0...@count]) \
40 | .concat("shapenor#{i}" for i in [0...@count])
41 |
42 | @signature = ''
43 |
44 | get_code: ->
45 | shape_multiplier = if @data_type == 'b' then "* 0.007874016" else ""
46 | uniform_lines = ["uniform float shapef[#{@count}];"]
47 | # This body uses attributes: shapeX and shapenorX where X is a number
48 | body_lines = [
49 | # Equivalent to /= 127.0, and roughly to normalize byte normals
50 | "normal *= 0.007874;"
51 | "float relf = 0.0;"
52 | "vec3 n;"
53 | (for i in [0...@count]
54 | "co += vec4(shape#{i}, 0.0) * shapef[#{i}] #{shape_multiplier};
55 | relf += shapef[#{i}];"
56 | )...
57 | # Interpolating normals instead of re-calculating them is wrong
58 | # But it's fast, completely unnoticeable in most cases,
59 | # and better than just not changing them (as many engines do)
60 | "normal *= clamp(1.0 - relf, 0.0, 1.0);"
61 | (for i in [0...@count]
62 | "normal += shapenor#{i} * 0.007874 * max(0.0, shapef[#{i}]);"
63 | )...
64 | ]
65 | return {uniform_lines, body_lines}
66 |
67 | get_data_store: (gl, prog) ->
68 | # In this case we're returninga list
69 | for i in [0...@count]
70 | gl.getUniformLocation prog, "shapef[#{i}]"
71 |
72 | # update_uniforms is called for every rendered mesh
73 | update_uniforms: (gl, store, mesh, submesh_index) ->
74 | for {value},i in @ordered_keys
75 | gl.uniform1f store[i], value
76 | return
77 |
78 |
79 | class ArmatureModifier
80 | constructor: (options) ->
81 | @type = 'ARMATURE'
82 | {@data_type, @bone_count} = options
83 | @attributes_needed = ['weights', 'b_indices']
84 | # TODO: Document this or get from code instead
85 | @signature = 'armature'+@bone_count
86 |
87 | get_code: ->
88 | weight_multiplier = if @data_type == 'B' then "* 0.00392156886" else ""
89 | uniform_lines = ["uniform mat4 bones[#{@bone_count}];"]
90 | body_lines = [
91 | 'vec4 blendco = vec4(0.0);'
92 | 'vec3 blendnor = vec3(0.0);'
93 | 'mat4 m; float w;'
94 | 'ivec4 inds = ivec4(b_indices);'
95 | 'for(int i=0;i<4;i++){'
96 | ' m = bones[inds[i]];'
97 | " w = weights[i]#{weight_multiplier};"
98 | ' blendco += m * co * w;'
99 | ' blendnor += mat3(m) * normal * w;'
100 | '}'
101 | 'co = blendco; normal = blendnor;'
102 | ]
103 | return {uniform_lines, body_lines}
104 |
105 | get_data_store: (gl, prog) ->
106 | for i in [0...@bone_count]
107 | gl.getUniformLocation prog, "bones[#{i}]"
108 |
109 | update_uniforms: (gl, store, mesh, submesh_index) ->
110 | {deform_bones} = mesh.armature
111 | map = mesh.bone_index_maps[submesh_index]
112 | if map?
113 | for bone_idx,i in map
114 | gl.uniformMatrix4fv store[i], false,
115 | deform_bones[bone_idx].ol_matrix.toJSON()
116 | else
117 | for i in [0...@bone_count] by 1
118 | gl.uniformMatrix4fv store[i], false,
119 | deform_bones[i].ol_matrix.toJSON()
120 | return
121 |
122 |
123 |
124 | module.exports = {ShapeKeyModifier, ArmatureModifier}
125 |
--------------------------------------------------------------------------------
/engine/viewport.coffee:
--------------------------------------------------------------------------------
1 | {vec2, vec3, quat} = require 'vmath'
2 | {DebugCamera} = require './debug_camera'
3 |
4 | # A viewport is a portion of the screen/canvas associated with a camera,
5 | # with a specific size. Usually there's only one viewport covering the whole
6 | # screen/canvas.
7 | #
8 | # Typically there's only a single viewport occupying the whole canvas.
9 | #
10 | # Created with `screen.add_viewport(camera)`` or automatically on first load
11 | # of a scene with active camera.
12 | # Available at `screen.viewports`.
13 | class Viewport
14 |
15 | constructor: (@context, @screen, @camera)->
16 | @rect = [0,0,1,1]
17 | @rect_pix = [0,0,0,0]
18 | @left = @bottom = @width = @height = 0
19 | @effects = []
20 | @effects_by_id = {}
21 | @clear_bits = 0
22 | @eye_shift = vec3.create()
23 | @right_eye_factor = 0
24 | @custom_fov = null
25 | @debug_camera = null
26 | @units_to_pixels = 100
27 | @_v = vec3.create()
28 | @requires_float_buffers = false
29 | @last_filter_should_blend = false
30 | @set_clear true, true
31 | @recalc_aspect()
32 |
33 | # @private
34 | # Recalculates viewport rects and camera aspect ratio.
35 | # Used in `screen.resize` and `screen.resize_soft`
36 | recalc_aspect: (is_soft) ->
37 | [x,y,w,h] = @rect
38 | {size_x, size_y} = @screen.framebuffer
39 | @left = size_x * x
40 | @bottom = size_y * y
41 | @width = size_x * w
42 | @height = size_y * h
43 | # TODO: Warn if several viewports with different ratios have same camera
44 | @camera.aspect_ratio = @width/@height
45 | @camera.update_projection()
46 | if @debug_camera?
47 | @debug_camera.aspect_ratio = @width/@height
48 | @debug_camera.update_projection()
49 | @rect_pix = [@left, @bottom, @width, @height]
50 | v = vec3.set @_v, 1,0,-1
51 | vec3.transformMat4 v, v, (@debug_camera ? @camera).projection_matrix
52 | @units_to_pixels = v.x * @width
53 | @pixels_to_units = 1/@units_to_pixels
54 | if not is_soft
55 | for effect in @effects
56 | effect.on_viewport_update this
57 | return
58 |
59 | # Sets whether color and depth buffers will be cleared
60 | # before rendering.
61 | # @param color [Boolean]
62 | # Whether to clear color with `scene.background_color`.
63 | # @param depth [Boolean] Whether to clear depth buffer.
64 | set_clear: (color, depth)->
65 | c = if color then 16384 else 0 # GL_COLOR_BUFFER_BIT
66 | c |= if depth then 256 else 0 # GL_DEPTH_BUFFER_BIT
67 | @clear_bits = c
68 |
69 | # Clones the viewport and adds it to the screen.
70 | # Note that it will be rendering over the same area unless rect is changed.
71 | # @return {Viewport}
72 | clone: (options={}) ->
73 | {
74 | copy_effects=true
75 | copy_behaviours=true
76 | } = options
77 | v = @screen.add_viewport @camera
78 | v.rect = @rect[...]
79 | if copy_effects
80 | v.effects = @effects[...]
81 | v.effects_by_id = Object.create @effects_by_id
82 | if copy_behaviours
83 | for behaviour in @context.behaviours
84 | if this in behaviour.viewports and
85 | behaviour != @debug_camera_behaviour
86 | # TODO: should we add and use behaviour.add_viewport()?
87 | behaviour.viewports.push v
88 | if behaviour._real_viewports != behaviour.viewports
89 | behaviour._real_viewports.push v
90 | return v
91 |
92 | # Returns size of viewport in pixels.
93 | # @return [vec2]
94 | get_size_px: ->
95 | return vec2.new @width, @height
96 |
97 | destroy: ->
98 | @clear_effects()
99 | idx = @screen.viewports.indexOf @
100 | if idx != -1
101 | @screen.viewports.splice idx, 1
102 | for behaviour in @context.behaviours
103 | idx = behaviour.viewports.indexOf @
104 | if idx != -1
105 | behaviour.viewports.splice idx, 1
106 | return
107 |
108 | # Add effect at the end of the stack
109 | add_effect: (effect) ->
110 | effect.on_viewport_update this
111 | @effects.push effect
112 | @effects_by_id[effect.id] = effect
113 | @_check_requires_float_buffers()
114 | return effect
115 |
116 | # Insert an effect at the specified index of the stack
117 | insert_effect: (index, effect) ->
118 | effect.on_viewport_update this
119 | @effects.splice index, 0, effect
120 | @effects_by_id[effect.id] = effect
121 | @_check_requires_float_buffers()
122 | return effect
123 |
124 | replace_effect: (before, after) ->
125 | index = @remove_effect(before)
126 | if index != -1
127 | @insert_effect(index, after)
128 | else
129 | @add_effect(after)
130 |
131 | # Remove an effect from the stack
132 | remove_effect: (index_or_effect)->
133 | index = index_or_effect
134 | if typeof index != 'number'
135 | index = @effects.indexOf index_or_effect
136 | if index != -1
137 | @effects.splice(index, 1)[0].on_viewport_remove?()
138 | @_check_requires_float_buffers()
139 | return index
140 |
141 | clear_effects: ->
142 | for effect in @effects
143 | effect.on_viewport_remove?()
144 | @_check_requires_float_buffers()
145 | @effects.splice 0
146 | return this
147 |
148 | ensure_shared_effect: (effect_class, a, b, c, d) ->
149 | for effect in @effects
150 | if effect.constructor == effect_class
151 | return effect
152 | return @insert_effect 0, new effect_class @context, a, b, c, d
153 |
154 | # Splits the viewport into two, side by side, by converting this to
155 | # the left one, and returning the right one.
156 | split_left_right: (options) ->
157 | @rect[2] *= .5
158 | v2 = @clone(options)
159 | v2.rect[0] += @rect[2]
160 | @recalc_aspect()
161 | v2.recalc_aspect()
162 | return v2
163 |
164 | # Splits the viewport into two, over/under, by converting this to
165 | # the top one, and returning the bottom one.
166 | split_top_bottom: (options) ->
167 | @rect[3] *= .5
168 | v2 = @clone(options)
169 | v2.rect[1] += @rect[3]
170 | @recalc_aspect()
171 | v2.recalc_aspect()
172 | return v2
173 |
174 | enable_debug_camera: ->
175 | if not @debug_camera_behaviour?
176 | @debug_camera_behaviour = new DebugCamera @camera.scene,
177 | viewports: [this]
178 | return true
179 | return false
180 |
181 | disable_debug_camera: ->
182 | if @debug_camera_behaviour?
183 | @debug_camera_behaviour.disable()
184 | @debug_camera_behaviour = null
185 | return true
186 | return false
187 |
188 | store_debug_camera: (name) ->
189 | if not @debug_camera_behaviour?
190 | throw Error "There is no debug camera."
191 | if not name?
192 | throw Error "Name argument is mandatory."
193 | {position, rotation} = @debug_camera
194 | localStorage[name] = JSON.stringify {position, rotation}
195 |
196 | load_debug_camera: (name) ->
197 | @enable_debug_camera()
198 | {position, rotation} = JSON.parse localStorage[name]
199 | vec3.set @debug_camera.position, position...
200 | quat.set @debug_camera.rotation, rotation...
201 |
202 | get_viewport_coordinates: (x, y) ->
203 | x -= @left
204 | y = @screen.height - y
205 | y = @screen.height - (y - @bottom)
206 | return {x, y}
207 |
208 | _check_requires_float_buffers: ->
209 | @requires_float_buffers = false
210 | for effect in @effects
211 | if effect.requires_float_source or effect.requires_float_destination
212 | @requires_float_buffers = true
213 | return
214 | return
215 |
216 |
217 | module.exports = {Viewport}
218 |
--------------------------------------------------------------------------------
/engine/webvr.coffee:
--------------------------------------------------------------------------------
1 |
2 | {vec3, quat, mat4, mat3} = require 'vmath'
3 | {CanvasScreen} = require './screen'
4 |
5 | class VRScreen extends CanvasScreen
6 | init: (@context, @HMD, @scene, options={}) ->
7 | if @context.vr_screen?
8 | throw Error "There's a VR screen already"
9 | {
10 | use_room_scale_parent=true
11 | mirror_zoom=1.3
12 | head0
13 | } = options
14 | if use_room_scale_parent and not @scene.active_camera.parent?
15 | throw Error "use_room_scale_parent requires the camera to have a
16 | parent. Add a parent or set use_room_scale_parent to false
17 | for a seated experience controller by the original camera."
18 | @context.vr_screen = this
19 | @canvas = @context.canvas
20 | {@framebuffer} = @context.canvas_screen
21 | @head_is_tracking = false
22 | @head = head ? new @context.GameObject
23 | @head.set_rotation_order 'Q'
24 | @scene.add_object @head
25 | # @head.parent_to @scene.active_camera, keep_transform: false
26 | for ob in @scene.active_camera.children
27 | ob.parent = @head
28 | if use_room_scale_parent
29 | @head.parent_to @scene.active_camera.parent, keep_transform: true
30 | @frame_data = new VRFrameData
31 | @old_pm0 = mat4.create()
32 | @left_orientation = quat.create()
33 | @right_orientation = quat.create()
34 | @last_time = performance.now()
35 | console.log 'HMD is', @HMD.displayName
36 | if not /Oculus/.test @HMD.displayName
37 | @is_wmr = true # we'll set it to false when we detect velocity
38 | @sst = mat4.create()
39 | @sst_inverse = mat4.create()
40 | @sst3 = mat3.create()
41 | if use_room_scale_parent
42 | @update_room_matrix()
43 | # @to_Z_up = mat4.new 1,0,0,0, 0,0,1,0, 0,-1,0,0, 0,0,0,1
44 | # left eye viewport
45 | camera = left_cam = @scene.active_camera.clone recursive: false
46 | camera.rotation_order = 'Q'
47 | mat4.identity camera.matrix_parent_inverse
48 | quat.identity camera.rotation
49 | if not use_room_scale_parent
50 | @scene.make_parent @scene.active_camera, camera
51 | v = @add_viewport camera
52 | # v.set_clear false, false
53 | v.rect = [0, 0, 0.5, 1]
54 | # right eye viewport
55 | camera = right_cam = @scene.active_camera.clone recursive: false
56 | camera.rotation_order = 'Q'
57 | mat4.identity camera.matrix_parent_inverse
58 | quat.identity camera.rotation
59 | if not use_room_scale_parent
60 | @scene.make_parent @scene.active_camera, camera
61 | v = @add_viewport camera
62 | v.set_clear false, false
63 | v.rect = [0.5, 0, 0.5, 1]
64 | v.right_eye_factor = 1
65 | # resize canvas and viewports
66 | check_size = =>
67 | if not @context.vr_screen
68 | return
69 | left_eye = @HMD.getEyeParameters("left")
70 | right_eye = @HMD.getEyeParameters("right")
71 | width = Math.max(left_eye.renderWidth, right_eye.renderWidth) * 2
72 | height = Math.max(left_eye.renderHeight, right_eye.renderHeight)
73 | if @width != width or @height != height
74 | @resize width, height
75 | # TODO: enable this line when it works somewhere.
76 | # setTimeout check_size, 1000
77 | check_size()
78 | @context.canvas_screen.enabled = false
79 | for behaviour in @context.enabled_behaviours
80 | behaviour.on_enter_vr? left_cam, right_cam
81 | {position, width, height, left, top} = @canvas.style
82 | @old_canvas_style = {position, width, height, left, top}
83 | if mirror_zoom and \
84 | @canvas.style.width == '100vw' and @canvas.style.height == '100vh'
85 | @set_mirror_zoom mirror_zoom
86 | # Daydream controller hack
87 | @gamepad_connected = false #gamepad_connected
88 | window.addEventListener 'gamepadconnected', =>
89 | @gamepad_connected = true
90 |
91 | set_mirror_zoom: (zoom) ->
92 | {renderWidth, renderHeight} = @HMD.getEyeParameters("left")
93 | ratio = renderWidth/renderHeight
94 | @canvas.style.position = 'fixed'
95 | @canvas.style.width = 200*zoom+'vw'
96 | @canvas.style.height = (100*zoom/ratio) + 'vw'
97 | @canvas.style.left = "#{-50*zoom+50}vw"
98 | @canvas.style.top = "calc( -#{50*zoom}vw + #{50*ratio}vh )"
99 |
100 | update_room_matrix: ->
101 | # @HMD.resetPose()
102 | hmd_sst = @HMD.stageParameters?.sittingToStandingTransform
103 | console.log hmd_sst
104 | if hmd_sst?
105 | mat4.copyArray @sst, hmd_sst
106 | # Edge
107 | # mat4.rotateX @sst, @sst, Math.PI/2
108 | else
109 | # Daydream
110 | # mat4.rotateX @sst, @sst, Math.PI/2
111 | @sst.m13 = 1.65 # average eye height
112 | mat4.rotateY @sst, @sst, Math.PI/2
113 | mat4.invert @sst_inverse, @sst
114 | mat3.fromMat4 @sst3, @sst
115 | return
116 |
117 | destroy: ->
118 | {position, width, height, left, top} = @old_canvas_style
119 | @canvas.style.position = position
120 | @canvas.style.width = width
121 | @canvas.style.height = height
122 | @canvas.style.left = left
123 | @canvas.style.top = top
124 | @context.vr_screen = null
125 | @context.screens.splice @context.screens.indexOf(this), 1
126 | @context.canvas_screen.width = 0 # force resize
127 | @context.canvas_screen.resize_to_canvas()
128 | @context.canvas_screen.enabled = true
129 | for ob in @head
130 | ob.parent = @scene.active_camera.children
131 | # TODO: Destroy cameras
132 | for behaviour in @context.enabled_behaviours
133 | behaviour.on_exit_vr?()
134 | return
135 |
136 | exit: ->
137 | @HMD.exitPresent()
138 | @destroy()
139 |
140 | pre_draw: ->
141 | {HMD} = this
142 | # Read pose
143 | return if not @frame_data? # not sure when this happens
144 | HMD.getFrameData @frame_data
145 | # Set position of VR cameras, etc
146 | {
147 | pose: {position, orientation, angularVelocity},
148 | leftProjectionMatrix,
149 | rightProjectionMatrix,
150 | leftViewMatrix,
151 | rightViewMatrix,
152 | } = @frame_data
153 | window.pose = @frame_data.pose
154 | {position: p0, rotation: r0, world_matrix: m0, projection_matrix: pm0} =
155 | @viewports[0].camera
156 | {position: p1, rotation: r1, world_matrix: m1, projection_matrix: pm1} =
157 | @viewports[1].camera
158 | # if position?
159 | # vec3.copyArray p0, position
160 | # vec3.copyArray p1, position
161 | # if orientation?
162 | # vec3.copyArray r0, orientation
163 | # vec3.copyArray r1, orientation
164 |
165 | if leftViewMatrix? and angularVelocity?
166 | [avx, avy, avz] = angularVelocity
167 | has_av = (avx or avy or avz)
168 | if has_av
169 | @is_wmr = false
170 | m4 = mat4.create()
171 | m3 = mat3.create()
172 | {sst_inverse} = this
173 | time = performance.now()
174 | delta_frames = Math.min 10, (time - @last_time)/11.11111111
175 |
176 | mat4.copyArray m4, leftViewMatrix
177 | # mat4.rotateX m4, m4, Math.PI/2
178 | mat4.mul m4, m4, sst_inverse
179 | mat4.invert m4, m4
180 | mat3.fromMat4 m3, m4
181 | quat.fromMat3 r0, m3
182 | quat.copy @left_orientation, r0
183 | # TODO: Test with other headsets.
184 | @head_is_tracking =
185 | not (p0.x == m4.m12 and p0.y == m4.m13 and p0.z == m4.m14)
186 | vec3.set p0, m4.m12, m4.m13, m4.m14
187 |
188 | mat4.copyArray m4, rightViewMatrix
189 | # mat4.rotateX m4, m4, Math.PI/2
190 | mat4.mul m4, m4, sst_inverse
191 | mat4.invert m4, m4
192 | mat3.fromMat4 m3, m4
193 | quat.fromMat3 r1, m3
194 | quat.copy @right_orientation, r1
195 | vec3.set p1, m4.m12, m4.m13, m4.m14
196 |
197 | vec3.lerp @head.position, p0, p1, .5
198 | quat.slerp @head.rotation, r0, r1, .5
199 |
200 | @last_time = time
201 |
202 | @viewports[0].camera._update_matrices()
203 | @viewports[1].camera._update_matrices()
204 | mat4.copyArray pm0, leftProjectionMatrix
205 | mat4.copyArray pm1, rightProjectionMatrix
206 | #TODO: detect headset
207 | # pm0.m09 = -pm0.m09
208 | # pm1.m09 = -pm1.m09
209 | if not mat4.equals pm0, @old_pm0
210 | # culling planes need to be updated
211 | mat4.invert @viewports[0].camera.projection_matrix_inv, pm0
212 | @viewports[0].camera._calculate_culling_planes()
213 | mat4.invert @viewports[1].camera.projection_matrix_inv, pm1
214 | @viewports[1].camera._calculate_culling_planes()
215 | mat4.copy @old_pm0, pm0
216 | return
217 |
218 | post_draw: ->
219 | @HMD.submitFrame()
220 |
221 |
222 | displays = null
223 | vrdisplaypresentchange = null
224 |
225 | exports.has_HMD = ->
226 | # NOTE: Firefox gets stuck all the time after calling this,
227 | # call it only if you're sure you have an HMD or it's not Firefox
228 | new Promise (resolve, reject) ->
229 | if not navigator.getVRDisplays
230 | if navigator.getVRDevices
231 | return reject "This browser only supports an old version
232 | of WebVR. Use a browser with WebVR 1.1 support"
233 | return reject "This browser doesn't support WebVR
234 | or is not enabled."
235 | navigator.getVRDisplays().then (_displays) ->
236 | displays = _displays
237 | HMD = null
238 | for display in displays
239 | if display instanceof VRDisplay and
240 | display.capabilities.canPresent
241 | HMD = display
242 | break
243 | if not HMD?
244 | return reject "No HMDs detected."
245 | resolve displays
246 |
247 | set_neck_model = exports.set_neck_model = (ctx, nm) ->
248 | ctx.neck_model = nm
249 | nq = quat.new 0,0,0,1
250 | quat.rotateX nq, nq, -nm.angle*Math.PI/180 # deg to rad
251 | nm.orig_neck = vec3.new 0,nm.length, 0
252 | vec3.transformQuat nm.orig_neck, nm.orig_neck, nq
253 | nm.neck = vec3.copy vec3.create(), nm.orig_neck
254 |
255 | exports.init = (scene, options={}) ->
256 | reject_reason = ''
257 | # Detect API support
258 | if not navigator.getVRDisplays
259 | if navigator.getVRDevices
260 | return Promise.reject "This browser only supports an old version
261 | of WebVR. Use a browser with WebVR 1.1 support"
262 | return Promise.reject "This browser doesn't support WebVR
263 | or is not enabled."
264 | # Find HMD
265 | ctx = scene.context
266 | if ctx.vr_screen?.HMD.isPresenting
267 | return Promise.resolve()
268 | {HMD} = options
269 | if not HMD?
270 | if not displays?
271 | return Promise.reject "Call hasVR first."
272 | for display in displays
273 | if display instanceof VRDisplay and display.capabilities.canPresent
274 | HMD = display
275 | break
276 | if not HMD?
277 | return Promise.reject "No HMDs detected. Conect an HMD,
278 | turn it on and try again."
279 |
280 | ctx._vrscene = scene
281 | # Request present
282 | if HMD.capabilities.canPresent
283 | if not HMD.isPresenting
284 | HMD.requestPresent [{source: ctx.canvas}]
285 | else
286 | # TODO: support non-presenting VR displays?
287 | return Promise.reject "Non-presenting VR displays are not supported"
288 |
289 | # Prepare scene after is presenting
290 | window.removeEventListener 'vrdisplaypresentchange', vrdisplaypresentchange
291 | new Promise (resolve, reject) ->
292 | window.addEventListener 'vrdisplaypresentchange',
293 | vrdisplaypresentchange = ->
294 | if HMD.isPresenting
295 | try
296 | if options.use_VR_position?
297 | ctx.use_VR_position = options.use_VR_position
298 | if options.neck_model?
299 | set_neck_model ctx, options.neck_model
300 | if not ctx.vr_screen?
301 | new VRScreen ctx, HMD, scene, options
302 | resolve()
303 | catch e
304 | reject(e)
305 | else
306 | window.removeEventListener 'vrdisplaypresentchange',
307 | vrdisplaypresentchange
308 | ctx.vr_screen?.destroy()
309 | return
310 |
--------------------------------------------------------------------------------
/main.js:
--------------------------------------------------------------------------------
1 | if ((typeof window === "undefined" || window === null) && typeof global !== "undefined") {
2 | global.window = global;
3 | }
4 | if(typeof process === "undefined") var process = {browser: true};
5 | if(process.browser || process.android){
6 | //Webpack code
7 | module.exports = require('./dist/myou.js');
8 | }else {
9 | //Electron code
10 | var req = eval('require');
11 | req('coffee-script/register');
12 | module.exports = req('./pack.coffee');
13 | }
14 |
--------------------------------------------------------------------------------
/pack.coffee:
--------------------------------------------------------------------------------
1 | require './engine/init'
2 | {Myou, create_canvas, create_full_window_canvas} = require './engine/myou'
3 | {Behaviour} = require './engine/behaviour'
4 |
5 | # geometry utils
6 | gmath =
7 | g2: require './engine/math_utils/g2'
8 | g3: require './engine/math_utils/g3'
9 |
10 | # vector utils
11 | vmath = require './engine/math_utils/vmath_extra'
12 |
13 | # math utils
14 | math = require './engine/math_utils/math_extra'
15 |
16 | module.exports = {
17 | #myou engine
18 | Myou, Behaviour, Behavior: Behaviour,
19 | #Utils
20 | create_canvas, create_full_window_canvas, gmath, vmath, math,
21 | }
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "myou-engine",
3 | "version": "0.5.2",
4 | "description": "Myou is a game engine for web, it features an editor based on Blender.",
5 | "main": "./dist/myou.js",
6 | "scripts": {
7 | "build": "webpack",
8 | "test": "echo \"Error: no test specified\" && exit 1",
9 | "prepare": "node ./node_modules/webpack/bin/webpack.js"
10 | },
11 | "dependencies": {
12 | },
13 | "devDependencies": {
14 | "coffee-loader": "^0.7.2",
15 | "coffee-script": "^1.10.0",
16 | "cubemap-sh": "^0.2.1",
17 | "file-loader": "^0.8.5",
18 | "fs-extra": "^4.0.2",
19 | "json-loader": "^0.5.4",
20 | "mersennetwister": "^0.2.3",
21 | "parse-dds": "^1.2.1",
22 | "quickhull3d": "^2.0.1",
23 | "raw-loader": "^0.5.1",
24 | "source-map-loader": "^0.1.5",
25 | "spur-events": "^0.1.8",
26 | "timsort": "^0.2.1",
27 | "url-loader": "^0.5.7",
28 | "vmath": "^1.4.7",
29 | "webpack": "^3.8.1",
30 | "whatwg-fetch": "^1.0.0"
31 | },
32 | "repository": {
33 | "type": "git",
34 | "url": "ssh://root@pixelements.net/root/repositories/myou-engine.git"
35 | },
36 | "keywords": [
37 | "webgl",
38 | "game",
39 | "engine",
40 | "blender"
41 | ],
42 | "author": "Alberto Torres Ruiz and Julio Manuel López (http://pixelements.net)",
43 | "license": "MIT",
44 | "coffeelintConfig": {
45 | "indentation": {
46 | "value": 4
47 | },
48 | "no_interpolation_in_single_quotes": {
49 | "level": "warn"
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | // This file has two parts:
4 | // The regular webpack config, enclosed in this function,
5 | // And handle_myou_config below to use in other webpack configs.
6 |
7 | module.exports = (env) => {
8 | var webpack = require('webpack');
9 | var config = {
10 | context: __dirname,
11 | entry: [
12 | __dirname + '/pack.coffee',
13 | ],
14 | stats: {
15 | colors: true,
16 | reasons: true
17 | },
18 | module: {
19 | loaders: [
20 | {
21 | test: /\.coffee$/,
22 | loaders: [
23 | 'coffee-loader',
24 | ]
25 | },
26 | ]
27 | },
28 | output: {
29 | path: __dirname + '/dist/',
30 | filename: 'myou.js',
31 | // export commonjs2 and var at the same time
32 | library: 'MyouEngine = module.exports',
33 | },
34 | plugins: [
35 | new webpack.BannerPlugin({
36 | banner: [
37 | '"use strict";',
38 | '/**',
39 | ' * Myou Engine',
40 | ' *',
41 | ' * Copyright (c) 2017 by Alberto Torres Ruiz ',
42 | ' * Copyright (c) 2017 by Julio Manuel López Tercero ',
43 | ' *',
44 | ' * Permission is hereby granted, free of charge, to any person obtaining a copy',
45 | ' * of this software and associated documentation files (the "Software"), to deal',
46 | ' * in the Software without restriction, including without limitation the rights',
47 | ' * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell',
48 | ' * copies of the Software, and to permit persons to whom the Software is',
49 | ' * furnished to do so, subject to the following conditions:',
50 | ' *',
51 | ' * The above copyright notice and this permission notice shall be included in',
52 | ' * all copies or substantial portions of the Software.',
53 | ' *',
54 | ' * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR',
55 | ' * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,',
56 | ' * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE',
57 | ' * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER',
58 | ' * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,',
59 | ' * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE',
60 | ' * SOFTWARE.',
61 | ' */'
62 | ].join('\n'),
63 | raw: true,
64 | }),
65 | ],
66 | resolve: {
67 | extensions: ['.webpack.js', '.web.js', '.js', '.coffee', '.json']
68 | },
69 | }
70 |
71 | env = env || {}
72 | if(env.sourcemaps){
73 | config.devtool = 'cheap-module-eval-source-map';
74 | }
75 | return config;
76 | }
77 |
78 | var fs = require('fs-extra');
79 | var path = require('path'), join = path.join;
80 |
81 | // This function will use the flags of the project to copy libraries,
82 | // And to touch the config (currently just adding the flags to the code)
83 | module.exports.handle_myou_config = function(webpack, config, flags, env){
84 | function copy_lib(name){
85 | console.log('Copying library '+name);
86 | fs.ensureDirSync(join(config.output.path, 'libs'));
87 | fs.copySync(
88 | join(__dirname, 'engine', 'libs', name),
89 | join(config.output.path, 'libs', name)
90 | );
91 | }
92 | if(flags.copy_bullet == null) flags.copy_bullet = true;
93 | if(flags.include_bullet && flags.copy_bullet){
94 | copy_lib('ammo.asm.js');
95 | copy_lib('ammo.wasm.js');
96 | copy_lib('ammo.wasm.wasm');
97 | }
98 | config.plugins = config.plugins || [];
99 | config.plugins.push(new webpack.DefinePlugin({
100 | global_myou_engine_webpack_flags: JSON.stringify(flags),
101 | }));
102 | return config;
103 | }
104 |
--------------------------------------------------------------------------------