944 lines
40 KiB
HTML
944 lines
40 KiB
HTML
<html>
|
|
<head><script src="//archive.org/includes/analytics.js?v=cf34f82" type="text/javascript"></script>
|
|
<script type="text/javascript">window.addEventListener('DOMContentLoaded',function(){var v=archive_analytics.values;v.service='wb';v.server_name='wwwb-app217.us.archive.org';v.server_ms=413;archive_analytics.send_pageview({});});</script>
|
|
<script type="text/javascript" src="https://web-static.archive.org/_static/js/bundle-playback.js?v=t1Bf4PY_" charset="utf-8"></script>
|
|
<script type="text/javascript" src="https://web-static.archive.org/_static/js/wombat.js?v=txqj7nKC" charset="utf-8"></script>
|
|
<script>window.RufflePlayer=window.RufflePlayer||{};window.RufflePlayer.config={"autoplay":"on","unmuteOverlay":"hidden"};</script>
|
|
<script type="text/javascript" src="https://web-static.archive.org/_static/js/ruffle/ruffle.js"></script>
|
|
<script type="text/javascript">
|
|
__wm.init("https://web.archive.org/web");
|
|
__wm.wombat("http://graphics.cs.brown.edu:80/games/quake/quake3.html","20160507164611","https://web.archive.org/","web","https://web-static.archive.org/_static/",
|
|
"1462639571");
|
|
</script>
|
|
<link rel="stylesheet" type="text/css" href="https://web-static.archive.org/_static/css/banner-styles.css?v=S1zqJCYt" />
|
|
<link rel="stylesheet" type="text/css" href="https://web-static.archive.org/_static/css/iconochive.css?v=qtvMKcIJ" />
|
|
<!-- End Wayback Rewrite JS Include -->
|
|
|
|
<title>
|
|
Rendering Quake 3 Maps
|
|
</title>
|
|
</head>
|
|
|
|
<body bgcolor="#FFFFFF"><!-- BEGIN WAYBACK TOOLBAR INSERT -->
|
|
<script>__wm.rw(0);</script>
|
|
<div id="wm-ipp-base" lang="en" style="display:none;direction:ltr;">
|
|
<div id="wm-ipp" style="position:fixed;left:0;top:0;right:0;">
|
|
<div id="donato" style="position:relative;width:100%;">
|
|
<div id="donato-base">
|
|
<iframe id="donato-if" src="https://archive.org/includes/donate.php?as_page=1&platform=wb&referer=https%3A//web.archive.org/web/20160507164611/http%3A//graphics.cs.brown.edu/games/quake/quake3.html"
|
|
scrolling="no" frameborder="0" style="width:100%; height:100%">
|
|
</iframe>
|
|
</div>
|
|
</div><div id="wm-ipp-inside">
|
|
<div id="wm-toolbar" style="position:relative;display:flex;flex-flow:row nowrap;justify-content:space-between;">
|
|
<div id="wm-logo" style="/*width:110px;*/padding-top:12px;">
|
|
<a href="/web/" title="Wayback Machine home page"><img src="https://web-static.archive.org/_static/images/toolbar/wayback-toolbar-logo-200.png" srcset="https://web-static.archive.org/_static/images/toolbar/wayback-toolbar-logo-100.png, https://web-static.archive.org/_static/images/toolbar/wayback-toolbar-logo-150.png 1.5x, https://web-static.archive.org/_static/images/toolbar/wayback-toolbar-logo-200.png 2x" alt="Wayback Machine" style="width:100px" border="0" /></a>
|
|
</div>
|
|
<div class="c" style="display:flex;flex-flow:column nowrap;justify-content:space-between;flex:1;">
|
|
<form class="u" style="display:flex;flex-direction:row;flex-wrap:nowrap;" target="_top" method="get" action="/web/submit" name="wmtb" id="wmtb"><input type="text" name="url" id="wmtbURL" value="http://graphics.cs.brown.edu/games/quake/quake3.html" onfocus="this.focus();this.select();" style="flex:1;"/><input type="hidden" name="type" value="replay" /><input type="hidden" name="date" value="20160507164611" /><input type="submit" value="Go" />
|
|
</form>
|
|
<div style="display:flex;flex-flow:row nowrap;align-items:flex-end;">
|
|
<div class="s" id="wm-nav-captures" style="flex:1;">
|
|
</div>
|
|
<div class="k">
|
|
<a href="" id="wm-graph-anchor">
|
|
<div id="wm-ipp-sparkline" title="Explore captures for this URL" style="position: relative">
|
|
<canvas id="wm-sparkline-canvas" width="725" height="27" border="0"></canvas>
|
|
</div>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="n">
|
|
<table>
|
|
<tbody>
|
|
<!-- NEXT/PREV MONTH NAV AND MONTH INDICATOR -->
|
|
<tr class="m">
|
|
<td class="b" nowrap="nowrap"><a href="https://web.archive.org/web/20150624071647/http://graphics.cs.brown.edu:80/games/quake/quake3.html" title="24 Jun 2015"><strong>Jun</strong></a></td>
|
|
<td class="c" id="displayMonthEl" title="You are here: 16:46:11 May 07, 2016">MAY</td>
|
|
<td class="f" nowrap="nowrap"><a href="https://web.archive.org/web/20160607203752/http://graphics.cs.brown.edu:80/games/quake/quake3.html" title="07 Jun 2016"><strong>Jun</strong></a></td>
|
|
</tr>
|
|
<!-- NEXT/PREV CAPTURE NAV AND DAY OF MONTH INDICATOR -->
|
|
<tr class="d">
|
|
<td class="b" nowrap="nowrap"><a href="https://web.archive.org/web/20150624071647/http://graphics.cs.brown.edu:80/games/quake/quake3.html" title="07:16:47 Jun 24, 2015"><img src="https://web-static.archive.org/_static/images/toolbar/wm_tb_prv_on.png" alt="Previous capture" width="14" height="16" border="0" /></a></td>
|
|
<td class="c" id="displayDayEl" style="width:34px;font-size:22px;white-space:nowrap;" title="You are here: 16:46:11 May 07, 2016">07</td>
|
|
<td class="f" nowrap="nowrap"><a href="https://web.archive.org/web/20160607203752/http://graphics.cs.brown.edu:80/games/quake/quake3.html" title="20:37:52 Jun 07, 2016"><img src="https://web-static.archive.org/_static/images/toolbar/wm_tb_nxt_on.png" alt="Next capture" width="14" height="16" border="0" /></a></td>
|
|
</tr>
|
|
<!-- NEXT/PREV YEAR NAV AND YEAR INDICATOR -->
|
|
<tr class="y">
|
|
<td class="b" nowrap="nowrap"><a href="https://web.archive.org/web/20150422041533/http://graphics.cs.brown.edu:80/games/quake/quake3.html" title="22 Apr 2015"><strong>2015</strong></a></td>
|
|
<td class="c" id="displayYearEl" title="You are here: 16:46:11 May 07, 2016">2016</td>
|
|
<td class="f" nowrap="nowrap"><a href="https://web.archive.org/web/20170519174315/http://graphics.cs.brown.edu:80/games/quake/quake3.html" title="19 May 2017"><strong>2017</strong></a></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="r" style="display:flex;flex-flow:column nowrap;align-items:flex-end;justify-content:space-between;">
|
|
<div id="wm-btns" style="text-align:right;height:23px;">
|
|
<span class="xxs">
|
|
<div id="wm-save-snapshot-success">success</div>
|
|
<div id="wm-save-snapshot-fail">fail</div>
|
|
<a id="wm-save-snapshot-open" href="#" title="Share via My Web Archive" >
|
|
<span class="iconochive-web"></span>
|
|
</a>
|
|
<a href="https://archive.org/account/login.php" title="Sign In" id="wm-sign-in">
|
|
<span class="iconochive-person"></span>
|
|
</a>
|
|
<span id="wm-save-snapshot-in-progress" class="iconochive-web"></span>
|
|
</span>
|
|
<a class="xxs" href="http://faq.web.archive.org/" title="Get some help using the Wayback Machine" style="top:-6px;"><span class="iconochive-question" style="color:rgb(87,186,244);font-size:160%;"></span></a>
|
|
<a id="wm-tb-close" href="#close" style="top:-2px;" title="Close the toolbar"><span class="iconochive-remove-circle" style="color:#888888;font-size:240%;"></span></a>
|
|
</div>
|
|
<div id="wm-share" class="xxs">
|
|
<a href="/web/20160507164611/http://web.archive.org/screenshot/http://graphics.cs.brown.edu/games/quake/quake3.html"
|
|
id="wm-screenshot"
|
|
title="screenshot">
|
|
<span class="wm-icon-screen-shot"></span>
|
|
</a>
|
|
<a href="#" id="wm-video" title="video">
|
|
<span class="iconochive-movies"></span>
|
|
</a>
|
|
<a id="wm-share-facebook" href="#" data-url="https://web.archive.org/web/20160507164611/http://graphics.cs.brown.edu:80/games/quake/quake3.html" title="Share on Facebook" style="margin-right:5px;" target="_blank"><span class="iconochive-facebook" style="color:#3b5998;font-size:160%;"></span></a>
|
|
<a id="wm-share-twitter" href="#" data-url="https://web.archive.org/web/20160507164611/http://graphics.cs.brown.edu:80/games/quake/quake3.html" title="Share on Twitter" style="margin-right:5px;" target="_blank"><span class="iconochive-twitter" style="color:#1dcaff;font-size:160%;"></span></a>
|
|
</div>
|
|
<div style="padding-right:2px;text-align:right;white-space:nowrap;">
|
|
<a id="wm-expand" class="wm-btn wm-closed" href="#expand" onclick="__wm.ex(event);return false;"><span id="wm-expand-icon" class="iconochive-down-solid"></span> <span class="xxs" style="font-size:80%;">About this capture</span></a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="wm-capinfo" style="border-top:1px solid #777;display:none; overflow: hidden">
|
|
<div id="wm-capinfo-notice" source="api"></div>
|
|
<div id="wm-capinfo-collected-by">
|
|
<div style="background-color:#666;color:#fff;font-weight:bold;text-align:center">COLLECTED BY</div>
|
|
<div style="padding:3px;position:relative" id="wm-collected-by-content">
|
|
<div style="display:inline-block;vertical-align:top;width:50%;">
|
|
<span class="c-logo" style="background-image:url(https://archive.org/services/img/alexacrawls);"></span>
|
|
Organization: <a style="color:#33f;" href="https://archive.org/details/alexacrawls" target="_new"><span class="wm-title">Alexa Crawls</span></a>
|
|
<div style="max-height:75px;overflow:hidden;position:relative;">
|
|
<div style="position:absolute;top:0;left:0;width:100%;height:75px;background:linear-gradient(to bottom,rgba(255,255,255,0) 0%,rgba(255,255,255,0) 90%,rgba(255,255,255,255) 100%);"></div>
|
|
Starting in 1996, <a href="http://www.alexa.com/">Alexa Internet</a> has been donating their crawl data to the Internet Archive. Flowing in every day, these data are added to the <a href="http://web.archive.org/">Wayback Machine</a> after an embargo period.
|
|
</div>
|
|
</div>
|
|
<div style="display:inline-block;vertical-align:top;width:49%;">
|
|
<span class="c-logo" style="background-image:url(https://archive.org/services/img/alexacrawls)"></span>
|
|
<div>Collection: <a style="color:#33f;" href="https://archive.org/details/alexacrawls" target="_new"><span class="wm-title">Alexa Crawls</span></a></div>
|
|
<div style="max-height:75px;overflow:hidden;position:relative;">
|
|
<div style="position:absolute;top:0;left:0;width:100%;height:75px;background:linear-gradient(to bottom,rgba(255,255,255,0) 0%,rgba(255,255,255,0) 90%,rgba(255,255,255,255) 100%);"></div>
|
|
Starting in 1996, <a href="http://www.alexa.com/">Alexa Internet</a> has been donating their crawl data to the Internet Archive. Flowing in every day, these data are added to the <a href="http://web.archive.org/">Wayback Machine</a> after an embargo period.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="wm-capinfo-timestamps">
|
|
<div style="background-color:#666;color:#fff;font-weight:bold;text-align:center" title="Timestamps for the elements of this page">TIMESTAMPS</div>
|
|
<div>
|
|
<div id="wm-capresources" style="margin:0 5px 5px 5px;max-height:250px;overflow-y:scroll !important"></div>
|
|
<div id="wm-capresources-loading" style="text-align:left;margin:0 20px 5px 5px;display:none"><img src="https://web-static.archive.org/_static/images/loading.gif" alt="loading" /></div>
|
|
</div>
|
|
</div>
|
|
</div></div></div></div><div id="wm-ipp-print">The Wayback Machine - https://web.archive.org/web/20160507164611/http://graphics.cs.brown.edu:80/games/quake/quake3.html</div>
|
|
<script type="text/javascript">//<![CDATA[
|
|
__wm.bt(725,27,25,2,"web","http://graphics.cs.brown.edu/games/quake/quake3.html","20160507164611",1996,"https://web-static.archive.org/_static/",["https://web-static.archive.org/_static/css/banner-styles.css?v=S1zqJCYt","https://web-static.archive.org/_static/css/iconochive.css?v=qtvMKcIJ"], false);
|
|
__wm.rw(1);
|
|
//]]></script>
|
|
<!-- END WAYBACK TOOLBAR INSERT -->
|
|
|
|
|
|
<a name="#top">
|
|
<b><font size="6">
|
|
Rendering Quake 3 Maps
|
|
</font></b>
|
|
|
|
<br>
|
|
<br>
|
|
Morgan McGuire
|
|
<br>July 11, 2003
|
|
<br>
|
|
<br>
|
|
|
|
<!---------------------------------------------------------------------------->
|
|
<a name="Intro">
|
|
<p>
|
|
<table border="1" cellpadding="0" cellspacing="0" width="100%" bgcolor="#ffffd0">
|
|
<td width="100%">
|
|
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
|
<td>
|
|
<b><font size="4">Introduction</font></b>
|
|
</td>
|
|
<td align="right">
|
|
<a href="#top">[top]</a>
|
|
</td>
|
|
</table>
|
|
</table>
|
|
<p>
|
|
This document describes how to render the basic geometry of a Quake 3
|
|
map using OpenGL. It describes how to texture and lightmap this
|
|
geometry, but does not describe how to render shaders, effects,
|
|
characters, or movers (elevators and doors).
|
|
<p>
|
|
|
|
This is intended as a sequel to Kekoa Proudfoot's <a href="https://web.archive.org/web/20160507164611/http://graphics.stanford.edu/~kekoa/q3/">Unofficial Quake 3 Map
|
|
Specs</a> document, which describes how to parse BSP files. It
|
|
therefore uses the same notation.
|
|
|
|
<p>This is an unofficial document. Quake 3 is a registered trademark
|
|
of <a href="https://web.archive.org/web/20160507164611/http://www.idsoftware.com/">id Software</a>, which does
|
|
not sponsor, authorize, or endorse this document.
|
|
|
|
<p>
|
|
This document describes the Quake 3 BSP file format as the author
|
|
understands it. While every effort has been made to ensure that the
|
|
contents of this document are accurate, the author does not guarantee that
|
|
any portion of this document is actually correct. In addition, the author
|
|
cannot be held responsible the consequences of the any use or misuse of the
|
|
information contained in this document.
|
|
|
|
<!---------------------------------------------------------------------------->
|
|
<a name="Overview">
|
|
<p>
|
|
<table border="1" cellpadding="0" cellspacing="0" width="100%" bgcolor="#ffffd0">
|
|
<td width="100%">
|
|
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
|
<td>
|
|
<b><font size="4">Overview</font></b>
|
|
</td>
|
|
<td align="right">
|
|
<a href="#top">[top]</a>
|
|
</td>
|
|
</table>
|
|
</table>
|
|
|
|
<p>
|
|
The rendering process has five steps:
|
|
|
|
<blockquote>
|
|
<br>1. Determine the set of visible faces.
|
|
<br>2. Partition the visible faces into <i>transparent</i> and <i>opaque</i> lists.
|
|
<br>3. Clear the frame buffer and render the sky box
|
|
<br>4. Render the opaque list in front-to-back order.
|
|
<br>5. Render the transparent list in back-to-front order.
|
|
</blockquote>
|
|
|
|
The first two steps do not involve any OpenGL calls. Step 3 renders a
|
|
cube centered at the viewer with a pre-warped texture to create the
|
|
illusion of a detailed 3D environment. The practice of creating and
|
|
rendering sky boxes is discussed elsewhere and is not detailed further
|
|
in this document. Steps 4 and 5 render the actual visible surfaces of
|
|
the map. The opaque list contains triangles that will be rendered
|
|
without alpha blending. It is sorted from front to back to take
|
|
advantage of early-out depth tests on newer graphics hardware. The
|
|
transparent list contains alpha blended surfaces which must be
|
|
rendered from back to front to generate a correct image. A
|
|
straightforward quicksort on the camera-space depth of first vertex of
|
|
each surface is sufficient for these purposes. For the kinds of maps
|
|
involved, splitting overlapping polygons for truly correct render
|
|
order or using a radix sort for faster sorting will only increase the
|
|
complexity of a renderer with improving the resulting image quality or
|
|
frame rate.
|
|
|
|
<p>
|
|
The <a href="#VisibleSurface">Visible Surface Determination</a>
|
|
section explains how to use the BSP tree and
|
|
precomputed visibility data to find visible faces.
|
|
|
|
<p>
|
|
The remaining steps are straightforward and not discussed in detail,
|
|
except for the actual face rendering. The vertex indexing scheme of
|
|
Quake 3 files can be confusing because there are two levels of
|
|
indirection. The <a href="#RenderingFaces">Rendering Faces</a>
|
|
section explains how to generate triangle sets from
|
|
the indices stored in a face.
|
|
|
|
<!---------------------------------------------------------------------------->
|
|
<a name="Data">
|
|
<p>
|
|
<table border="1" cellpadding="0" cellspacing="0" width="100%" bgcolor="#ffffd0">
|
|
<td width="100%">
|
|
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
|
<td>
|
|
<b><font size="4">Data Structures</font></b>
|
|
</td>
|
|
<td align="right">
|
|
<a href="#top">[top]</a>
|
|
</td>
|
|
</table>
|
|
</table>
|
|
|
|
<p>
|
|
A Quake 3 map contains multiple models. The first of these is always
|
|
a static mesh that is the map itself and the remainders are "movers"
|
|
like doors and elevators. This document is restricted to model[0]; it
|
|
does not address the movers.
|
|
|
|
<p>
|
|
Assume the in-memory data structures mimic those in the file
|
|
structure, and that the overarching class is named Q3Map. There are a
|
|
few cases where I used a Vector3 in this document instead of float[3]
|
|
to make the code easier to read. Not all data from the file is used
|
|
for rendering. For example, the brushs are used for collision
|
|
detection but ignored during rendering. The subset of data used
|
|
during rendering is (links are to Proudfoot's structure definitions):
|
|
|
|
<p>
|
|
<table border="0" cellspacing="0" cellpadding="0" width="100%">
|
|
<th align="left">Lump Index
|
|
<th align="left">Lump Name
|
|
<th align="left">Description
|
|
<tr>
|
|
|
|
<tr><td valign="top">1<td valign="top"><a href="https://web.archive.org/web/20160507164611/http://graphics.stanford.edu/~kekoa/q3/#Textures">Textures</a>
|
|
<td>Surface descriptions (assume these have been converted to OpenGL textures).
|
|
|
|
<tr><td valign="top">2<td valign="top"><a href="https://web.archive.org/web/20160507164611/http://graphics.stanford.edu/~kekoa/q3/#Planes">Planes</a>
|
|
<td>Planes used by map geometry.
|
|
|
|
<tr><td valign="top">3<td valign="top"><a href="https://web.archive.org/web/20160507164611/http://graphics.stanford.edu/~kekoa/q3/#Nodes">Nodes</a>
|
|
<td>BSP tree nodes.
|
|
|
|
<tr><td valign="top">4<td valign="top"><a href="https://web.archive.org/web/20160507164611/http://graphics.stanford.edu/~kekoa/q3/#Leaves">Leaves</a>
|
|
<td>BSP tree leaves.
|
|
|
|
<tr><td valign="top">5<td valign="top"><a href="https://web.archive.org/web/20160507164611/http://graphics.stanford.edu/~kekoa/q3/#Leaffaces">Leaffaces</a>
|
|
<td>Lists of face indices, one list per leaf.
|
|
|
|
<tr><td valign="top">7<td valign="top"><a href="https://web.archive.org/web/20160507164611/http://graphics.stanford.edu/~kekoa/q3/#Models">Models</a>
|
|
<td>Descriptions of rigid world geometry in map (we only use model[0]).
|
|
|
|
<tr><td valign="top">10<td valign="top"><a href="https://web.archive.org/web/20160507164611/http://graphics.stanford.edu/~kekoa/q3/#Vertexes">Vertexes</a>
|
|
<td>Vertices used to describe faces.
|
|
|
|
<tr><td valign="top">11<td valign="top"><a href="https://web.archive.org/web/20160507164611/http://graphics.stanford.edu/~kekoa/q3/#Meshverts">Meshverts</a>
|
|
<td>Lists of offsets, one list per mesh.
|
|
|
|
<tr><td valign="top">13<td valign="top"><a href="https://web.archive.org/web/20160507164611/http://graphics.stanford.edu/~kekoa/q3/#Faces">Faces</a>
|
|
<td>Surface geometry.
|
|
|
|
<tr><td valign="top">14<td valign="top"><a href="https://web.archive.org/web/20160507164611/http://graphics.stanford.edu/~kekoa/q3/#Lightmaps">Lightmaps</a>
|
|
<td>Packed lightmap data (assume these have been converted to an OpenGL texture)
|
|
|
|
<tr><td valign="top">16<td valign="top"><a href="https://web.archive.org/web/20160507164611/http://graphics.stanford.edu/~kekoa/q3/#Visdata">Visdata</a>
|
|
<td>Cluster-cluster visibility data.
|
|
</table>
|
|
|
|
<p>
|
|
|
|
There are additional data used during rendering that do not appear in
|
|
the file. These are:
|
|
|
|
<p>
|
|
<a name="Camera">
|
|
<b><font size="4">
|
|
Camera camera
|
|
</font></b>
|
|
<p>
|
|
A camera description that contains viewer position and frustum
|
|
parameters. The Camera class must have accessors for these parameters
|
|
and a method, isVisible(float min[3], float max[3]). This method
|
|
returns true if the world space bounding box with the specified
|
|
corners has non-zero intersection with the camera's view frustum.
|
|
<p>
|
|
|
|
<a name="AlreadyVisible">
|
|
<b><font size="4">
|
|
Set<int> alreadyVisible
|
|
</font></b>
|
|
|
|
<p>Set of indices of faces that are already visible. This is used to
|
|
prevent the same face from being rendered multiple times. A general
|
|
set implementation is not necessary. Because the face indices are
|
|
consecutive integers, a bit-set can provide an efficient
|
|
implementation.
|
|
</td></tr>
|
|
<p>
|
|
|
|
<a name="VisibleFace">
|
|
<b><font size="4">
|
|
Array<int> visibleFace
|
|
</font></b>
|
|
<p>
|
|
Set of indices of faces that are visible; that is, the members of the
|
|
alreadyVisible set. For efficiency this is maintained as a separate
|
|
array instead of iterating through the set.
|
|
<p>
|
|
|
|
<a name="Patch">
|
|
<b><font size="4">
|
|
Array<Patch> patch
|
|
</font></b>
|
|
<p>
|
|
Patches are <a href="https://web.archive.org/web/20160507164611/http://graphics.stanford.edu/~kekoa/q3/#Faces">Faces</a> that
|
|
describe sets of biquadratic Bezier surfaces. Each Patch contains an
|
|
array of <a href="Bezier">Bezier</a> instances, which are described later
|
|
in this document.
|
|
<p>
|
|
These Beziers are tessellated into triangles during loading so they
|
|
can be rendered as triangle strips. Your implementation must create
|
|
this tessellation and add an index into the patch array for patch
|
|
faces.
|
|
<p>
|
|
|
|
<!---------------------------------------------------------------------------->
|
|
<a name="Coordinates">
|
|
<p>
|
|
<table border="1" cellpadding="0" cellspacing="0" width="100%" bgcolor="#ffffd0">
|
|
<td width="100%">
|
|
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
|
<td>
|
|
<b><font size="4">Coordinate System</font></b>
|
|
</td>
|
|
<td align="right">
|
|
<a href="#top">[top]</a>
|
|
</td>
|
|
</table>
|
|
</table>
|
|
|
|
<p>
|
|
Quake 3 uses a coordinate system where the x-axis points East, the
|
|
y-axis points South, and the z-axis points vertically downward. If
|
|
you prefer a coordinate system where the y-axis points vertically
|
|
upward and the z-axis points South, you can use the following function
|
|
to convert from Quake 3 coordinates to your coordinate system.
|
|
|
|
<p>
|
|
<table bgcolor="#E5EEEE" width="75%" align="CENTER"><tr><td>
|
|
<blockquote><pre>
|
|
|
|
void swizzle(Vector3& v) {
|
|
float temp = v.y;
|
|
v.y = v.z;
|
|
v.z = -temp;
|
|
}</pre></blockquote></td></tr></table>
|
|
|
|
<p>
|
|
|
|
When swizzling data, you must convert the vertex positions, vertex
|
|
normals, plane normals, and all bounding box min and max vectors. The
|
|
Quake coordinate system is also scaled so that one meter is about 0.03
|
|
units. You may wish to change this scale factor. If you scale vertex
|
|
positions remember to also scale plane distances, and min and max
|
|
vectors appropriately.
|
|
<p>
|
|
|
|
Depending on the conventions of your rendering system, you may also
|
|
want to invert Quake's lightmap texture coordinates to (1 - s, 1 - t)
|
|
or (s, 1 - t). It is usually easy to tell when light map texture
|
|
coordinates need to be inverted by looking at a rendering.
|
|
|
|
<p>
|
|
|
|
<!---------------------------------------------------------------------------->
|
|
<a name="VisibleSurface">
|
|
<p>
|
|
<table border="1" cellpadding="0" cellspacing="0" width="100%" bgcolor="#ffffd0">
|
|
<td width="100%">
|
|
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
|
<td>
|
|
<b><font size="4">Visible Face Determination</font></b>
|
|
</td>
|
|
<td align="right">
|
|
<a href="#top">[top]</a>
|
|
</td>
|
|
</table>
|
|
</table>
|
|
|
|
<p>
|
|
The input to the visible face determination step is the camera (and
|
|
the map). The output is the visibleFace array, which contains the
|
|
indices of all faces that are potentially visible to that camera.
|
|
During the step, the alreadyVisible set is used to prevent a face
|
|
index from being added to the visibleFace array more than once.
|
|
|
|
<p>
|
|
Two notes before we look at this process in more detail. First, the
|
|
output is an array of <i>potentially</i> visible faces. A z-buffer
|
|
test and frustum clipping (both typically provided by hardware) are
|
|
still needed to generate the exactly visible set. Second,
|
|
as Max McGuire says in his <a href="https://web.archive.org/web/20160507164611/http://www.flipcode.com/tutorials/tut_q2levels.shtml">Quake 2 BSP File Format</a>,
|
|
|
|
<blockquote><blockquote>Many people incorrectly associate the BSP tree with the visibility
|
|
algorithm used by Quake and similar engines. As described above, the
|
|
visible surface determination is done using a precomputed PVS. The BSP
|
|
tree is primarily used to divide the map into regions and to quickly
|
|
determine which region the camera is in. As a result, it isn't that
|
|
fundamental to any of the rendering algorithms used in Quake and any
|
|
data structure giving a spatial subdivision (like an octree or a k-D
|
|
tree) could be used instead. BSP trees are very simple however, and
|
|
they are useful for some of the other non-rendering tasks in the Quake
|
|
engine.
|
|
</blockquote></blockquote>
|
|
|
|
<p>
|
|
|
|
|
|
To determine the set of visible faces:
|
|
|
|
<blockquote>
|
|
1. <a href="#VisFindCluster">Find <i>visCluster</i></a>, the index of the cluster containing the camera position.
|
|
<br>2. <a href="#SelectVisibleLeaves">Select all leaves visible from that cluster</a>.
|
|
<br>3. <a href="#FaceIterate">Iterate through all faces in those clusters</a>.
|
|
</blockquote>
|
|
|
|
<p>
|
|
<a name="VisFindCluster">
|
|
<p>
|
|
<b><font size="4">
|
|
Find the camera cluster (visCluster)
|
|
</font></b>
|
|
<p>
|
|
Recall that a Quake 3 map is divided into convex spaces called
|
|
<i>leaves</i>. Adjacent leaves are joined into <i>clusters</i>. The
|
|
map file contains precomputed visibility information at the cluster
|
|
level, which is stored in the <a href="https://web.archive.org/web/20160507164611/http://graphics.stanford.edu/~kekoa/q3/#Visdata">visData</a> bit
|
|
array.
|
|
|
|
<p>
|
|
|
|
The index of the cluster containing the camera is
|
|
<code>leaf[index].cluster</code>, where <i>index</i> is the index of
|
|
the leaf containing the camera. To find the index of the leaf
|
|
containing the camera, walk the BSP tree.
|
|
<p>
|
|
The root node is index 0 in the nodeArray. Each node has a splitting
|
|
plane associated with it. This plane divides space into two child
|
|
subnodes. If the camera lies in front of the splitting plane, recurse
|
|
into the front node. Otherwise recurse into the back node. We repeat
|
|
this process until a BSP leaf is reached.
|
|
<p>
|
|
In the Node data structure, a leaf is denoted by a negative child node
|
|
index. To convert the negative index into a legal leaf index, negate
|
|
and subtract one.
|
|
<p>
|
|
|
|
The following function takes a camera position as input. It walks the
|
|
BSP tree until a leaf is found and then returns the index of that
|
|
leaf. Remember that this is return value is a leaf index, not a
|
|
cluster index. The cluster index is stored in the leaf.
|
|
|
|
<p>
|
|
<table bgcolor="#E5EEEE" width="75%" align="CENTER"><tr><td>
|
|
<blockquote><pre>
|
|
|
|
int Q3Map::findLeaf(const Vector3& camPos) const {
|
|
|
|
int index = 0;
|
|
|
|
while (index >= 0) {
|
|
const Node& node = nodeArray[index];
|
|
const Plane& plane = planeArray[node.plane];
|
|
|
|
// Distance from point to a plane
|
|
const double distance =
|
|
plane.normal.dot(camPos) - plane.distance;
|
|
|
|
if (distance >= 0) {
|
|
index = node.front;
|
|
} else {
|
|
index = node.back;
|
|
}
|
|
}
|
|
|
|
return -index - 1;
|
|
}
|
|
</pre></blockquote></td></tr></table>
|
|
|
|
<p>
|
|
<a name="SelectVisibleLeaves">
|
|
<p>
|
|
<b><font size="4">
|
|
Select visible leaves
|
|
</font></b>
|
|
<p>
|
|
|
|
To find all visible leaves, iterate through the entire leaf array and
|
|
cull all leaves that are not in visible clusters or are outside the
|
|
view frustum. The visible cluster test should be performed first
|
|
because it is very efficient and more likely to fail.
|
|
|
|
<p>
|
|
|
|
The visData bit array contains precomputed visibility data between
|
|
clusters. If the cluster with index <i>a</i> can potentially be seen
|
|
by a viewer in the cluster with index <i>b</i>, then bit (a + b *
|
|
visData.sz_vecs * 8) of visData.vecs has value 1. Otherwise, that bit
|
|
has value 0.
|
|
|
|
<p>
|
|
|
|
The following function uses bitwise operators to efficiently extract
|
|
the relevant bit. The inputs are the index of the cluster containing
|
|
the camera and the index of the cluster to be tested. The function
|
|
returns true if the test cluster is potentially visible, otherwise it
|
|
returns false. A call to the function typically looks like
|
|
<code>isClusterVisible(visCluster, leaf[L].cluster)</code>.
|
|
|
|
<p>
|
|
<table bgcolor="#E5EEEE" width="75%" align="CENTER"><tr><td>
|
|
<blockquote><pre>
|
|
|
|
bool Q3Map::isClusterVisible(int visCluster, int testCluster) const {
|
|
|
|
if ((visData.bitsets == NULL) || (visCluster < 0)) {
|
|
return true;
|
|
}
|
|
|
|
int i = (visCluster * visData.bytesPerCluster) + (testCluster >> 3);
|
|
uint8 visSet = visData.bitsets[i];
|
|
|
|
return (visSet & (1 << (testCluster & 7))) != 0;
|
|
}
|
|
</pre></blockquote></td></tr></table>
|
|
<p>
|
|
|
|
In the function, the expression (testCluster >> 3) computes
|
|
(testCluster / 8), i.e. the byte within visData that contains
|
|
information about the given cluster. The expression (1 <<
|
|
(testCluster & 7)) creates a bit mask that selects bit (testCluster
|
|
mod 8) within that byte.
|
|
<p>
|
|
|
|
The visData information only considers the position of the viewer and
|
|
not the orientation. Orientation is handled by the frustum culling
|
|
step. The leaf contains two corners of its bounding box, min and max.
|
|
If <code>camera.isVisible(leaf[L].min, leaf[L].max)</code> returns
|
|
false, the leaf should be dropped from consideration because it is
|
|
outside the view frustum. Note that some of the faces in the leaf are
|
|
also in adjacent leaves and may therefore still be visible-- the other
|
|
leaves will take care of that when we iterate through them.
|
|
|
|
<p>
|
|
|
|
<p>
|
|
<a name="FaceIterate">
|
|
<p>
|
|
<b><font size="4">
|
|
Iterate through faces
|
|
</font></b>
|
|
<p>
|
|
A leaf contains all faces that have non-zero intersection with the
|
|
leaf volume. The faces in leaf with index L have indices
|
|
<code>leaf[L].firstFace</code> through <code>(leaf[L].firstFace +
|
|
leaf[L].facesCount - 1)</code>.
|
|
|
|
<p>
|
|
|
|
Because a face may protrude out of the leaf, the same face may be in
|
|
multiple leaves. Use the <a href="#AlreadyVisible">alreadyVisible</a>
|
|
set to avoid touching the same face twice. A simple code snippet for
|
|
this is:
|
|
|
|
<p>
|
|
<table bgcolor="#E5EEEE" width="75%" align="CENTER"><tr><td>
|
|
<blockquote><pre>
|
|
|
|
for (int i = 0; i < leaf[L].facesCount; ++i) {
|
|
const int f = i + leaf[L].firstFace;
|
|
if (! alreadyVisible.contains(f)) {
|
|
alreadyVisible.insert(f);
|
|
visibleFaces.append(f);
|
|
}
|
|
}
|
|
</pre></blockquote></td></tr></table>
|
|
<p>
|
|
|
|
<p>
|
|
|
|
<!---------------------------------------------------------------------------->
|
|
<a name="#RenderingFaces">
|
|
<p>
|
|
<table border="1" cellpadding="0" cellspacing="0" width="100%" bgcolor="#ffffd0">
|
|
<td width="100%">
|
|
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
|
<td>
|
|
<b><font size="4">Rendering Faces</font></b>
|
|
</td>
|
|
<td align="right">
|
|
<a href="#top">[top]</a>
|
|
</td>
|
|
</table>
|
|
</table>
|
|
|
|
<p>
|
|
|
|
There are four kinds of <a href="https://web.archive.org/web/20160507164611/http://graphics.stanford.edu/~kekoa/q3/#Faces">Faces</a> in a
|
|
Quake 3 map: polygons, patches, meshes, and billboards. Polygons and
|
|
meshes are collections of triangles. A patch is a bezier-spline patch that
|
|
must be tessellated into triangles for rendering. Billboards are polygons
|
|
whose orientation changes to always face the viewer.
|
|
|
|
<p>
|
|
|
|
<a href="#RenderMesh">Polygons and meshes</a> are rendered in the same
|
|
manner. The position, texture coordinate, and lightmap coordinate are
|
|
stored in the vertex array. Using OpenGL vertex arrays, these can be
|
|
referenced with a single index by setting the active vertex pointer
|
|
and tex coord pointers into the same array, offset by the memory
|
|
location within a Vertex for each type of coordinate.
|
|
|
|
<p>
|
|
<a href="#RenderPatch">Patches</a> are tessellated into triangles,
|
|
either during loading or per-frame, and rendered as triangle strips.
|
|
The tessellation process creates vertices that did not exist in the
|
|
original mesh, so each patch contains its own vertex array instead
|
|
of using the global one stored in the map file.
|
|
|
|
<p>
|
|
|
|
Although handling the shaders and effects that can be stored in Quake
|
|
3 maps is more complicated, simple alpha blending can be supported to
|
|
render translucent surfaces correctly. When a texture contains an
|
|
alpha channel, enable blending and select the
|
|
<code>glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)</code>
|
|
blending mode. Alpha blended faces should not be backfaced culled;
|
|
they appear to have only one polygon for both sides (there is probably
|
|
a two-sided polygon flag somewhere that is the correct place to obtain
|
|
such information).
|
|
|
|
<p>
|
|
<a name="RenderMesh">
|
|
<p>
|
|
<b><font size="4">
|
|
Render a mesh
|
|
</font></b>
|
|
<p>
|
|
|
|
Each face mesh, <i>curFace</i> of type Face describes a mesh
|
|
containing (curFace.meshVertexesCount / 3) triangles. The indices
|
|
into the vertex array for the vertices of these triangles are
|
|
themselves indirected. The <a href="https://web.archive.org/web/20160507164611/http://graphics.stanford.edu/~kekoa/q3/#Meshverts>">meshVertex</a>
|
|
array stores the indices of the vertices, in the correct order to
|
|
create triangle lists. For a given face, these are
|
|
<code>meshVertex[curFace.firstMeshVertex]</code> through
|
|
<code>meshVertex[curFace.firstMeshVertex + curFace.mushVertexesCount -
|
|
1]</code>. The meshVertex values are also offet by
|
|
<code>curFace.firstVertex</code>.
|
|
<p>
|
|
|
|
This indexing scheme, although confusing, is arranged conveniently for
|
|
using glDrawElements to render the triangle lists. The following code
|
|
renders a mesh using this function.
|
|
|
|
<p>
|
|
|
|
<table bgcolor="#E5EEEE" width="75%" align="CENTER"><tr><td>
|
|
<blockquote><pre>
|
|
|
|
const Face& curFace = face[visibleFace[f]];
|
|
static const stride = sizeof(Vertex); // BSP Vertex, not float[3]
|
|
const int offset = face.firstVertex;
|
|
|
|
glVertexPointer(3, GL_FLOAT, stride, &(vertex[offset].position));
|
|
|
|
glClientActiveTextureARB(GL_TEXTURE0_ARB);
|
|
glTexCoordPointer(2, GL_FLOAT, stride, &(vertex[offset].textureCoord));
|
|
|
|
glClientActiveTextureARB(GL_TEXTURE1_ARB);
|
|
glTexCoordPointer(2, GL_FLOAT, stride, &(vertex[offset].lightmapCoord));
|
|
|
|
glDrawElements(GL_TRIANGLES, curFace.meshVertexesCount,
|
|
GL_UNSIGNED_INT, &meshVertex;[curFace.firstMeshVertex]);
|
|
</pre></blockquote></td></tr></table>
|
|
<p>
|
|
|
|
In the above code, the firstMeshVertex offset is applied directly to
|
|
the vertex pointer since there is no other provision for offsetting
|
|
the indices with glDrawElements.
|
|
|
|
<a name="RenderPatch">
|
|
<p>
|
|
<b><font size="4">
|
|
Render a patch
|
|
</font></b>
|
|
<p>
|
|
|
|
Patches are surfaces defined by an array of Bezier curves. These
|
|
curves are represented by the following data structure.
|
|
<p>
|
|
<a name="Bezier">
|
|
<table bgcolor="#E5EEEE" width="75%" align="CENTER"><tr><td>
|
|
<blockquote><pre>
|
|
class Bezier {
|
|
private:
|
|
int level;
|
|
Array<Vertex> vertex;
|
|
Array<uint32> indexes;
|
|
Array<int32> trianglesPerRow;
|
|
Array<uint32*> rowIndexes;
|
|
|
|
public:
|
|
Vertex controls[9];
|
|
|
|
void tessellate(int level);
|
|
void render();
|
|
};
|
|
</pre></blockquote></td></tr></table>
|
|
<p>
|
|
|
|
The controls array contains the 9 control points for this curve. The
|
|
Beziers form a grid within a patch, so adjacent Beziers will share
|
|
three of these. The Bezier curves must be tessellated prior to
|
|
rendering. The <i>level</i> of the tessellation is the number of
|
|
edges into which each side of a 2D curve is subdivided. The total
|
|
number of triangles in the tessellation is <code>(2 * pow(level,
|
|
2))</code>. The remainder of the private data is the tessellation,
|
|
stored in a form we can pass directly to glMultiDrawElements. The
|
|
pointers in the rowIndexes array point into the indexes array; they
|
|
are not referring to separately allocated memory.
|
|
|
|
<p>
|
|
The tessellate method computes the private data for rendering from the
|
|
control points (which must themselves be set up during loading of the
|
|
containing patch). Any number between five and 10 is a reasonable
|
|
subdivision level for most maps. The intent of subdivision is to
|
|
provide smoother curves on faster machines by increasing the level at
|
|
runtime. Another use for subdivision is to allocate more polygons to
|
|
larger curves-- implementors are free to provide their own metric for
|
|
choosing a good subdivision level.
|
|
<p>
|
|
|
|
The following is an implementation of the tessellate method with
|
|
structure based on the tessellator in the Paul Baker's <a href="https://web.archive.org/web/20160507164611/http://users.ox.ac.uk/~univ1234/opengl/octagon/octagon.htm">Octagon</a> project.</i>
|
|
|
|
<p>
|
|
<table bgcolor="#E5EEEE" width="75%" align="CENTER"><tr><td>
|
|
<blockquote><pre>
|
|
|
|
void Bezier::tessellate(int L) {
|
|
level = L;
|
|
|
|
// The number of vertices along a side is 1 + num edges
|
|
const int L1 = L + 1;
|
|
|
|
vertex.resize(L1 * L1);
|
|
|
|
// Compute the vertices
|
|
int i;
|
|
|
|
for (i = 0; i <= L; ++i) {
|
|
double a = (double)i / L;
|
|
double b = 1 - a;
|
|
|
|
vertex[i] =
|
|
controls[0] * (b * b) +
|
|
controls[3] * (2 * b * a) +
|
|
controls[6] * (a * a);
|
|
}
|
|
|
|
for (i = 1; i <= L; ++i) {
|
|
double a = (double)i / L;
|
|
double b = 1.0 - a;
|
|
|
|
BSPVertex temp[3];
|
|
|
|
int j;
|
|
for (j = 0; j < 3; ++j) {
|
|
int k = 3 * j;
|
|
temp[j] =
|
|
controls[k + 0] * (b * b) +
|
|
controls[k + 1] * (2 * b * a) +
|
|
controls[k + 2] * (a * a);
|
|
}
|
|
|
|
for(j = 0; j <= L; ++j) {
|
|
double a = (double)j / L;
|
|
double b = 1.0 - a;
|
|
|
|
vertex[i * L1 + j]=
|
|
temp[0] * (b * b) +
|
|
temp[1] * (2 * b * a) +
|
|
temp[2] * (a * a);
|
|
}
|
|
}
|
|
|
|
|
|
// Compute the indices
|
|
int row;
|
|
indexes.resize(L * (L + 1) * 2);
|
|
|
|
for (row = 0; row < L; ++row) {
|
|
for(int col = 0; col <= L; ++col) {
|
|
indexes[(row * (L + 1) + col) * 2 + 1] = row * L1 + col;
|
|
indexes[(row * (L + 1) + col) * 2] = (row + 1) * L1 + col;
|
|
}
|
|
}
|
|
|
|
trianglesPerRow.resize(L);
|
|
rowIndexes.resize(L);
|
|
for (row = 0; row < L; ++row) {
|
|
trianglesPerRow[row] = 2 * L1;
|
|
rowIndexes[row] = &indexes;[row * 2 * L1];
|
|
}
|
|
|
|
}</pre></blockquote></td></tr></table>
|
|
<p>
|
|
|
|
Once constructed, this data can be rendered directly with vertex arrays:
|
|
<p>
|
|
|
|
<table bgcolor="#E5EEEE" width="75%" align="CENTER"><tr><td>
|
|
<blockquote><pre>
|
|
|
|
void Bezier::render() {
|
|
glVertexPointer(3, GL_FLOAT,sizeof(BSPVertex), &vertex;[0].position);
|
|
|
|
glClientActiveTextureARB(GL_TEXTURE0_ARB);
|
|
glTexCoordPointer(2, GL_FLOAT,sizeof(BSPVertex), &vertex;[0].textureCoord);
|
|
|
|
glClientActiveTextureARB(GL_TEXTURE1_ARB);
|
|
glTexCoordPointer(2, GL_FLOAT, sizeof(BSPVertex), &vertex;[0].lightmapCoord);
|
|
|
|
glMultiDrawElementsEXT(GL_TRIANGLE_STRIP, trianglesPerRow.getCArray(),
|
|
GL_UNSIGNED_INT, (const void **)(rowIndexes.getCArray()), patch.level);
|
|
}</pre></blockquote></td></tr></table>
|
|
|
|
<p>
|
|
<hr>
|
|
<table border="0" width="100%" cellspacing="0" cellpadding="0">
|
|
<td align="left" valign="top">
|
|
<em><font size="3">Copyright © 2003 Morgan McGuire. All rights
|
|
reserved.</font></em>
|
|
<td align="right" valign="top">
|
|
<em><font size="3">morgan@cs.brown.edu</font></em>
|
|
</table>
|
|
|
|
<p>
|
|
<b>Acknowledgements</b>
|
|
<br>Kris Taeleman answered a question while I was working on this document, and I used the following resources:
|
|
<br>Max McGuire's <a href="https://web.archive.org/web/20160507164611/http://www.flipcode.com/tutorials/tut_q2levels.shtml">Quake 2 BSP File Format</a>,
|
|
<br>Kekoa Proudfoot's
|
|
<a href="https://web.archive.org/web/20160507164611/http://graphics.stanford.edu/~kekoa/q3/">Unofficial Quake 3 Map Specs</a>,
|
|
<br>Ben "Digiben" Humphrey's <a href="https://web.archive.org/web/20160507164611/http://www.misofruit.co.kr/seojewoo/programming/opengl/Quake3Format.htm">Unofficial Quake 3 BSP Format</a>,
|
|
<br>Nathan Ostgard's <a href="https://web.archive.org/web/20160507164611/http://www.nathanostgard.com/tutorials/quake3/collision/">Quake 3 BSP Collision Detection</a>,
|
|
<br>Leonardo Boselli's <a href="https://web.archive.org/web/20160507164611/http://sourceforge.net/projects/apocalyx/">Apocalyx</a> source code,
|
|
<br>Paul Baker's <a href="https://web.archive.org/web/20160507164611/http://users.ox.ac.uk/~univ1234/opengl/octagon/octagon.htm">Octagon</a> source code,
|
|
<br>The Aside Software <a href="https://web.archive.org/web/20160507164611/http://talika.eii.us.es/~titan/titan/rnews.html">Titan</a> source code,
|
|
<br>The <a href="https://web.archive.org/web/20160507164611/http://etud.epita.fr:8000/~bilalt_j/q3tools/q3radiant_man/q3rmanhtml.htm">Q3Radient Manual</a>
|
|
|
|
|
|
<p>
|
|
<font size="2">
|
|
Keywords: quake 3 quake3 q3 arena quake3arena q3arena map bsp file render opengl spec specs format vertex order
|
|
</font>
|
|
|
|
</body>
|
|
</html>
|
|
|
|
|
|
<!--
|
|
FILE ARCHIVED ON 16:46:11 May 07, 2016 AND RETRIEVED FROM THE
|
|
INTERNET ARCHIVE ON 21:12:17 Feb 18, 2024.
|
|
JAVASCRIPT APPENDED BY WAYBACK MACHINE, COPYRIGHT INTERNET ARCHIVE.
|
|
|
|
ALL OTHER CONTENT MAY ALSO BE PROTECTED BY COPYRIGHT (17 U.S.C.
|
|
SECTION 108(a)(3)).
|
|
-->
|
|
<!--
|
|
playback timings (ms):
|
|
exclusion.robots: 2.144 (7)
|
|
exclusion.robots.policy: 2.002 (7)
|
|
cdx.remote: 0.758 (7)
|
|
esindex: 0.111 (7)
|
|
LoadShardBlock: 1023.154 (24)
|
|
PetaboxLoader3.datanode: 713.073 (25)
|
|
PetaboxLoader3.resolve: 88.188 (2)
|
|
load_resource: 93.243
|
|
--> |