├── .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 | [](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 |
--------------------------------------------------------------------------------