/* Copyright (c) 2002-2012 Croteam Ltd. All rights reserved. */ #include "StdAfx.h" #include // global variables used in terrain editing functions UWORD *_puwBuffer=NULL; Rect _rect; PIX _srcExtraW=0; PIX _srcExtraH=0; CTextureData *_ptdBrush=NULL; CTextureData *_ptdDistributionRandomNoise=NULL; CTextureData *_ptdContinousRandomNoise=NULL; UWORD *_puwNoiseTarget=NULL; PIX _pixNoiseTargetW=0; PIX _pixNoiseTargetH=0; FLOAT _fStrength=0.0f; // fbm noise buffer FLOAT *_pafWhiteNoise=NULL; #define WNOISE 64 // undo variables UWORD *_puwUndoTerrain=NULL; Rect _rectUndo; BOOL _bUndoStart=FALSE; BufferType _btUndoBufferType=BT_INVALID; INDEX _iUndoBufferData=-1; INDEX _iTerrainEntityID=-1; // filter matrices FLOAT _afFilterFineBlur[5][5]= { {0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, {0.0f, 0.0625f, 0.0625f, 0.0625f, 0.0f}, {0.0f, 0.0625f, 0.5f, 0.0625f, 0.0f}, {0.0f, 0.0625f, 0.0625f, 0.0625f, 0.0f}, {0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, }; FLOAT _afFilterBlurMore[5][5]= { {0.0277777f, 0.0277777f, 0.0277777f, 0.0277777f, 0.0277777f}, {0.0277777f, 0.0555555f, 0.0555555f, 0.0555555f, 0.0277777f}, {0.0277777f, 0.0555555f, 0.0111111f, 0.0555555f, 0.0277777f}, {0.0277777f, 0.0555555f, 0.0555555f, 0.0555555f, 0.0277777f}, {0.0277777f, 0.0277777f, 0.0277777f, 0.0277777f, 0.0277777f}, }; FLOAT _afFilterEdgeDetect[5][5]= { {0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, {0.0f, 1.0f, 1.0f, 1.0f, 0.0f}, {0.0f, 1.0f, -7.0f, 1.0f, 0.0f}, {0.0f, 1.0f, 1.0f, 1.0f, 0.0f}, {0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, }; FLOAT _afFilterEmboss[5][5]= { {0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, {0.0f, 1.0f, 1.0f, -1.0f, 0.0f}, {0.0f, 1.0f, 1.0f, -1.0f, 0.0f}, {0.0f, 1.0f, -1.0f, -1.0f, 0.0f}, {0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, }; FLOAT _afFilterSharpen[5][5]= { {0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, {0.0f, -1.0f, -1.0f, -1.0f, 0.0f}, {0.0f, -1.0f, 16.0f, -1.0f, 0.0f}, {0.0f, -1.0f, -1.0f, -1.0f, 0.0f}, {0.0f, 0.0f, 0.0f, 0.0f, 0.0f}, }; FLOAT GetBrushMultiplier(INDEX x, INDEX y) { if(_ptdBrush==NULL) return 1.0f; { COLOR col=_ptdBrush->GetTexel(x,y); FLOAT fResult=FLOAT(col>>24)/255.0f; return fResult; } } void ApplyAddPaint(UWORD uwMin, UWORD uwMax) { for(INDEX y=0; y<_rect.Height(); y++) { for(INDEX x=0; x<_rect.Width(); x++) { FLOAT fBrushMultiplier=GetBrushMultiplier(x,y); if( fBrushMultiplier==0.0f) continue; INDEX iOffset=y*_rect.Width()+x; FLOAT fValue=_puwBuffer[iOffset]; fValue+=fBrushMultiplier*_fStrength*32.0f*theApp.m_fPaintPower; fValue=Clamp(fValue,FLOAT(uwMin),FLOAT(uwMax)); _puwBuffer[iOffset]=fValue; } } } void ApplyRNDNoise(void) { CTerrain *ptrTerrain=GetTerrain(); if( ptrTerrain==NULL) return; FLOAT fMaxNoise=theApp.m_fNoiseAltitude/ptrTerrain->tr_vTerrainSize(2)*65535.0f; for(INDEX y=0; y<_rect.Height(); y++) { for(INDEX x=0; x<_rect.Width(); x++) { FLOAT fBrushMultiplier=GetBrushMultiplier(x,y); INDEX iPixSrc=y*_rect.Width()+x; FLOAT fInfluence=_puwBuffer[iPixSrc]; FLOAT fRnd=FLOAT(rand())/RAND_MAX-0.5f; FLOAT fValue=_puwBuffer[iPixSrc]; FLOAT fMaxRandomized=fValue+fRnd*fMaxNoise; FLOAT fFilterPower=Clamp(fBrushMultiplier*_fStrength,0.0f,1.0f); FLOAT fResult=Lerp( fValue, fMaxRandomized, fFilterPower); UWORD uwResult=Clamp(UWORD(fResult),MIN_UWORD,MAX_UWORD); _puwBuffer[iPixSrc]=uwResult; } } } FLOAT GetDistributionNoise( INDEX x, INDEX y, FLOAT fRandom) { INDEX iw=_ptdDistributionRandomNoise->GetPixWidth(); INDEX ih=_ptdDistributionRandomNoise->GetPixHeight(); INDEX ctSize=iw*ih; INDEX iOffset=abs(INDEX(iw*y+x+fRandom*ctSize)%ctSize); COLOR col=_ptdDistributionRandomNoise->td_pulFrames[iOffset]; FLOAT fResult=FLOAT(col&0xFF)/255.0f; return fResult; } FLOAT GetContinousNoise( INDEX x, INDEX y, FLOAT fRandom) { INDEX iw=_ptdContinousRandomNoise->GetPixWidth(); INDEX ih=_ptdContinousRandomNoise->GetPixHeight(); INDEX ctSize=iw*ih; INDEX iOffset=abs(INDEX(iw*y+x+fRandom*ctSize)%ctSize); COLOR col=_ptdContinousRandomNoise->td_pulFrames[iOffset]; FLOAT fResult=FLOAT(col&0xFF)/255.0f; return fResult; } void ApplyContinousNoise(void) { CTerrain *ptrTerrain=GetTerrain(); if( ptrTerrain==NULL) return; FLOAT fMaxNoise=theApp.m_fNoiseAltitude/ptrTerrain->tr_vTerrainSize(2)*65535.0f; for(INDEX y=0; y<_rect.Height(); y++) { INDEX oy=y+_rect.rc_iTop; for(INDEX x=0; x<_rect.Width(); x++) { INDEX ox=x+_rect.rc_iLeft; FLOAT fBrushMultiplier=GetBrushMultiplier(x,y); INDEX iPixSrc=y*_rect.Width()+x; FLOAT fInfluence=_puwBuffer[iPixSrc]; FLOAT fRnd=GetContinousNoise( ox, oy, 0.0f)-0.5f; FLOAT fValue=_puwBuffer[iPixSrc]; FLOAT fMaxRandomized=fValue+fRnd*fMaxNoise; fMaxRandomized=Clamp(fMaxRandomized,(FLOAT)MIN_UWORD,(FLOAT)MAX_UWORD); FLOAT fFilterPower=Clamp(fBrushMultiplier*_fStrength,0.0f,1.0f); FLOAT fResult=Lerp( fValue, fMaxRandomized, fFilterPower); UWORD uwResult=Clamp((UWORD)fResult,MIN_UWORD,MAX_UWORD); _puwBuffer[iPixSrc]=uwResult; } } } void ApplyPosterize(void) { CTerrain *ptrTerrain=GetTerrain(); if( ptrTerrain==NULL) return; FLOAT fStepUW=theApp.m_fPosterizeStep/ptrTerrain->tr_vTerrainSize(2)*65535.0f; for(INDEX y=0; y<_rect.Height(); y++) { for(INDEX x=0; x<_rect.Width(); x++) { FLOAT fBrushMultiplier=GetBrushMultiplier(x,y); if(fBrushMultiplier==0.0f) continue; INDEX iPixSrc=y*_rect.Width()+x; FLOAT fValue=_puwBuffer[iPixSrc]; FLOAT fPosterized=(INDEX(fValue/fStepUW))*fStepUW+1.0f; UWORD uwResult=Clamp(UWORD(fPosterized),MIN_UWORD,MAX_UWORD); _puwBuffer[iPixSrc]=uwResult; } } } void ApplyFilterMatrix(FLOAT afFilterMatrix[5][5]) { INDEX ctBuffBytes=_rect.Width()*_rect.Height()*sizeof(UWORD); UWORD *puwDst=(UWORD*)AllocMemory(ctBuffBytes); memcpy(puwDst,_puwBuffer,ctBuffBytes); for(INDEX y=0; y<_rect.Height()-_srcExtraH*2; y++) { for(INDEX x=0; x<_rect.Width()-_srcExtraW*2; x++) { INDEX iPixDst=(y+_srcExtraH)*_rect.Width()+x+_srcExtraW; FLOAT fBrushMultiplier=GetBrushMultiplier(x,y); FLOAT fDivSum=0.0f; FLOAT fSum=0.0f; for(INDEX j=0; j<5; j++) { for(INDEX i=0; i<5; i++) { FLOAT fWeight=(afFilterMatrix)[i][j]; fDivSum+=fWeight; INDEX iPixSrc=(y+j)*_rect.Width()+(x+i); FLOAT fInfluence=_puwBuffer[iPixSrc]; fSum+=fInfluence*fWeight; } } UWORD uwMax=Clamp(UWORD(fSum/fDivSum),MIN_UWORD,MAX_UWORD); FLOAT fFilterPower=Clamp(fBrushMultiplier*_fStrength/64.0f,0.0f,1.0f); UWORD uwResult=Lerp( puwDst[iPixDst], uwMax, fFilterPower); puwDst[iPixDst]=uwResult; } } memcpy(_puwBuffer,puwDst,ctBuffBytes); FreeMemory( puwDst); } static INDEX _iTerrainWidth=0; static UWORD *_puwHeightMap=NULL; static INDEX _iRandomDX=0; void SetHMPixel( UWORD pix, INDEX x, INDEX y) { UWORD *pdest=_puwHeightMap+y*_iTerrainWidth+x; if (*pdest==65535) { *pdest=pix; } } UWORD GetHMPixel(INDEX x, INDEX y) { UWORD *pdest=_puwHeightMap+y*_iTerrainWidth+x; return *pdest; } UWORD RandomizePixel(FLOAT fmid, FLOAT fdmax) { FLOAT fRand=((FLOAT)rand())/RAND_MAX-0.5f; FLOAT fd=fdmax*fRand; FLOAT fres=Clamp(fmid+fd,0.0f,65536.0f); return fres; } void SubdivideAndDisplace(INDEX x, INDEX y, INDEX idx, FLOAT fdMax) { FLOAT flu=GetHMPixel(x,y); FLOAT fru=GetHMPixel(x+idx,y); FLOAT frd=GetHMPixel(x+idx,y+idx); FLOAT fld=GetHMPixel(x,y+idx); if( fdMax<_iRandomDX) { SetHMPixel(RandomizePixel((flu+fru)/2.0f,fdMax), x+idx/2, y ); // middle top SetHMPixel(RandomizePixel((fld+frd)/2.0f,fdMax), x+idx/2, y+idx ); // middle bottom SetHMPixel(RandomizePixel((fru+frd)/2.0f,fdMax), x+idx, y+idx/2); // right middle SetHMPixel(RandomizePixel((flu+fld)/2.0f,fdMax), x, y+idx/2); // left middle SetHMPixel(RandomizePixel((flu+fru+fld+frd)/4.0f,fdMax), x+idx/2, y+idx/2); // middle } else { SetHMPixel(RandomizePixel(65536.0f/2.0f,fdMax), x+idx/2, y ); // middle top SetHMPixel(RandomizePixel(65536.0f/2.0f,fdMax), x+idx/2, y+idx ); // middle bottom SetHMPixel(RandomizePixel(65536.0f/2.0f,fdMax), x+idx, y+idx/2); // right middle SetHMPixel(RandomizePixel(65536.0f/2.0f,fdMax), x, y+idx/2); // left middle SetHMPixel(RandomizePixel(65536.0f/2.0f,fdMax), x+idx/2, y+idx/2); // middle } fdMax*=0.5f; if(idx>1) { SubdivideAndDisplace(x ,y , idx/2, fdMax); SubdivideAndDisplace(x+idx/2,y , idx/2, fdMax); SubdivideAndDisplace(x ,y+idx/2, idx/2, fdMax); SubdivideAndDisplace(x+idx/2,y+idx/2, idx/2, fdMax); } } Rect GetTerrainRect(void) { Rect rect; rect.rc_iLeft=0; rect.rc_iRight=0; rect.rc_iTop=0; rect.rc_iBottom=0; CTerrain *ptrTerrain=GetTerrain(); if( ptrTerrain==NULL) return rect; rect.rc_iLeft=0; rect.rc_iRight=ptrTerrain->tr_pixHeightMapWidth; rect.rc_iTop=0; rect.rc_iBottom=ptrTerrain->tr_pixHeightMapHeight; return rect; } FLOAT GetWrappedPixelValue( INDEX x, INDEX y) { INDEX iWrapX=(x+WNOISE)%WNOISE; INDEX iWrapY=(y+WNOISE)%WNOISE; return _pafWhiteNoise[iWrapY*WNOISE+iWrapX]; } void RandomizeWhiteNoise(void) { if(_pafWhiteNoise==NULL) { _pafWhiteNoise=(FLOAT *)AllocMemory(WNOISE*WNOISE*sizeof(FLOAT)); } FLOAT *pfTemp=_pafWhiteNoise; for(INDEX i=0; i=0; iOctave--) { FLOAT fOctaveOffset=0.0f; if( bRandomOffest) { fOctaveOffset=_pafWhiteNoise[iOctave]; } for(INDEX y=0; y0) { fValue=fValue+fAdd; } pafFBM[iOffset]=fValue; if(fValue>fMax) fMax=fValue; if(fValuetr_pixHeightMapWidth; PIX pixTerrainH=ptrTerrain->tr_pixHeightMapHeight; FLOAT *pafFBM=GenerateTerrain_FBMBuffer( pixTerrainW, pixTerrainH, theApp.m_iFBMOctaves, theApp.m_fFBMHighFrequencyStep,theApp.m_fFBMStepFactor, theApp.m_fFBMMaxAmplitude, theApp.m_fFBMfAmplitudeDecreaser, theApp.m_bFBMAddNegativeValues, theApp.m_bFBMRandomOffset, fMin, fMax); // convert buffer to height map FLOAT fSizeY=ptrTerrain->tr_vTerrainSize(2); FLOAT fConvertFactor=(theApp.m_fFBMMaxAmplitude/fSizeY)*MAX_UWORD; // set height map for(INDEX iPix=0; iPixuwHeightMax) uwHeightMax=uwHeight; if( uwHeightObtain_t( theApp.m_fnContinousNoiseTexture); _ptdContinousRandomNoise->Force(TEX_STATIC|TEX_CONSTANT); } catch( char *strError) { (void) strError; WarningMessage("Unable to obtain continous random noise texture!\nError: %s", strError); return FALSE; } return TRUE; } void FreeContinousNoiseTexture( void) { _pTextureStock->Release( _ptdContinousRandomNoise); } BOOL SetupDistributionNoiseTexture( void) { try { _ptdDistributionRandomNoise=_pTextureStock->Obtain_t( theApp.m_fnDistributionNoiseTexture); _ptdDistributionRandomNoise->Force(TEX_STATIC|TEX_CONSTANT); } catch( char *strError) { (void) strError; WarningMessage("Unable to obtain distribution random noise texture!\nError: %s", strError); return FALSE; } return TRUE; } void FreeDistributionNoiseTexture( void) { _pTextureStock->Release( _ptdDistributionRandomNoise); } FLOAT StepUp(FLOAT fCur, FLOAT fMin, FLOAT fMax) { if( fCur<=fMin) return 0.0f; if( fCur>=fMax) return 1.0f; return (fCur-fMin)/(fMax-fMin); } FLOAT StepDown(FLOAT fCur, FLOAT fMin, FLOAT fMax) { if( fCur<=fMin) return 1.0f; if( fCur>=fMax) return 0.0f; return (fMax-fCur)/(fMax-fMin); } FLOAT3D NormalFrom4Points(const FLOAT3D &v0, const FLOAT3D &v1, const FLOAT3D &v2, const FLOAT3D &v3, FLOAT fLerpX, FLOAT fLerpZ) { FLOAT fHDeltaX = Lerp(v1(2)-v0(2), v3(2)-v2(2), fLerpZ); FLOAT fHDeltaZ = Lerp(v0(2)-v2(2), v1(2)-v3(2), fLerpX); FLOAT fDeltaX = v1(1) - v0(1); FLOAT fDeltaZ = v0(3) - v2(3); FLOAT3D vNormal; vNormal(2) = sqrt(1 / (((fHDeltaX*fHDeltaX)/(fDeltaX*fDeltaX)) + ((fHDeltaZ*fHDeltaZ)/(fDeltaZ*fDeltaZ)) + 1)); vNormal(1) = sqrt(vNormal(2)*vNormal(2) * ((fHDeltaX*fHDeltaX) / (fDeltaX*fDeltaX))); vNormal(3) = sqrt(vNormal(2)*vNormal(2) * ((fHDeltaZ*fHDeltaZ) / (fDeltaZ*fDeltaZ))); if (fHDeltaX>0) { vNormal(1) = -vNormal(1); } if (fHDeltaZ<0) { vNormal(3) = -vNormal(3); } //ASSERT(Abs(vNormal.Length()-1)<0.01); return vNormal; } FLOAT3D GetPoint(CTerrain *ptrTerrain, INDEX iX, INDEX iY) { const FLOAT3D &vStretch = ptrTerrain->tr_vStretch; iX = Clamp(iX, INDEX(0), ptrTerrain->tr_pixHeightMapWidth); iY = Clamp(iY, INDEX(0), ptrTerrain->tr_pixHeightMapHeight); return FLOAT3D( FLOAT(iX)*vStretch(1), (FLOAT)ptrTerrain->tr_auwHeightMap[iX+iY*ptrTerrain->tr_pixHeightMapWidth] * vStretch(2), FLOAT(iY)*vStretch(3)); } UWORD GetSlope(CTerrain *ptrTerrain, INDEX iX, INDEX iY) { FLOAT3D av[9]; INDEX iHMapWidth = ptrTerrain->tr_pixHeightMapWidth; FLOAT3D vStretch = ptrTerrain->tr_vStretch; av[0] = GetPoint(ptrTerrain, iX-1, iY-1); av[1] = GetPoint(ptrTerrain, iX , iY-1); av[2] = GetPoint(ptrTerrain, iX+1, iY-1); av[3] = GetPoint(ptrTerrain, iX-1, iY ); av[4] = GetPoint(ptrTerrain, iX , iY ); av[5] = GetPoint(ptrTerrain, iX+1, iY ); av[6] = GetPoint(ptrTerrain, iX-1, iY+1); av[7] = GetPoint(ptrTerrain, iX , iY+1); av[8] = GetPoint(ptrTerrain, iX+1, iY+1); FLOAT3D avN[4]; FLOAT3D vNormal; avN[0] = NormalFrom4Points(av[0], av[1], av[3], av[4], 1, 1); avN[1] = NormalFrom4Points(av[1], av[2], av[4], av[5], 0, 1); avN[2] = NormalFrom4Points(av[3], av[4], av[6], av[7], 1, 0); avN[3] = NormalFrom4Points(av[4], av[5], av[7], av[8], 0, 0); vNormal = avN[0]+avN[1]+avN[2]+avN[3]; vNormal.Normalize(); return (1-vNormal(2))*65535; } void GenerateLayerDistribution(INDEX iForLayer, Rect rect) { if(!SetupDistributionNoiseTexture()) return; CTerrain *ptrTerrain=GetTerrain(); if( ptrTerrain==NULL) return; // obtain buffer UWORD *puwAltitude=GetBufferForEditing(ptrTerrain, rect, BT_HEIGHT_MAP, 0); INDEX ctSize=rect.Width()*rect.Height()*sizeof(UWORD); UWORD *puwSlope=(UWORD *)AllocMemory(ctSize); // prepare slope buffer for(INDEX y=0; ytr_atlLayers.Count(); iLayer++) { if( iForLayer!=-1 && iLayer!=iForLayer) continue; CTerrainLayer *ptlLayer=GetLayer(iLayer); if(!ptlLayer->tl_bAutoRegenerated) continue; // get layer UWORD *puwMask=GetBufferForEditing(ptrTerrain, rect, BT_LAYER_MASK, iLayer); for(INDEX y=0; ytl_fMaxAltitude-ptlLayer->tl_fMinAltitude; FLOAT fSlopeRange=ptlLayer->tl_fMaxSlope-ptlLayer->tl_fMinSlope; // get coverage influence FLOAT fCoverageInfluence=1.0f; FLOAT fCoverageRandom=GetDistributionNoise( ox, oy, ptlLayer->tl_fCoverageRandom); fCoverageInfluence=StepDown(fCoverageRandom, ptlLayer->tl_fCoverage, ptlLayer->tl_fCoverage+ptlLayer->tl_fCoverageNoise); // get min altitude influence FLOAT fMinAltitudeInfluence=1.0f; if(ptlLayer->tl_bApplyMinAltitude) { FLOAT fMinAltitudeNoise=(GetDistributionNoise( ox, oy, ptlLayer->tl_fMinAltitudeRandom)-0.5f)*ptlLayer->tl_fMinAltitudeNoise; FLOAT fAltMinFade1=ptlLayer->tl_fMinAltitude+fAltitudeRange*ptlLayer->tl_fMinAltitudeFade; fMinAltitudeInfluence=StepUp(fAltitudeRatio+fMinAltitudeNoise, ptlLayer->tl_fMinAltitude, fAltMinFade1); } // get max altitude influence FLOAT fMaxAltitudeInfluence=1.0f; if(ptlLayer->tl_bApplyMaxAltitude) { FLOAT fMaxAltitudeNoise=(GetDistributionNoise( ox, oy, ptlLayer->tl_fMaxAltitudeRandom)-0.5f)*ptlLayer->tl_fMaxAltitudeNoise; FLOAT fAltMaxFade1=ptlLayer->tl_fMaxAltitude-fAltitudeRange*ptlLayer->tl_fMaxAltitudeFade; fMaxAltitudeInfluence=StepDown(fAltitudeRatio+fMaxAltitudeNoise, fAltMaxFade1, ptlLayer->tl_fMaxAltitude); } // get min slope influence FLOAT fMinSlopeInfluence=1.0f; if(ptlLayer->tl_bApplyMinSlope) { FLOAT fMinSlopeNoise=(GetDistributionNoise( ox, oy, ptlLayer->tl_fMinSlopeRandom)-0.5f)*ptlLayer->tl_fMinSlopeNoise; FLOAT fSlopeMinFade1=ptlLayer->tl_fMinSlope+fSlopeRange*ptlLayer->tl_fMinSlopeFade; fMinSlopeInfluence=StepUp(fSlopeRatio+fMinSlopeNoise, ptlLayer->tl_fMinSlope, fSlopeMinFade1); } // get max slope influence FLOAT fMaxSlopeInfluence=1.0f; if(ptlLayer->tl_bApplyMaxSlope) { FLOAT fMaxSlopeNoise=(GetDistributionNoise( ox, oy, ptlLayer->tl_fMaxSlopeRandom)-0.5f)*ptlLayer->tl_fMaxSlopeNoise; FLOAT fSlopeMaxFade1=ptlLayer->tl_fMaxSlope-fSlopeRange*ptlLayer->tl_fMaxSlopeFade; fMaxSlopeInfluence=StepDown(fSlopeRatio+fMaxSlopeNoise, fSlopeMaxFade1, ptlLayer->tl_fMaxSlope); } // calculate result of all influences puwMask[iOffset]= 65535.0f* fCoverageInfluence* fMinAltitudeInfluence* fMaxAltitudeInfluence* fMinSlopeInfluence* fMaxSlopeInfluence; } } // apply buffer change SetBufferForEditing(ptrTerrain, puwMask, rect, BT_LAYER_MASK, iLayer); theApp.GetActiveDocument()->SetModifiedFlag( TRUE); FreeMemory(puwMask); } FreeMemory(puwAltitude); theApp.m_ctTerrainPageCanvas.MarkChanged(); FreeDistributionNoiseTexture(); } void GenerateLayerDistribution(INDEX iForLayer) { CTerrain *ptrTerrain=GetTerrain(); if( ptrTerrain==NULL) return; Rect rect; rect.rc_iLeft=0; rect.rc_iRight=ptrTerrain->tr_pixHeightMapWidth; rect.rc_iTop=0; rect.rc_iBottom=ptrTerrain->tr_pixHeightMapHeight; GenerateLayerDistribution(iForLayer, rect); } void RecalculateShadows(void) { CTerrain *ptrTerrain=GetTerrain(); if( ptrTerrain==NULL) return; ptrTerrain->UpdateShadowMap(); } void OptimizeLayers(void) { CTerrain *ptrTerrain=GetTerrain(); if( ptrTerrain==NULL) return; Rect rect; rect.rc_iLeft=0; rect.rc_iRight=ptrTerrain->tr_pixHeightMapWidth; rect.rc_iTop=0; rect.rc_iBottom=ptrTerrain->tr_pixHeightMapHeight; CStaticArray apuwLayers; // obtain buffer INDEX ctLayers=ptrTerrain->tr_atlLayers.Count(); apuwLayers.New(ctLayers); INDEX iLayer, iOffset; for( iLayer=0; iLayer>8 != 0) ctDrawnBefore++; } } // optimize for overdraw for(iOffset=0; iOffset=0; i--) { UWORD *puwCurr=apuwLayers[i]+iOffset; // if should optimize if(bOptimize) { // clear mask *puwCurr=0; } else { if( (*puwCurr)>>8 == 255) { // clear mask for all layers beneath current one bOptimize=TRUE; } } } } // count overdraw after optimisation INDEX ctDrawnAfter=0; for(iOffset=0; iOffset>8 != 0) ctDrawnAfter++; } } // make a report about optimisation success CMainFrame* pMainFrame = STATIC_DOWNCAST(CMainFrame, AfxGetMainWnd()); CTString strReport; strReport.PrintF("Overdraw before optimization: %g. Overdraw after optimization: %g", FLOAT(ctDrawnBefore)/ctPixels, FLOAT(ctDrawnAfter)/ctPixels); pMainFrame->SetStatusBarMessage( strReport, STATUS_LINE_PANE, 5.0f); // free buffers for( iLayer=0; iLayerSetModifiedFlag( TRUE); theApp.m_ctTerrainPageCanvas.MarkChanged(); } BOOL _bIsUpToDate=TRUE; Rect _rectDiscarded; void DiscardLayerDistribution(Rect rect) { if(_bIsUpToDate) { _rectDiscarded.rc_iLeft=rect.rc_iLeft; _rectDiscarded.rc_iRight=rect.rc_iRight; _rectDiscarded.rc_iTop=rect.rc_iTop; _rectDiscarded.rc_iBottom=rect.rc_iBottom; } else { if(rect.rc_iLeft < _rectDiscarded.rc_iLeft) _rectDiscarded.rc_iLeft=rect.rc_iLeft; if(rect.rc_iRight > _rectDiscarded.rc_iRight) _rectDiscarded.rc_iRight=rect.rc_iRight; if(rect.rc_iTop < _rectDiscarded.rc_iTop) _rectDiscarded.rc_iTop=rect.rc_iTop; if(rect.rc_iBottom > _rectDiscarded.rc_iBottom) _rectDiscarded.rc_iBottom=rect.rc_iBottom; } _bIsUpToDate=FALSE; } void UpdateLayerDistribution(void) { if(_bIsUpToDate || !theApp.m_Preferences.ap_bAutoUpdateTerrainDistribution) return; // update layer distribution GenerateLayerDistribution(-1, _rectDiscarded); _bIsUpToDate=TRUE; } void ApplyFilterOntoTerrain(void) { EditTerrain(NULL, FLOAT3D(0,0,0), theApp.m_fFilterPower*16.0f, TE_ALTITUDE_FILTER); } void ApplySmoothOntoTerrain(void) { EditTerrain(NULL, FLOAT3D(0,0,0), theApp.m_fSmoothPower*16.0f, TE_ALTITUDE_SMOOTH); } void ApplyEqualizeOntoTerrain(void) { EditTerrain(NULL, FLOAT3D(0,0,0), 1.0f, TE_ALTITUDE_EQUALIZE); } void ApplyGenerateTerrain(void) { EditTerrain(NULL, FLOAT3D(0,0,0), 1.0f, TE_GENERATE_TERRAIN); } void ApplyRndNoiseOntoTerrain(void) { EditTerrain(NULL, FLOAT3D(0,0,0), theApp.m_fNoiseAltitude, TE_ALTITUDE_RND_NOISE); } void ApplyContinousNoiseOntoTerrain(void) { EditTerrain(NULL, FLOAT3D(0,0,0), theApp.m_fNoiseAltitude, TE_ALTITUDE_CONTINOUS_NOISE); } void ApplyMinimumOntoTerrain(void) { EditTerrain(NULL, FLOAT3D(0,0,0), 1.0f, TE_ALTITUDE_MINIMUM); } void ApplyMaximumOntoTerrain(void) { EditTerrain(NULL, FLOAT3D(0,0,0), 1.0f, TE_ALTITUDE_MAXIMUM); } void ApplyFlattenOntoTerrain(void) { EditTerrain(NULL, FLOAT3D(0,0,0), 1.0f, TE_ALTITUDE_FLATTEN); } void ApplyPosterizeOntoTerrain(void) { EditTerrain(NULL, FLOAT3D(0,0,0), theApp.m_fPosterizeStep, TE_ALTITUDE_POSTERIZE); } CEntity *GetEntityForID(ULONG iEntityID) { CWorldEditorDoc* pDoc = theApp.GetActiveDocument(); FOREACHINDYNAMICCONTAINER(pDoc->m_woWorld.wo_cenEntities, CEntity, iten) { if(iten->en_ulID==iEntityID) return &*iten; } return NULL; } // constructor CTerrainUndo::CTerrainUndo() { tu_puwUndoBuffer=NULL; tu_puwRedoBuffer=NULL; } void DeleteOneUndo(CTerrainUndo *ptrud) { if(ptrud->tu_puwUndoBuffer!=NULL) FreeMemory(ptrud->tu_puwUndoBuffer); if(ptrud->tu_puwRedoBuffer!=NULL) FreeMemory(ptrud->tu_puwRedoBuffer); delete ptrud; } void DeleteTerrainUndo(CWorldEditorDoc* pDoc) { for(INDEX iUndo=0; iUndom_dcTerrainUndo.Count(); iUndo++) { CTerrainUndo *ptu=&pDoc->m_dcTerrainUndo[iUndo]; pDoc->m_dcTerrainUndo.Remove(ptu); DeleteOneUndo(ptu); } } CTerrain *GetUndoTerrain(ULONG ulEntityID) { // obtain terrain entity CEntity *penTerrain=GetEntityForID(_iTerrainEntityID); if(penTerrain==NULL) { return NULL; } // obtain terrain CTerrain *ptrTerrain=penTerrain->GetTerrain(); return ptrTerrain; } void ApplyTerrainUndo(CTerrainUndo *ptrud) { CWorldEditorDoc* pDoc = theApp.GetActiveDocument(); CTerrain *ptrTerrain=GetUndoTerrain(ptrud->tu_ulEntityID); if(ptrTerrain==NULL) { DeleteOneUndo(ptrud); return; } // obtain and store redo buffer if(ptrud->tu_puwRedoBuffer==NULL) { ptrud->tu_puwRedoBuffer=GetBufferForEditing(ptrTerrain, ptrud->tu_rcRect, ptrud->tu_btUndoBufferType, ptrud->tu_iUndoBufferData); } // apply buffer change (undo) SetBufferForEditing(ptrTerrain, ptrud->tu_puwUndoBuffer, ptrud->tu_rcRect, ptrud->tu_btUndoBufferType, ptrud->tu_iUndoBufferData); pDoc->m_iCurrentTerrainUndo--; DiscardLayerDistribution(ptrud->tu_rcRect); pDoc->SetModifiedFlag( TRUE); theApp.m_ctTerrainPageCanvas.MarkChanged(); } void ApplyTerrainRedo(CTerrainUndo *ptrud) { CWorldEditorDoc* pDoc = theApp.GetActiveDocument(); CTerrain *ptrTerrain=GetUndoTerrain(ptrud->tu_ulEntityID); if(ptrTerrain==NULL) { DeleteOneUndo(ptrud); return; } if(ptrud->tu_puwRedoBuffer==NULL) return; // apply buffer change (redo) SetBufferForEditing(ptrTerrain, ptrud->tu_puwRedoBuffer, ptrud->tu_rcRect, ptrud->tu_btUndoBufferType, ptrud->tu_iUndoBufferData); pDoc->m_iCurrentTerrainUndo++; DiscardLayerDistribution(ptrud->tu_rcRect); pDoc->SetModifiedFlag( TRUE); theApp.m_ctTerrainPageCanvas.MarkChanged(); } UWORD *ExtractUndoRect(PIX pixTerrainWidth) { INDEX ctBuffBytes=_rectUndo.Width()*_rectUndo.Height()*sizeof(UWORD); UWORD *puwBuff=(UWORD*)AllocMemory(ctBuffBytes); if(puwBuff==NULL) return NULL; UWORD *puwBuffTemp=puwBuff; for(INDEX y=_rectUndo.rc_iTop; y<_rectUndo.rc_iBottom; y++) { for(INDEX x=_rectUndo.rc_iLeft; x<_rectUndo.rc_iRight; x++) { INDEX iOffset=y*pixTerrainWidth+x; UWORD uwValue=_puwUndoTerrain[iOffset]; *puwBuffTemp=uwValue; puwBuffTemp++; } } return puwBuff; } void TerrainEditBegin(void) { if( !(theApp.m_Preferences.ap_iMemoryForTerrainUndo>0)) { return; } _bUndoStart=TRUE; } void RemoveRedoList(void) { CWorldEditorDoc* pDoc = theApp.GetActiveDocument(); CDynamicContainer dcTemp; for( INDEX itu=0; itum_iCurrentTerrainUndo+1; itu++) { dcTemp.Add(&pDoc->m_dcTerrainUndo[itu]); } for( INDEX ituDel=pDoc->m_iCurrentTerrainUndo+1; ituDelm_dcTerrainUndo.Count(); ituDel++) { DeleteOneUndo(&pDoc->m_dcTerrainUndo[ituDel]); } pDoc->m_dcTerrainUndo.MoveContainer(dcTemp); } void LimitMemoryConsumption(INDEX iNewConsumption) { CWorldEditorDoc* pDoc = theApp.GetActiveDocument(); INDEX ctUndos=pDoc->m_dcTerrainUndo.Count(); INDEX iLastValid=-1; INDEX iSum=iNewConsumption; for(INDEX iUndo=ctUndos-1; iUndo>=0; iUndo--) { CTerrainUndo *ptu=&pDoc->m_dcTerrainUndo[iUndo]; INDEX iMemory=ptu->tu_rcRect.Width()*ptu->tu_rcRect.Height()*sizeof(UWORD); if(ptu->tu_puwRedoBuffer!=NULL) { iSum=iSum+iMemory*2; } else { iSum=iSum+iMemory; } if(iSum>theApp.m_Preferences.ap_iMemoryForTerrainUndo*1024*1024) { iLastValid=iUndo+1; break; } } if( iLastValid!=-1) { CDynamicContainer dcTemp; for( INDEX itu=iLastValid; itum_dcTerrainUndo[itu]); } for( INDEX ituDel=0; ituDelm_dcTerrainUndo[ituDel]); } pDoc->m_dcTerrainUndo.MoveContainer(dcTemp); if(pDoc->m_iCurrentTerrainUndo>=iLastValid) { pDoc->m_iCurrentTerrainUndo=pDoc->m_iCurrentTerrainUndo-iLastValid; } } } void TerrainEditEnd(void) { if( !(theApp.m_Preferences.ap_iMemoryForTerrainUndo>0)) { return; } CWorldEditorDoc* pDoc = theApp.GetActiveDocument(); // obtain terrain entity CEntity *penTerrain=GetEntityForID(_iTerrainEntityID); if(penTerrain==NULL) { if(_puwUndoTerrain!=NULL) { FreeMemory(_puwUndoTerrain); _puwUndoTerrain=NULL; } return; } // obtain terrain CTerrain *ptrTerrain=penTerrain->GetTerrain(); if(ptrTerrain==NULL) { if(_puwUndoTerrain!=NULL) { FreeMemory(_puwUndoTerrain); _puwUndoTerrain=NULL; } return; } // we will add undo, clear all existing redo's RemoveRedoList(); // remember undo CTerrainUndo *ptrud=new CTerrainUndo; INDEX iNewConsumption=_rectUndo.Width()*_rectUndo.Height()*sizeof(UWORD); LimitMemoryConsumption(iNewConsumption); ptrud->tu_ulEntityID=_iTerrainEntityID; ptrud->tu_rcRect=_rectUndo; ptrud->tu_btUndoBufferType=_btUndoBufferType; ptrud->tu_iUndoBufferData=_iUndoBufferData; ptrud->tu_puwUndoBuffer=ExtractUndoRect(ptrTerrain->tr_pixHeightMapWidth); if(ptrud->tu_puwUndoBuffer!=NULL) { pDoc->m_dcTerrainUndo.Add(ptrud); } else { delete ptrud; } pDoc->m_iCurrentTerrainUndo=pDoc->m_dcTerrainUndo.Count()-1; // release obtained terrain buffer if(_puwUndoTerrain!=NULL) { FreeMemory(_puwUndoTerrain); _puwUndoTerrain=NULL; } } CTileInfo::CTileInfo() { ti_ix=-1; ti_iy=-1; ti_bSwapXY=FALSE; ti_bFlipX=FALSE; ti_bFlipY=FALSE; } void ObtainLayerTileInfo(CDynamicContainer *pdcTileInfo, CTextureData *ptdTexture, INDEX &ctTilesPerRow) { CTFileName fnTexture=ptdTexture->GetName(); CTFileName fnTileInfo=fnTexture.NoExt()+CTString(".tli"); INDEX ctParsedLines=0; try { char achrLine[ 256]; CTFileStream strm; strm.Open_t( fnTileInfo); FOREVER { CDynamicContainer dcTokens; strm.GetLine_t(achrLine, 256); ctParsedLines++; char achrSeparators[] = " ,"; char *pchrToken = strtok( achrLine, achrSeparators); while( pchrToken != NULL ) { CTString *pstrToken=new CTString(); *pstrToken=CTString( pchrToken); dcTokens.Add(pstrToken); // next token pchrToken = strtok( NULL, achrSeparators); } // if no tokens parsed if(dcTokens.Count()==0) continue; INDEX iToken=0; // analyze parsed tokens if(dcTokens[iToken]=="TilesPerRow") { // there must be at least 1 token for 'TilesPerRow' indentifier if(dcTokens.Count()-1-iToken<1) { throw("You must enter number of tiles per raw."); } ctTilesPerRow=0; INDEX iResultTPR=sscanf(dcTokens[iToken+1], "%d", &ctTilesPerRow); if(iResultTPR<=0) { ctTilesPerRow=0; throw("Unable to parse count of tiles per row."); } } else if(dcTokens[iToken]=="Tile") { // there must be at least 2 tokens for 'Tile' indentifier if(dcTokens.Count()-1-iToken<2) { throw("You must enter 2 coordinates per tile."); } INDEX x,y; INDEX iResultX=sscanf(dcTokens[iToken+1], "%d", &x); if(iResultX<=0) { throw("Unable to parse x coordinate."); } INDEX iResultY=sscanf(dcTokens[iToken+2], "%d", &y); if(iResultY<=0) { throw("Unable to parse y coordinate."); } if(x<=0 || y<=0) { throw("Tile coordinates must be greater than 0."); } // jump over coordinate tokens iToken+=3; // add tile info CTileInfo *pti=new CTileInfo(); pti->ti_ix=x-1; pti->ti_iy=y-1; for( INDEX iFlagToken=iToken; iFlagTokenti_bSwapXY=TRUE; } else if(dcTokens[iFlagToken]=="FlipX") { pti->ti_bFlipX=TRUE; } else if(dcTokens[iFlagToken]==";") { break; } else if(dcTokens[iFlagToken]=="FlipY") { pti->ti_bFlipY=TRUE; } else { throw("Unrecognizable character found."); } } pdcTileInfo->Add(pti); } // clear allocated tokens for(INDEX i=0; itl_ltType!=LT_TILE || ptlLayer->tl_ptdTexture==NULL) return; CDynamicContainer dcTileInfo; INDEX ctTilesPerRaw=0; ObtainLayerTileInfo( &dcTileInfo, ptlLayer->tl_ptdTexture, ctTilesPerRaw); INDEX ctTiles=dcTileInfo.Count(); if(ctTilesPerRaw==0 || ctTiles==0) return; ptlLayer->SetTilesPerRow(ctTilesPerRaw); ptlLayer->tl_iSelectedTile= Clamp( ptlLayer->tl_iSelectedTile, (INDEX)0, INDEX(ctTiles-1) ); if(ptlLayer->tl_iSelectedTile==-1) return; CTileInfo &ti=dcTileInfo[ptlLayer->tl_iSelectedTile]; // _rect holds terrain size if(_fStrength>0) { UWORD uwValue= dcTileInfo[ptlLayer->tl_iSelectedTile].ti_iy*ctTilesPerRaw+ dcTileInfo[ptlLayer->tl_iSelectedTile].ti_ix; if(ti.ti_bFlipX) uwValue|=TL_FLIPX; if(ti.ti_bFlipY) uwValue|=TL_FLIPY; if(ti.ti_bSwapXY) uwValue|=TL_SWAPXY; uwValue|=TL_VISIBLE; _puwBuffer[0]=uwValue<<8; } else { _puwBuffer[0]=0; } /* vidjeti sta ne radi sa brisanjem tile-ova ne pogadja se dobro lokacija tile-a ispod misa +view layer on/off ne discarda pretender texture +ne crta se dobro trenutno selektirani tile i preklapa se animirani (under mouse) +rotirane i flipane tileove crtati +nesto zapinje kad se prvi put otvara info window +ubaciti skrolanje tileova na mouse wheel +pick tile */ // free allocated tile info structures for(INDEX i=0; iTE_BRUSH_ALTITUDE_START && teToolTE_ALTITUDE_START && teToolTE_BRUSH_LAYER_START && teToolTE_LAYER_START && teToolTE_BRUSH_EDGE_START && teToolGetPixWidth()/2-_srcExtraW; _rect.rc_iRight=pt.pt_iX+(ptdBrush->GetPixWidth()-ptdBrush->GetPixWidth()/2)+_srcExtraW; _rect.rc_iTop=pt.pt_iY-ptdBrush->GetPixHeight()/2-_srcExtraH; _rect.rc_iBottom=pt.pt_iY+(ptdBrush->GetPixHeight()-ptdBrush->GetPixHeight()/2)+_srcExtraH; } // perform operation on whole terrain area else { _rect.rc_iLeft=0; _rect.rc_iRight=ptrTerrain->tr_pixHeightMapWidth; _rect.rc_iTop=0; _rect.rc_iBottom=ptrTerrain->tr_pixHeightMapHeight; } Rect rectTerrain; rectTerrain.rc_iLeft=0; rectTerrain.rc_iTop=0; rectTerrain.rc_iRight=ptrTerrain->tr_pixHeightMapWidth; rectTerrain.rc_iBottom=ptrTerrain->tr_pixHeightMapHeight; BOOL bAutoRememberUndo=FALSE; // if should apply undo for whole terrain if( (teTool>TE_ALTITUDE_START && teToolTE_LAYER_START && teTooltr_penEntity->en_ulID; _puwUndoTerrain=GetBufferForEditing(ptrTerrain, rectTerrain, btBufferType, iBufferData); } // editing in progress, update undo data else { // update undo rect if(_rect.rc_iLeft < _rectUndo.rc_iLeft) _rectUndo.rc_iLeft=_rect.rc_iLeft; if(_rect.rc_iRight > _rectUndo.rc_iRight) _rectUndo.rc_iRight=_rect.rc_iRight; if(_rect.rc_iTop < _rectUndo.rc_iTop) _rectUndo.rc_iTop=_rect.rc_iTop; if(_rect.rc_iBottom > _rectUndo.rc_iBottom) _rectUndo.rc_iBottom=_rect.rc_iBottom; } // clamp undo rect to terrain size _rectUndo.rc_iLeft=Clamp(_rectUndo.rc_iLeft, rectTerrain.rc_iLeft, rectTerrain.rc_iRight); _rectUndo.rc_iRight=Clamp(_rectUndo.rc_iRight, rectTerrain.rc_iLeft, rectTerrain.rc_iRight); _rectUndo.rc_iTop=Clamp(_rectUndo.rc_iTop, rectTerrain.rc_iTop, rectTerrain.rc_iBottom); _rectUndo.rc_iBottom=Clamp(_rectUndo.rc_iBottom, rectTerrain.rc_iTop, rectTerrain.rc_iBottom); // obtain buffer _puwBuffer=GetBufferForEditing(ptrTerrain, _rect, btBufferType, iBufferData); switch( teTool) { case TE_BRUSH_ALTITUDE_PAINT: { ApplyAddPaint(MIN_UWORD,MAX_UWORD); break; } case TE_BRUSH_EDGE_ERASE: { _fStrength=-fStrength; ApplyAddPaint(MIN_UWORD,MAX_UWORD); break; } case TE_BRUSH_LAYER_PAINT: { _fStrength=fStrength*32.0f; ApplyAddPaint(MIN_UWORD,MAX_UWORD); break; } case TE_BRUSH_ALTITUDE_SMOOTH: case TE_BRUSH_LAYER_SMOOTH: case TE_ALTITUDE_SMOOTH: case TE_LAYER_SMOOTH: { _fStrength=fStrength*theApp.m_fSmoothPower; ApplyFilterMatrix(_afFilterBlurMore); break; } case TE_BRUSH_ALTITUDE_FILTER: case TE_BRUSH_LAYER_FILTER: case TE_LAYER_FILTER: case TE_ALTITUDE_FILTER: { _fStrength=fStrength*theApp.m_fFilterPower; switch(theApp.m_iFilter) { case FLT_FINEBLUR: ApplyFilterMatrix(_afFilterFineBlur ); break; case FLT_SHARPEN: ApplyFilterMatrix(_afFilterSharpen ); break; case FLT_EMBOSS: ApplyFilterMatrix(_afFilterEmboss ); break; case FLT_EDGEDETECT: ApplyFilterMatrix(_afFilterEdgeDetect ); break; } break; } case TE_BRUSH_ALTITUDE_MINIMUM: case TE_ALTITUDE_MINIMUM: { _fStrength=0; ApplyAddPaint(theApp.m_uwEditAltitude,MAX_UWORD); break; } case TE_BRUSH_ALTITUDE_MAXIMUM: case TE_ALTITUDE_MAXIMUM: { _fStrength=0; ApplyAddPaint(MIN_UWORD, theApp.m_uwEditAltitude); break; } case TE_BRUSH_ALTITUDE_FLATTEN: case TE_ALTITUDE_FLATTEN: { _fStrength=0; ApplyAddPaint(theApp.m_uwEditAltitude, theApp.m_uwEditAltitude); break; } case TE_BRUSH_ALTITUDE_POSTERIZE: case TE_ALTITUDE_POSTERIZE: { ApplyPosterize(); break; } case TE_BRUSH_ALTITUDE_RND_NOISE: case TE_BRUSH_LAYER_RND_NOISE: case TE_ALTITUDE_RND_NOISE: case TE_LAYER_RND_NOISE: { ApplyRNDNoise(); break; } case TE_BRUSH_ALTITUDE_CONTINOUS_NOISE: case TE_BRUSH_LAYER_CONTINOUS_NOISE: case TE_ALTITUDE_CONTINOUS_NOISE: case TE_LAYER_CONTINOUS_NOISE: { if(!SetupContinousNoiseTexture()) return; ApplyContinousNoise(); FreeContinousNoiseTexture(); break; } case TE_GENERATE_TERRAIN: { GenerateTerrain(); break; } case TE_ALTITUDE_EQUALIZE: { EqualizeBuffer(); break; } case TE_CLEAR_LAYER_MASK: { for(INDEX i=0; i<_rect.Width()*_rect.Height(); i++) { _puwBuffer[i]=0; } break; } case TE_FILL_LAYER_MASK: { for(INDEX i=0; i<_rect.Width()*_rect.Height(); i++) { _puwBuffer[i]=MAX_UWORD; } break; } case TE_TILE_PAINT: { TilePaintTool(); break; } } // apply buffer change SetBufferForEditing(ptrTerrain, _puwBuffer, _rect, btBufferType, iBufferData); theApp.GetActiveDocument()->SetModifiedFlag( TRUE); FreeMemory(_puwBuffer); // mark rect for layer distribution updating if(teTool!=TE_TILE_PAINT) { DiscardLayerDistribution(_rect); } theApp.m_ctTerrainPageCanvas.MarkChanged(); if(bAutoRememberUndo) { TerrainEditEnd(); } }