├── .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 | --------------------------------------------------------------------------------