├── .gitignore ├── vse_doom.blend ├── readme.md ├── run_doom.py └── render.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | doom1.wad 3 | *.blend1 4 | /.vs 5 | -------------------------------------------------------------------------------- /vse_doom.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aras-p/blender-vse-doom/HEAD/vse_doom.blend -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Doom running in Blender Video Sequence Editor timeline 2 | 3 | Here's a video (click):
4 | [![Doom in Blender VSE timeline](https://img.youtube.com/vi/Y2iDZjteMs8/0.jpg)](https://www.youtube.com/watch?v=Y2iDZjteMs8) 5 | 6 | A modal blender operator that loads doom file, creates 7 | VSE timeline full of color strips (80 columns, 60 rows), listens to 8 | keyboard input for player control, renders doom frame and updates the 9 | VSE color strip colors to match the rendered result. Escape key finishes 10 | the operator. 11 | 12 | > I have no idea what I'm doing, so it is entirely possible that the 13 | > operator is written in a completely nonsensical way! 14 | 15 | All the Doom-specific heavy lifting is in `render.py`, written by 16 | Mark Dufour and is completely unrelated to Blender. It is just a tiny 17 | pure Python Doom loader/renderer. I took it from 18 | "[Minimal DOOM WAD renderer](https://github.com/shedskin/shedskin/blob/6c30bbe617/examples/doom/render.py)" 19 | and made two small edits to avoid division by zero exceptions that I was getting. 20 | 21 | 22 | ## Instructions 23 | 24 | 1. Download code of this repository. 25 | 1. Put `doom1.wad` next to `vse_doom.blend` file, can get it from [doomwiki](https://doomwiki.org/wiki/DOOM1.WAD) 26 | or other places. 27 | 1. Open `vse_doom.blend` file in Blender. The file is made with Blender 4.0, I have tested up to Blender 4.4. 28 | 1. Click "Run Script" atop of the script editor window. 29 | 1. This will create 80x60 color strips in the VSE timeline below, load E1M1 from `doom1.wad` and start a modal operator 30 | that reacts to arrow keys to move. Escape finishes the operator and returns to "normal blender". 31 | 32 | 33 | ## Performance 34 | 35 | This runs pretty slow (~3fps) currently (blender 4.1-4.4), due to various VSE shenanigans. Updating colors of 36 | almost 5000 color strips is slow, it turns out. 37 | 38 | The pure-python Doom renderer, while perhaps written in not performant Python way, is not the slow part here. 39 | It takes about 7 milliseconds to render 80x60 frame. It takes almost 300 milliseconds to update the colors 40 | of all the VSE strips, in comparison. I will see if I can optimize it somehow! 41 | 42 | > In Blender 4.0 or earlier it runs even slower, because redrawing the VSE timeline with 5000 strips 43 | > takes about 100 milliseconds; that is no longer slow in 4.1+ (1-2ms). 44 | 45 | A blog post with more details: https://aras-p.info/blog/2025/01/17/Doom-in-Blender-VSE/ 46 | 47 | ## .blend file contents 48 | 49 | The `vse_doom.blend` file itself contains pretty much nothing, except this text script block: 50 | 51 | ``` 52 | import bpy 53 | import os 54 | import sys 55 | 56 | # add blend file folder to import path, so that we can use .py scripts next to the file 57 | blen_dir = os.path.normpath(os.path.join(__file__, "..", "..")) 58 | if blen_dir not in sys.path: 59 | sys.path.append(blen_dir) 60 | 61 | # make sure to reload run_doom/render modules so we can do edits while blender is running 62 | if "run_doom" in sys.modules: 63 | del sys.modules["run_doom"] 64 | if "render" in sys.modules: 65 | del sys.modules["render"] 66 | 67 | import run_doom 68 | 69 | def register(): 70 | try: 71 | bpy.utils.register_class(run_doom.DoomOperator) 72 | except: 73 | pass # was already registered 74 | 75 | def unregister(): 76 | bpy.utils.unregister_class(run_doom.DoomOperator) 77 | 78 | if __name__ == "__main__": 79 | register() 80 | bpy.ops.lol.doom_player('INVOKE_DEFAULT') 81 | ``` 82 | -------------------------------------------------------------------------------- /run_doom.py: -------------------------------------------------------------------------------- 1 | # A modal blender operator that loads doom file, creates 2 | # VSE timeline full of color strips, listens to keyboard input for player 3 | # control, renders doom frame and updates the VSE color strip colors 4 | # to match the rendered result. Escape key finishes the operator. 5 | # 6 | # I have no idea what I'm doing, so it is entirely possible that the 7 | # operator is written in a completely nonsensical way! 8 | # 9 | # All the Doom-specific heavy lifting is in render.py, written by 10 | # Mark Dufour and is completely unrelated to Blender. It is just a tiny 11 | # pure Python Doom loader/renderer. 12 | 13 | import bpy 14 | import os 15 | import math 16 | import time 17 | 18 | import render 19 | 20 | class DoomOperator(bpy.types.Operator): 21 | bl_idname = "lol.doom_player" 22 | bl_label = "Doom Player" 23 | 24 | def __init__(self, *args, **kwargs): 25 | super().__init__(*args, **kwargs) 26 | self.timer = None 27 | self.strip_lookup = None 28 | self.map = None 29 | self.blender_palette = None 30 | self.player = None 31 | 32 | def create_strips(self): 33 | bpy.context.scene.sequence_editor_clear() 34 | if not bpy.context.scene.sequence_editor: 35 | bpy.context.scene.sequence_editor_create() 36 | 37 | width = render.WIDTH 38 | height = render.HEIGHT 39 | dur = 10 40 | self.strip_lookup = [None] * (width * height) 41 | 42 | for y in range(height): 43 | for x in range(width): 44 | start = x * dur + 1 45 | strip = bpy.context.scene.sequence_editor.sequences.new_effect( 46 | name=f"C{x}_{y}", 47 | type='COLOR', 48 | channel=y + 1, 49 | frame_start=start, 50 | frame_end=start + dur - 1 51 | ) 52 | strip.select = False 53 | idx = (height - 1 - y) * width + x 54 | self.strip_lookup[idx] = strip 55 | bpy.context.scene.frame_end = width * dur 56 | 57 | 58 | def update_strips(self, data): 59 | num = len(data) 60 | for idx in range(num): 61 | val = data[idx] 62 | col = self.blender_palette[val] 63 | self.strip_lookup[idx].color = col 64 | 65 | def modal(self, context, event): 66 | # Escape stops 67 | if event.type == 'ESC': 68 | context.window_manager.event_timer_remove(self.timer) 69 | context.window.cursor_modal_restore() 70 | return {'FINISHED'} 71 | 72 | if event.type == 'TIMER': 73 | self.frame_update() 74 | elif event.type == 'LEFT_ARROW' and event.value == 'PRESS': 75 | self.player.angle += math.radians(20) 76 | elif event.type == 'RIGHT_ARROW' and event.value == 'PRESS': 77 | self.player.angle -= math.radians(20) 78 | elif event.type == 'UP_ARROW' and event.value == 'PRESS': 79 | self.move_player(7.0, 0) 80 | elif event.type == 'DOWN_ARROW' and event.value == 'PRESS': 81 | self.move_player(-7.0, 0) 82 | 83 | return {'RUNNING_MODAL'} 84 | 85 | def move_player(self, dist, strafe): 86 | dx = dist * math.cos(self.player.angle + strafe) 87 | dy = dist * math.sin(self.player.angle + strafe) 88 | self.player.x += dx 89 | self.player.y += dy 90 | 91 | def invoke(self, context, event): 92 | context.window.cursor_modal_set('HAND') 93 | 94 | # create strips 95 | t0 = time.perf_counter() 96 | self.create_strips() 97 | t1 = time.perf_counter() 98 | print(f"doom: created {render.WIDTH}x{render.HEIGHT} ({render.WIDTH*render.HEIGHT}) strips in {(t1-t0)*1000:.1f}ms") 99 | self.frame_count = 0 100 | 101 | # load doom map 102 | blen_dir = os.path.normpath(os.path.join(__file__, "..")) 103 | mapname = 'E1M1' 104 | t0 = time.perf_counter() 105 | self.map = render.Map(f"{blen_dir}/doom1.wad", mapname) 106 | t1 = time.perf_counter() 107 | print(f"doom: loaded {mapname} in {(t1-t0)*1000:.1f}ms") 108 | palette = self.map.palette 109 | self.blender_palette = [(r/255, g/255, b/255) for r,g,b in palette] 110 | self.player = self.map.player 111 | 112 | wm = context.window_manager 113 | self.timer = wm.event_timer_add(0.1, window=context.window) 114 | wm.modal_handler_add(self) 115 | return {'RUNNING_MODAL'} 116 | 117 | def frame_update(self): 118 | 119 | # update player 120 | pl = self.player 121 | pl.z = pl.floor_h + 48 122 | pl.update() 123 | 124 | # render frame and update strip colors 125 | t0 = time.perf_counter() 126 | buf = render.render(self.map, self.frame_count) 127 | t1 = time.perf_counter() 128 | self.update_strips(buf) 129 | t2 = time.perf_counter() 130 | if self.frame_count % 8 == 0: 131 | print(f"doom: render {(t1-t0)*1000:.1f}ms strip color update {(t2-t1)*1000:.1f}ms") 132 | 133 | self.frame_count += 1 134 | -------------------------------------------------------------------------------- /render.py: -------------------------------------------------------------------------------- 1 | # From "Minimal DOOM WAD renderer" by Mark Dufour, 2 | # https://github.com/shedskin/shedskin/blob/6c30bbe617/examples/doom/render.py 3 | # with two changes for avoiding division by zero exceptions: 4 | # `max(lowerDy, 0.00001)` and `max(upperDy, 0.00001)` 5 | 6 | import math 7 | from struct import unpack_from 8 | import random 9 | 10 | WIDTH = 80 11 | HEIGHT = 60 12 | 13 | WIDTH_2 = WIDTH//2 14 | HEIGHT_2 = HEIGHT//2 15 | HEIGHT_INV = 1.0 / WIDTH 16 | 17 | TAN_45_DEG = math.tan(math.radians(45)) 18 | 19 | FLOOR_Y_INV = [1.0 / (y - HEIGHT_2) if y > HEIGHT_2 else 0.0 20 | for y in range(HEIGHT)] 21 | 22 | CEIL_Y_INV = [1.0 / (HEIGHT_2 - y) if y < HEIGHT_2 else 0.0 23 | for y in range(HEIGHT)] 24 | 25 | OSCILLATION = [int(13 + 13 * math.sin(2 * math.pi * (i / 255))) 26 | for i in range(256)] 27 | 28 | 29 | class Vertex: 30 | def __init__(self, x, y): 31 | self.x = x 32 | self.y = y 33 | 34 | 35 | class Sidedef: 36 | def __init__(self, offset_x, offset_y, upper_texture, lower_texture, 37 | middle_texture, sector): 38 | self.offset_x = offset_x 39 | self.offset_y = offset_y 40 | self.upper_texture = upper_texture 41 | self.lower_texture = lower_texture 42 | self.middle_texture = middle_texture 43 | self.sector = sector 44 | self.skyhack = False 45 | 46 | 47 | class Linedef: 48 | def __init__(self, vertex_start, vertex_end, special_type, sidedef_front, 49 | sidedef_back): 50 | self.vertex_start = vertex_start 51 | self.vertex_end = vertex_end 52 | self.special_type = special_type 53 | self.sidedef_front = sidedef_front 54 | self.sidedef_back = sidedef_back 55 | 56 | 57 | class Sector: 58 | def __init__(self, floor_h, ceil_h, floor_texture, ceil_texture, 59 | light_level, special_type, floor_flat, ceil_flat, ceil_pic): 60 | self.floor_h = floor_h 61 | self.ceil_h = ceil_h 62 | self.floor_texture = floor_texture 63 | self.ceil_texture = ceil_texture 64 | self.light_level = light_level 65 | self.special_type = special_type 66 | self.floor_flat = floor_flat 67 | self.ceil_flat = ceil_flat 68 | self.ceil_pic = ceil_pic 69 | 70 | self.random = [random.random() < 0.5 for i in range(256)] 71 | 72 | 73 | class SubSector: 74 | def __init__(self, segs): 75 | self.segs = segs 76 | 77 | 78 | class Seg: 79 | def __init__(self, vertex_start, vertex_end, angle, linedef, 80 | sidedef_front, sidedef_back, is_portal, offset, 81 | sector_front, sector_back): 82 | self.vertex_start = vertex_start 83 | self.vertex_end = vertex_end 84 | self.angle = angle 85 | self.linedef = linedef 86 | self.sidedef_front = sidedef_front 87 | self.sidedef_back = sidedef_back 88 | self.is_portal = is_portal 89 | self.offset = offset 90 | self.sector_front = sector_front 91 | self.sector_back = sector_back 92 | 93 | self.length = math.hypot(vertex_end.x - vertex_start.x, 94 | vertex_end.y - vertex_start.y) 95 | 96 | 97 | class Flat: 98 | def __init__(self, data): 99 | self.data = [[[d[64*y+x] for y in range(64)] 100 | for x in range(64)] for d in data] 101 | 102 | def get_data(self, frame_count): 103 | return self.data[(frame_count >> 4) % len(self.data)] 104 | 105 | 106 | class BSPNode: 107 | def __init__(self, partition_x, partition_y, change_partition_x, 108 | change_partition_y, rchild_id, lchild_id): 109 | self.partition_x = partition_x 110 | self.partition_y = partition_y 111 | self.change_partition_x = change_partition_x 112 | self.change_partition_y = change_partition_y 113 | self.rchild_id = rchild_id 114 | self.lchild_id = lchild_id 115 | 116 | def visit(self, map_, subsectors=None): 117 | if subsectors is None: 118 | subsectors = [] 119 | player = map_.player 120 | px = player.x - self.partition_x 121 | py = player.y - self.partition_y 122 | 123 | closest_id, farthest_id = self.lchild_id, self.rchild_id 124 | if py * self.change_partition_x <= px * self.change_partition_y: 125 | closest_id, farthest_id = farthest_id, closest_id 126 | 127 | for child_id in (closest_id, farthest_id): 128 | if child_id < 0: 129 | subsectors.append(map_.subsectors[child_id & 0x7fff]) 130 | else: 131 | map_.bspnodes[child_id].visit(map_, subsectors) 132 | return subsectors 133 | 134 | 135 | class Thing: 136 | def __init__(self, x, y, angle, type_): 137 | self.x = float(x) 138 | self.y = float(y) 139 | self.angle = math.radians(90) 140 | self.type_ = type_ 141 | 142 | 143 | class Player: 144 | def __init__(self, thing): 145 | self.x = thing.x 146 | self.y = thing.y 147 | self.z = 0.0 148 | self.angle = thing.angle 149 | self.floor_h = 48.0 150 | self.direction = Vec2(math.cos(self.angle), math.sin(self.angle)) 151 | 152 | def update(self): 153 | self.direction = Vec2(math.cos(self.angle), math.sin(self.angle)) 154 | 155 | 156 | class Texture: 157 | def __init__(self, name, data, width, height): 158 | self.name = name 159 | self.data = data 160 | self.width = width 161 | self.height = height 162 | 163 | 164 | class Picture: 165 | def __init__(self, data): 166 | width, height, offset_x, offset_y = unpack_from('= self.start and end <= self.end: 422 | return 423 | 424 | # no overlap, so node does not apply to span 425 | if start > self.end or end < self.start: 426 | return 427 | 428 | # reduce span to overlap with node 429 | if start <= self.start: 430 | start = self.start 431 | 432 | if end >= self.end: 433 | end = self.end 434 | 435 | if add: 436 | # unpartitioned, unoccluded node covered fully by span 437 | if (not self.occluded and not self.partitioned and 438 | start <= self.start and end >= self.end): 439 | result.append(start) 440 | result.append(end) 441 | self.occluded = True 442 | return 443 | 444 | # partition if needed 445 | if not self.partitioned: 446 | if start == self.start: 447 | self.partitionPoint = end 448 | else: 449 | self.partitionPoint = start - 1 450 | 451 | self.left = ClipBufferNode(self.start, self.partitionPoint) 452 | self.right = ClipBufferNode(self.partitionPoint + 1, self.end) 453 | self.partitioned = True 454 | 455 | else: 456 | if not self.partitioned: 457 | result.append(start) 458 | result.append(end) 459 | return 460 | 461 | # recurse into left and right 462 | if start <= self.partitionPoint and end <= self.partitionPoint: 463 | self.left.checkSpan(start, end, result, add) 464 | 465 | elif start <= self.partitionPoint and end > self.partitionPoint: 466 | self.left.checkSpan(start, self.partitionPoint, result, add) 467 | self.right.checkSpan(self.partitionPoint + 1, end, result, add) 468 | 469 | elif start > self.partitionPoint and end > self.partitionPoint: 470 | self.right.checkSpan(start, end, result, add) 471 | 472 | # left and right occluded, so node fully occluded 473 | if add and self.left.occluded and self.right.occluded: 474 | self.occluded = True 475 | 476 | 477 | def get_special_light(sector, frame_count): 478 | special_type = sector.special_type 479 | 480 | if special_type in (1, 17): 481 | if sector.random[(frame_count & 0xff0) >> 4]: 482 | return 10 483 | elif special_type in (2, 12): 484 | if (frame_count % 120) < 60: 485 | return 10 486 | elif special_type in (3, 13): 487 | if (frame_count % 240) < 120: 488 | return 10 489 | elif special_type == 8: 490 | return OSCILLATION[frame_count & 0xff] 491 | 492 | return 0 493 | 494 | 495 | def get_wall_colormap(colormaps, currentZ, seg, frame_count): 496 | sector = seg.sector_front 497 | 498 | colorMapIndex = int((currentZ - 5) * 0.05) 499 | colorMapIndex = min(colorMapIndex, 32 - (sector.light_level >> 3)) 500 | 501 | colorMapIndex += ((((seg.angle + 8192) & 0x7fff) - 16384) & 0x7fff) // 3200 502 | colorMapIndex += get_special_light(sector, frame_count) 503 | 504 | colorMapIndex = max(min(colorMapIndex, 31), 0) 505 | return colormaps[colorMapIndex] 506 | 507 | 508 | def get_flat_colormap(colormaps, currentZ, seg, frame_count): 509 | sector = seg.sector_front 510 | 511 | colorMapIndex = int((currentZ - 5) * 0.05) 512 | colorMapIndex = min(colorMapIndex, 32 - (sector.light_level >> 3)) 513 | 514 | colorMapIndex += get_special_light(sector, frame_count) 515 | 516 | colorMapIndex = max(min(colorMapIndex, 31), 0) 517 | return colormaps[colorMapIndex] 518 | 519 | 520 | def draw_wall_col(drawsurf, x, middleMinY, middleMaxY, wallTexture, 521 | currentTextureX, currentZ, middleTextureY, 522 | middleTextureYStep, colormap): 523 | width = wallTexture.width 524 | height = wallTexture.height 525 | wallTextureData = wallTexture.data 526 | 527 | tx = int(currentTextureX * currentZ) % width 528 | for y in range(middleMinY, middleMaxY): 529 | ty = int(middleTextureY) % height 530 | drawsurf[y*WIDTH+x] = colormap.data[wallTextureData[tx][ty]] 531 | middleTextureY += middleTextureYStep 532 | 533 | 534 | def draw_flat_col(drawsurf, x, ceilMin, ceilMax, seg, player, flatTexture, 535 | flat_h, INV, sign, colormaps, frame_count): 536 | for y in range(ceilMin, ceilMax): 537 | z = sign * WIDTH_2 * (-flat_h + player.z) * INV[y] 538 | 539 | colormap = get_flat_colormap(colormaps, z, seg, frame_count) 540 | 541 | playerDir = player.direction 542 | px = playerDir.x * z + player.x 543 | py = playerDir.y * z + player.y 544 | 545 | lateralLength = TAN_45_DEG * z 546 | 547 | leftX = -playerDir.y * lateralLength + px 548 | leftY = playerDir.x * lateralLength + py 549 | rightX = playerDir.y * lateralLength + px 550 | rightY = -playerDir.x * lateralLength + py 551 | 552 | dx = (rightX - leftX) * HEIGHT_INV 553 | dy = (rightY - leftY) * HEIGHT_INV 554 | 555 | tx = int(leftX + dx * x) & 0x3f 556 | ty = int(leftY + dy * x) & 0x3f 557 | 558 | drawsurf[y*WIDTH+x] = colormap.data[flatTexture[tx][ty]] 559 | 560 | 561 | def draw_sky_col(drawsurf, x, upperMinY, upperMaxY, seg, player): 562 | ceil_pic = seg.sector_front.ceil_pic 563 | ceilingTextureWidth = ceil_pic.width 564 | ceilingTextureHeight = ceil_pic.height 565 | ceilTextureData = ceil_pic.data 566 | 567 | normPlayerAngle = player.angle % (2 * math.pi) 568 | if normPlayerAngle < 0: 569 | normPlayerAngle += 2 * math.pi 570 | 571 | textureOffsetX = ceilingTextureWidth * (normPlayerAngle / (math.pi * 0.5)) 572 | dx = ceilingTextureWidth / WIDTH 573 | dy = ceilingTextureHeight / (WIDTH//2) 574 | 575 | for y in range(upperMinY, upperMaxY): 576 | tx = int(dx * x - textureOffsetX) % ceilingTextureWidth 577 | ty = int(y * dy) % ceilingTextureHeight 578 | drawsurf[y*WIDTH+x] = ceilTextureData[tx][ty] 579 | 580 | 581 | def draw_seg(seg, map_, drawsurf, scrXA, scrXB, cbuffer, za, zb, textureX0, 582 | textureX1, frontSidedef, lowerOcclusion, upperOcclusion, 583 | frame_count): 584 | # get non-occluded clips from cbuffer 585 | cbufferResult = [] 586 | cbuffer.checkSpan(scrXA, scrXB, cbufferResult, not seg.is_portal) 587 | 588 | # no visible clips 589 | if not cbufferResult: 590 | return 591 | 592 | player = map_.player 593 | colormaps = map_.colormaps 594 | sector_front = seg.sector_front 595 | sector_back = seg.sector_back 596 | 597 | # front side 598 | frontCeil = sector_front.ceil_h - player.z 599 | frontFloor = sector_front.floor_h - player.z 600 | scrYAFrontCeil = WIDTH_2 * (frontCeil / -za) + HEIGHT_2 601 | scrYAFrontFloor = WIDTH_2 * (frontFloor / -za) + HEIGHT_2 602 | scrYBFrontCeil = WIDTH_2 * (frontCeil / -zb) + HEIGHT_2 603 | scrYBFrontFloor = WIDTH_2 * (frontFloor / -zb) + HEIGHT_2 604 | 605 | # back side 606 | if seg.is_portal: 607 | backCeil = sector_back.ceil_h - player.z 608 | backFloor = sector_back.floor_h - player.z 609 | scrYABackCeil = WIDTH_2 * (backCeil / -za) + HEIGHT_2 610 | scrYABackFloor = WIDTH_2 * (backFloor / -za) + HEIGHT_2 611 | scrYBBackCeil = WIDTH_2 * (backCeil / -zb) + HEIGHT_2 612 | scrYBBackFloor = WIDTH_2 * (backFloor / -zb) + HEIGHT_2 613 | hasLowerWall = backFloor > frontFloor 614 | hasUpperWall = backCeil < frontCeil 615 | else: 616 | backCeil = 0 617 | backFloor = 0 618 | scrYABackCeil = 0 619 | scrYABackFloor = 0 620 | scrYBBackCeil = 0 621 | scrYBBackFloor = 0 622 | hasLowerWall = False 623 | hasUpperWall = False 624 | 625 | # calculate steps 626 | dxInv = 1.0 / (scrXB - scrXA) 627 | zInvStep = (1 / zb - 1 / za) * dxInv 628 | textureXStep = (textureX1 / zb - textureX0 / za) * dxInv 629 | middleCeilStep = (scrYBFrontCeil - scrYAFrontCeil) * dxInv 630 | middlefloorStep = (scrYBFrontFloor - scrYAFrontFloor) * dxInv 631 | lowerCeilStep = (scrYBBackFloor - scrYABackFloor) * dxInv 632 | lowerfloorStep = (scrYBFrontFloor - scrYAFrontFloor) * dxInv 633 | upperCeilStep = (scrYBFrontCeil - scrYAFrontCeil) * dxInv 634 | upperFloorStep = (scrYBBackCeil - scrYABackCeil) * dxInv 635 | 636 | # loop over non-occluded seg clips 637 | for clip in range(0, len(cbufferResult), 2): 638 | clipLeft = cbufferResult[clip] 639 | clipRight = cbufferResult[clip+1] 640 | 641 | currentMiddleCeil = scrYAFrontCeil 642 | currentMiddleFloor = scrYAFrontFloor 643 | 644 | currentLowerCeil = scrYABackFloor 645 | currentLowerFloor = scrYAFrontFloor 646 | currentUpperCeil = scrYAFrontCeil 647 | currentUpperFloor = scrYABackCeil 648 | 649 | currentZInv = 1 / za 650 | currentTextureX = textureX0 / za 651 | scrLeft = scrXA 652 | scrRight = scrXB 653 | 654 | # narrow to clip 655 | if scrLeft < clipLeft: 656 | dif = clipLeft - scrXA 657 | currentTextureX += dif * textureXStep 658 | currentZInv += dif * zInvStep 659 | currentMiddleCeil += dif * middleCeilStep 660 | currentMiddleFloor += dif * middlefloorStep 661 | 662 | if hasLowerWall: 663 | currentLowerCeil += dif * lowerCeilStep 664 | currentLowerFloor += dif * lowerfloorStep 665 | 666 | if hasUpperWall: 667 | currentUpperCeil += dif * upperCeilStep 668 | currentUpperFloor += dif * upperFloorStep 669 | 670 | scrLeft = clipLeft 671 | 672 | if scrRight > clipRight: 673 | scrRight = clipRight 674 | 675 | # draw clip column-wise 676 | for x in range(scrLeft, scrRight+1): 677 | currentZ = 1.0 / currentZInv 678 | colormap = get_wall_colormap(colormaps, currentZ, seg, frame_count) 679 | 680 | middleMaxY = int(currentMiddleFloor) 681 | middleMinY = int(currentMiddleCeil) 682 | middleDy = middleMaxY - middleMinY 683 | 684 | if middleDy == 0: # on collision with wall 685 | middleTextureYStep = 0 686 | else: 687 | middleTextureYStep = (frontCeil - frontFloor) / middleDy 688 | middleTextureY = frontSidedef.offset_y 689 | 690 | if middleMinY < lowerOcclusion[x]: 691 | dif = lowerOcclusion[x] - middleMinY 692 | middleTextureY = \ 693 | dif * middleTextureYStep + frontSidedef.offset_y 694 | middleMinY = lowerOcclusion[x] 695 | 696 | middleMaxY = min(middleMaxY, upperOcclusion[x]) 697 | 698 | # middle wall 699 | middle_texture = frontSidedef.middle_texture 700 | if not seg.is_portal and middle_texture is not None: 701 | draw_wall_col(drawsurf, x, middleMinY, middleMaxY, 702 | middle_texture, currentTextureX, currentZ, 703 | middleTextureY, middleTextureYStep, colormap) 704 | 705 | # floor 706 | ceilMin = int(max(lowerOcclusion[x], middleMaxY)) 707 | if ceilMin < upperOcclusion[x]: 708 | floor_flat = sector_front.floor_flat.get_data(frame_count) 709 | draw_flat_col(drawsurf, x, ceilMin, upperOcclusion[x], seg, 710 | player, floor_flat, sector_front.floor_h, 711 | FLOOR_Y_INV, 1, colormaps, frame_count) 712 | upperOcclusion[x] = ceilMin 713 | 714 | # lower wall 715 | if hasLowerWall: 716 | lowerMaxY = int(currentLowerFloor) 717 | lowerMinY = int(currentLowerCeil) 718 | 719 | lowerDy = lowerMaxY - lowerMinY 720 | lowerTextureYStep = (backFloor - frontFloor) / max(lowerDy, 0.00001) 721 | lowerTextureY = frontSidedef.offset_y 722 | 723 | if lowerMinY < lowerOcclusion[x]: 724 | dif = lowerOcclusion[x] - lowerMinY 725 | lowerTextureY = \ 726 | dif * lowerTextureYStep + frontSidedef.offset_y 727 | lowerMinY = lowerOcclusion[x] 728 | 729 | lowerMaxY = min(lowerMaxY, upperOcclusion[x]) 730 | 731 | lower_texture = frontSidedef.lower_texture 732 | if lower_texture is not None: 733 | draw_wall_col(drawsurf, x, lowerMinY, lowerMaxY, 734 | lower_texture, currentTextureX, currentZ, 735 | lowerTextureY, lowerTextureYStep, 736 | colormap) 737 | 738 | if lowerMinY < upperOcclusion[x]: 739 | upperOcclusion[x] = lowerMinY 740 | 741 | currentLowerCeil += lowerCeilStep 742 | currentLowerFloor += lowerfloorStep 743 | 744 | # ceil 745 | ceilMax = int(min(upperOcclusion[x], middleMinY)) 746 | if ceilMax > lowerOcclusion[x]: 747 | # sky 748 | if sector_front.ceil_pic is not None: 749 | draw_sky_col(drawsurf, x, lowerOcclusion[x], ceilMax, seg, 750 | player) 751 | # ceil 752 | else: 753 | ceil_flat = sector_front.ceil_flat.get_data(frame_count) 754 | draw_flat_col(drawsurf, x, lowerOcclusion[x], ceilMax, seg, 755 | player, ceil_flat, sector_front.ceil_h, 756 | CEIL_Y_INV, -1, colormaps, frame_count) 757 | 758 | lowerOcclusion[x] = middleMinY 759 | 760 | # upper wall 761 | if hasUpperWall: 762 | upperMaxY = int(currentUpperFloor) 763 | upperMinY = int(currentUpperCeil) 764 | 765 | upperDy = upperMaxY - upperMinY 766 | upperTextureYStep = (frontCeil - backCeil) / max(upperDy, 0.00001) 767 | upperTextureY = frontSidedef.offset_y 768 | 769 | if upperMinY < lowerOcclusion[x]: 770 | dif = lowerOcclusion[x] - upperMinY 771 | upperTextureY = \ 772 | dif * upperTextureYStep + frontSidedef.offset_y 773 | upperMinY = lowerOcclusion[x] 774 | 775 | upperMaxY = min(upperMaxY, upperOcclusion[x]) 776 | upper_texture = frontSidedef.upper_texture 777 | 778 | # sky 779 | if frontSidedef.skyhack or upper_texture is None: 780 | if sector_front.ceil_pic is not None: 781 | draw_sky_col(drawsurf, x, upperMinY, upperMaxY, seg, 782 | player) 783 | # wall 784 | else: 785 | draw_wall_col(drawsurf, x, upperMinY, upperMaxY, 786 | upper_texture, currentTextureX, currentZ, 787 | upperTextureY, upperTextureYStep, colormap) 788 | 789 | if upperMaxY > lowerOcclusion[x]: 790 | lowerOcclusion[x] = upperMaxY 791 | 792 | currentUpperCeil += upperCeilStep 793 | currentUpperFloor += upperFloorStep 794 | 795 | currentMiddleCeil += middleCeilStep 796 | currentMiddleFloor += middlefloorStep 797 | currentZInv += zInvStep 798 | currentTextureX += textureXStep 799 | 800 | 801 | def render(map_, frame_count): 802 | drawsurf = bytearray(WIDTH * HEIGHT) 803 | 804 | lowerOcclusion = WIDTH * [0] 805 | upperOcclusion = WIDTH * [HEIGHT] 806 | 807 | cbuffer = ClipBufferNode(0, WIDTH-1) 808 | 809 | subsectors = map_.bspnodes[-1].visit(map_) 810 | 811 | player = map_.player 812 | player.floor_h = subsectors[0].segs[0].sector_front.floor_h 813 | 814 | for subsector in subsectors: 815 | if cbuffer.occluded: 816 | break 817 | 818 | for seg in subsector.segs: 819 | if cbuffer.occluded: 820 | break 821 | 822 | # backface/frustrum culling 823 | pa = seg.vertex_start 824 | pb = seg.vertex_end 825 | v0 = Vec2(pa.x - player.x, pa.y - player.y) 826 | v1 = Vec2(pb.x - player.x, pb.y - player.y) 827 | v2 = Vec2(player.direction.x, player.direction.y) 828 | za = v2.dot(v0) 829 | zb = v2.dot(v1) 830 | v3 = Vec2(-v2.y, v2.x) 831 | xa = v3.dot(v0) 832 | xb = v3.dot(v1) 833 | 834 | if not (za <= 0.1 and zb <= 0.1): 835 | frontSidedef = seg.sidedef_front 836 | textureX0 = seg.offset + frontSidedef.offset_x 837 | textureX1 = seg.offset + seg.length + frontSidedef.offset_x 838 | 839 | if za <= 0.1: 840 | p = (zb - 0.1) / (zb - za) 841 | xa = xb + p * (xa - xb) 842 | textureX0 = textureX1 + p * (textureX0 - textureX1) 843 | za = 0.1 844 | 845 | elif zb <= 0.1: 846 | p = (za - 0.1) / (za - zb) 847 | xb = xa + p * (xb - xa) 848 | textureX1 = textureX0 + p * (textureX1 - textureX0) 849 | zb = 0.1 850 | 851 | scrXA = int(WIDTH_2 * xa / -za) + WIDTH_2 852 | scrXB = int(WIDTH_2 * xb / -zb) + WIDTH_2 853 | 854 | if scrXA < scrXB: 855 | draw_seg(seg, map_, drawsurf, scrXA, scrXB, cbuffer, za, 856 | zb, textureX0, textureX1, frontSidedef, 857 | lowerOcclusion, upperOcclusion, frame_count) 858 | 859 | return drawsurf 860 | 861 | 862 | #if __name__ == '__main__': 863 | # map_ = Map('DOOM1.WAD', 'E1M1') 864 | # map_.player.update() 865 | # render(map_, 0) 866 | --------------------------------------------------------------------------------