/* ** Command & Conquer Generals Zero Hour(tm) ** Copyright 2025 Electronic Arts Inc. ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . */ // FILE: W3DSnow.h ///////////////////////////////////////////////////////// #include "W3DDevice/GameClient/W3DSnow.h" #include "W3DDevice/GameClient/heightmap.h" #include "GameClient/View.h" #include "WW3D2/dx8wrapper.h" #include "WW3D2/rinfo.h" #include "WW3D2/camera.h" #include "WW3D2/assetmgr.h" #ifdef _INTERNAL // for occasional debugging... //#pragma optimize("", off) //#pragma MESSAGE("************************************** WARNING, optimization disabled for debugging purposes") #endif #define D3DFVF_POINTVERTEX (D3DFVF_XYZ) #define SNOW_BUFFER_SIZE 4096 //size of vertex buffer holding particles. #define SNOW_BATCH_SIZE 2048 //we render at most this many particles per drawprimitive call. This number * 6 must be less than 65536 to fit into index buffer. struct POINTVERTEX { Vector3 v; //center of particle. }; W3DSnowManager::W3DSnowManager(void) { m_indexBuffer=NULL; m_snowTexture=NULL; m_VertexBufferD3D=NULL; } W3DSnowManager::~W3DSnowManager() { ReleaseResources(); } void W3DSnowManager::init( void ) { SnowManager::init(); ReAcquireResources(); } /** Releases all W3D/D3D assets before a reset.. */ void W3DSnowManager::ReleaseResources(void) { REF_PTR_RELEASE(m_snowTexture); if (m_VertexBufferD3D) m_VertexBufferD3D->Release(); m_VertexBufferD3D=NULL; REF_PTR_RELEASE(m_indexBuffer); } /** (Re)allocates all W3D/D3D assets after a reset.. */ Bool W3DSnowManager::ReAcquireResources(void) { ReleaseResources(); if (!TheWeatherSetting->m_snowEnabled) return TRUE; //no need for resources if snow is disabled. if (TheWeatherSetting->m_usePointSprites && DX8Wrapper::Get_Current_Caps()->Support_PointSprites()) { LPDIRECT3DDEVICE8 m_pDev=DX8Wrapper::_Get_D3D_Device8(); DEBUG_ASSERTCRASH(m_pDev, ("Trying to ReAquireResources on W3DSnowManager without device")); if (m_VertexBufferD3D == NULL) { // Create vertex buffer if (FAILED(m_pDev->CreateVertexBuffer ( SNOW_BUFFER_SIZE*sizeof(POINTVERTEX), D3DUSAGE_WRITEONLY|D3DUSAGE_DYNAMIC|D3DUSAGE_POINTS, D3DFVF_POINTVERTEX, D3DPOOL_DEFAULT, &m_VertexBufferD3D ))) return FALSE; } } else { m_indexBuffer=NEW_REF(DX8IndexBufferClass,(SNOW_BATCH_SIZE *6)); //allocate 2 triangles per flake, each with 3 indices. // Fill up the IB with static vertex indices that will be used for all smudges. { DX8IndexBufferClass::WriteLockClass lockIdxBuffer(m_indexBuffer); UnsignedShort *ib=lockIdxBuffer.Get_Index_Array(); //quad of 4 triangles: // 0-----3 // |\ /| // | X | // |/ \| // 1-----2 Int vbCount=0; for (Int i=0; iGet_Texture(TheWeatherSetting->m_snowTexture.str()); m_dwBase = SNOW_BUFFER_SIZE; m_dwDiscard = SNOW_BUFFER_SIZE; m_dwFlush = SNOW_BATCH_SIZE; return TRUE; } void W3DSnowManager::updateIniSettings(void) { //Call base class SnowManager::updateIniSettings(); if (m_snowTexture && stricmp(m_snowTexture->Get_Texture_Name(),TheWeatherSetting->m_snowTexture.str()) != 0) { REF_PTR_RELEASE(m_snowTexture); m_snowTexture = WW3DAssetManager::Get_Instance()->Get_Texture(TheWeatherSetting->m_snowTexture.str()); } } void W3DSnowManager::reset( void ) { SnowManager::reset(); } void W3DSnowManager::update(void) { m_time += WW3D::Get_Frame_Time() / 1000.0f; //find current time offset, adjusting for overflow m_time=fmod(m_time,m_fullTimePeriod); } #define MAXIMUM_CAMERA_DISTANCE 100000 //maximum distance of camera position from world origin. #define ISPOW2(x) (x && (x & (x-1)) == 0) //is a number a power of 2? #define MODPOW2(x,y) ((x) & (y-1)) //mod '%' operator for powers of 2. // Helper function to stuff a FLOAT into a DWORD argument inline DWORD FtoDW( FLOAT f ) { return *((DWORD*)&f); } /*Recursively subdivide the large snow box enclosing the camera until we reach some predefined leaf size. This method is used so that very few off-screen particles end up getting rendered. Culling them individually would be too expensive since we're dealing with 1000's for this effect.*/ void W3DSnowManager::renderSubBox(RenderInfoClass &rinfo, Int originX, Int originY, Int cubeDimX, Int cubeDimY ) { //check if this box is too large and needs subdivision Int boxDimX=cubeDimX - originX; Int boxDimY=cubeDimY - originY; Int halfX=REAL_TO_INT_CEIL(boxDimX*0.5f); Int halfY=REAL_TO_INT_CEIL(boxDimY*0.5f); CameraClass &camera=rinfo.Camera; MinMaxAABoxClass mmbox; if (boxDimX > m_leafDim) { //subdivide the box if (boxDimY > m_leafDim) { //subdivide in both directions //Upper left mmbox.MinCorner.Set(originX*m_emitterSpacing-m_cullOverscan, (originY + halfY)*m_emitterSpacing-m_cullOverscan, m_snowCeiling-m_boxDimensions); mmbox.MaxCorner.Set((originX + halfX)*m_emitterSpacing+m_cullOverscan, cubeDimY*m_emitterSpacing+m_cullOverscan, m_snowCeiling); if (CollisionMath::Overlap_Test(camera.Get_Frustum(),mmbox) != CollisionMath::OUTSIDE) renderSubBox(rinfo, originX, originY + halfY, originX + halfX, cubeDimY); //Upper right mmbox.MinCorner.Set((originX + halfX)*m_emitterSpacing-m_cullOverscan, (originY + halfY)*m_emitterSpacing-m_cullOverscan, m_snowCeiling-m_boxDimensions); mmbox.MaxCorner.Set(cubeDimX*m_emitterSpacing+m_cullOverscan, cubeDimY*m_emitterSpacing+m_cullOverscan, m_snowCeiling); if (CollisionMath::Overlap_Test(camera.Get_Frustum(),mmbox) != CollisionMath::OUTSIDE) renderSubBox(rinfo, originX + halfX, originY + halfY,cubeDimX, cubeDimY); //Lower left mmbox.MinCorner.Set(originX*m_emitterSpacing-m_cullOverscan, originY*m_emitterSpacing-m_cullOverscan, m_snowCeiling-m_boxDimensions); mmbox.MaxCorner.Set((originX + halfX)*m_emitterSpacing+m_cullOverscan, (originY + halfY)*m_emitterSpacing+m_cullOverscan, m_snowCeiling); if (CollisionMath::Overlap_Test(camera.Get_Frustum(),mmbox) != CollisionMath::OUTSIDE) renderSubBox(rinfo, originX,originY,originX + halfX, originY + halfY); //Lower right mmbox.MinCorner.Set((originX + halfX)*m_emitterSpacing-m_cullOverscan, originY*m_emitterSpacing-m_cullOverscan, m_snowCeiling-m_boxDimensions); mmbox.MaxCorner.Set(cubeDimX*m_emitterSpacing+m_cullOverscan, (originY + halfY)*m_emitterSpacing+m_cullOverscan, m_snowCeiling); if (CollisionMath::Overlap_Test(camera.Get_Frustum(),mmbox) != CollisionMath::OUTSIDE) renderSubBox(rinfo, originX + halfX, originY, cubeDimX, originY + halfY); return; } else { //only subdivide in x direction. //Left mmbox.MinCorner.Set(originX*m_emitterSpacing-m_cullOverscan, originY*m_emitterSpacing-m_cullOverscan, m_snowCeiling-m_boxDimensions); mmbox.MaxCorner.Set((originX + halfX)*m_emitterSpacing+m_cullOverscan, cubeDimY*m_emitterSpacing+m_cullOverscan, m_snowCeiling); if (CollisionMath::Overlap_Test(camera.Get_Frustum(),mmbox) != CollisionMath::OUTSIDE) renderSubBox(rinfo, originX, originY, originX + halfX, cubeDimY); //Right mmbox.MinCorner.Set((originX + halfX)*m_emitterSpacing-m_cullOverscan, originY*m_emitterSpacing-m_cullOverscan, m_snowCeiling-m_boxDimensions); mmbox.MaxCorner.Set(cubeDimX*m_emitterSpacing+m_cullOverscan, cubeDimY*m_emitterSpacing+m_cullOverscan, m_snowCeiling); if (CollisionMath::Overlap_Test(camera.Get_Frustum(),mmbox) != CollisionMath::OUTSIDE) renderSubBox(rinfo, originX + halfX, originY, cubeDimX, cubeDimY); return; } } else if (boxDimY > m_leafDim) { //only subdivide in y direction //Top mmbox.MinCorner.Set(originX*m_emitterSpacing-m_cullOverscan, (originY+halfY)*m_emitterSpacing-m_cullOverscan, m_snowCeiling-m_boxDimensions); mmbox.MaxCorner.Set(cubeDimX*m_emitterSpacing+m_cullOverscan, cubeDimY*m_emitterSpacing+m_cullOverscan, m_snowCeiling); if (CollisionMath::Overlap_Test(camera.Get_Frustum(),mmbox) != CollisionMath::OUTSIDE) renderSubBox(rinfo, originX, originY+halfY,cubeDimX, cubeDimY); //Bottom mmbox.MinCorner.Set(originX*m_emitterSpacing-m_cullOverscan, originY*m_emitterSpacing-m_cullOverscan, m_snowCeiling-m_boxDimensions); mmbox.MaxCorner.Set(cubeDimX*m_emitterSpacing+m_cullOverscan, (originY + halfY)*m_emitterSpacing+m_cullOverscan, m_snowCeiling); if (CollisionMath::Overlap_Test(camera.Get_Frustum(),mmbox) != CollisionMath::OUTSIDE) renderSubBox(rinfo, originX, originY, cubeDimX, originY + halfY); return; } //Box too small to subdivide so render it. //Find total number of particles that need rendering. Int totalPart=(cubeDimY-originY)*(cubeDimX-originX); if (!totalPart) return; //nothing to render. Int y=originY; //loop counter. Int cubeOriginXRemainder = originX; //loop counter - adjusted when not all particles fit into render buffer. Vector3 snowCenter; m_totalRendered += totalPart; while (totalPart) { Int batchSize=totalPart; if (batchSize > m_dwFlush) batchSize = m_dwFlush; if((m_dwBase + batchSize) > m_dwDiscard) m_dwBase = 0; POINTVERTEX* verts; if(m_VertexBufferD3D->Lock(m_dwBase * sizeof(POINTVERTEX), batchSize * sizeof(POINTVERTEX), (unsigned char **) &verts, m_dwBase ? D3DLOCK_NOOVERWRITE : D3DLOCK_DISCARD) != D3D_OK ) return; //couldn't lock buffer. Int numberInBatch=0; for (;y= batchSize) { cubeOriginXRemainder = x; goto flush_particles; } //Get initial height from noise table. We add a large value to make sure it's positive. Then //modulate by table dimensions to find a value. Int noiseOffset=MODPOW2(x+MAXIMUM_CAMERA_DISTANCE,SNOW_NOISE_X)+MODPOW2(y+MAXIMUM_CAMERA_DISTANCE,SNOW_NOISE_Y)*SNOW_NOISE_X; if (noiseOffset > (SNOW_NOISE_X * SNOW_NOISE_Y)) noiseOffset = 0; //this should never happen but check to prevent buffer over/under flow. //find current height Real h0=m_snowCeiling-fmod(m_heightTraveled+m_startingHeights[noiseOffset],m_boxDimensions); //find world-space position of snow flake snowCenter.Set(x*m_emitterSpacing,y*m_emitterSpacing,h0); //Adjust position so snow flakes don't fall straight down. snowCenter.X += m_amplitude * WWMath::Fast_Sin( h0 * m_frequencyScaleX + (Real)x); snowCenter.Y += m_amplitude * WWMath::Fast_Sin( h0 * m_frequencyScaleY + (Real)y); *(Vector3 *)verts=snowCenter; verts++; numberInBatch++; } //getting here means we did not overflow the render buffer, so reset x origin to normal. cubeOriginXRemainder = originX; //reset to normal amount } flush_particles: m_VertexBufferD3D->Unlock(); //Render any particles that may be queued up. if (numberInBatch) { Debug_Statistics::Record_DX8_Polys_And_Vertices(numberInBatch*2,numberInBatch*4,ShaderClass::_PresetOpaqueShader); DX8Wrapper::_Get_D3D_Device8()->DrawPrimitive( D3DPT_POINTLIST, m_dwBase, numberInBatch); totalPart -= numberInBatch; m_dwBase += numberInBatch; } } } void W3DSnowManager::render(RenderInfoClass &rinfo) { if (!TheWeatherSetting->m_snowEnabled || !m_isVisible) return; Int usePointSprites = DX8Wrapper::Get_Current_Caps()->Support_PointSprites() && TheWeatherSetting->m_usePointSprites; //make sure the noise table is powers of 2 in dimensions. WWASSERT(ISPOW2(SNOW_NOISE_X) && ISPOW2(SNOW_NOISE_Y)); //CameraClass &camera=rinfo.Camera; const Coord3D &cPos=TheTacticalView->get3DCameraPosition(); Vector3 camPos(cPos.x,cPos.y,cPos.z); //Number of emitters from cube center to edge of visible extent. Int mumEmittersInHalf=(Int)floor(m_boxDimensions / m_emitterSpacing * 0.5f); //Find origin of visible cube surrounding camera. Int cubeCenterX=(Int)floor(camPos.X/m_emitterSpacing); Int cubeCenterY=(Int)floor(camPos.Y/m_emitterSpacing); //Find extents of visible cube surrounding camera. Int cubeOriginX=cubeCenterX - mumEmittersInHalf; //top/left extents. Int cubeOriginY=cubeCenterY - mumEmittersInHalf; Int cubeDimX=cubeCenterX + mumEmittersInHalf; //bottom/right extents. Int cubeDimY=cubeCenterY + mumEmittersInHalf; const FrustumClass & frustum = rinfo.Camera.Get_Frustum(); AABoxClass bbox; //Get a bounding box around our visible universe. Bounded by terrain and the sky //so much tighter fitting volume than what's actually visible. This will cull //particles falling under the ground. TheTerrainRenderObject->getMaximumVisibleBox(frustum, &bbox, TRUE); //Particles move outside the visible box as a result of local sine movement //so adjust bounding box to include them. bbox.Extent.X += m_amplitude+m_quadSize; bbox.Extent.Y += m_amplitude+m_quadSize; //Clip our visible snow rendering box if ((cubeOriginX * m_emitterSpacing ) < (bbox.Center.X - bbox.Extent.X)) cubeOriginX = (Int)floor ((bbox.Center.X - bbox.Extent.X)/m_emitterSpacing); if ((cubeOriginY * m_emitterSpacing ) < (bbox.Center.Y - bbox.Extent.Y)) cubeOriginY = (Int)floor ((bbox.Center.Y - bbox.Extent.Y)/m_emitterSpacing); if ((cubeDimX * m_emitterSpacing ) > (bbox.Center.X + bbox.Extent.X)) cubeDimX = (Int)floor ((bbox.Center.X + bbox.Extent.X)/m_emitterSpacing); if ((cubeDimY * m_emitterSpacing ) > (bbox.Center.Y + bbox.Extent.Y)) cubeDimY = (Int)floor ((bbox.Center.Y + bbox.Extent.Y)/m_emitterSpacing); if ((cubeDimY - cubeOriginY) < 0 || (cubeDimX-cubeOriginX) < 0) return; //entire snow box is culled by either x or y screen boundary. //Find total number of particles that need rendering. Int totalPart=(cubeDimY-cubeOriginY)*(cubeDimX-cubeOriginX); if (totalPart <= 0) return; //nothing to render. //Height at the top of the cube with camera at center. m_snowCeiling = camPos.Z + m_boxDimensions/2.0f; //Offset to allow cube extents to move with camera. Real cameraOffset = fmod (camPos.Z,m_boxDimensions); m_heightTraveled=m_time*m_velocity+cameraOffset; //height that snow flake traveled this frame. Matrix4x4 identity(true); DX8Wrapper::Set_Transform(D3DTS_WORLD,identity); DX8Wrapper::Set_Shader(ShaderClass::_PresetAlphaShader); VertexMaterialClass *vmat=VertexMaterialClass::Get_Preset(VertexMaterialClass::PRELIT_DIFFUSE); DX8Wrapper::Set_Material(vmat); REF_PTR_RELEASE(vmat); //make sure we have all the resources we need if (usePointSprites && !m_VertexBufferD3D) ReAcquireResources(); if (!usePointSprites && !m_indexBuffer) ReAcquireResources(); DX8Wrapper::Set_Texture(0,m_snowTexture); if (!usePointSprites) { renderAsQuads(rinfo,cubeOriginX,cubeOriginY,cubeDimX,cubeDimY); return; } Vector3 snowCenter; DX8Wrapper::Apply_Render_State_Changes(); // Set the render states for using point sprites DX8Wrapper::Set_DX8_Render_State( D3DRS_POINTSPRITEENABLE, TRUE ); DX8Wrapper::Set_DX8_Render_State( D3DRS_POINTSCALEENABLE, TRUE ); DX8Wrapper::Set_DX8_Render_State( D3DRS_POINTSIZE, FtoDW(m_pointSize) ); DX8Wrapper::Set_DX8_Render_State( D3DRS_POINTSIZE_MIN, FtoDW(m_minPointSize) ); DX8Wrapper::Set_DX8_Render_State( D3DRS_POINTSIZE_MAX, FtoDW(m_maxPointSize) ); DX8Wrapper::Set_DX8_Render_State( D3DRS_POINTSCALE_A, FtoDW(0.00f) ); DX8Wrapper::Set_DX8_Render_State( D3DRS_POINTSCALE_B, FtoDW(0.00f) ); DX8Wrapper::Set_DX8_Render_State( D3DRS_POINTSCALE_C, FtoDW(1.00f) ); DX8Wrapper::_Get_D3D_Device8()->SetStreamSource( 0, m_VertexBufferD3D, sizeof(POINTVERTEX) ); DX8Wrapper::_Get_D3D_Device8()->SetVertexShader( D3DFVF_POINTVERTEX ); m_dwBase = SNOW_BUFFER_SIZE; //start with a new vertex buffer each frame. m_leafDim = 45; //cull boxes that are 20x20 emitters in size. Making them much smaller will result in too many draw calls. m_totalRendered = 0; //keep track of how many particles were rendered. //Particle centers can deviate from center by by amplitude of sine offset. They also have radius m_quadSize. //Enlarge culling bounds to compensate. m_cullOverscan = m_amplitude+m_quadSize; renderSubBox(rinfo,cubeOriginX,cubeOriginY,cubeDimX,cubeDimY); // Reset render states DX8Wrapper::Set_DX8_Render_State( D3DRS_POINTSPRITEENABLE, FALSE ); DX8Wrapper::Set_DX8_Render_State( D3DRS_POINTSCALEENABLE, FALSE ); } /**For hardware that doesn't support point sprites*/ void W3DSnowManager::renderAsQuads(RenderInfoClass &rinfo, Int cubeOriginX, Int cubeOriginY, Int cubeDimX, Int cubeDimY) { Matrix4x4 proj; Matrix3D view; Vector3 snowCenter; Vector3 snowCenterVS; CameraClass &camera=rinfo.Camera; camera.Get_View_Matrix(&view); camera.Get_Projection_Matrix(&proj); Vector3 vertex_offsets[4] = { Vector3(-0.5f, 0.5f, 0.0f), Vector3(-0.5f, -0.5f, 0.0f), Vector3(0.5f, -0.5f, 0.0f), Vector3(0.5f, 0.5f, 0.0f) }; Vector2 quad_uvs[4] = { Vector2(0.0f, 0.0f), Vector2(0.0f, 1.0f), Vector2(1.0f, 1.0f), Vector2(1.0f, 0.0f) }; //pre-multiple the offsets by particle size for (Int i=0; i<4; i++) { vertex_offsets[i] *= m_quadSize; } Matrix4x4 identity(true); DX8Wrapper::Set_Transform(D3DTS_VIEW,identity); DX8Wrapper::Set_Index_Buffer(m_indexBuffer,0); Int y=cubeOriginY; //loop counter. Int cubeOriginXRemainder = cubeOriginX; //loop counter - adjusted when not all particles fit into render buffer. //Find total number of particles that need rendering. Int totalPart=(cubeDimY-cubeOriginY)*(cubeDimX-cubeOriginX); m_totalRendered += totalPart; while (totalPart) { Int batchSize=totalPart; if (batchSize > SNOW_BATCH_SIZE) batchSize = SNOW_BATCH_SIZE; Int numberInBatch=0; DynamicVBAccessClass vb_access(BUFFER_TYPE_DYNAMIC_DX8,dynamic_fvf_type,batchSize*4); //allocate 4 verts per flake { DynamicVBAccessClass::WriteLockClass lock(&vb_access); VertexFormatXYZNDUV2* verts=lock.Get_Formatted_Vertex_Array(); for (;y= batchSize) { cubeOriginXRemainder = x; goto flush_particles; } //Get initial height from noise table. We add a large value to make sure it's positive. Then //modulate by table dimensions to find a value. Int noiseOffset=MODPOW2(x+MAXIMUM_CAMERA_DISTANCE,SNOW_NOISE_X)+MODPOW2(y+MAXIMUM_CAMERA_DISTANCE,SNOW_NOISE_Y)*SNOW_NOISE_X; if (noiseOffset > (SNOW_NOISE_X * SNOW_NOISE_Y)) noiseOffset = 0; //this should never happen but check to prevent buffer over/under flow. //find current height Real h0=m_snowCeiling-fmod(m_heightTraveled+m_startingHeights[noiseOffset],m_boxDimensions); //find world-space position of snow flake snowCenter.Set(x*m_emitterSpacing,y*m_emitterSpacing,h0); //Get view-space position Matrix3D::Transform_Vector(view,snowCenter,&snowCenterVS); //Adjust position so snow flakes don't fall straight down. snowCenterVS.X += m_amplitude * WWMath::Fast_Sin( h0 * m_frequencyScaleX + (Real)x); snowCenterVS.Y += m_amplitude * WWMath::Fast_Sin( h0 * m_frequencyScaleY + (Real)y); for (Int i=0; i<4; i++) { *(Vector3 *)verts=snowCenterVS + vertex_offsets[i]; verts->nx=0; //keep AGP write-combining active verts->ny=0; verts->nz=0; verts->diffuse=0xffffffff; //set to opaque verts->u1=quad_uvs[i].X; verts->v1=quad_uvs[i].Y; verts->u2=0; //keep AGP write-combining active verts->v2=0; verts++; } numberInBatch++; } //getting here means we did not overflow the render buffer, so reset x origin to normal. cubeOriginXRemainder = cubeOriginX; //reset to normal amount } flush_particles: numberInBatch; //need something at goto destination - stupid c compiler. } //Render any particles that may be queued up. if (numberInBatch) { DX8Wrapper::Set_Vertex_Buffer(vb_access); DX8Wrapper::Draw_Triangles( 0,numberInBatch*2, 0, numberInBatch*4); totalPart -= numberInBatch; } } }